chzzk-python 0.1.0__py3-none-any.whl
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.
- chzzk/__init__.py +175 -0
- chzzk/_version.py +34 -0
- chzzk/api/__init__.py +29 -0
- chzzk/api/base.py +147 -0
- chzzk/api/category.py +65 -0
- chzzk/api/channel.py +218 -0
- chzzk/api/chat.py +239 -0
- chzzk/api/live.py +212 -0
- chzzk/api/restriction.py +147 -0
- chzzk/api/session.py +389 -0
- chzzk/api/user.py +47 -0
- chzzk/auth/__init__.py +34 -0
- chzzk/auth/models.py +115 -0
- chzzk/auth/oauth.py +452 -0
- chzzk/auth/token.py +119 -0
- chzzk/client.py +515 -0
- chzzk/exceptions/__init__.py +37 -0
- chzzk/exceptions/errors.py +130 -0
- chzzk/http/__init__.py +68 -0
- chzzk/http/client.py +310 -0
- chzzk/http/endpoints.py +52 -0
- chzzk/models/__init__.py +86 -0
- chzzk/models/category.py +18 -0
- chzzk/models/channel.py +69 -0
- chzzk/models/chat.py +63 -0
- chzzk/models/common.py +23 -0
- chzzk/models/live.py +78 -0
- chzzk/models/restriction.py +18 -0
- chzzk/models/session.py +161 -0
- chzzk/models/user.py +14 -0
- chzzk/py.typed +0 -0
- chzzk/realtime/__init__.py +8 -0
- chzzk/realtime/client.py +635 -0
- chzzk_python-0.1.0.dist-info/METADATA +314 -0
- chzzk_python-0.1.0.dist-info/RECORD +37 -0
- chzzk_python-0.1.0.dist-info/WHEEL +4 -0
- chzzk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
chzzk/auth/oauth.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""OAuth client for Chzzk authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from chzzk.auth.models import (
|
|
10
|
+
AuthorizationCodeRequest,
|
|
11
|
+
RefreshTokenRequest,
|
|
12
|
+
RevokeTokenRequest,
|
|
13
|
+
Token,
|
|
14
|
+
TokenResponse,
|
|
15
|
+
TokenTypeHint,
|
|
16
|
+
)
|
|
17
|
+
from chzzk.exceptions import InvalidStateError, TokenExpiredError
|
|
18
|
+
from chzzk.http import AUTH_INTERLOCK_URL, AUTH_REVOKE_URL, AUTH_TOKEN_URL
|
|
19
|
+
from chzzk.http.client import AsyncHTTPClient, HTTPClient
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class TokenStorage(Protocol):
|
|
27
|
+
"""Protocol for token storage implementations."""
|
|
28
|
+
|
|
29
|
+
def get_token(self) -> Token | None:
|
|
30
|
+
"""Retrieve the stored token."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def save_token(self, token: Token) -> None:
|
|
34
|
+
"""Save a token."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def delete_token(self) -> None:
|
|
38
|
+
"""Delete the stored token."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InMemoryTokenStorage:
|
|
43
|
+
"""In-memory token storage implementation."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
self._token: Token | None = None
|
|
47
|
+
|
|
48
|
+
def get_token(self) -> Token | None:
|
|
49
|
+
"""Retrieve the stored token."""
|
|
50
|
+
return self._token
|
|
51
|
+
|
|
52
|
+
def save_token(self, token: Token) -> None:
|
|
53
|
+
"""Save a token."""
|
|
54
|
+
self._token = token
|
|
55
|
+
|
|
56
|
+
def delete_token(self) -> None:
|
|
57
|
+
"""Delete the stored token."""
|
|
58
|
+
self._token = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ChzzkOAuth:
|
|
62
|
+
"""Synchronous OAuth client for Chzzk authentication.
|
|
63
|
+
|
|
64
|
+
This client handles the OAuth 2.0 authorization code flow for Chzzk,
|
|
65
|
+
including token exchange, refresh, and revocation.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> oauth = ChzzkOAuth(
|
|
69
|
+
... client_id="your-client-id",
|
|
70
|
+
... client_secret="your-client-secret",
|
|
71
|
+
... redirect_uri="http://localhost:8080/callback",
|
|
72
|
+
... )
|
|
73
|
+
>>> auth_url, state = oauth.get_authorization_url()
|
|
74
|
+
>>> # User visits auth_url and gets redirected back with code
|
|
75
|
+
>>> token = oauth.exchange_code(code="auth_code", state=state)
|
|
76
|
+
>>> access_token = token.access_token
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
client_id: str,
|
|
82
|
+
client_secret: str,
|
|
83
|
+
redirect_uri: str,
|
|
84
|
+
*,
|
|
85
|
+
token_storage: TokenStorage | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize the OAuth client.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
client_id: Your Chzzk application's client ID.
|
|
91
|
+
client_secret: Your Chzzk application's client secret.
|
|
92
|
+
redirect_uri: The redirect URI registered with your application.
|
|
93
|
+
token_storage: Optional custom token storage. Defaults to InMemoryTokenStorage.
|
|
94
|
+
"""
|
|
95
|
+
self.client_id = client_id
|
|
96
|
+
self.client_secret = client_secret
|
|
97
|
+
self.redirect_uri = redirect_uri
|
|
98
|
+
self._storage = token_storage or InMemoryTokenStorage()
|
|
99
|
+
self._http = HTTPClient()
|
|
100
|
+
self._pending_state: str | None = None
|
|
101
|
+
|
|
102
|
+
def get_authorization_url(self, *, state: str | None = None) -> tuple[str, str]:
|
|
103
|
+
"""Generate the authorization URL for user authentication.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
state: Optional state parameter. If not provided, a random one is generated.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A tuple of (authorization_url, state).
|
|
110
|
+
"""
|
|
111
|
+
if state is None:
|
|
112
|
+
state = secrets.token_urlsafe(32)
|
|
113
|
+
|
|
114
|
+
self._pending_state = state
|
|
115
|
+
|
|
116
|
+
params = {
|
|
117
|
+
"clientId": self.client_id,
|
|
118
|
+
"redirectUri": self.redirect_uri,
|
|
119
|
+
"state": state,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
url = f"{AUTH_INTERLOCK_URL}?{urlencode(params)}"
|
|
123
|
+
return url, state
|
|
124
|
+
|
|
125
|
+
def exchange_code(
|
|
126
|
+
self,
|
|
127
|
+
code: str,
|
|
128
|
+
state: str,
|
|
129
|
+
*,
|
|
130
|
+
validate_state: bool = True,
|
|
131
|
+
) -> Token:
|
|
132
|
+
"""Exchange an authorization code for access and refresh tokens.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
code: The authorization code received from the callback.
|
|
136
|
+
state: The state parameter received from the callback.
|
|
137
|
+
validate_state: Whether to validate the state parameter. Defaults to True.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The Token object containing access and refresh tokens.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
InvalidStateError: If state validation fails.
|
|
144
|
+
"""
|
|
145
|
+
if validate_state and self._pending_state and state != self._pending_state:
|
|
146
|
+
raise InvalidStateError(
|
|
147
|
+
f"State mismatch: expected '{self._pending_state}', got '{state}'"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
request = AuthorizationCodeRequest(
|
|
151
|
+
client_id=self.client_id,
|
|
152
|
+
client_secret=self.client_secret,
|
|
153
|
+
code=code,
|
|
154
|
+
state=state,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
response_data = self._http.post(
|
|
158
|
+
AUTH_TOKEN_URL,
|
|
159
|
+
json=request.model_dump(by_alias=True),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
token_response = TokenResponse.model_validate(response_data)
|
|
163
|
+
token = Token.from_response(token_response)
|
|
164
|
+
self._storage.save_token(token)
|
|
165
|
+
self._pending_state = None
|
|
166
|
+
|
|
167
|
+
return token
|
|
168
|
+
|
|
169
|
+
def refresh_token(self, refresh_token: str | None = None) -> Token:
|
|
170
|
+
"""Refresh the access token using a refresh token.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
refresh_token: Optional refresh token to use. If not provided,
|
|
174
|
+
uses the stored token's refresh token.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The new Token object.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
TokenExpiredError: If no refresh token is available.
|
|
181
|
+
"""
|
|
182
|
+
if refresh_token is None:
|
|
183
|
+
stored_token = self._storage.get_token()
|
|
184
|
+
if stored_token is None:
|
|
185
|
+
raise TokenExpiredError("No token available for refresh")
|
|
186
|
+
refresh_token = stored_token.refresh_token
|
|
187
|
+
|
|
188
|
+
request = RefreshTokenRequest(
|
|
189
|
+
client_id=self.client_id,
|
|
190
|
+
client_secret=self.client_secret,
|
|
191
|
+
refresh_token=refresh_token,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
response_data = self._http.post(
|
|
195
|
+
AUTH_TOKEN_URL,
|
|
196
|
+
json=request.model_dump(by_alias=True),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
token_response = TokenResponse.model_validate(response_data)
|
|
200
|
+
token = Token.from_response(token_response)
|
|
201
|
+
self._storage.save_token(token)
|
|
202
|
+
|
|
203
|
+
return token
|
|
204
|
+
|
|
205
|
+
def revoke_token(
|
|
206
|
+
self,
|
|
207
|
+
token: str | None = None,
|
|
208
|
+
token_type_hint: TokenTypeHint = TokenTypeHint.ACCESS_TOKEN,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Revoke a token.
|
|
211
|
+
|
|
212
|
+
This revokes both access and refresh tokens associated with the same
|
|
213
|
+
authentication (same client_id and user).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
token: Optional token to revoke. If not provided, uses the stored access token.
|
|
217
|
+
token_type_hint: The type of token being revoked.
|
|
218
|
+
"""
|
|
219
|
+
if token is None:
|
|
220
|
+
stored_token = self._storage.get_token()
|
|
221
|
+
if stored_token is None:
|
|
222
|
+
return
|
|
223
|
+
token = stored_token.access_token
|
|
224
|
+
|
|
225
|
+
request = RevokeTokenRequest(
|
|
226
|
+
client_id=self.client_id,
|
|
227
|
+
client_secret=self.client_secret,
|
|
228
|
+
token=token,
|
|
229
|
+
token_type_hint=token_type_hint,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self._http.post(
|
|
233
|
+
AUTH_REVOKE_URL,
|
|
234
|
+
json=request.model_dump(by_alias=True),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self._storage.delete_token()
|
|
238
|
+
|
|
239
|
+
def get_token(self) -> Token | None:
|
|
240
|
+
"""Get the stored token.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
The stored Token object or None if no token is stored.
|
|
244
|
+
"""
|
|
245
|
+
return self._storage.get_token()
|
|
246
|
+
|
|
247
|
+
def close(self) -> None:
|
|
248
|
+
"""Close the HTTP client."""
|
|
249
|
+
self._http.close()
|
|
250
|
+
|
|
251
|
+
def __enter__(self) -> ChzzkOAuth:
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def __exit__(self, *_: object) -> None:
|
|
255
|
+
self.close()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class AsyncChzzkOAuth:
|
|
259
|
+
"""Asynchronous OAuth client for Chzzk authentication.
|
|
260
|
+
|
|
261
|
+
This client handles the OAuth 2.0 authorization code flow for Chzzk,
|
|
262
|
+
including token exchange, refresh, and revocation.
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
>>> async with AsyncChzzkOAuth(
|
|
266
|
+
... client_id="your-client-id",
|
|
267
|
+
... client_secret="your-client-secret",
|
|
268
|
+
... redirect_uri="http://localhost:8080/callback",
|
|
269
|
+
... ) as oauth:
|
|
270
|
+
... auth_url, state = oauth.get_authorization_url()
|
|
271
|
+
... # User visits auth_url and gets redirected back with code
|
|
272
|
+
... token = await oauth.exchange_code(code="auth_code", state=state)
|
|
273
|
+
... access_token = token.access_token
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
client_id: str,
|
|
279
|
+
client_secret: str,
|
|
280
|
+
redirect_uri: str,
|
|
281
|
+
*,
|
|
282
|
+
token_storage: TokenStorage | None = None,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Initialize the async OAuth client.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
client_id: Your Chzzk application's client ID.
|
|
288
|
+
client_secret: Your Chzzk application's client secret.
|
|
289
|
+
redirect_uri: The redirect URI registered with your application.
|
|
290
|
+
token_storage: Optional custom token storage. Defaults to InMemoryTokenStorage.
|
|
291
|
+
"""
|
|
292
|
+
self.client_id = client_id
|
|
293
|
+
self.client_secret = client_secret
|
|
294
|
+
self.redirect_uri = redirect_uri
|
|
295
|
+
self._storage = token_storage or InMemoryTokenStorage()
|
|
296
|
+
self._http = AsyncHTTPClient()
|
|
297
|
+
self._pending_state: str | None = None
|
|
298
|
+
|
|
299
|
+
def get_authorization_url(self, *, state: str | None = None) -> tuple[str, str]:
|
|
300
|
+
"""Generate the authorization URL for user authentication.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
state: Optional state parameter. If not provided, a random one is generated.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
A tuple of (authorization_url, state).
|
|
307
|
+
"""
|
|
308
|
+
if state is None:
|
|
309
|
+
state = secrets.token_urlsafe(32)
|
|
310
|
+
|
|
311
|
+
self._pending_state = state
|
|
312
|
+
|
|
313
|
+
params = {
|
|
314
|
+
"clientId": self.client_id,
|
|
315
|
+
"redirectUri": self.redirect_uri,
|
|
316
|
+
"state": state,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
url = f"{AUTH_INTERLOCK_URL}?{urlencode(params)}"
|
|
320
|
+
return url, state
|
|
321
|
+
|
|
322
|
+
async def exchange_code(
|
|
323
|
+
self,
|
|
324
|
+
code: str,
|
|
325
|
+
state: str,
|
|
326
|
+
*,
|
|
327
|
+
validate_state: bool = True,
|
|
328
|
+
) -> Token:
|
|
329
|
+
"""Exchange an authorization code for access and refresh tokens.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
code: The authorization code received from the callback.
|
|
333
|
+
state: The state parameter received from the callback.
|
|
334
|
+
validate_state: Whether to validate the state parameter. Defaults to True.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The Token object containing access and refresh tokens.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
InvalidStateError: If state validation fails.
|
|
341
|
+
"""
|
|
342
|
+
if validate_state and self._pending_state and state != self._pending_state:
|
|
343
|
+
raise InvalidStateError(
|
|
344
|
+
f"State mismatch: expected '{self._pending_state}', got '{state}'"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
request = AuthorizationCodeRequest(
|
|
348
|
+
client_id=self.client_id,
|
|
349
|
+
client_secret=self.client_secret,
|
|
350
|
+
code=code,
|
|
351
|
+
state=state,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
response_data = await self._http.post(
|
|
355
|
+
AUTH_TOKEN_URL,
|
|
356
|
+
json=request.model_dump(by_alias=True),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
token_response = TokenResponse.model_validate(response_data)
|
|
360
|
+
token = Token.from_response(token_response)
|
|
361
|
+
self._storage.save_token(token)
|
|
362
|
+
self._pending_state = None
|
|
363
|
+
|
|
364
|
+
return token
|
|
365
|
+
|
|
366
|
+
async def refresh_token(self, refresh_token: str | None = None) -> Token:
|
|
367
|
+
"""Refresh the access token using a refresh token.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
refresh_token: Optional refresh token to use. If not provided,
|
|
371
|
+
uses the stored token's refresh token.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
The new Token object.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
TokenExpiredError: If no refresh token is available.
|
|
378
|
+
"""
|
|
379
|
+
if refresh_token is None:
|
|
380
|
+
stored_token = self._storage.get_token()
|
|
381
|
+
if stored_token is None:
|
|
382
|
+
raise TokenExpiredError("No token available for refresh")
|
|
383
|
+
refresh_token = stored_token.refresh_token
|
|
384
|
+
|
|
385
|
+
request = RefreshTokenRequest(
|
|
386
|
+
client_id=self.client_id,
|
|
387
|
+
client_secret=self.client_secret,
|
|
388
|
+
refresh_token=refresh_token,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
response_data = await self._http.post(
|
|
392
|
+
AUTH_TOKEN_URL,
|
|
393
|
+
json=request.model_dump(by_alias=True),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
token_response = TokenResponse.model_validate(response_data)
|
|
397
|
+
token = Token.from_response(token_response)
|
|
398
|
+
self._storage.save_token(token)
|
|
399
|
+
|
|
400
|
+
return token
|
|
401
|
+
|
|
402
|
+
async def revoke_token(
|
|
403
|
+
self,
|
|
404
|
+
token: str | None = None,
|
|
405
|
+
token_type_hint: TokenTypeHint = TokenTypeHint.ACCESS_TOKEN,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Revoke a token.
|
|
408
|
+
|
|
409
|
+
This revokes both access and refresh tokens associated with the same
|
|
410
|
+
authentication (same client_id and user).
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
token: Optional token to revoke. If not provided, uses the stored access token.
|
|
414
|
+
token_type_hint: The type of token being revoked.
|
|
415
|
+
"""
|
|
416
|
+
if token is None:
|
|
417
|
+
stored_token = self._storage.get_token()
|
|
418
|
+
if stored_token is None:
|
|
419
|
+
return
|
|
420
|
+
token = stored_token.access_token
|
|
421
|
+
|
|
422
|
+
request = RevokeTokenRequest(
|
|
423
|
+
client_id=self.client_id,
|
|
424
|
+
client_secret=self.client_secret,
|
|
425
|
+
token=token,
|
|
426
|
+
token_type_hint=token_type_hint,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
await self._http.post(
|
|
430
|
+
AUTH_REVOKE_URL,
|
|
431
|
+
json=request.model_dump(by_alias=True),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
self._storage.delete_token()
|
|
435
|
+
|
|
436
|
+
def get_token(self) -> Token | None:
|
|
437
|
+
"""Get the stored token.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
The stored Token object or None if no token is stored.
|
|
441
|
+
"""
|
|
442
|
+
return self._storage.get_token()
|
|
443
|
+
|
|
444
|
+
async def close(self) -> None:
|
|
445
|
+
"""Close the HTTP client."""
|
|
446
|
+
await self._http.close()
|
|
447
|
+
|
|
448
|
+
async def __aenter__(self) -> AsyncChzzkOAuth:
|
|
449
|
+
return self
|
|
450
|
+
|
|
451
|
+
async def __aexit__(self, *_: object) -> None:
|
|
452
|
+
await self.close()
|
chzzk/auth/token.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Token storage implementations for Chzzk OAuth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from chzzk.auth.models import Token
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileTokenStorage:
|
|
17
|
+
"""File-based token storage implementation.
|
|
18
|
+
|
|
19
|
+
Stores tokens as JSON in a local file. Useful for CLI applications
|
|
20
|
+
or simple server applications that need persistent token storage.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> storage = FileTokenStorage(".chzzk_token.json")
|
|
24
|
+
>>> oauth = ChzzkOAuth(
|
|
25
|
+
... client_id="...",
|
|
26
|
+
... client_secret="...",
|
|
27
|
+
... redirect_uri="...",
|
|
28
|
+
... token_storage=storage,
|
|
29
|
+
... )
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, file_path: str | Path) -> None:
|
|
33
|
+
"""Initialize file-based token storage.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to the token storage file.
|
|
37
|
+
"""
|
|
38
|
+
self._file_path = Path(file_path)
|
|
39
|
+
|
|
40
|
+
def get_token(self) -> Token | None:
|
|
41
|
+
"""Retrieve the stored token from file."""
|
|
42
|
+
if not self._file_path.exists():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(self._file_path.read_text(encoding="utf-8"))
|
|
47
|
+
return Token.model_validate(data)
|
|
48
|
+
except (json.JSONDecodeError, OSError):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def save_token(self, token: Token) -> None:
|
|
52
|
+
"""Save a token to file."""
|
|
53
|
+
self._file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
self._file_path.write_text(
|
|
55
|
+
token.model_dump_json(indent=2),
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def delete_token(self) -> None:
|
|
60
|
+
"""Delete the stored token file."""
|
|
61
|
+
if self._file_path.exists():
|
|
62
|
+
self._file_path.unlink()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CallbackTokenStorage:
|
|
66
|
+
"""Callback-based token storage implementation.
|
|
67
|
+
|
|
68
|
+
Allows integration with external storage systems like databases
|
|
69
|
+
or Redis by providing custom callback functions.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> async def load_from_db(user_id: str) -> Token | None:
|
|
73
|
+
... # Load token from database
|
|
74
|
+
... pass
|
|
75
|
+
...
|
|
76
|
+
>>> async def save_to_db(user_id: str, token: Token) -> None:
|
|
77
|
+
... # Save token to database
|
|
78
|
+
... pass
|
|
79
|
+
...
|
|
80
|
+
>>> async def delete_from_db(user_id: str) -> None:
|
|
81
|
+
... # Delete token from database
|
|
82
|
+
... pass
|
|
83
|
+
...
|
|
84
|
+
>>> storage = CallbackTokenStorage(
|
|
85
|
+
... get_callback=lambda: load_from_db("user123"),
|
|
86
|
+
... save_callback=lambda t: save_to_db("user123", t),
|
|
87
|
+
... delete_callback=lambda: delete_from_db("user123"),
|
|
88
|
+
... )
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
get_callback: Callable[[], Token | None],
|
|
95
|
+
save_callback: Callable[[Token], None],
|
|
96
|
+
delete_callback: Callable[[], None],
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Initialize callback-based token storage.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
get_callback: Function to retrieve the stored token.
|
|
102
|
+
save_callback: Function to save a token.
|
|
103
|
+
delete_callback: Function to delete the stored token.
|
|
104
|
+
"""
|
|
105
|
+
self._get_callback = get_callback
|
|
106
|
+
self._save_callback = save_callback
|
|
107
|
+
self._delete_callback = delete_callback
|
|
108
|
+
|
|
109
|
+
def get_token(self) -> Token | None:
|
|
110
|
+
"""Retrieve the stored token via callback."""
|
|
111
|
+
return self._get_callback()
|
|
112
|
+
|
|
113
|
+
def save_token(self, token: Token) -> None:
|
|
114
|
+
"""Save a token via callback."""
|
|
115
|
+
self._save_callback(token)
|
|
116
|
+
|
|
117
|
+
def delete_token(self) -> None:
|
|
118
|
+
"""Delete the stored token via callback."""
|
|
119
|
+
self._delete_callback()
|