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.
- {ossapi-3.3.1 → ossapi-3.3.3}/PKG-INFO +3 -3
- {ossapi-3.3.1 → ossapi-3.3.3}/README.md +2 -2
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/enums.py +7 -2
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/models.py +5 -2
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/ossapiv2.py +68 -18
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/ossapiv2_async.py +5 -6
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi.egg-info/PKG-INFO +3 -3
- {ossapi-3.3.1 → ossapi-3.3.3}/pyproject.toml +1 -1
- {ossapi-3.3.1 → ossapi-3.3.3}/tests/test_endpoints.py +2 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/LICENSE +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/__init__.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/encoder.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/mod.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/ossapi.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/replay.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi/utils.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi.egg-info/SOURCES.txt +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi.egg-info/dependency_links.txt +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi.egg-info/requires.txt +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/ossapi.egg-info/top_level.txt +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/setup.cfg +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/tests/__init__.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/tests/test_cursor.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/tests/test_models.py +0 -0
- {ossapi-3.3.1 → ossapi-3.3.3}/tests/test_v1.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ossapi
|
|
3
|
-
Version: 3.3.
|
|
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
|
|
89
|
+
from ossapi import OssapiAsync
|
|
90
90
|
|
|
91
|
-
api =
|
|
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
|
|
75
|
+
from ossapi import OssapiAsync
|
|
76
76
|
|
|
77
|
-
api =
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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 =
|
|
572
|
-
|
|
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
|
|
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
|
-
|
|
331
|
-
|
|
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
|
|
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.
|
|
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
|
|
89
|
+
from ossapi import OssapiAsync
|
|
90
90
|
|
|
91
|
-
api =
|
|
91
|
+
api = OssapiAsync(client_id, client_secret)
|
|
92
92
|
|
|
93
93
|
async def main():
|
|
94
94
|
await api.user("tybug")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|