ossapi 3.1.9__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ossapi
3
- Version: 3.1.9
3
+ Version: 3.3.0
4
4
  Summary: Complete python wrapper for osu! api v2 and v1.
5
5
  Author-email: Liam DeVoe <orionldevoe@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/circleguard/ossapi
@@ -14,13 +14,14 @@ License-File: LICENSE
14
14
 
15
15
  # ossapi ([documentation](https://circleguard.github.io/ossapi/)) [![PyPI version](https://badge.fury.io/py/ossapi.svg)](https://pypi.org/project/ossapi/)
16
16
 
17
- ossapi is a python wrapper for the osu! api which has complete coverage of both [api v2](https://osu.ppy.sh/docs/index.html) and [api v1](https://github.com/ppy/osu-api/wiki). ossapi provides both sync (`Ossapi`) and async (`OssapiAsync`) versions for api v2.
17
+ ossapi is the definitive python wrapper for the osu! api. ossapi has complete coverage of [api v2](https://osu.ppy.sh/docs/index.html) and [api v1](https://github.com/ppy/osu-api/wiki), and provides both sync (`Ossapi`) and async (`OssapiAsync`) versions for api v2.
18
18
 
19
19
  If you need support or would like to contribute, feel free to ask in the `#ossapi` channel of the [circleguard discord](https://discord.gg/e84qxkQ).
20
20
 
21
21
  * [Installation](#installation)
22
22
  * [Quickstart](#quickstart)
23
23
  * [Async](#async)
24
+ * [Lazer](#lazer)
24
25
  * [Endpoints](#endpoints)
25
26
  * [Beatmaps](#endpoints-beatmaps)
26
27
  * [Beatmapsets](#endpoints-beatmapsets)
@@ -73,7 +74,7 @@ from ossapi import Ossapi
73
74
  api = Ossapi(client_id, client_secret)
74
75
 
75
76
  # see docs for full list of endpoints
76
- print(api.user("tybug2").username)
77
+ print(api.user("tybug").username)
77
78
  print(api.user(12092800, mode="osu").username)
78
79
  print(api.beatmap(221777).id)
79
80
  ```
@@ -89,13 +90,29 @@ from ossapi import Ossapi
89
90
  api = Ossapi(client_id, client_secret)
90
91
 
91
92
  async def main():
92
- await api.user("tybug2")
93
+ await api.user("tybug")
93
94
 
94
95
  asyncio.run(main())
95
96
  ```
96
97
 
97
98
  [Read more about OssapiAsync on the docs.](https://circleguard.github.io/ossapi/async.html)
98
99
 
100
+ ## Lazer
101
+
102
+ You can retrieve lazer-specific data (scores, leaderboards, etc) with ossapi:
103
+
104
+ ```python
105
+ from ossapi import Ossapi
106
+
107
+ api_lazer = Ossapi(client_id, client_secret, domain="lazer")
108
+
109
+ # best score on the lazer server (lazer + osu scores combined)
110
+ scores = api_lazer.user_scores(12092800, "best")
111
+ print(scores[0].pp)
112
+ ```
113
+
114
+ [Read more about domains on the docs.](https://circleguard.github.io/ossapi/domains.html)
115
+
99
116
  ## Endpoints
100
117
 
101
118
  All endpoints for api v2.
@@ -170,7 +187,7 @@ All endpoints for api v2.
170
187
 
171
188
  ## API v1 Usage
172
189
 
173
- You can get your api v1 key at <https://osu.ppy.sh/p/api/>. Note that due to a [redirection bug](https://github.com/ppy/osu-web/issues/2867), you may need to log in and wait 30 seconds before being able to access the api page through the above link.
190
+ You can get your api v1 key at <https://osu.ppy.sh/home/account/edit#legacy-api>.
174
191
 
175
192
  Basic usage:
176
193
 
@@ -1,12 +1,13 @@
1
1
  # ossapi ([documentation](https://circleguard.github.io/ossapi/)) [![PyPI version](https://badge.fury.io/py/ossapi.svg)](https://pypi.org/project/ossapi/)
2
2
 
3
- ossapi is a python wrapper for the osu! api which has complete coverage of both [api v2](https://osu.ppy.sh/docs/index.html) and [api v1](https://github.com/ppy/osu-api/wiki). ossapi provides both sync (`Ossapi`) and async (`OssapiAsync`) versions for api v2.
3
+ ossapi is the definitive python wrapper for the osu! api. ossapi has complete coverage of [api v2](https://osu.ppy.sh/docs/index.html) and [api v1](https://github.com/ppy/osu-api/wiki), and provides both sync (`Ossapi`) and async (`OssapiAsync`) versions for api v2.
4
4
 
5
5
  If you need support or would like to contribute, feel free to ask in the `#ossapi` channel of the [circleguard discord](https://discord.gg/e84qxkQ).
6
6
 
7
7
  * [Installation](#installation)
8
8
  * [Quickstart](#quickstart)
9
9
  * [Async](#async)
10
+ * [Lazer](#lazer)
10
11
  * [Endpoints](#endpoints)
11
12
  * [Beatmaps](#endpoints-beatmaps)
12
13
  * [Beatmapsets](#endpoints-beatmapsets)
@@ -59,7 +60,7 @@ from ossapi import Ossapi
59
60
  api = Ossapi(client_id, client_secret)
60
61
 
61
62
  # see docs for full list of endpoints
62
- print(api.user("tybug2").username)
63
+ print(api.user("tybug").username)
63
64
  print(api.user(12092800, mode="osu").username)
64
65
  print(api.beatmap(221777).id)
65
66
  ```
@@ -75,13 +76,29 @@ from ossapi import Ossapi
75
76
  api = Ossapi(client_id, client_secret)
76
77
 
77
78
  async def main():
78
- await api.user("tybug2")
79
+ await api.user("tybug")
79
80
 
80
81
  asyncio.run(main())
81
82
  ```
82
83
 
83
84
  [Read more about OssapiAsync on the docs.](https://circleguard.github.io/ossapi/async.html)
84
85
 
86
+ ## Lazer
87
+
88
+ You can retrieve lazer-specific data (scores, leaderboards, etc) with ossapi:
89
+
90
+ ```python
91
+ from ossapi import Ossapi
92
+
93
+ api_lazer = Ossapi(client_id, client_secret, domain="lazer")
94
+
95
+ # best score on the lazer server (lazer + osu scores combined)
96
+ scores = api_lazer.user_scores(12092800, "best")
97
+ print(scores[0].pp)
98
+ ```
99
+
100
+ [Read more about domains on the docs.](https://circleguard.github.io/ossapi/domains.html)
101
+
85
102
  ## Endpoints
86
103
 
87
104
  All endpoints for api v2.
@@ -156,7 +173,7 @@ All endpoints for api v2.
156
173
 
157
174
  ## API v1 Usage
158
175
 
159
- You can get your api v1 key at <https://osu.ppy.sh/p/api/>. Note that due to a [redirection bug](https://github.com/ppy/osu-web/issues/2867), you may need to log in and wait 30 seconds before being able to access the api page through the above link.
176
+ You can get your api v1 key at <https://osu.ppy.sh/home/account/edit#legacy-api>.
160
177
 
161
178
  Basic usage:
162
179
 
@@ -6,7 +6,7 @@ from importlib import metadata
6
6
 
7
7
  from ossapi.ossapi import (OssapiV1, ReplayUnavailableException,
8
8
  InvalidKeyException, APIException)
9
- from ossapi.ossapiv2 import Ossapi, Grant, Scope
9
+ from ossapi.ossapiv2 import Ossapi, Grant, Scope, Domain
10
10
  from ossapi.models import (Beatmap, BeatmapCompact, BeatmapUserScore,
11
11
  ForumTopicAndPosts, Search, CommentBundle, Cursor, Score,
12
12
  BeatmapsetSearchResult, ModdingHistoryEventsBundle, User, Rankings,
@@ -17,7 +17,8 @@ from ossapi.models import (Beatmap, BeatmapCompact, BeatmapUserScore,
17
17
  UserCompact, BeatmapsetCompact, ForumPoll, Room, RoomPlaylistItem,
18
18
  RoomPlaylistItemMod, RoomLeaderboardScore, RoomLeaderboardUserScore,
19
19
  RoomLeaderboard, Match, Matches, MatchResponse, ScoreMatchInfo, MatchGame,
20
- MatchEventDetail, MatchEvent, ScoringType, TeamType)
20
+ MatchEventDetail, MatchEvent, ScoringType, TeamType, StatisticsVariant,
21
+ Events)
21
22
  from ossapi.enums import (GameMode, ScoreType, RankingFilter, RankingType,
22
23
  UserBeatmapType, BeatmapDiscussionPostSort, UserLookupKey,
23
24
  BeatmapsetEventType, CommentableType, CommentSort, ForumTopicSort,
@@ -26,7 +27,7 @@ from ossapi.enums import (GameMode, ScoreType, RankingFilter, RankingType,
26
27
  BeatmapsetSearchCategory, BeatmapsetSearchMode,
27
28
  BeatmapsetSearchExplicitContent, BeatmapsetSearchLanguage,
28
29
  BeatmapsetSearchGenre, NewsPostKey, BeatmapsetSearchSort, RoomType,
29
- RoomCategory, RoomSearchType, MatchEventType)
30
+ RoomCategory, RoomSearchType, MatchEventType, Variant, EventsSort)
30
31
  from ossapi.mod import Mod
31
32
  from ossapi.replay import Replay
32
33
  from ossapi.encoder import ModelEncoder, serialize_model
@@ -42,7 +43,7 @@ __all__ = [
42
43
  "OssapiV1", "ReplayUnavailableException", "InvalidKeyException",
43
44
  "APIException",
44
45
  # OssapiV2 core
45
- "Ossapi", "OssapiAsync", "Grant", "Scope",
46
+ "Ossapi", "OssapiAsync", "Grant", "Scope", "Domain",
46
47
  # OssapiV2 models
47
48
  "Beatmap", "BeatmapCompact", "BeatmapUserScore", "ForumTopicAndPosts",
48
49
  "Search", "CommentBundle", "Cursor", "Score", "BeatmapsetSearchResult",
@@ -55,7 +56,7 @@ __all__ = [
55
56
  "Room", "RoomPlaylistItem", "RoomPlaylistItemMod", "RoomLeaderboardScore",
56
57
  "RoomLeaderboardUserScore", "RoomLeaderboard", "Match", "Matches",
57
58
  "MatchResponse", "ScoreMatchInfo", "MatchGame", "MatchEventDetail",
58
- "MatchEvent",
59
+ "MatchEvent", "StatisticsVariant", "Events",
59
60
  # OssapiV2 enums
60
61
  "GameMode", "ScoreType", "RankingFilter", "RankingType",
61
62
  "UserBeatmapType", "BeatmapDiscussionPostSort", "UserLookupKey",
@@ -66,7 +67,7 @@ __all__ = [
66
67
  "BeatmapsetSearchExplicitContent", "BeatmapsetSearchLanguage",
67
68
  "BeatmapsetSearchGenre", "NewsPostKey", "BeatmapsetSearchSort", "RoomType",
68
69
  "RoomCategory", "RoomSearchType", "MatchEventType", "ScoringType",
69
- "TeamType",
70
+ "TeamType", "Variant", "EventsSort",
70
71
  # OssapiV2 exceptions
71
72
  "AccessDeniedError", "TokenExpiredError", "InsufficientScopeError",
72
73
  # misc
@@ -88,10 +88,8 @@ class UserAccountHistoryType(EnumModel):
88
88
  SILENCE = "silence"
89
89
 
90
90
  class MessageType(EnumModel):
91
- DISQUALIFY = "disqualify"
92
91
  HYPE = "hype"
93
92
  MAPPER_NOTE = "mapper_note"
94
- NOMINATION_RESET = "nomination_reset"
95
93
  PRAISE = "praise"
96
94
  PROBLEM = "problem"
97
95
  REVIEW = "review"
@@ -217,6 +215,7 @@ class RoomCategory(EnumModel):
217
215
  # 430a2/resources/js/interfaces/room-json.ts#L7
218
216
  NORMAL = "normal"
219
217
  SPOTLIGHT = "spotlight"
218
+ FEATURED_ARTIST = "featured_artist"
220
219
 
221
220
  class MatchEventType(EnumModel):
222
221
  # https://github.dev/ppy/osu-web/blob/3d1586392102b05f2a3b264905c4dbb7b2
@@ -246,6 +245,10 @@ class TeamType(EnumModel):
246
245
  TEAM_VS = "team-vs"
247
246
  TAG_TEAM_VS = "tag-team-vs"
248
247
 
248
+ class Variant(EnumModel):
249
+ # can't start a python identifier with an integer
250
+ KEY_4 = "4k"
251
+ KEY_7 = "7k"
249
252
 
250
253
 
251
254
  # ===============
@@ -298,6 +301,7 @@ class ChannelType(EnumModel):
298
301
  TEMPORARY = "TEMPORARY"
299
302
  PM = "PM"
300
303
  GROUP = "GROUP"
304
+ ANNOUNCE = "ANNOUNCE"
301
305
 
302
306
  class CommentableType(EnumModel):
303
307
  NEWS_POST = "news_post"
@@ -379,8 +383,8 @@ class BeatmapsetSearchLanguage(EnumModel):
379
383
  ENGLISH = 2
380
384
  JAPANESE = 3
381
385
  CHINESE = 4
382
- KOREAN = 6
383
386
  INSTRUMENTAL = 5
387
+ KOREAN = 6
384
388
  FRENCH = 7
385
389
  GERMAN = 8
386
390
  SWEDISH = 9
@@ -425,6 +429,9 @@ class RoomSearchType(EnumModel):
425
429
  PARTICIPATED = "participated"
426
430
  ENDED = "ended"
427
431
 
432
+ class EventsSort(EnumModel):
433
+ NEW = "id_desc"
434
+ OLD = "id_asc"
428
435
 
429
436
 
430
437
  # =================
@@ -609,6 +616,14 @@ class ForumPostBody(Model):
609
616
  html: str
610
617
  raw: str
611
618
 
619
+ class ForumPollText(Model):
620
+ bbcode: str
621
+ html: str
622
+
623
+ class ForumPollTitle(Model):
624
+ bbcode: str
625
+ html: str
626
+
612
627
  class ReviewsConfig(Model):
613
628
  max_blocks: int
614
629
 
@@ -16,7 +16,8 @@ from ossapi.enums import (UserAccountHistory, ProfileBanner, UserBadge, Country,
16
16
  BeatmapsetEventType, UserRelationType, UserLevel, UserGradeCounts,
17
17
  GithubUser, ChangelogSearch, ForumTopicType, ForumPostBody, ForumTopicSort,
18
18
  ChannelType, ReviewsConfig, NewsSearch, Nomination, RankHighest, RoomType,
19
- RoomCategory, MatchEventType, ScoringType, TeamType)
19
+ RoomCategory, MatchEventType, ScoringType, TeamType, Variant, ForumPollText,
20
+ ForumPollTitle)
20
21
  from ossapi.utils import Datetime, Model, BaseModel, Field
21
22
 
22
23
  T = TypeVar("T")
@@ -86,7 +87,6 @@ class UserCompact(Model):
86
87
  # ---------------
87
88
  avatar_url: str
88
89
  country_code: str
89
- default_group: str
90
90
  id: int
91
91
  is_active: bool
92
92
  is_bot: bool
@@ -107,14 +107,13 @@ class UserCompact(Model):
107
107
  blocks: Optional[UserRelation]
108
108
  country: Optional[Country]
109
109
  cover: Optional[Cover]
110
+ default_group: Optional[str]
110
111
  favourite_beatmapset_count: Optional[int]
111
- # undocumented
112
112
  follow_user_mapping: Optional[List[int]]
113
113
  follower_count: Optional[int]
114
114
  friends: Optional[List[UserRelation]]
115
115
  graveyard_beatmapset_count: Optional[int]
116
116
  groups: Optional[List[UserGroup]]
117
- # undocumented
118
117
  guest_beatmapset_count: Optional[int]
119
118
  is_admin: Optional[bool]
120
119
  is_bng: Optional[bool]
@@ -130,7 +129,11 @@ class UserCompact(Model):
130
129
  mapping_follower_count: Optional[int]
131
130
  monthly_playcounts: Optional[List[UserMonthlyPlaycount]]
132
131
  page: Optional[UserPage]
132
+ pending_beatmapset_count: Optional[int]
133
133
  previous_usernames: Optional[List[str]]
134
+ # deprecated, replaced by rank_history
135
+ rankHistory: Optional[RankHistory]
136
+ rank_history: Optional[RankHistory]
134
137
  # deprecated, replaced by ranked_beatmapset_count
135
138
  ranked_and_approved_beatmapset_count: Optional[int]
136
139
  ranked_beatmapset_count: Optional[int]
@@ -143,13 +146,10 @@ class UserCompact(Model):
143
146
  support_level: Optional[int]
144
147
  # deprecated, replaced by pending_beatmapset_count
145
148
  unranked_beatmapset_count: Optional[int]
146
- pending_beatmapset_count: Optional[int]
147
149
  unread_pm_count: Optional[int]
148
150
  user_achievements: Optional[List[UserAchievement]]
149
151
  user_preferences: Optional[UserProfileCustomization]
150
- rank_history: Optional[RankHistory]
151
- # deprecated, replaced by rank_history
152
- rankHistory: Optional[RankHistory]
152
+
153
153
 
154
154
  def expand(self) -> User:
155
155
  return self._fk_user(self.id)
@@ -264,17 +264,17 @@ class BeatmapsetCompact(Model):
264
264
  creator: str
265
265
  favourite_count: int
266
266
  id: int
267
+ nsfw: bool
268
+ offset: int
267
269
  play_count: int
268
270
  preview_url: str
269
271
  source: str
270
272
  status: RankStatus
273
+ spotlight: bool
271
274
  title: str
272
275
  title_unicode: str
273
276
  user_id: int
274
277
  video: bool
275
- nsfw: bool
276
- offset: int
277
- spotlight: bool
278
278
  # documented as being in `Beatmapset` only, but returned by
279
279
  # `api.beatmapset_events` which uses a `BeatmapsetCompact`.
280
280
  hype: Optional[Hype]
@@ -283,6 +283,7 @@ class BeatmapsetCompact(Model):
283
283
  # ---------------
284
284
  beatmaps: Optional[List[Beatmap]]
285
285
  converts: Optional[Any]
286
+ current_nominations: Optional[List[Nomination]]
286
287
  current_user_attributes: Optional[Any]
287
288
  description: Optional[Any]
288
289
  discussions: Optional[Any]
@@ -291,12 +292,12 @@ class BeatmapsetCompact(Model):
291
292
  has_favourited: Optional[bool]
292
293
  language: Optional[Any]
293
294
  nominations: Optional[Any]
295
+ pack_tags: Optional[List[str]]
294
296
  ratings: Optional[Any]
295
297
  recent_favourites: Optional[Any]
296
298
  related_users: Optional[Any]
297
- _user: Optional[UserCompact] = Field(name="user")
298
- # undocumented
299
299
  track_id: Optional[int]
300
+ _user: Optional[UserCompact] = Field(name="user")
300
301
 
301
302
  def expand(self) -> Beatmapset:
302
303
  return self._fk_beatmapset(self.id)
@@ -308,6 +309,7 @@ class Beatmapset(BeatmapsetCompact):
308
309
  availability: Availability
309
310
  bpm: float
310
311
  can_be_hyped: bool
312
+ deleted_at: Optional[Datetime]
311
313
  discussion_enabled: bool
312
314
  discussion_locked: bool
313
315
  is_scoreable: bool
@@ -319,9 +321,6 @@ class Beatmapset(BeatmapsetCompact):
319
321
  storyboard: bool
320
322
  submitted_date: Optional[Datetime]
321
323
  tags: str
322
- current_nominations: Optional[List[Nomination]]
323
- deleted_at: Optional[Datetime]
324
- pack_tags: List[str]
325
324
 
326
325
  def expand(self) -> Beatmapset:
327
326
  return self
@@ -434,18 +433,18 @@ class Comment(Model):
434
433
  class CommentBundle(Model):
435
434
  commentable_meta: List[CommentableMeta]
436
435
  comments: List[Comment]
436
+ cursor: CursorT
437
437
  has_more: bool
438
438
  has_more_id: Optional[int]
439
439
  included_comments: List[Comment]
440
440
  pinned_comments: Optional[List[Comment]]
441
+ # TODO this should be type CommentSort
441
442
  sort: str
442
443
  top_level_count: Optional[int]
443
444
  total: Optional[int]
444
445
  user_follow: bool
445
446
  user_votes: List[int]
446
447
  users: List[UserCompact]
447
- # undocumented
448
- cursor: CursorT
449
448
 
450
449
  class ForumPost(Model):
451
450
  created_at: Datetime
@@ -477,11 +476,27 @@ class ForumTopic(Model):
477
476
  type: ForumTopicType
478
477
  updated_at: Datetime
479
478
  user_id: int
480
- poll: Any
479
+ poll: Optional[ForumPollModel]
481
480
 
482
481
  def user(self) -> User:
483
482
  return self._fk_user(self.user_id)
484
483
 
484
+ class ForumPollModel(Model):
485
+ allow_vote_change: bool
486
+ ended_at: Optional[Datetime]
487
+ hide_incomplete_results: bool
488
+ last_vote_at: Optional[Datetime]
489
+ max_votes: int
490
+ options: List[ForumPollOption]
491
+ started_at: Datetime
492
+ title: ForumPollTitle
493
+ total_vote_count: int
494
+
495
+ class ForumPollOption(Model):
496
+ id: int
497
+ text: ForumPollText
498
+ vote_count: Optional[int]
499
+
485
500
  class ForumTopicAndPosts(Model):
486
501
  cursor: CursorT
487
502
  search: ForumTopicSearch
@@ -579,13 +594,9 @@ class BeatmapsetDiscussion(Model):
579
594
  can_be_resolved: bool
580
595
  can_grant_kudosu: bool
581
596
  created_at: Datetime
582
- # documented as non-optional, api.beatmapset_events() might give a null
583
- # response for this? but very rarely. need to find a repro case
584
597
  current_user_attributes: Any
585
598
  updated_at: Datetime
586
599
  deleted_at: Optional[Datetime]
587
- # similarly as for current_user_attributes, in the past this has been null
588
- # but can't find a repro case
589
600
  last_post_at: Datetime
590
601
  kudosu_denied: bool
591
602
  starting_post: Optional[BeatmapsetDiscussionPost]
@@ -613,6 +624,8 @@ class BeatmapsetDiscussionVote(Model):
613
624
  beatmapset_discussion_id: int
614
625
  created_at: Datetime
615
626
  updated_at: Datetime
627
+ # TODO is this field ever actually returned? not documented and can't find
628
+ # a repro case.
616
629
  cursor_string: Optional[str]
617
630
 
618
631
  def user(self):
@@ -731,6 +744,7 @@ class Build(Model):
731
744
  version: Optional[str]
732
745
  changelog_entries: Optional[List[ChangelogEntry]]
733
746
  versions: Optional[Versions]
747
+ youtube_id: Optional[str]
734
748
 
735
749
  class Versions(Model):
736
750
  next: Optional[Build]
@@ -851,8 +865,8 @@ class BeatmapDifficultyAttributes(Model):
851
865
  stamina_difficulty: Optional[float]
852
866
  rhythm_difficulty: Optional[float]
853
867
  colour_difficulty: Optional[float]
854
- approach_raty: Optional[float]
855
- great_hit_windoy: Optional[float]
868
+ approach_rate: Optional[float]
869
+ great_hit_window: Optional[float]
856
870
 
857
871
  # ctb attributes
858
872
  approach_rate: Optional[float]
@@ -861,6 +875,9 @@ class BeatmapDifficultyAttributes(Model):
861
875
  great_hit_window: Optional[float]
862
876
  score_multiplier: Optional[float]
863
877
 
878
+ class Events(Model):
879
+ cursor_string: str
880
+ events: List[Event]
864
881
 
865
882
 
866
883
  # ================
@@ -1032,15 +1049,14 @@ class ChatChannel(Model):
1032
1049
  channel_id: int
1033
1050
  description: Optional[str]
1034
1051
  icon: Optional[str]
1035
- # documented as non-optional (to see that it can be null, pm tillerino)
1036
1052
  moderated: Optional[bool]
1037
1053
  name: str
1038
1054
  type: ChannelType
1039
1055
  uuid: Optional[str]
1056
+ message_length_limit: int
1040
1057
 
1041
1058
  # optional fields
1042
1059
  # ---------------
1043
- first_message_id: Optional[int]
1044
1060
  last_message_id: Optional[int]
1045
1061
  last_read_id: Optional[int]
1046
1062
  recent_messages: Optional[List[ChatMessage]]
@@ -1089,31 +1105,38 @@ class UserRelation(Model):
1089
1105
  def target(self) -> Union[User, UserCompact]:
1090
1106
  return self._fk_user(self.target_id, existing=self.target)
1091
1107
 
1108
+ class StatisticsVariant(Model):
1109
+ mode: GameMode
1110
+ variant: Variant
1111
+ country_rank: Optional[int]
1112
+ global_rank: Optional[int]
1113
+ pp: float
1092
1114
 
1093
1115
  class UserStatistics(Model):
1094
- level: UserLevel
1095
- pp: float
1096
- ranked_score: int
1116
+ count_100: int
1117
+ count_300: int
1118
+ count_50: int
1119
+ count_miss: int
1120
+ country_rank: Optional[int]
1121
+ grade_counts: UserGradeCounts
1097
1122
  hit_accuracy: float
1123
+ is_ranked: bool
1124
+ level: UserLevel
1125
+ maximum_combo: int
1098
1126
  play_count: int
1099
1127
  play_time: int
1100
- total_score: int
1101
- total_hits: int
1102
- maximum_combo: int
1103
- replays_watched_by_others: int
1104
- is_ranked: bool
1105
- grade_counts: UserGradeCounts
1106
- country_rank: Optional[int]
1128
+ pp: float
1129
+ pp_exp: float
1107
1130
  global_rank: Optional[int]
1131
+ global_rank_exp: Optional[float]
1132
+ # deprecated, replaced by global_rank and country_rank
1108
1133
  rank: Optional[Any]
1134
+ ranked_score: int
1135
+ replays_watched_by_others: int
1136
+ total_hits: int
1137
+ total_score: int
1109
1138
  user: Optional[UserCompact]
1110
- variants: Optional[Any]
1111
- global_rank_exp: Optional[float]
1112
- pp_exp: float
1113
- count_100: int
1114
- count_300: int
1115
- count_50: int
1116
- count_miss: int
1139
+ variants: Optional[List[StatisticsVariant]]
1117
1140
 
1118
1141
  class UserStatisticsRulesets(Model):
1119
1142
  # undocumented