Релиз 0.1

This commit is contained in:
2025-11-25 22:30:20 +03:00
commit 337c71986b
10 changed files with 1091 additions and 0 deletions

319
src/osuclient/api.py Normal file
View File

@ -0,0 +1,319 @@
from typing import Literal
from aiohttpx import status as st
from aiohttpx.client import AioHTTPXClient
from . import schema as s
class osuAPIClient(AioHTTPXClient):
def __init__(
self,
redis_url: str,
client_id: str,
client_secret: str,
callback_url: str,
):
self.base_uri = 'https://osu.ppy.sh/api/v2'
self.client_id = client_id
self.client_secret = client_secret
self.callback_url = callback_url
super().__init__(
base_url=self.base_uri,
redis_url=redis_url,
key='osu',
limit=10,
logger='osu! API',
)
async def get_beatmap_favorites(
self, access_token: str, cache_time: int | None = None
):
req = await self.get(
'/me/beatmapset-favourites',
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.FavoriteBeatmaps.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def get_beatmap_packs(
self,
access_token: str,
packs_type: Literal[
'standard',
'featured',
'tournament',
'loved',
'chart',
'theme',
'artist',
] = 'standard',
cursor_string: str | None = None,
cache_time: int | None = None,
):
req = await self.get(
'/beatmaps/packs',
params=self.clean_dict(
{'type': packs_type, 'cursor_string': cursor_string}
),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.BeatmapPacks.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def get_beatmap_pack(
self,
access_token: str,
pack: str,
legacy_only: int = 0,
cache_time: int | None = None,
):
req = await self.get(
f'/beatmaps/packs/{pack}',
params=self.clean_dict({'legacy_only': legacy_only}),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.BeatmapPack.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case st.NOT_FOUND:
raise s.Error(req.status_code, 'Not Found')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def lookup_beatmap(
self,
access_token: str,
checksum: str | None = None,
filename: str | None = None,
beatmap_id: int | None = None,
cache_time: int | None = None,
):
req = await self.get(
'/beatmaps/lookup',
params=self.clean_dict(
{
'checksum': checksum,
'filename': filename,
'beatmap_id': beatmap_id,
}
),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.BeatmapExtended.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case st.NOT_FOUND:
raise s.Error(req.status_code, 'Not Found')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def get_user_beatmap_score(
self,
access_token: str,
beatmap: int,
user: int,
cache_time: int | None = None,
):
req = await self.get(
f'/beatmaps/{beatmap}/scores/users/{user}',
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.BeatmapUserScore.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case st.NOT_FOUND:
raise s.Error(req.status_code, 'Not Found')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def get_user_beatmap_scores(
self,
access_token: str,
beatmap: int,
user: int,
legacy_only: int = 0,
ruleset: Literal['fruits', 'mania', 'osu', 'taiko'] | None = None,
cache_time: int | None = None,
):
req = await self.get(
f'/beatmaps/{beatmap}/scores/users/{user}/all',
params=self.clean_dict(
{'legacy_only': legacy_only, 'ruleset': ruleset}
),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.UserBeatmapScores.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case st.NOT_FOUND:
raise s.Error(req.status_code, 'Not Found')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
async def get_beatmap_scores(
self,
access_token: str,
beatmap: int,
legacy_only: int = 0,
mode: Literal['fruits', 'mania', 'osu', 'taiko'] | None = None,
mods: str | None = None,
ranking_type: str | None = None,
cache_time: int | None = None,
):
req = await self.get(
f'/beatmaps/{beatmap}/scores',
params=self.clean_dict(
{
'legacy_only': legacy_only,
'mode': mode,
'mods': mods,
'ranking_type': ranking_type,
}
),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return s.BeatmapScores.model_validate(req.json())
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
# TODO: implement endpoints
# https://osu.ppy.sh/docs/index.html#get-beatmaps
async def get_user_scores(
self,
access_token: str,
user: int,
score_type: Literal['best', 'firsts', 'recent'],
legacy_only: int = 0,
include_fails: int = 0,
mode: Literal['fruits', 'mania', 'osu', 'taiko'] | None = None,
limit: int | None = None,
offset: int | None = None,
cache_time: int | None = None,
):
req = await self.get(
f'/users/{user}/scores/{score_type}',
params=self.clean_dict(
{
'legacy_only': legacy_only,
'include_fails': include_fails,
'mode': mode,
'limit': limit,
'offset': offset,
}
),
headers=self.clean_dict(
{
'Authorization': f'Bearer {access_token}',
'X-Cache-TTL': cache_time,
}
),
)
match req.status_code:
case st.OK:
return [s.Score.model_validate(score) for score in req.json()]
case st.NOT_FOUND:
raise s.Error(req.status_code, 'Not Found')
case st.UNAUTHORIZED:
raise s.Error(req.status_code, 'Unauthorized')
case _:
self.logger.error(req.text)
raise s.Error(500, 'Internal Server Error')
# TODO: implement other endpoints
# https://osu.ppy.sh/docs/index.html#get-user-beatmaps