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,174 @@
1
+ """Async DeferredPost model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ # Imports from core
9
+ from pararamio_aio._core import PararamNotFound
10
+ from pararamio_aio._core.utils.helpers import format_datetime
11
+
12
+ from .base import BaseModel
13
+
14
+ if TYPE_CHECKING:
15
+ from ..client import AsyncPararamio
16
+
17
+ __all__ = ("DeferredPost",)
18
+
19
+
20
+ class DeferredPost(BaseModel):
21
+ """Async DeferredPost model for scheduled posts."""
22
+
23
+ _data: dict[str, Any] # Type hint for mypy
24
+
25
+ def __init__(self, client: AsyncPararamio, id: int, **kwargs):
26
+ """Initialize async deferred post.
27
+
28
+ Args:
29
+ client: AsyncPararamio client
30
+ id: Deferred post ID
31
+ **kwargs: Additional post data
32
+ """
33
+ super().__init__(client, id=id, **kwargs)
34
+ self.id = id
35
+
36
+ @property
37
+ def user_id(self) -> int:
38
+ """Get post author user ID."""
39
+ return self._data.get("user_id", 0)
40
+
41
+ @property
42
+ def chat_id(self) -> int:
43
+ """Get target chat ID."""
44
+ return self._data.get("chat_id", 0)
45
+
46
+ @property
47
+ def text(self) -> str:
48
+ """Get post text."""
49
+ # Check both top-level and nested data
50
+ text = self._data.get("text")
51
+ if text is None and "data" in self._data:
52
+ text = self._data["data"].get("text")
53
+ return text or ""
54
+
55
+ @property
56
+ def reply_no(self) -> int | None:
57
+ """Get reply post number if any."""
58
+ reply_no = self._data.get("reply_no")
59
+ if reply_no is None and "data" in self._data:
60
+ reply_no = self._data["data"].get("reply_no")
61
+ return reply_no
62
+
63
+ @property
64
+ def time_created(self) -> datetime | None:
65
+ """Get creation time."""
66
+ return self._data.get("time_created")
67
+
68
+ @property
69
+ def time_sending(self) -> datetime | None:
70
+ """Get scheduled sending time."""
71
+ return self._data.get("time_sending")
72
+
73
+ @property
74
+ def data(self) -> dict[str, Any]:
75
+ """Get additional post data."""
76
+ return self._data.get("data", {})
77
+
78
+ async def load(self) -> DeferredPost:
79
+ """Load full deferred post data from API.
80
+
81
+ Returns:
82
+ Self with updated data
83
+
84
+ Raises:
85
+ PararamNotFound: If post not found
86
+ """
87
+ posts = await self.get_deferred_posts(self.client)
88
+
89
+ for post in posts:
90
+ if post.id == self.id:
91
+ self._data = post._data
92
+ return self
93
+
94
+ raise PararamNotFound(f"Deferred post with id {self.id} not found")
95
+
96
+ async def delete(self) -> bool:
97
+ """Delete this deferred post.
98
+
99
+ Returns:
100
+ True if successful
101
+ """
102
+ url = f"/msg/deferred/{self.id}"
103
+ await self.client.api_delete(url)
104
+ return True
105
+
106
+ @classmethod
107
+ async def create(
108
+ cls,
109
+ client: AsyncPararamio,
110
+ chat_id: int,
111
+ text: str,
112
+ *,
113
+ time_sending: datetime,
114
+ reply_no: int | None = None,
115
+ quote_range: tuple[int, int] | None = None,
116
+ ) -> DeferredPost:
117
+ """Create a new deferred (scheduled) post.
118
+
119
+ Args:
120
+ client: AsyncPararamio client
121
+ chat_id: Target chat ID
122
+ text: Post text
123
+ time_sending: When to send the post
124
+ reply_no: Optional post number to reply to
125
+ quote_range: Optional quote range as (start, end) tuple
126
+
127
+ Returns:
128
+ Created DeferredPost object
129
+ """
130
+ url = "/msg/deferred"
131
+ data = {
132
+ "chat_id": chat_id,
133
+ "text": text,
134
+ "time_sending": format_datetime(time_sending),
135
+ "reply_no": reply_no,
136
+ "quote_range": quote_range,
137
+ }
138
+
139
+ response = await client.api_post(url, data)
140
+
141
+ return cls(
142
+ client,
143
+ id=int(response["deferred_post_id"]),
144
+ chat_id=chat_id,
145
+ data=data,
146
+ time_sending=time_sending,
147
+ **response,
148
+ )
149
+
150
+ @classmethod
151
+ async def get_deferred_posts(cls, client: AsyncPararamio) -> list[DeferredPost]:
152
+ """Get all deferred posts for the current user.
153
+
154
+ Args:
155
+ client: AsyncPararamio client
156
+
157
+ Returns:
158
+ List of DeferredPost objects
159
+ """
160
+ url = "/msg/deferred"
161
+ response = await client.api_get(url)
162
+ posts_data = response.get("posts", [])
163
+
164
+ return [cls(client, **post_data) for post_data in posts_data]
165
+
166
+ def __str__(self) -> str:
167
+ """String representation."""
168
+ return self.text or f"DeferredPost({self.id})"
169
+
170
+ def __eq__(self, other) -> bool:
171
+ """Check equality."""
172
+ if not isinstance(other, DeferredPost):
173
+ return False
174
+ return self.id == other.id
@@ -0,0 +1,103 @@
1
+ """Async File model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+ from urllib.parse import quote
7
+
8
+ from .base import BaseModel
9
+
10
+ if TYPE_CHECKING:
11
+ from ..client import AsyncPararamio
12
+
13
+ __all__ = ("File",)
14
+
15
+
16
+ class File(BaseModel):
17
+ """Async File model with explicit loading."""
18
+
19
+ def __init__(self, client: AsyncPararamio, guid: str, name: str | None = None, **kwargs):
20
+ """Initialize async file.
21
+
22
+ Args:
23
+ client: AsyncPararamio client
24
+ guid: File GUID
25
+ name: Optional file name
26
+ **kwargs: Additional file data
27
+ """
28
+ super().__init__(client, guid=guid, name=name, **kwargs)
29
+ self.guid = guid
30
+
31
+ @property
32
+ def name(self) -> str | None:
33
+ """Get file name."""
34
+ return self._data.get("name") or self._data.get("filename")
35
+
36
+ @property
37
+ def mime_type(self) -> str | None:
38
+ """Get file MIME type."""
39
+ return self._data.get("mime_type") or self._data.get("type")
40
+
41
+ @property
42
+ def size(self) -> int | None:
43
+ """Get file size in bytes."""
44
+ return self._data.get("size")
45
+
46
+ @property
47
+ def chat_id(self) -> int | None:
48
+ """Get associated chat ID."""
49
+ return self._data.get("chat_id")
50
+
51
+ @property
52
+ def organization_id(self) -> int | None:
53
+ """Get associated organization ID."""
54
+ return self._data.get("organization_id")
55
+
56
+ @property
57
+ def reply_no(self) -> int | None:
58
+ """Get associated reply number."""
59
+ return self._data.get("reply_no")
60
+
61
+ async def download(self) -> bytes:
62
+ """Download file content.
63
+
64
+ Returns:
65
+ File content as bytes
66
+ """
67
+ if not self.name:
68
+ raise ValueError("File name is required for download")
69
+
70
+ # Use the client's download_file method
71
+ bio = await self.client.download_file(self.guid, self.name)
72
+ return bio.read()
73
+
74
+ async def delete(self) -> bool:
75
+ """Delete this file.
76
+
77
+ Returns:
78
+ True if successful
79
+ """
80
+ try:
81
+ result = await self.client.delete_file(self.guid)
82
+ return result.get("success", True)
83
+ except (AttributeError, KeyError, TypeError, ValueError):
84
+ return False
85
+
86
+ def get_download_url(self) -> str:
87
+ """Get file download URL.
88
+
89
+ Returns:
90
+ Download URL string
91
+ """
92
+ if not self.name:
93
+ raise ValueError("File name is required for download URL")
94
+
95
+ return f"https://file.pararam.io/download/{self.guid}/{quote(self.name)}"
96
+
97
+ def __str__(self) -> str:
98
+ """String representation."""
99
+ return self.name or f"File({self.guid})"
100
+
101
+ def __repr__(self) -> str:
102
+ """Detailed representation."""
103
+ return f"<File(guid={self.guid}, name={self.name}, size={self.size})>"
@@ -0,0 +1,361 @@
1
+ """Async Group model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ # Imports from core
9
+ from pararamio_aio._core import (
10
+ PararamioRequestException,
11
+ PararamioValidationException,
12
+ PararamNotFound,
13
+ )
14
+ from pararamio_aio._core.utils.helpers import join_ids
15
+
16
+ from .base import BaseModel
17
+
18
+ if TYPE_CHECKING:
19
+ from ..client import AsyncPararamio
20
+
21
+ __all__ = ("Group",)
22
+
23
+
24
+ class Group(BaseModel):
25
+ """Async Group model with explicit loading."""
26
+
27
+ def __init__(self, client: AsyncPararamio, id: int, name: str | None = None, **kwargs):
28
+ """Initialize async group.
29
+
30
+ Args:
31
+ client: AsyncPararamio client
32
+ id: Group ID
33
+ name: Optional group name
34
+ **kwargs: Additional group data
35
+ """
36
+ super().__init__(client, id=id, name=name, **kwargs)
37
+ self.id = id
38
+
39
+ @property
40
+ def name(self) -> str | None:
41
+ """Get group name."""
42
+ return self._data.get("name")
43
+
44
+ @property
45
+ def unique_name(self) -> str | None:
46
+ """Get group unique name."""
47
+ return self._data.get("unique_name")
48
+
49
+ @property
50
+ def description(self) -> str | None:
51
+ """Get group description."""
52
+ return self._data.get("description")
53
+
54
+ @property
55
+ def email_domain(self) -> str | None:
56
+ """Get group email domain."""
57
+ return self._data.get("email_domain")
58
+
59
+ @property
60
+ def time_created(self) -> datetime | None:
61
+ """Get group creation time."""
62
+ return self._data.get("time_created")
63
+
64
+ @property
65
+ def time_updated(self) -> datetime | None:
66
+ """Get group last update time."""
67
+ return self._data.get("time_updated")
68
+
69
+ @property
70
+ def is_active(self) -> bool:
71
+ """Check if group is active."""
72
+ return self._data.get("active", True)
73
+
74
+ @property
75
+ def members_count(self) -> int:
76
+ """Get group members count."""
77
+ return self._data.get("members_count", 0)
78
+
79
+ async def load(self) -> Group:
80
+ """Load full group data from API.
81
+
82
+ Returns:
83
+ Self with updated data
84
+ """
85
+ groups = await self.client.get_groups_by_ids([self.id])
86
+ if not groups:
87
+ raise PararamNotFound(f"Group {self.id} not found")
88
+
89
+ # Update our data with loaded data
90
+ self._data.update(groups[0]._data)
91
+ return self
92
+
93
+ async def get_members(self) -> list[int]:
94
+ """Get group member user IDs.
95
+
96
+ Returns:
97
+ List of user IDs
98
+ """
99
+ url = f"/group/{self.id}/members"
100
+ response = await self.client.api_get(url)
101
+ return response.get("members", [])
102
+
103
+ async def add_members(self, user_ids: list[int]) -> bool:
104
+ """Add members to group.
105
+
106
+ Args:
107
+ user_ids: List of user IDs to add
108
+
109
+ Returns:
110
+ True if successful
111
+ """
112
+ url = f"/group/{self.id}/members"
113
+ data = {"user_ids": user_ids}
114
+ response = await self.client.api_post(url, data)
115
+ return response.get("success", False)
116
+
117
+ async def add_member(self, user_id: int, reload: bool = True) -> None:
118
+ """Add a single member to group.
119
+
120
+ Args:
121
+ user_id: User ID to add
122
+ reload: Whether to reload group data after operation
123
+
124
+ Raises:
125
+ PararamioRequestException: If operation fails
126
+ """
127
+ url = f"/core/group/{self.id}/users/{user_id}"
128
+ response = await self.client.api_post(url)
129
+
130
+ if response.get("result") == "OK":
131
+ # Update local cache if we have users data
132
+ if "users" in self._data:
133
+ if user_id not in self._data["users"]:
134
+ self._data["users"].append(user_id)
135
+
136
+ if reload:
137
+ await self.load()
138
+ else:
139
+ raise PararamioRequestException(f"Failed to add user {user_id} to group {self.id}")
140
+
141
+ async def remove_members(self, user_ids: list[int]) -> bool:
142
+ """Remove members from group.
143
+
144
+ Args:
145
+ user_ids: List of user IDs to remove
146
+
147
+ Returns:
148
+ True if successful
149
+ """
150
+ # Use DELETE with query parameters instead of request body
151
+ url = f"/group/{self.id}/members?user_ids={join_ids(user_ids)}"
152
+ response = await self.client.api_delete(url)
153
+ return response.get("success", False)
154
+
155
+ async def remove_member(self, user_id: int, reload: bool = True) -> None:
156
+ """Remove a single member from group.
157
+
158
+ Args:
159
+ user_id: User ID to remove
160
+ reload: Whether to reload group data after operation
161
+
162
+ Raises:
163
+ PararamioRequestException: If operation fails
164
+ """
165
+ url = f"/core/group/{self.id}/users/{user_id}"
166
+ response = await self.client.api_delete(url)
167
+
168
+ if response.get("result") == "OK":
169
+ # Update local cache if we have users data
170
+ if "users" in self._data and user_id in self._data["users"]:
171
+ self._data["users"].remove(user_id)
172
+
173
+ # Also remove from admins if present
174
+ if "admins" in self._data and user_id in self._data["admins"]:
175
+ self._data["admins"].remove(user_id)
176
+
177
+ if reload:
178
+ await self.load()
179
+ else:
180
+ raise PararamioRequestException(f"Failed to remove user {user_id} from group {self.id}")
181
+
182
+ async def add_admins(self, admin_ids: list[int]) -> bool:
183
+ """Add admin users to the group.
184
+
185
+ Args:
186
+ admin_ids: List of user IDs to make admins
187
+
188
+ Returns:
189
+ True if successful
190
+ """
191
+ url = f"/core/group/{self.id}/admins/{join_ids(admin_ids)}"
192
+ response = await self.client.api_post(url)
193
+ return response.get("result") == "OK"
194
+
195
+ async def update_settings(self, **kwargs) -> bool:
196
+ """Update group settings.
197
+
198
+ Args:
199
+ **kwargs: Settings to update (name, description, etc.)
200
+
201
+ Returns:
202
+ True if successful
203
+ """
204
+ # Filter allowed fields
205
+ allowed_fields = {"unique_name", "name", "description", "email_domain"}
206
+ data = {k: v for k, v in kwargs.items() if k in allowed_fields}
207
+
208
+ if not data:
209
+ return False
210
+
211
+ url = f"/group/{self.id}"
212
+ response = await self.client.api_put(url, data)
213
+
214
+ # Update local data
215
+ if response.get("success"):
216
+ self._data.update(data)
217
+ return True
218
+
219
+ return False
220
+
221
+ async def edit(self, changes: dict[str, str | None], reload: bool = True) -> None:
222
+ """Edit group settings.
223
+
224
+ Args:
225
+ changes: Dictionary of fields to change
226
+ reload: Whether to reload group data after operation
227
+
228
+ Raises:
229
+ PararamioValidationException: If invalid fields provided
230
+ """
231
+ # Define editable fields
232
+ editable_fields = ["unique_name", "name", "description", "email_domain"]
233
+
234
+ # Validate fields
235
+ invalid_fields = set(changes.keys()) - set(editable_fields)
236
+ if invalid_fields:
237
+ raise PararamioValidationException(
238
+ f"Invalid fields: {invalid_fields}. Valid fields are: {editable_fields}"
239
+ )
240
+
241
+ # Ensure we have current data
242
+ if not self._data.get("name"):
243
+ await self.load()
244
+
245
+ url = f"/core/group/{self.id}"
246
+ response = await self.client.api_put(url, changes)
247
+
248
+ if response.get("result") == "OK":
249
+ # Update local data
250
+ self._data.update(changes)
251
+
252
+ if reload:
253
+ await self.load()
254
+
255
+ async def delete(self) -> bool:
256
+ """Delete this group.
257
+
258
+ Returns:
259
+ True if successful
260
+ """
261
+ url = f"/group/{self.id}"
262
+ response = await self.client.api_delete(url)
263
+ return response.get("success", False)
264
+
265
+ # @classmethod
266
+ # async def search(cls, client: AsyncPararamio, query: str) -> list[Group]:
267
+ # """Search for groups.
268
+ #
269
+ # DEPRECATED: Group search is not available in the API.
270
+ # Groups might be returned as part of user search results.
271
+ #
272
+ # Args:
273
+ # client: AsyncPararamio client
274
+ # query: Search query
275
+ #
276
+ # Returns:
277
+ # List of found groups
278
+ # """
279
+ # from urllib.parse import quote
280
+ # # Use the same endpoint as user search (they seem to be combined)
281
+ # url = f"/user/search?flt={quote(query)}"
282
+ # response = await client.api_get(url)
283
+ #
284
+ # groups = []
285
+ # for group_data in response.get("groups", []):
286
+ # group = cls.from_dict(client, group_data)
287
+ # groups.append(group)
288
+ #
289
+ # return groups
290
+
291
+ @classmethod
292
+ async def create(
293
+ cls,
294
+ client: AsyncPararamio,
295
+ name: str,
296
+ unique_name: str,
297
+ description: str = "",
298
+ email_domain: str | None = None,
299
+ ) -> Group:
300
+ """Create a new group.
301
+
302
+ Args:
303
+ client: AsyncPararamio client
304
+ name: Group display name
305
+ unique_name: Group unique identifier
306
+ description: Group description
307
+ email_domain: Optional email domain
308
+
309
+ Returns:
310
+ Created group object
311
+ """
312
+ data = {
313
+ "name": name,
314
+ "unique_name": unique_name,
315
+ "description": description,
316
+ }
317
+
318
+ if email_domain:
319
+ data["email_domain"] = email_domain
320
+
321
+ response = await client.api_post("/group", data)
322
+ group_id = response["group_id"]
323
+
324
+ group = await client.get_group_by_id(group_id)
325
+ if group is None:
326
+ raise ValueError(f"Failed to retrieve created group with id {group_id}")
327
+ return group
328
+
329
+ def __eq__(self, other) -> bool:
330
+ """Check equality with another group."""
331
+ if not isinstance(other, Group):
332
+ return False
333
+ return self.id == other.id
334
+
335
+ def __str__(self) -> str:
336
+ """String representation."""
337
+ return self.name or f"Group({self.id})"
338
+
339
+ @classmethod
340
+ async def search(cls, client: AsyncPararamio, search_string: str) -> list[Group]:
341
+ """Search for groups.
342
+
343
+ Note: This uses the user search endpoint which also returns groups.
344
+
345
+ Args:
346
+ client: AsyncPararamio client
347
+ search_string: Search query
348
+
349
+ Returns:
350
+ List of matching groups
351
+ """
352
+ from urllib.parse import quote # pylint: disable=import-outside-toplevel
353
+
354
+ # Use the same endpoint as user search (they seem to be combined)
355
+ url = f"/user/search?flt={quote(search_string)}&self=false"
356
+ response = await client.api_get(url)
357
+ groups = []
358
+ for group_data in response.get("groups", []):
359
+ group = cls.from_dict(client, group_data)
360
+ groups.append(group)
361
+ return groups