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/api/session.py ADDED
@@ -0,0 +1,389 @@
1
+ """Session API service for Chzzk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from chzzk.api.base import AsyncBaseService, BaseService
6
+ from chzzk.http import (
7
+ SESSIONS_AUTH_CLIENT_URL,
8
+ SESSIONS_AUTH_URL,
9
+ SESSIONS_CLIENT_URL,
10
+ SESSIONS_SUBSCRIBE_CHAT_URL,
11
+ SESSIONS_SUBSCRIBE_DONATION_URL,
12
+ SESSIONS_SUBSCRIBE_SUBSCRIPTION_URL,
13
+ SESSIONS_UNSUBSCRIBE_CHAT_URL,
14
+ SESSIONS_UNSUBSCRIBE_DONATION_URL,
15
+ SESSIONS_UNSUBSCRIBE_SUBSCRIPTION_URL,
16
+ SESSIONS_URL,
17
+ )
18
+ from chzzk.models.session import SessionAuthResponse, SessionInfo, SessionListResponse
19
+
20
+
21
+ class SessionService(BaseService):
22
+ """Synchronous Session API service.
23
+
24
+ Provides access to session-related API endpoints for WebSocket event subscriptions.
25
+ """
26
+
27
+ def create_session(self) -> SessionAuthResponse:
28
+ """Create a session for WebSocket connection (user authentication).
29
+
30
+ Creates a session URL using access token authentication.
31
+ Maximum 3 connections per user.
32
+
33
+ Returns:
34
+ SessionAuthResponse containing the WebSocket connection URL.
35
+
36
+ Raises:
37
+ InvalidTokenError: If the access token is invalid.
38
+ ChzzkAPIError: If the API request fails.
39
+ """
40
+ data = self._http.get(SESSIONS_AUTH_URL, headers=self._get_token_headers())
41
+ return SessionAuthResponse.model_validate(data)
42
+
43
+ def create_client_session(self) -> SessionAuthResponse:
44
+ """Create a session for WebSocket connection (client authentication).
45
+
46
+ Creates a session URL using client credentials authentication.
47
+ Maximum 10 connections per client.
48
+
49
+ Returns:
50
+ SessionAuthResponse containing the WebSocket connection URL.
51
+
52
+ Raises:
53
+ InvalidClientError: If the client credentials are invalid.
54
+ ChzzkAPIError: If the API request fails.
55
+ """
56
+ data = self._http.get(SESSIONS_AUTH_CLIENT_URL, headers=self._get_client_headers())
57
+ return SessionAuthResponse.model_validate(data)
58
+
59
+ def get_sessions(self, *, size: int = 20, page: int = 0) -> list[SessionInfo]:
60
+ """Get session list (user authentication).
61
+
62
+ Retrieves sessions created with access token authentication.
63
+
64
+ Args:
65
+ size: Number of sessions to retrieve (1-50). Default is 20.
66
+ page: Page number to retrieve (0-indexed). Default is 0.
67
+
68
+ Returns:
69
+ List of SessionInfo objects.
70
+
71
+ Raises:
72
+ InvalidTokenError: If the access token is invalid.
73
+ ChzzkAPIError: If the API request fails.
74
+ """
75
+ params = {"size": size, "page": page}
76
+ data = self._http.get(SESSIONS_URL, params=params, headers=self._get_token_headers())
77
+ response = SessionListResponse.model_validate(data)
78
+ return response.data
79
+
80
+ def get_client_sessions(self, *, size: int = 20, page: int = 0) -> list[SessionInfo]:
81
+ """Get session list (client authentication).
82
+
83
+ Retrieves sessions created with client credentials authentication.
84
+
85
+ Args:
86
+ size: Number of sessions to retrieve (1-50). Default is 20.
87
+ page: Page number to retrieve (0-indexed). Default is 0.
88
+
89
+ Returns:
90
+ List of SessionInfo objects.
91
+
92
+ Raises:
93
+ InvalidClientError: If the client credentials are invalid.
94
+ ChzzkAPIError: If the API request fails.
95
+ """
96
+ params = {"size": size, "page": page}
97
+ headers = self._get_client_headers()
98
+ data = self._http.get(SESSIONS_CLIENT_URL, params=params, headers=headers)
99
+ response = SessionListResponse.model_validate(data)
100
+ return response.data
101
+
102
+ def subscribe_chat(self, session_key: str) -> None:
103
+ """Subscribe to chat events for a session.
104
+
105
+ Maximum 30 events (chat, donation, subscription) per session.
106
+
107
+ Args:
108
+ session_key: The session key to subscribe events to.
109
+
110
+ Raises:
111
+ InvalidTokenError: If the access token is invalid.
112
+ ChzzkAPIError: If the API request fails.
113
+ """
114
+ self._http.post(
115
+ SESSIONS_SUBSCRIBE_CHAT_URL,
116
+ params={"sessionKey": session_key},
117
+ headers=self._get_token_headers(),
118
+ )
119
+
120
+ def subscribe_donation(self, session_key: str) -> None:
121
+ """Subscribe to donation events for a session.
122
+
123
+ Maximum 30 events (chat, donation, subscription) per session.
124
+
125
+ Args:
126
+ session_key: The session key to subscribe events to.
127
+
128
+ Raises:
129
+ InvalidTokenError: If the access token is invalid.
130
+ ChzzkAPIError: If the API request fails.
131
+ """
132
+ self._http.post(
133
+ SESSIONS_SUBSCRIBE_DONATION_URL,
134
+ params={"sessionKey": session_key},
135
+ headers=self._get_token_headers(),
136
+ )
137
+
138
+ def subscribe_subscription(self, session_key: str) -> None:
139
+ """Subscribe to subscription events for a session.
140
+
141
+ Maximum 30 events (chat, donation, subscription) per session.
142
+
143
+ Args:
144
+ session_key: The session key to subscribe events to.
145
+
146
+ Raises:
147
+ InvalidTokenError: If the access token is invalid.
148
+ ChzzkAPIError: If the API request fails.
149
+ """
150
+ self._http.post(
151
+ SESSIONS_SUBSCRIBE_SUBSCRIPTION_URL,
152
+ params={"sessionKey": session_key},
153
+ headers=self._get_token_headers(),
154
+ )
155
+
156
+ def unsubscribe_chat(self, session_key: str) -> None:
157
+ """Unsubscribe from chat events for a session.
158
+
159
+ Args:
160
+ session_key: The session key to unsubscribe events from.
161
+
162
+ Raises:
163
+ InvalidTokenError: If the access token is invalid.
164
+ ChzzkAPIError: If the API request fails.
165
+ """
166
+ self._http.post(
167
+ SESSIONS_UNSUBSCRIBE_CHAT_URL,
168
+ params={"sessionKey": session_key},
169
+ headers=self._get_token_headers(),
170
+ )
171
+
172
+ def unsubscribe_donation(self, session_key: str) -> None:
173
+ """Unsubscribe from donation events for a session.
174
+
175
+ Args:
176
+ session_key: The session key to unsubscribe events from.
177
+
178
+ Raises:
179
+ InvalidTokenError: If the access token is invalid.
180
+ ChzzkAPIError: If the API request fails.
181
+ """
182
+ self._http.post(
183
+ SESSIONS_UNSUBSCRIBE_DONATION_URL,
184
+ params={"sessionKey": session_key},
185
+ headers=self._get_token_headers(),
186
+ )
187
+
188
+ def unsubscribe_subscription(self, session_key: str) -> None:
189
+ """Unsubscribe from subscription events for a session.
190
+
191
+ Args:
192
+ session_key: The session key to unsubscribe events from.
193
+
194
+ Raises:
195
+ InvalidTokenError: If the access token is invalid.
196
+ ChzzkAPIError: If the API request fails.
197
+ """
198
+ self._http.post(
199
+ SESSIONS_UNSUBSCRIBE_SUBSCRIPTION_URL,
200
+ params={"sessionKey": session_key},
201
+ headers=self._get_token_headers(),
202
+ )
203
+
204
+
205
+ class AsyncSessionService(AsyncBaseService):
206
+ """Asynchronous Session API service.
207
+
208
+ Provides access to session-related API endpoints for WebSocket event subscriptions.
209
+ """
210
+
211
+ async def create_session(self) -> SessionAuthResponse:
212
+ """Create a session for WebSocket connection (user authentication).
213
+
214
+ Creates a session URL using access token authentication.
215
+ Maximum 3 connections per user.
216
+
217
+ Returns:
218
+ SessionAuthResponse containing the WebSocket connection URL.
219
+
220
+ Raises:
221
+ InvalidTokenError: If the access token is invalid.
222
+ ChzzkAPIError: If the API request fails.
223
+ """
224
+ data = await self._http.get(SESSIONS_AUTH_URL, headers=await self._get_token_headers())
225
+ return SessionAuthResponse.model_validate(data)
226
+
227
+ async def create_client_session(self) -> SessionAuthResponse:
228
+ """Create a session for WebSocket connection (client authentication).
229
+
230
+ Creates a session URL using client credentials authentication.
231
+ Maximum 10 connections per client.
232
+
233
+ Returns:
234
+ SessionAuthResponse containing the WebSocket connection URL.
235
+
236
+ Raises:
237
+ InvalidClientError: If the client credentials are invalid.
238
+ ChzzkAPIError: If the API request fails.
239
+ """
240
+ data = await self._http.get(SESSIONS_AUTH_CLIENT_URL, headers=self._get_client_headers())
241
+ return SessionAuthResponse.model_validate(data)
242
+
243
+ async def get_sessions(self, *, size: int = 20, page: int = 0) -> list[SessionInfo]:
244
+ """Get session list (user authentication).
245
+
246
+ Retrieves sessions created with access token authentication.
247
+
248
+ Args:
249
+ size: Number of sessions to retrieve (1-50). Default is 20.
250
+ page: Page number to retrieve (0-indexed). Default is 0.
251
+
252
+ Returns:
253
+ List of SessionInfo objects.
254
+
255
+ Raises:
256
+ InvalidTokenError: If the access token is invalid.
257
+ ChzzkAPIError: If the API request fails.
258
+ """
259
+ params = {"size": size, "page": page}
260
+ data = await self._http.get(
261
+ SESSIONS_URL, params=params, headers=await self._get_token_headers()
262
+ )
263
+ response = SessionListResponse.model_validate(data)
264
+ return response.data
265
+
266
+ async def get_client_sessions(self, *, size: int = 20, page: int = 0) -> list[SessionInfo]:
267
+ """Get session list (client authentication).
268
+
269
+ Retrieves sessions created with client credentials authentication.
270
+
271
+ Args:
272
+ size: Number of sessions to retrieve (1-50). Default is 20.
273
+ page: Page number to retrieve (0-indexed). Default is 0.
274
+
275
+ Returns:
276
+ List of SessionInfo objects.
277
+
278
+ Raises:
279
+ InvalidClientError: If the client credentials are invalid.
280
+ ChzzkAPIError: If the API request fails.
281
+ """
282
+ params = {"size": size, "page": page}
283
+ data = await self._http.get(
284
+ SESSIONS_CLIENT_URL, params=params, headers=self._get_client_headers()
285
+ )
286
+ response = SessionListResponse.model_validate(data)
287
+ return response.data
288
+
289
+ async def subscribe_chat(self, session_key: str) -> None:
290
+ """Subscribe to chat events for a session.
291
+
292
+ Maximum 30 events (chat, donation, subscription) per session.
293
+
294
+ Args:
295
+ session_key: The session key to subscribe events to.
296
+
297
+ Raises:
298
+ InvalidTokenError: If the access token is invalid.
299
+ ChzzkAPIError: If the API request fails.
300
+ """
301
+ await self._http.post(
302
+ SESSIONS_SUBSCRIBE_CHAT_URL,
303
+ params={"sessionKey": session_key},
304
+ headers=await self._get_token_headers(),
305
+ )
306
+
307
+ async def subscribe_donation(self, session_key: str) -> None:
308
+ """Subscribe to donation events for a session.
309
+
310
+ Maximum 30 events (chat, donation, subscription) per session.
311
+
312
+ Args:
313
+ session_key: The session key to subscribe events to.
314
+
315
+ Raises:
316
+ InvalidTokenError: If the access token is invalid.
317
+ ChzzkAPIError: If the API request fails.
318
+ """
319
+ await self._http.post(
320
+ SESSIONS_SUBSCRIBE_DONATION_URL,
321
+ params={"sessionKey": session_key},
322
+ headers=await self._get_token_headers(),
323
+ )
324
+
325
+ async def subscribe_subscription(self, session_key: str) -> None:
326
+ """Subscribe to subscription events for a session.
327
+
328
+ Maximum 30 events (chat, donation, subscription) per session.
329
+
330
+ Args:
331
+ session_key: The session key to subscribe events to.
332
+
333
+ Raises:
334
+ InvalidTokenError: If the access token is invalid.
335
+ ChzzkAPIError: If the API request fails.
336
+ """
337
+ await self._http.post(
338
+ SESSIONS_SUBSCRIBE_SUBSCRIPTION_URL,
339
+ params={"sessionKey": session_key},
340
+ headers=await self._get_token_headers(),
341
+ )
342
+
343
+ async def unsubscribe_chat(self, session_key: str) -> None:
344
+ """Unsubscribe from chat events for a session.
345
+
346
+ Args:
347
+ session_key: The session key to unsubscribe events from.
348
+
349
+ Raises:
350
+ InvalidTokenError: If the access token is invalid.
351
+ ChzzkAPIError: If the API request fails.
352
+ """
353
+ await self._http.post(
354
+ SESSIONS_UNSUBSCRIBE_CHAT_URL,
355
+ params={"sessionKey": session_key},
356
+ headers=await self._get_token_headers(),
357
+ )
358
+
359
+ async def unsubscribe_donation(self, session_key: str) -> None:
360
+ """Unsubscribe from donation events for a session.
361
+
362
+ Args:
363
+ session_key: The session key to unsubscribe events from.
364
+
365
+ Raises:
366
+ InvalidTokenError: If the access token is invalid.
367
+ ChzzkAPIError: If the API request fails.
368
+ """
369
+ await self._http.post(
370
+ SESSIONS_UNSUBSCRIBE_DONATION_URL,
371
+ params={"sessionKey": session_key},
372
+ headers=await self._get_token_headers(),
373
+ )
374
+
375
+ async def unsubscribe_subscription(self, session_key: str) -> None:
376
+ """Unsubscribe from subscription events for a session.
377
+
378
+ Args:
379
+ session_key: The session key to unsubscribe events from.
380
+
381
+ Raises:
382
+ InvalidTokenError: If the access token is invalid.
383
+ ChzzkAPIError: If the API request fails.
384
+ """
385
+ await self._http.post(
386
+ SESSIONS_UNSUBSCRIBE_SUBSCRIPTION_URL,
387
+ params={"sessionKey": session_key},
388
+ headers=await self._get_token_headers(),
389
+ )
chzzk/api/user.py ADDED
@@ -0,0 +1,47 @@
1
+ """User API service for Chzzk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from chzzk.api.base import AsyncBaseService, BaseService
6
+ from chzzk.http import USER_ME_URL
7
+ from chzzk.models.user import UserInfo
8
+
9
+
10
+ class UserService(BaseService):
11
+ """Synchronous User API service.
12
+
13
+ Provides access to user-related API endpoints.
14
+ """
15
+
16
+ def get_me(self) -> UserInfo:
17
+ """Get the current authenticated user's information.
18
+
19
+ Returns:
20
+ UserInfo containing the user's channel ID and name.
21
+
22
+ Raises:
23
+ InvalidTokenError: If the access token is invalid.
24
+ ChzzkAPIError: If the API request fails.
25
+ """
26
+ data = self._http.get(USER_ME_URL, headers=self._get_token_headers())
27
+ return UserInfo.model_validate(data)
28
+
29
+
30
+ class AsyncUserService(AsyncBaseService):
31
+ """Asynchronous User API service.
32
+
33
+ Provides access to user-related API endpoints.
34
+ """
35
+
36
+ async def get_me(self) -> UserInfo:
37
+ """Get the current authenticated user's information.
38
+
39
+ Returns:
40
+ UserInfo containing the user's channel ID and name.
41
+
42
+ Raises:
43
+ InvalidTokenError: If the access token is invalid.
44
+ ChzzkAPIError: If the API request fails.
45
+ """
46
+ data = await self._http.get(USER_ME_URL, headers=await self._get_token_headers())
47
+ return UserInfo.model_validate(data)
chzzk/auth/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """Authentication module for Chzzk SDK."""
2
+
3
+ from chzzk.auth.models import (
4
+ AuthorizationCodeRequest,
5
+ GrantType,
6
+ RefreshTokenRequest,
7
+ RevokeTokenRequest,
8
+ Token,
9
+ TokenResponse,
10
+ TokenTypeHint,
11
+ )
12
+ from chzzk.auth.oauth import (
13
+ AsyncChzzkOAuth,
14
+ ChzzkOAuth,
15
+ InMemoryTokenStorage,
16
+ TokenStorage,
17
+ )
18
+ from chzzk.auth.token import CallbackTokenStorage, FileTokenStorage
19
+
20
+ __all__ = [
21
+ "AsyncChzzkOAuth",
22
+ "AuthorizationCodeRequest",
23
+ "CallbackTokenStorage",
24
+ "ChzzkOAuth",
25
+ "FileTokenStorage",
26
+ "GrantType",
27
+ "InMemoryTokenStorage",
28
+ "RefreshTokenRequest",
29
+ "RevokeTokenRequest",
30
+ "Token",
31
+ "TokenResponse",
32
+ "TokenStorage",
33
+ "TokenTypeHint",
34
+ ]
chzzk/auth/models.py ADDED
@@ -0,0 +1,115 @@
1
+ """Pydantic models for Chzzk OAuth authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+ from enum import StrEnum
7
+ from typing import Self
8
+
9
+ from pydantic import BaseModel, Field, computed_field
10
+
11
+
12
+ class GrantType(StrEnum):
13
+ """OAuth grant type enumeration."""
14
+
15
+ AUTHORIZATION_CODE = "authorization_code"
16
+ REFRESH_TOKEN = "refresh_token"
17
+
18
+
19
+ class TokenTypeHint(StrEnum):
20
+ """Token type hint for revocation."""
21
+
22
+ ACCESS_TOKEN = "access_token"
23
+ REFRESH_TOKEN = "refresh_token"
24
+
25
+
26
+ class TokenResponse(BaseModel):
27
+ """Response model for token endpoint (camelCase from API)."""
28
+
29
+ access_token: str = Field(alias="accessToken")
30
+ refresh_token: str = Field(alias="refreshToken")
31
+ token_type: str = Field(alias="tokenType")
32
+ expires_in: int = Field(alias="expiresIn")
33
+ scope: str | None = None
34
+
35
+ model_config = {"populate_by_name": True}
36
+
37
+
38
+ class Token(BaseModel):
39
+ """Token model with expiration tracking.
40
+
41
+ This model stores the token data along with the timestamp when it was issued,
42
+ allowing for accurate expiration checking.
43
+ """
44
+
45
+ access_token: str
46
+ refresh_token: str
47
+ token_type: str = "Bearer"
48
+ expires_in: int
49
+ scope: str | None = None
50
+ issued_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
51
+
52
+ model_config = {"populate_by_name": True}
53
+
54
+ @computed_field
55
+ @property
56
+ def expires_at(self) -> datetime:
57
+ """Calculate the expiration time of the access token."""
58
+ return self.issued_at + timedelta(seconds=self.expires_in)
59
+
60
+ @property
61
+ def is_expired(self) -> bool:
62
+ """Check if the access token is expired.
63
+
64
+ Returns True if the token has expired or will expire within 60 seconds.
65
+ """
66
+ buffer = timedelta(seconds=60)
67
+ return datetime.now(UTC) >= (self.expires_at - buffer)
68
+
69
+ @classmethod
70
+ def from_response(cls, response: TokenResponse) -> Self:
71
+ """Create a Token instance from a TokenResponse."""
72
+ return cls(
73
+ access_token=response.access_token,
74
+ refresh_token=response.refresh_token,
75
+ token_type=response.token_type,
76
+ expires_in=response.expires_in,
77
+ scope=response.scope,
78
+ )
79
+
80
+
81
+ class AuthorizationCodeRequest(BaseModel):
82
+ """Request model for authorization code token exchange."""
83
+
84
+ grant_type: str = Field(default=GrantType.AUTHORIZATION_CODE, serialization_alias="grantType")
85
+ client_id: str = Field(serialization_alias="clientId")
86
+ client_secret: str = Field(serialization_alias="clientSecret")
87
+ code: str
88
+ state: str
89
+
90
+ model_config = {"populate_by_name": True}
91
+
92
+
93
+ class RefreshTokenRequest(BaseModel):
94
+ """Request model for token refresh."""
95
+
96
+ grant_type: str = Field(default=GrantType.REFRESH_TOKEN, serialization_alias="grantType")
97
+ client_id: str = Field(serialization_alias="clientId")
98
+ client_secret: str = Field(serialization_alias="clientSecret")
99
+ refresh_token: str = Field(serialization_alias="refreshToken")
100
+
101
+ model_config = {"populate_by_name": True}
102
+
103
+
104
+ class RevokeTokenRequest(BaseModel):
105
+ """Request model for token revocation."""
106
+
107
+ client_id: str = Field(serialization_alias="clientId")
108
+ client_secret: str = Field(serialization_alias="clientSecret")
109
+ token: str
110
+ token_type_hint: str = Field(
111
+ default=TokenTypeHint.ACCESS_TOKEN,
112
+ serialization_alias="tokenTypeHint",
113
+ )
114
+
115
+ model_config = {"populate_by_name": True}