ossapi 3.3.1__tar.gz → 3.3.3__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.3.1
3
+ Version: 3.3.3
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
@@ -86,9 +86,9 @@ ossapi provides an async variant, `OssapiAsync`, which has an identical interfac
86
86
 
87
87
  ```python
88
88
  import asyncio
89
- from ossapi import Ossapi
89
+ from ossapi import OssapiAsync
90
90
 
91
- api = Ossapi(client_id, client_secret)
91
+ api = OssapiAsync(client_id, client_secret)
92
92
 
93
93
  async def main():
94
94
  await api.user("tybug")
@@ -72,9 +72,9 @@ ossapi provides an async variant, `OssapiAsync`, which has an identical interfac
72
72
 
73
73
  ```python
74
74
  import asyncio
75
- from ossapi import Ossapi
75
+ from ossapi import OssapiAsync
76
76
 
77
- api = Ossapi(client_id, client_secret)
77
+ api = OssapiAsync(client_id, client_secret)
78
78
 
79
79
  async def main():
80
80
  await api.user("tybug")
@@ -86,6 +86,7 @@ class UserAccountHistoryType(EnumModel):
86
86
  NOTE = "note"
87
87
  RESTRICTION = "restriction"
88
88
  SILENCE = "silence"
89
+ TOURNAMENT_BAN = "tournament_ban"
89
90
 
90
91
  class MessageType(EnumModel):
91
92
  HYPE = "hype"
@@ -281,6 +282,8 @@ class UserBeatmapType(EnumModel):
281
282
  MOST_PLAYED = "most_played"
282
283
  RANKED = "ranked"
283
284
  PENDING = "pending"
285
+ GUEST = "guest"
286
+ NOMINATED = "nominated"
284
287
 
285
288
  class BeatmapDiscussionPostSort(EnumModel):
286
289
  NEW = "id_desc"
@@ -474,9 +477,11 @@ class ProfileBanner(Model):
474
477
 
475
478
  class UserAccountHistory(Model):
476
479
  description: Optional[str]
477
- type: UserAccountHistoryType
478
- timestamp: Datetime
480
+ id: int
479
481
  length: int
482
+ permanent: bool
483
+ timestamp: Datetime
484
+ type: UserAccountHistoryType
480
485
 
481
486
 
482
487
  class UserBadge(Model):
@@ -154,6 +154,9 @@ class UserCompact(Model):
154
154
  def expand(self) -> User:
155
155
  return self._fk_user(self.id)
156
156
 
157
+ def refresh(self) -> User:
158
+ return self._fk_user(self.id)
159
+
157
160
  class User(UserCompact):
158
161
  comments_count: int
159
162
  cover_url: str
@@ -571,7 +574,7 @@ class BeatmapsetDiscussionPost(Model):
571
574
  updated_at: Datetime
572
575
  deleted_at: Optional[Datetime]
573
576
 
574
- def user(self) -> user:
577
+ def user(self) -> User:
575
578
  return self._fk_user(self.user_id)
576
579
 
577
580
  def last_editor(self) -> Optional[User]:
@@ -1136,7 +1139,7 @@ class UserStatistics(Model):
1136
1139
  total_hits: int
1137
1140
  total_score: int
1138
1141
  user: Optional[UserCompact]
1139
- variants: Optional[List[StatisticsVariant]]
1142
+ variants: Optional[List[StatisticsVariant]] #!
1140
1143
 
1141
1144
  class UserStatisticsRulesets(Model):
1142
1145
  # undocumented
@@ -15,7 +15,7 @@ import sys
15
15
 
16
16
  from requests_oauthlib import OAuth2Session
17
17
  from oauthlib.oauth2 import (BackendApplicationClient, TokenExpiredError,
18
- AccessDeniedError)
18
+ AccessDeniedError, OAuth2Error)
19
19
  from oauthlib.oauth2.rfc6749.errors import InsufficientScopeError
20
20
  from oauthlib.oauth2.rfc6749.tokens import OAuth2Token
21
21
  import osrparse
@@ -56,7 +56,9 @@ from ossapi.replay import Replay
56
56
  # details).
57
57
  GameModeT = Union[GameMode, str]
58
58
  ScoreTypeT = Union[ScoreType, str]
59
- ModT = Union[Mod, str, int, list]
59
+ # XXX this cannot be recursively typed without breaking our runtime type hint
60
+ # inspection.
61
+ ModT = Union[Mod, str, int, list[Union[Mod, str, int]]]
60
62
  RankingFilterT = Union[RankingFilter, str]
61
63
  RankingTypeT = Union[RankingType, str]
62
64
  UserBeatmapTypeT = Union[UserBeatmapType, str]
@@ -213,6 +215,20 @@ def request(scope, *, requires_user=False, category):
213
215
  return decorator
214
216
 
215
217
 
218
+ class ReauthenticationRequired(Exception):
219
+ """
220
+ Indicates that either the user has revoked this application from their
221
+ account, or osu-web itself has invalidated the refresh token associated with
222
+ this application.
223
+
224
+ This exception is only raised when a manual access_token is passed to
225
+ Ossapi, to bypass Ossapi's default authentication methods. The expectation
226
+ is that in these cases, the consumer has their own way of authenticating
227
+ with the user. That method should be used here to handle the
228
+ reauthentication.
229
+ """
230
+ pass
231
+
216
232
  class Grant(Enum):
217
233
  """
218
234
  The grant types used by the api.
@@ -259,10 +275,9 @@ class Ossapi:
259
275
  The redirect uri for the client. Must be passed if using the
260
276
  authorization code grant. This must exactly match the redirect uri on
261
277
  the client's settings page. Additionally, in order for ossapi to receive
262
- authentication from this redirect uri, it must be a port on localhost.
263
- So "http://localhost:3914/", "http://localhost:727/", etc are all valid
264
- redirect uris. You can change your client's redirect uri from its
265
- settings page.
278
+ authentication from this redirect uri, it must be a port on localhost,
279
+ e.g. "http://localhost:3914/". You can change your client's redirect uri
280
+ from its settings page.
266
281
  scopes: List[str]
267
282
  What scopes to request when authenticating.
268
283
  grant: Grant or str
@@ -382,6 +397,8 @@ class Ossapi:
382
397
  raise ValueError("`redirect_uri` must be passed if the "
383
398
  "authorization code grant is used.")
384
399
 
400
+ # whether the consumer passed a token to ossapi to bypass authentication
401
+ self.access_token_passed = False
385
402
  token = None
386
403
  if access_token is not None:
387
404
  # allow refresh_token to be null for the case of client credentials
@@ -393,6 +410,7 @@ class Ossapi:
393
410
  "refresh_token": refresh_token
394
411
  }
395
412
  token = OAuth2Token(params)
413
+ self.access_token_passed = True
396
414
 
397
415
  self.session = self.authenticate(token=token)
398
416
 
@@ -453,6 +471,8 @@ class Ossapi:
453
471
  with this OssapiV2's parameters, or from a fresh authentication if no
454
472
  such file exists.
455
473
  """
474
+
475
+ # try saved token file first
456
476
  if self.token_file.exists() or token is not None:
457
477
  if token is None:
458
478
  with open(self.token_file, "rb") as f:
@@ -473,6 +493,10 @@ class Ossapi:
473
493
  token_updater=self._save_token,
474
494
  scope=[scope.value for scope in self.scopes])
475
495
 
496
+ # otherwise, authorize from scratch
497
+ return self._new_grant()
498
+
499
+ def _new_grant(self):
476
500
  if self.grant is Grant.CLIENT_CREDENTIALS:
477
501
  return self._new_client_grant(self.client_id, self.client_secret)
478
502
 
@@ -553,9 +577,30 @@ class Ossapi:
553
577
  params = self._format_params(params)
554
578
  # also format data for post requests
555
579
  data = self._format_params(data)
556
- try:
557
- r = self.session.request(method, f"{self.base_url}{url}",
580
+
581
+ def make_request():
582
+ return self.session.request(method, f"{self.base_url}{url}",
558
583
  params=params, data=data)
584
+
585
+ def reauthenticate_and_retry():
586
+ # don't automatically re-authenticate if the user passed an access
587
+ # token. They should handle re-authentication with the user
588
+ # manually (since they may have a bespoke system, like a website).
589
+ if self.access_token_passed:
590
+ self.log.info("refresh token is invalid. raising for consumer "
591
+ "to handle since access token was passed originally.")
592
+ raise ReauthenticationRequired()
593
+
594
+ self.log.info("refresh token invalid, re-authenticating (grant: "
595
+ f"{self.grant})")
596
+ # don't use .authenticate, that falls back to cached tokens. go
597
+ # straight to authenticating from scratch.
598
+ self.session = self._new_grant()
599
+ # redo the request now that we have a valid session
600
+ return make_request()
601
+
602
+ try:
603
+ r = make_request()
559
604
  except TokenExpiredError:
560
605
  # provide "auto refreshing" for client credentials grant. The client
561
606
  # grant doesn't actually provide a refresh token, so we can't hook
@@ -568,11 +613,23 @@ class Ossapi:
568
613
  self.session = self._new_client_grant(self.client_id,
569
614
  self.client_secret)
570
615
  # redo the request now that we have a valid token
571
- r = self.session.request(method, f"{self.base_url}{url}",
572
- params=params, data=data)
616
+ r = make_request()
617
+ except OAuth2Error as e:
618
+ if e.description != "The refresh token is invalid.":
619
+ raise
620
+
621
+ r = reauthenticate_and_retry()
573
622
 
574
623
  self.log.info(f"made {method} request to {r.request.url}, data {data}")
575
624
  json_ = r.json()
625
+
626
+ # occurs if a client gets revoked and the token hasn't officially
627
+ # expired yet (so it doesn't error earlier up in the chain with
628
+ # Oauth2Error).
629
+ if json_ == {"authentication": "basic"}:
630
+ r = reauthenticate_and_retry()
631
+ json_ = r.json()
632
+
576
633
  self.log.debug(f"received json: \n{json.dumps(json_, indent=4)}")
577
634
  self._check_response(json_, r.url)
578
635
 
@@ -586,13 +643,6 @@ class Ossapi:
586
643
  raise ValueError(f"api returned an error of `{json_['error']}` for "
587
644
  f"a request to {unquote(url)}")
588
645
 
589
- # Shouldn't happen in normal usage. Might occur if a client gets revoked
590
- # and we still have a local token.pickel saved for it. But I haven't
591
- # tested.
592
- if json_ == {"authentication": "basic"}:
593
- raise ValueError(f"Invalid authentication. json: {json_}, url: "
594
- f"{url}")
595
-
596
646
  def _get(self, type_, url, params={}):
597
647
  return self._request(type_, "GET", url, params=params)
598
648
 
@@ -802,7 +852,7 @@ class Ossapi:
802
852
  try:
803
853
  type_hints = get_type_hints(type_)
804
854
  except TypeError:
805
- assert type(type_) is _GenericAlias # pylint: disable=unidiomatic-typecheck
855
+ assert type(type_) is _GenericAlias
806
856
 
807
857
  signature_type = get_origin(type_)
808
858
  type_hints = get_type_hints(signature_type)
@@ -122,7 +122,7 @@ class Oauth2SessionAsync(OAuth2Session):
122
122
  # details).
123
123
  GameModeT = Union[GameMode, str]
124
124
  ScoreTypeT = Union[ScoreType, str]
125
- ModT = Union[Mod, str, int, list]
125
+ ModT = Union[Mod, str, int, list[Union[Mod, str, int]]]
126
126
  RankingFilterT = Union[RankingFilter, str]
127
127
  RankingTypeT = Union[RankingType, str]
128
128
  UserBeatmapTypeT = Union[UserBeatmapType, str]
@@ -326,10 +326,9 @@ class OssapiAsync:
326
326
  The redirect uri for the client. Must be passed if using the
327
327
  authorization code grant. This must exactly match the redirect uri on
328
328
  the client's settings page. Additionally, in order for ossapi to receive
329
- authentication from this redirect uri, it must be a port on localhost.
330
- So "http://localhost:3914/", "http://localhost:727/", etc are all valid
331
- redirect uris. You can change your client's redirect uri from its
332
- settings page.
329
+ authentication from this redirect uri, it must be a port on localhost,
330
+ e.g. "http://localhost:3914/". You can change your client's redirect uri
331
+ from its settings page.
333
332
  scopes: List[str]
334
333
  What scopes to request when authenticating.
335
334
  grant: Grant or str
@@ -890,7 +889,7 @@ class OssapiAsync:
890
889
  try:
891
890
  type_hints = get_type_hints(type_)
892
891
  except TypeError:
893
- assert type(type_) is _GenericAlias # pylint: disable=unidiomatic-typecheck
892
+ assert type(type_) is _GenericAlias
894
893
 
895
894
  signature_type = get_origin(type_)
896
895
  type_hints = get_type_hints(signature_type)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ossapi
3
- Version: 3.3.1
3
+ Version: 3.3.3
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
@@ -86,9 +86,9 @@ ossapi provides an async variant, `OssapiAsync`, which has an identical interfac
86
86
 
87
87
  ```python
88
88
  import asyncio
89
- from ossapi import Ossapi
89
+ from ossapi import OssapiAsync
90
90
 
91
- api = Ossapi(client_id, client_secret)
91
+ api = OssapiAsync(client_id, client_secret)
92
92
 
93
93
  async def main():
94
94
  await api.user("tybug")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ossapi"
3
- version = "3.3.1"
3
+ version = "3.3.3"
4
4
  description = "Complete python wrapper for osu! api v2 and v1."
5
5
  readme = "README.md"
6
6
  keywords = ["osu!", "wrapper", "api", "python"]
@@ -107,6 +107,8 @@ class TestSearchBeatmaps(TestCase):
107
107
  class TestUser(TestCase):
108
108
  def test_deserialize(self):
109
109
  api.user(12092800)
110
+ # user with an account_history (tournament ban)
111
+ api.user(9997093)
110
112
 
111
113
  def test_key(self):
112
114
  # make sure it automatically falls back to username if not specified
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes