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.
- pararamio_aio/__init__.py +78 -0
- pararamio_aio/_core/__init__.py +125 -0
- pararamio_aio/_core/_types.py +120 -0
- pararamio_aio/_core/base.py +143 -0
- pararamio_aio/_core/client_protocol.py +90 -0
- pararamio_aio/_core/constants/__init__.py +7 -0
- pararamio_aio/_core/constants/base.py +9 -0
- pararamio_aio/_core/constants/endpoints.py +84 -0
- pararamio_aio/_core/cookie_decorator.py +208 -0
- pararamio_aio/_core/cookie_manager.py +1222 -0
- pararamio_aio/_core/endpoints.py +67 -0
- pararamio_aio/_core/exceptions/__init__.py +6 -0
- pararamio_aio/_core/exceptions/auth.py +91 -0
- pararamio_aio/_core/exceptions/base.py +124 -0
- pararamio_aio/_core/models/__init__.py +17 -0
- pararamio_aio/_core/models/base.py +66 -0
- pararamio_aio/_core/models/chat.py +92 -0
- pararamio_aio/_core/models/post.py +65 -0
- pararamio_aio/_core/models/user.py +54 -0
- pararamio_aio/_core/py.typed +2 -0
- pararamio_aio/_core/utils/__init__.py +73 -0
- pararamio_aio/_core/utils/async_requests.py +417 -0
- pararamio_aio/_core/utils/auth_flow.py +202 -0
- pararamio_aio/_core/utils/authentication.py +235 -0
- pararamio_aio/_core/utils/captcha.py +92 -0
- pararamio_aio/_core/utils/helpers.py +336 -0
- pararamio_aio/_core/utils/http_client.py +199 -0
- pararamio_aio/_core/utils/requests.py +424 -0
- pararamio_aio/_core/validators.py +78 -0
- pararamio_aio/_types.py +29 -0
- pararamio_aio/client.py +989 -0
- pararamio_aio/constants/__init__.py +16 -0
- pararamio_aio/cookie_manager.py +15 -0
- pararamio_aio/exceptions/__init__.py +31 -0
- pararamio_aio/exceptions/base.py +1 -0
- pararamio_aio/file_operations.py +232 -0
- pararamio_aio/models/__init__.py +32 -0
- pararamio_aio/models/activity.py +127 -0
- pararamio_aio/models/attachment.py +141 -0
- pararamio_aio/models/base.py +83 -0
- pararamio_aio/models/bot.py +274 -0
- pararamio_aio/models/chat.py +722 -0
- pararamio_aio/models/deferred_post.py +174 -0
- pararamio_aio/models/file.py +103 -0
- pararamio_aio/models/group.py +361 -0
- pararamio_aio/models/poll.py +275 -0
- pararamio_aio/models/post.py +643 -0
- pararamio_aio/models/team.py +403 -0
- pararamio_aio/models/user.py +239 -0
- pararamio_aio/py.typed +2 -0
- pararamio_aio/utils/__init__.py +18 -0
- pararamio_aio/utils/authentication.py +383 -0
- pararamio_aio/utils/requests.py +75 -0
- pararamio_aio-2.1.1.dist-info/METADATA +269 -0
- pararamio_aio-2.1.1.dist-info/RECORD +57 -0
- pararamio_aio-2.1.1.dist-info/WHEEL +5 -0
- 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)
|