diff --git a/pyproject.toml b/pyproject.toml index e46fd6f..dc1c4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "osuclient" -version = "0.1.0" +version = "0.2.0" description = "Client for osu! API" readme = "README.md" authors = [ diff --git a/src/osuclient/api.py b/src/osuclient/api.py index c07eac5..45adae7 100644 --- a/src/osuclient/api.py +++ b/src/osuclient/api.py @@ -317,3 +317,39 @@ class osuAPIClient(AioHTTPXClient): # TODO: implement other endpoints # https://osu.ppy.sh/docs/index.html#get-user-beatmaps + + async def get_user( + self, + access_token: str, + user_id: int, + mode: Literal['fruits', 'mania', 'osu', 'taiko'] | None = None, + key: Literal['username', 'id'] | None = None, + ): + url = f'/users/{user_id}' + + if mode: + url += f'/{mode}' + + req = await self.get( + url, + params=self.clean_dict({'key': key}), + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + ) + + match req.status_code: + case st.OK: + return s.GetUserSchema.model_validate(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') diff --git a/src/osuclient/schema.py b/src/osuclient/schema.py index f09eb73..6d57c00 100644 --- a/src/osuclient/schema.py +++ b/src/osuclient/schema.py @@ -24,6 +24,11 @@ class UserToken(Token): refresh_token: str +class Country(BaseModel): + code: str + name: str + + class Beatmap(BaseModel): beatmapset_id: int difficulty_rating: float @@ -33,11 +38,6 @@ class Beatmap(BaseModel): total_length: int user_id: int version: str - beatmapset: 'Beatmapset | BeatmapsetExtended | None' = None - checksum: str | None = None - current_user_playcount: int - max_combo: int - owners: list['BeatmapOwner'] class BeatmapDifficultyAttributes(BaseModel): @@ -99,8 +99,6 @@ class BeatmapPack(BaseModel): ruleset_id: int | None tag: str url: str - beatmapsets: list['Beatmapset'] | None = None - user_completion_data: 'UserCompletionData | None' = None class BeatmapPlaycount(BaseModel): @@ -138,23 +136,6 @@ class Beatmapset(BaseModel): title_unicode: str user_id: int video: bool - beatmaps: list['Beatmap | BeatmapExtended'] - # converts - current_nominations: list['Nomination'] - # current_user_attributes - # description - # discussions - # events - # genre - has_favourited: bool - # language - # nominations - pack_tags: list[str] - # ratings - # recent_favourites - # related_users - # user - track_id: int class Covers(BaseModel): @@ -248,7 +229,6 @@ class BeatmapsetExtended(Beatmapset): storyboard: bool submitted_date: datetime | None = None tags: str - has_favourited: bool class Build(BaseModel): @@ -259,8 +239,6 @@ class Build(BaseModel): users: int version: str | None = None youtube_id: str | None = None - changelog_entries: list['ChangelogEntry'] | None = None - versions: list['Versions'] | None = None class Versions(BaseModel): @@ -279,9 +257,6 @@ class ChangelogEntry(BaseModel): title: str | None = None entry_type: str = Field(alias='type') url: str | None = None - github_user: 'GithubUser | None' = None - message: str | None = None - message_html: str | None = None class ChatChannel(BaseModel): @@ -302,11 +277,6 @@ class ChatChannel(BaseModel): message_length_limit: int moderated: bool uuid: str | None = None - current_user_attributes: 'CurrentUserAttributes | None' = None - last_read_id: int | None = None - last_message_id: int | None = None - recent_messages: list['ChatMessage'] | None = None - users: list[int] | None = None class ChatMessage(BaseModel): @@ -318,7 +288,6 @@ class ChatMessage(BaseModel): timestamp: datetime message_type: str = Field(alias='type') uuid: str | None = None - sender: 'User' class Comment(BaseModel): @@ -465,12 +434,79 @@ class UpdateStream(BaseModel): pass +class UserAccountHistory(BaseModel): + description: str | None = None + id: int + length: int + permanent: bool + timestamp: datetime + type: Literal['note', 'restriction', 'silence'] + + +class ProfileBanner(BaseModel): + id: int + tournament_id: int + image: str | None = None + image_2x: str | None = Field(alias='image@2x') + + +class UserBadge(BaseModel): + awarded_at: datetime + description: str + image_2x_url: str = Field(alias='image@2x_url') + image_url: str + url: str + + +class UserKudosu(BaseModel): + available: int + total: int + + +class UserRankHighest(BaseModel): + rank: int + updated_at: datetime + + +class UserMonthlyPlaycount(BaseModel): + start_date: datetime + count: int + + class User(BaseModel): - pass + avatar_url: str | None = None + country_code: str + default_group: str | None = None + id: int + is_active: bool + is_bot: bool + is_deleted: bool + is_online: bool + is_supporter: bool + last_visit: datetime | None = None + pm_friends_only: bool + profile_colour: str | None = None + username: str class UserExtended(User): - pass + discord: str | None = None + has_supported: bool + interests: str | None = None + join_date: datetime + location: str | None = None + max_blocks: int + max_friends: int + occupation: str | None = None + playmode: Literal['osu', 'taiko', 'fruits', 'mania'] + playstyle: list[str] + post_count: int + profile_hue: int | None = None + profile_order: list[str] + title: str | None = None + title_url: str | None = None + twitter: str | None = None + website: str | None = None class UserGroup(BaseModel): @@ -481,8 +517,39 @@ class UserSilence(BaseModel): pass +class UserStatisticsGradeCounts(BaseModel): + a: int + s: int + sh: int + ss: int + ssh: int + + +class UserStatisticsLevel(BaseModel): + current: int + progress: float + + class UserStatistics(BaseModel): - pass + count_100: int + count_300: int + count_50: int + count_miss: int + country_rank: int | None = None + grade_counts: UserStatisticsGradeCounts + hit_accuracy: float + is_ranked: bool + level: UserStatisticsLevel + maximum_combo: int + play_count: int + play_time: int + pp: float + global_rank: int | None = None + global_rank_exp: int | None = None + ranked_score: int + replays_watched_by_others: int + total_hits: int + total_score: int class WikiPage(BaseModel): @@ -499,3 +566,38 @@ class BeatmapPacks(BaseModel): class UserBeatmapScores(BaseModel): scores: list['Score'] + + +class GetUserSchema(UserExtended): + account_history: list[UserAccountHistory] + active_tournament_banners: list[ProfileBanner] + badges: list[UserBadge] + beatmap_playcounts_count: int + country: Country + # cover + favourite_beatmapset_count: int + follow_user_mapping: list[int] + follower_count: int + graveyard_beatmapset_count: int + groups: list[UserGroup] + guest_beatmapset_count: int + is_restricted: bool | None = None + kudosu: UserKudosu + loved_beatmapset_count: int + mapping_follower_count: int + monthly_playcounts: list[UserMonthlyPlaycount] + nominated_beatmapset_count: int + # page + pending_beatmapset_count: int + previous_usernames: list[str] + rank_highest: UserRankHighest | None = None + # rank_history + ranked_beatmapset_count: int + # replays_watched_counts + scores_best_count: int + scores_first_count: int + scores_recent_count: int + session_verified: bool + statistics: UserStatistics + support_level: int + # user_achievements