pararamio-aio 2.1.1__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.
Files changed (57) hide show
  1. pararamio_aio/__init__.py +78 -0
  2. pararamio_aio/_core/__init__.py +125 -0
  3. pararamio_aio/_core/_types.py +120 -0
  4. pararamio_aio/_core/base.py +143 -0
  5. pararamio_aio/_core/client_protocol.py +90 -0
  6. pararamio_aio/_core/constants/__init__.py +7 -0
  7. pararamio_aio/_core/constants/base.py +9 -0
  8. pararamio_aio/_core/constants/endpoints.py +84 -0
  9. pararamio_aio/_core/cookie_decorator.py +208 -0
  10. pararamio_aio/_core/cookie_manager.py +1222 -0
  11. pararamio_aio/_core/endpoints.py +67 -0
  12. pararamio_aio/_core/exceptions/__init__.py +6 -0
  13. pararamio_aio/_core/exceptions/auth.py +91 -0
  14. pararamio_aio/_core/exceptions/base.py +124 -0
  15. pararamio_aio/_core/models/__init__.py +17 -0
  16. pararamio_aio/_core/models/base.py +66 -0
  17. pararamio_aio/_core/models/chat.py +92 -0
  18. pararamio_aio/_core/models/post.py +65 -0
  19. pararamio_aio/_core/models/user.py +54 -0
  20. pararamio_aio/_core/py.typed +2 -0
  21. pararamio_aio/_core/utils/__init__.py +73 -0
  22. pararamio_aio/_core/utils/async_requests.py +417 -0
  23. pararamio_aio/_core/utils/auth_flow.py +202 -0
  24. pararamio_aio/_core/utils/authentication.py +235 -0
  25. pararamio_aio/_core/utils/captcha.py +92 -0
  26. pararamio_aio/_core/utils/helpers.py +336 -0
  27. pararamio_aio/_core/utils/http_client.py +199 -0
  28. pararamio_aio/_core/utils/requests.py +424 -0
  29. pararamio_aio/_core/validators.py +78 -0
  30. pararamio_aio/_types.py +29 -0
  31. pararamio_aio/client.py +989 -0
  32. pararamio_aio/constants/__init__.py +16 -0
  33. pararamio_aio/cookie_manager.py +15 -0
  34. pararamio_aio/exceptions/__init__.py +31 -0
  35. pararamio_aio/exceptions/base.py +1 -0
  36. pararamio_aio/file_operations.py +232 -0
  37. pararamio_aio/models/__init__.py +32 -0
  38. pararamio_aio/models/activity.py +127 -0
  39. pararamio_aio/models/attachment.py +141 -0
  40. pararamio_aio/models/base.py +83 -0
  41. pararamio_aio/models/bot.py +274 -0
  42. pararamio_aio/models/chat.py +722 -0
  43. pararamio_aio/models/deferred_post.py +174 -0
  44. pararamio_aio/models/file.py +103 -0
  45. pararamio_aio/models/group.py +361 -0
  46. pararamio_aio/models/poll.py +275 -0
  47. pararamio_aio/models/post.py +643 -0
  48. pararamio_aio/models/team.py +403 -0
  49. pararamio_aio/models/user.py +239 -0
  50. pararamio_aio/py.typed +2 -0
  51. pararamio_aio/utils/__init__.py +18 -0
  52. pararamio_aio/utils/authentication.py +383 -0
  53. pararamio_aio/utils/requests.py +75 -0
  54. pararamio_aio-2.1.1.dist-info/METADATA +269 -0
  55. pararamio_aio-2.1.1.dist-info/RECORD +57 -0
  56. pararamio_aio-2.1.1.dist-info/WHEEL +5 -0
  57. pararamio_aio-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,383 @@
1
+ """Async authentication utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ import aiohttp
9
+
10
+ # Import from core
11
+ from pararamio_aio._core import (
12
+ AUTH_INIT_URL,
13
+ AUTH_LOGIN_URL,
14
+ AUTH_NEXT_URL,
15
+ AUTH_TOTP_URL,
16
+ XSRF_HEADER_NAME,
17
+ AuthenticationFlow,
18
+ InvalidCredentialsException,
19
+ PararamioAuthenticationException,
20
+ PararamioHTTPRequestException,
21
+ PararamioSecondFactorAuthenticationException,
22
+ RateLimitException,
23
+ RateLimitHandler,
24
+ TwoFactorFailedException,
25
+ build_url,
26
+ generate_otp,
27
+ prepare_headers,
28
+ )
29
+
30
+ __all__ = (
31
+ "async_authenticate",
32
+ "get_async_xsrf_token",
33
+ "async_do_second_step",
34
+ "async_do_second_step_with_code",
35
+ )
36
+
37
+
38
+ async def get_async_xsrf_token(session: aiohttp.ClientSession) -> str:
39
+ """Get XSRF token from /auth/init endpoint.
40
+
41
+ Args:
42
+ session: aiohttp session
43
+
44
+ Returns:
45
+ XSRF token string
46
+
47
+ Raises:
48
+ PararamioAuthenticationException: If failed to get token
49
+ """
50
+ url = build_url(AUTH_INIT_URL)
51
+ async with session.get(url) as response:
52
+ if response.status == 200:
53
+ xsrf_token = response.headers.get("X-Xsrftoken")
54
+ if xsrf_token:
55
+ return xsrf_token
56
+
57
+ raise PararamioAuthenticationException("Failed to get XSRF token")
58
+
59
+
60
+ async def _make_auth_request(
61
+ session: aiohttp.ClientSession,
62
+ url: str,
63
+ method: str = "GET",
64
+ data: dict | None = None,
65
+ headers: dict | None = None,
66
+ rate_limit_handler: RateLimitHandler = None,
67
+ wait_auth_limit: bool = False,
68
+ ) -> tuple[bool, dict]:
69
+ """Make authentication request with error handling.
70
+
71
+ Returns:
72
+ Tuple of (success, response_data)
73
+ """
74
+ full_url = build_url(url)
75
+
76
+ try:
77
+ if method == "POST" and data:
78
+ async with session.post(full_url, json=data, headers=headers) as response:
79
+ if response.status == 429 and rate_limit_handler:
80
+ # Handle rate limiting
81
+ retry_after = rate_limit_handler.handle_rate_limit(dict(response.headers))
82
+ if wait_auth_limit:
83
+ await asyncio.sleep(retry_after)
84
+ # Retry the request after waiting
85
+ return await _make_auth_request(
86
+ session, url, method, data, headers, rate_limit_handler, wait_auth_limit
87
+ )
88
+ raise RateLimitException(
89
+ f"Rate limit exceeded. Retry after {retry_after} seconds",
90
+ retry_after=retry_after,
91
+ )
92
+
93
+ return await _handle_auth_response(response, full_url)
94
+ else: # GET
95
+ async with session.get(full_url, headers=headers) as response:
96
+ return await _handle_auth_response(response, full_url)
97
+
98
+ except aiohttp.ClientError as e:
99
+ raise PararamioAuthenticationException(f"Request failed: {e}") from e
100
+
101
+
102
+ async def _handle_auth_response(
103
+ response: aiohttp.ClientResponse,
104
+ full_url: str,
105
+ ) -> tuple[bool, dict[str, Any]]:
106
+ """Handle authentication response with common logic."""
107
+ if response.status == 200:
108
+ return True, await response.json()
109
+ if response.status < 500:
110
+ return False, await response.json()
111
+ raise PararamioHTTPRequestException(
112
+ full_url,
113
+ response.status,
114
+ f"HTTP {response.status}",
115
+ dict(response.headers),
116
+ await response.text(),
117
+ )
118
+
119
+
120
+ async def async_do_second_step(
121
+ session: aiohttp.ClientSession,
122
+ headers: dict[str, str],
123
+ key: str,
124
+ rate_limit_handler: RateLimitHandler,
125
+ wait_auth_limit: bool = False,
126
+ ) -> tuple[bool, dict[str, str]]:
127
+ """
128
+ Do second step authentication with TOTP key asynchronously.
129
+
130
+ Args:
131
+ session: aiohttp session
132
+ headers: Headers to send
133
+ key: TOTP key to generate one time code
134
+ rate_limit_handler: Rate limit handler instance
135
+ wait_auth_limit: Wait for rate limit instead of raising exception
136
+
137
+ Returns:
138
+ Tuple of (success, response_data)
139
+
140
+ Raises:
141
+ PararamioSecondFactorAuthenticationException: If 2FA fails
142
+ """
143
+ if not key:
144
+ raise PararamioSecondFactorAuthenticationException("key can not be empty")
145
+
146
+ try:
147
+ code = generate_otp(key)
148
+ except Exception as e:
149
+ raise PararamioSecondFactorAuthenticationException("Invalid second step key") from e
150
+
151
+ return await async_do_second_step_with_code(
152
+ session, headers, code, rate_limit_handler, wait_auth_limit
153
+ )
154
+
155
+
156
+ async def async_do_second_step_with_code(
157
+ session: aiohttp.ClientSession,
158
+ headers: dict[str, str],
159
+ code: str,
160
+ rate_limit_handler: RateLimitHandler,
161
+ wait_auth_limit: bool = False,
162
+ ) -> tuple[bool, dict[str, str]]:
163
+ """
164
+ Do second step authentication with TOTP code asynchronously.
165
+
166
+ Args:
167
+ session: aiohttp session
168
+ headers: Headers to send
169
+ code: 6 digits code
170
+ rate_limit_handler: Rate limit handler instance
171
+ wait_auth_limit: Wait for rate limit instead of raising exception
172
+
173
+ Returns:
174
+ Tuple of (success, response_data)
175
+
176
+ Raises:
177
+ PararamioSecondFactorAuthenticationException: If 2FA fails
178
+ """
179
+ if not code:
180
+ raise PararamioSecondFactorAuthenticationException("code can not be empty")
181
+ if len(code) != 6:
182
+ raise PararamioSecondFactorAuthenticationException("code must be 6 digits len")
183
+
184
+ totp_data = AuthenticationFlow.prepare_totp_data(code)
185
+ return await _make_auth_request(
186
+ session,
187
+ AUTH_TOTP_URL,
188
+ "POST",
189
+ totp_data,
190
+ headers,
191
+ rate_limit_handler,
192
+ wait_auth_limit,
193
+ )
194
+
195
+
196
+ async def async_authenticate(
197
+ session: aiohttp.ClientSession,
198
+ login: str,
199
+ password: str,
200
+ key: str | None = None,
201
+ wait_auth_limit: bool = False,
202
+ second_step_fn: Any = None,
203
+ second_step_arg: str | None = None,
204
+ ) -> tuple[bool, str]:
205
+ """Authenticate with Pararamio API asynchronously.
206
+
207
+ Follows the unified authentication flow from core.
208
+
209
+ Args:
210
+ session: aiohttp session
211
+ login: User login
212
+ password: User password
213
+ key: Authentication key (for automatic TOTP generation)
214
+ wait_auth_limit: Wait for rate limit instead of raising exception
215
+ second_step_fn: Optional async function for second step authentication
216
+ second_step_arg: Argument for second step function (key or code)
217
+
218
+ Returns:
219
+ Tuple of (success, xsrf_token)
220
+
221
+ Raises:
222
+ Various authentication exceptions
223
+ """
224
+ rate_limit_handler = RateLimitHandler()
225
+
226
+ # Check rate limiting
227
+ await _check_rate_limit(rate_limit_handler, wait_auth_limit)
228
+
229
+ try:
230
+ # Step 1: Get XSRF token and login
231
+ xsrf_token, headers = await _perform_login(
232
+ session, login, password, rate_limit_handler, wait_auth_limit
233
+ )
234
+
235
+ # Step 2: Handle second step authentication
236
+ await _handle_second_step(
237
+ session,
238
+ headers,
239
+ key,
240
+ second_step_fn,
241
+ second_step_arg,
242
+ rate_limit_handler,
243
+ wait_auth_limit,
244
+ )
245
+
246
+ # Step 3: Complete auth flow
247
+ await _complete_auth_flow(session, headers, rate_limit_handler, wait_auth_limit)
248
+
249
+ # Clear rate limit on success
250
+ rate_limit_handler.clear()
251
+
252
+ return True, xsrf_token
253
+
254
+ except RateLimitException:
255
+ # Let rate limit exceptions bubble up
256
+ raise
257
+ except Exception as e:
258
+ if not isinstance(e, (InvalidCredentialsException, TwoFactorFailedException)):
259
+ raise PararamioAuthenticationException(f"Authentication failed: {e}") from e
260
+ raise
261
+
262
+
263
+ async def _check_rate_limit(rate_limit_handler: RateLimitHandler, wait_auth_limit: bool) -> None:
264
+ """Check and handle rate limiting."""
265
+ should_wait, wait_seconds = rate_limit_handler.should_wait()
266
+ if should_wait:
267
+ if wait_auth_limit:
268
+ await asyncio.sleep(wait_seconds)
269
+ else:
270
+ raise RateLimitException(
271
+ f"Rate limit exceeded. Retry after {wait_seconds} seconds", retry_after=wait_seconds
272
+ )
273
+
274
+
275
+ async def _perform_login(
276
+ session: aiohttp.ClientSession,
277
+ login: str,
278
+ password: str,
279
+ rate_limit_handler: RateLimitHandler,
280
+ wait_auth_limit: bool,
281
+ ) -> tuple[str, dict[str, str]]:
282
+ """Perform login and handle XSRF retry if needed."""
283
+ xsrf_token = await get_async_xsrf_token(session)
284
+ headers = prepare_headers(xsrf_token=xsrf_token)
285
+
286
+ login_data = AuthenticationFlow.prepare_login_data(login, password)
287
+
288
+ success, resp = await _make_auth_request(
289
+ session,
290
+ AUTH_LOGIN_URL,
291
+ "POST",
292
+ login_data,
293
+ headers,
294
+ rate_limit_handler,
295
+ wait_auth_limit,
296
+ )
297
+
298
+ if not success:
299
+ # Parse error and check if we need new XSRF
300
+ error_type, error_msg = AuthenticationFlow.parse_error_response(resp)
301
+
302
+ if AuthenticationFlow.should_retry_with_new_xsrf(error_type, error_msg):
303
+ # Retry with new XSRF token
304
+ xsrf_token = await get_async_xsrf_token(session)
305
+ headers[XSRF_HEADER_NAME] = xsrf_token
306
+
307
+ success, _ = await _make_auth_request(
308
+ session,
309
+ AUTH_LOGIN_URL,
310
+ "POST",
311
+ login_data,
312
+ headers,
313
+ rate_limit_handler,
314
+ wait_auth_limit,
315
+ )
316
+
317
+ if not success:
318
+ raise InvalidCredentialsException("Login failed after XSRF retry")
319
+ else:
320
+ raise InvalidCredentialsException(f"Login failed: {error_msg}")
321
+
322
+ return xsrf_token, headers
323
+
324
+
325
+ async def _handle_second_step(
326
+ session: aiohttp.ClientSession,
327
+ headers: dict[str, str],
328
+ key: str | None,
329
+ second_step_fn: Any,
330
+ second_step_arg: str | None,
331
+ rate_limit_handler: RateLimitHandler,
332
+ wait_auth_limit: bool,
333
+ ) -> None:
334
+ """Handle second step authentication (TOTP or custom)."""
335
+ if second_step_fn is not None and second_step_arg:
336
+ # Use provided second step function
337
+ success, _ = await second_step_fn(
338
+ session, headers, second_step_arg, rate_limit_handler, wait_auth_limit
339
+ )
340
+ if not success:
341
+ raise TwoFactorFailedException("Second factor authentication failed")
342
+ elif key:
343
+ # Use default TOTP with key
344
+ try:
345
+ code = generate_otp(key)
346
+ except Exception as e:
347
+ raise TwoFactorFailedException("Invalid TOTP key") from e
348
+
349
+ totp_data = AuthenticationFlow.prepare_totp_data(code)
350
+ success, _ = await _make_auth_request(
351
+ session,
352
+ AUTH_TOTP_URL,
353
+ "POST",
354
+ totp_data,
355
+ headers,
356
+ rate_limit_handler,
357
+ wait_auth_limit,
358
+ )
359
+
360
+ if not success:
361
+ raise TwoFactorFailedException("TOTP authentication failed")
362
+
363
+
364
+ async def _complete_auth_flow(
365
+ session: aiohttp.ClientSession,
366
+ headers: dict[str, str],
367
+ rate_limit_handler: RateLimitHandler,
368
+ wait_auth_limit: bool,
369
+ ) -> None:
370
+ """Complete the authentication flow."""
371
+ success, _ = await _make_auth_request(
372
+ session, AUTH_NEXT_URL, "GET", {}, headers, rate_limit_handler, wait_auth_limit
373
+ )
374
+
375
+ if not success:
376
+ raise PararamioAuthenticationException("Failed to complete auth flow")
377
+
378
+ success, _ = await _make_auth_request(
379
+ session, AUTH_INIT_URL, "GET", {}, headers, rate_limit_handler, wait_auth_limit
380
+ )
381
+
382
+ if not success:
383
+ raise PararamioAuthenticationException("Failed to initialize session")
@@ -0,0 +1,75 @@
1
+ """Async request utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import aiohttp
8
+
9
+ # Import from core
10
+ from pararamio_aio._core import PararamioHTTPRequestException, build_url
11
+
12
+ # Define BOT_KEY_PARAM locally if not in core
13
+ BOT_KEY_PARAM = "key"
14
+
15
+ __all__ = ("async_api_request", "async_bot_request")
16
+
17
+
18
+ async def async_api_request(
19
+ session: aiohttp.ClientSession,
20
+ url: str,
21
+ method: str = "GET",
22
+ data: dict | None = None,
23
+ headers: dict | None = None,
24
+ ) -> dict:
25
+ """Make async API request.
26
+
27
+ Args:
28
+ session: aiohttp session
29
+ url: API endpoint URL
30
+ method: HTTP method
31
+ data: Request data
32
+ headers: Request headers
33
+
34
+ Returns:
35
+ JSON response as dict
36
+ """
37
+ full_url = build_url(url)
38
+
39
+ async with session.request(method, full_url, data=data, headers=headers or {}) as response:
40
+ if response.status != 200:
41
+ raise PararamioHTTPRequestException(
42
+ full_url,
43
+ response.status,
44
+ f"HTTP {response.status}",
45
+ dict(response.headers),
46
+ await response.text(),
47
+ )
48
+ return await response.json()
49
+
50
+
51
+ async def async_bot_request(
52
+ url: str,
53
+ key: str,
54
+ method: str = "GET",
55
+ data: dict | None = None,
56
+ headers: dict | None = None,
57
+ ) -> dict[str, Any]:
58
+ """Make an authenticated bot API request.
59
+
60
+ Args:
61
+ url: API endpoint
62
+ key: Bot API key
63
+ method: HTTP method
64
+ data: Request data
65
+ headers: Additional headers
66
+
67
+ Returns:
68
+ Response data as dictionary
69
+ """
70
+ # Add bot key to URL
71
+ separator = "&" if "?" in url else "?"
72
+ full_url = f"{url}{separator}{BOT_KEY_PARAM}={key}"
73
+
74
+ async with aiohttp.ClientSession() as session:
75
+ return await async_api_request(session, full_url, method, data, headers)