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,16 @@
|
|
1
|
+
"""Constants for pararamio_aio package."""
|
2
|
+
|
3
|
+
# Import all constants from core
|
4
|
+
from pararamio_aio._core.constants import (
|
5
|
+
BASE_API_URL,
|
6
|
+
POSTS_LIMIT,
|
7
|
+
REQUEST_TIMEOUT,
|
8
|
+
XSRF_HEADER_NAME,
|
9
|
+
)
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"BASE_API_URL",
|
13
|
+
"POSTS_LIMIT",
|
14
|
+
"REQUEST_TIMEOUT",
|
15
|
+
"XSRF_HEADER_NAME",
|
16
|
+
]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""Cookie managers for asynchronous Pararamio client."""
|
2
|
+
|
3
|
+
from pararamio_aio._core.cookie_manager import (
|
4
|
+
AsyncCookieManager,
|
5
|
+
AsyncFileCookieManager,
|
6
|
+
AsyncInMemoryCookieManager,
|
7
|
+
AsyncRedisCookieManager,
|
8
|
+
)
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"AsyncCookieManager",
|
12
|
+
"AsyncFileCookieManager",
|
13
|
+
"AsyncInMemoryCookieManager",
|
14
|
+
"AsyncRedisCookieManager",
|
15
|
+
]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Exceptions for pararamio_aio package."""
|
2
|
+
|
3
|
+
from pararamio_aio._core.exceptions import (
|
4
|
+
PararamioAuthenticationException,
|
5
|
+
PararamioCaptchaAuthenticationException,
|
6
|
+
PararamioException,
|
7
|
+
PararamioHTTPRequestException,
|
8
|
+
PararamioPasswordAuthenticationException,
|
9
|
+
PararamioRequestException,
|
10
|
+
PararamioSecondFactorAuthenticationException,
|
11
|
+
PararamioValidationException,
|
12
|
+
PararamioXSFRRequestError,
|
13
|
+
PararamModelNotLoaded,
|
14
|
+
PararamNoNextPost,
|
15
|
+
PararamNoPrevPost,
|
16
|
+
)
|
17
|
+
|
18
|
+
__all__ = [
|
19
|
+
"PararamioException",
|
20
|
+
"PararamioAuthenticationException",
|
21
|
+
"PararamioCaptchaAuthenticationException",
|
22
|
+
"PararamioPasswordAuthenticationException",
|
23
|
+
"PararamioSecondFactorAuthenticationException",
|
24
|
+
"PararamioXSFRRequestError",
|
25
|
+
"PararamioHTTPRequestException",
|
26
|
+
"PararamioRequestException",
|
27
|
+
"PararamioValidationException",
|
28
|
+
"PararamModelNotLoaded",
|
29
|
+
"PararamNoPrevPost",
|
30
|
+
"PararamNoNextPost",
|
31
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Base exceptions for pararamio_aio package."""
|
@@ -0,0 +1,232 @@
|
|
1
|
+
"""Async file operations for Pararamio client."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import mimetypes
|
6
|
+
import os
|
7
|
+
from io import BytesIO
|
8
|
+
from typing import TYPE_CHECKING, BinaryIO
|
9
|
+
from urllib.parse import quote
|
10
|
+
|
11
|
+
import aiohttp
|
12
|
+
from pararamio_aio._core import (
|
13
|
+
REQUEST_TIMEOUT,
|
14
|
+
UPLOAD_TIMEOUT,
|
15
|
+
PararamioHTTPRequestException,
|
16
|
+
)
|
17
|
+
|
18
|
+
# File upload base URL
|
19
|
+
FILE_UPLOAD_URL = "https://file.pararam.io"
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
pass
|
23
|
+
|
24
|
+
__all__ = (
|
25
|
+
"async_xupload_file",
|
26
|
+
"async_delete_file",
|
27
|
+
"async_download_file",
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
async def multipart_encode_async(
|
32
|
+
fd: BinaryIO | BytesIO,
|
33
|
+
*,
|
34
|
+
fields: list[tuple[str, str | None | int]] | None = None,
|
35
|
+
boundary: str | None = None,
|
36
|
+
form_field_name: str = "data",
|
37
|
+
filename: str | None = None,
|
38
|
+
content_type: str | None = None,
|
39
|
+
) -> bytes:
|
40
|
+
"""
|
41
|
+
Async version of multipart encoding for file uploads.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
fd: A file-like object opened in binary mode that is to be included in the payload.
|
45
|
+
fields: An optional list of tuples representing additional form fields.
|
46
|
+
boundary: An optional string used to separate parts of the multipart message.
|
47
|
+
form_field_name: The name of the form field for the file being uploaded.
|
48
|
+
filename: An optional string representing the filename for the file being uploaded.
|
49
|
+
content_type: An optional string representing the content type of the file.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
A bytes object representing the encoded multipart/form-data payload.
|
53
|
+
"""
|
54
|
+
if fields is None:
|
55
|
+
fields = []
|
56
|
+
if boundary is None:
|
57
|
+
boundary = "FORM-BOUNDARY"
|
58
|
+
|
59
|
+
body = BytesIO()
|
60
|
+
|
61
|
+
# Write fields
|
62
|
+
if fields:
|
63
|
+
for key, value in fields:
|
64
|
+
if value is None:
|
65
|
+
continue
|
66
|
+
body.write(f"--{boundary}\r\n".encode())
|
67
|
+
body.write(f'Content-Disposition: form-data; name="{key}"'.encode())
|
68
|
+
body.write(f"\r\n\r\n{value}\r\n".encode())
|
69
|
+
|
70
|
+
# Get filename if not provided
|
71
|
+
if not filename and hasattr(fd, "name"):
|
72
|
+
filename = os.path.basename(fd.name)
|
73
|
+
|
74
|
+
if not content_type and filename:
|
75
|
+
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
76
|
+
|
77
|
+
# Write file data
|
78
|
+
fd.seek(0)
|
79
|
+
body.write(f"--{boundary}\r\n".encode())
|
80
|
+
body.write(
|
81
|
+
f'Content-Disposition: form-data; name="{form_field_name}"; '
|
82
|
+
f'filename="{filename}"\r\n'.encode()
|
83
|
+
)
|
84
|
+
body.write(f"Content-Type: {content_type}\r\n\r\n".encode())
|
85
|
+
body.write(fd.read())
|
86
|
+
body.write(f"\r\n--{boundary}--\r\n\r\n".encode())
|
87
|
+
|
88
|
+
return body.getvalue()
|
89
|
+
|
90
|
+
|
91
|
+
async def async_xupload_file(
|
92
|
+
session: aiohttp.ClientSession,
|
93
|
+
fp: BinaryIO | BytesIO,
|
94
|
+
fields: list[tuple[str, str | None | int]],
|
95
|
+
*,
|
96
|
+
filename: str | None = None,
|
97
|
+
content_type: str | None = None,
|
98
|
+
headers: dict[str, str] | None = None,
|
99
|
+
timeout: int = UPLOAD_TIMEOUT,
|
100
|
+
) -> dict:
|
101
|
+
"""
|
102
|
+
Async version of xupload_file for uploading files to Pararamio.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
session: aiohttp client session
|
106
|
+
fp: A binary file-like object to upload
|
107
|
+
fields: A list of tuples containing field names and values
|
108
|
+
filename: Optional filename for the upload
|
109
|
+
content_type: Optional MIME type of the file
|
110
|
+
headers: Optional additional headers
|
111
|
+
timeout: Timeout in seconds for the upload
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Dictionary with upload response data
|
115
|
+
"""
|
116
|
+
url = f"{FILE_UPLOAD_URL}/upload"
|
117
|
+
boundary = "FORM-BOUNDARY"
|
118
|
+
|
119
|
+
# Prepare headers
|
120
|
+
_headers = {
|
121
|
+
"User-agent": "pararamio-aio",
|
122
|
+
"Accept": "application/json",
|
123
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
124
|
+
}
|
125
|
+
if headers:
|
126
|
+
_headers.update(headers)
|
127
|
+
|
128
|
+
# Encode multipart data
|
129
|
+
data = await multipart_encode_async(
|
130
|
+
fp,
|
131
|
+
fields=fields,
|
132
|
+
filename=filename,
|
133
|
+
content_type=content_type,
|
134
|
+
boundary=boundary,
|
135
|
+
)
|
136
|
+
|
137
|
+
# Make request
|
138
|
+
timeout_obj = aiohttp.ClientTimeout(total=timeout)
|
139
|
+
async with session.post(url, data=data, headers=_headers, timeout=timeout_obj) as response:
|
140
|
+
if response.status == 200:
|
141
|
+
return await response.json()
|
142
|
+
raise PararamioHTTPRequestException(
|
143
|
+
url,
|
144
|
+
response.status,
|
145
|
+
f"HTTP {response.status}",
|
146
|
+
dict(response.headers),
|
147
|
+
await response.text(),
|
148
|
+
)
|
149
|
+
|
150
|
+
|
151
|
+
async def async_delete_file(
|
152
|
+
session: aiohttp.ClientSession,
|
153
|
+
guid: str,
|
154
|
+
headers: dict[str, str] | None = None,
|
155
|
+
timeout: int = REQUEST_TIMEOUT,
|
156
|
+
) -> dict:
|
157
|
+
"""
|
158
|
+
Async version of delete_file for deleting files from Pararamio.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
session: aiohttp client session
|
162
|
+
guid: The GUID of the file to delete
|
163
|
+
headers: Optional additional headers
|
164
|
+
timeout: Timeout in seconds for the request
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Dictionary with deletion response data
|
168
|
+
"""
|
169
|
+
url = f"{FILE_UPLOAD_URL}/delete/{guid}"
|
170
|
+
|
171
|
+
_headers = {
|
172
|
+
"User-agent": "pararamio-aio",
|
173
|
+
"Accept": "application/json",
|
174
|
+
}
|
175
|
+
if headers:
|
176
|
+
_headers.update(headers)
|
177
|
+
|
178
|
+
timeout_obj = aiohttp.ClientTimeout(total=timeout)
|
179
|
+
async with session.delete(url, headers=_headers, timeout=timeout_obj) as response:
|
180
|
+
if response.status in (200, 204):
|
181
|
+
if response.status == 204:
|
182
|
+
return {"success": True}
|
183
|
+
return await response.json()
|
184
|
+
raise PararamioHTTPRequestException(
|
185
|
+
url,
|
186
|
+
response.status,
|
187
|
+
f"HTTP {response.status}",
|
188
|
+
dict(response.headers),
|
189
|
+
await response.text(),
|
190
|
+
)
|
191
|
+
|
192
|
+
|
193
|
+
async def async_download_file(
|
194
|
+
session: aiohttp.ClientSession,
|
195
|
+
guid: str,
|
196
|
+
filename: str,
|
197
|
+
headers: dict[str, str] | None = None,
|
198
|
+
timeout: int = REQUEST_TIMEOUT,
|
199
|
+
) -> BytesIO:
|
200
|
+
"""
|
201
|
+
Async version of download_file for downloading files from Pararamio.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
session: aiohttp client session
|
205
|
+
guid: The GUID of the file to download
|
206
|
+
filename: The filename for the download
|
207
|
+
headers: Optional additional headers
|
208
|
+
timeout: Timeout in seconds for the request
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
BytesIO object containing the downloaded file content
|
212
|
+
"""
|
213
|
+
url = f"{FILE_UPLOAD_URL}/download/{guid}/{quote(filename)}"
|
214
|
+
|
215
|
+
_headers = {
|
216
|
+
"User-agent": "pararamio-aio",
|
217
|
+
}
|
218
|
+
if headers:
|
219
|
+
_headers.update(headers)
|
220
|
+
|
221
|
+
timeout_obj = aiohttp.ClientTimeout(total=timeout)
|
222
|
+
async with session.get(url, headers=_headers, timeout=timeout_obj) as response:
|
223
|
+
if response.status == 200:
|
224
|
+
content = await response.read()
|
225
|
+
return BytesIO(content)
|
226
|
+
raise PararamioHTTPRequestException(
|
227
|
+
url,
|
228
|
+
response.status,
|
229
|
+
f"HTTP {response.status}",
|
230
|
+
dict(response.headers),
|
231
|
+
await response.text(),
|
232
|
+
)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"""Async models for Pararamio API."""
|
2
|
+
|
3
|
+
from .activity import Activity, ActivityAction
|
4
|
+
from .attachment import Attachment
|
5
|
+
from .bot import AsyncPararamioBot
|
6
|
+
from .chat import Chat
|
7
|
+
from .deferred_post import DeferredPost
|
8
|
+
from .file import File
|
9
|
+
from .group import Group
|
10
|
+
from .poll import Poll, PollOption
|
11
|
+
from .post import Post
|
12
|
+
from .team import Team, TeamMember, TeamMemberStatus
|
13
|
+
from .user import User, UserSearchResult
|
14
|
+
|
15
|
+
__all__ = (
|
16
|
+
"Chat",
|
17
|
+
"User",
|
18
|
+
"UserSearchResult",
|
19
|
+
"Post",
|
20
|
+
"Group",
|
21
|
+
"File",
|
22
|
+
"Team",
|
23
|
+
"TeamMember",
|
24
|
+
"TeamMemberStatus",
|
25
|
+
"Poll",
|
26
|
+
"PollOption",
|
27
|
+
"AsyncPararamioBot",
|
28
|
+
"DeferredPost",
|
29
|
+
"Activity",
|
30
|
+
"ActivityAction",
|
31
|
+
"Attachment",
|
32
|
+
)
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""Async Activity model."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
from enum import Enum
|
7
|
+
from typing import Any, Callable, Coroutine
|
8
|
+
|
9
|
+
# Imports from core
|
10
|
+
from pararamio_aio._core.utils.helpers import parse_iso_datetime
|
11
|
+
|
12
|
+
__all__ = ("ActivityAction", "Activity")
|
13
|
+
|
14
|
+
|
15
|
+
class ActivityAction(Enum):
|
16
|
+
"""Activity action types."""
|
17
|
+
|
18
|
+
ONLINE = "online"
|
19
|
+
OFFLINE = "offline"
|
20
|
+
AWAY = "away"
|
21
|
+
READ = "thread-read"
|
22
|
+
POST = "thread-post"
|
23
|
+
CALL = "calling"
|
24
|
+
CALL_END = "endcall"
|
25
|
+
|
26
|
+
|
27
|
+
class Activity:
|
28
|
+
"""User activity record."""
|
29
|
+
|
30
|
+
def __init__(self, action: ActivityAction, time: datetime):
|
31
|
+
"""Initialize activity.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
action: Activity action type
|
35
|
+
time: Activity timestamp
|
36
|
+
"""
|
37
|
+
self.action = action
|
38
|
+
self.time = time
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def from_api_data(cls, data: dict[str, str]) -> Activity:
|
42
|
+
"""Create Activity from API response data.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
data: API response data
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
Activity instance
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
ValueError: If time format is invalid
|
52
|
+
"""
|
53
|
+
time = parse_iso_datetime(data, "datetime")
|
54
|
+
if time is None:
|
55
|
+
raise ValueError("Invalid time format")
|
56
|
+
|
57
|
+
return cls(
|
58
|
+
action=ActivityAction(data["action"]),
|
59
|
+
time=time,
|
60
|
+
)
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
async def get_activity(
|
64
|
+
cls,
|
65
|
+
page_loader: Callable[..., Coroutine[Any, Any, dict[str, Any]]],
|
66
|
+
start: datetime,
|
67
|
+
end: datetime,
|
68
|
+
actions: list[ActivityAction] | None = None,
|
69
|
+
) -> list[Activity]:
|
70
|
+
"""Get user activity within date range.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
page_loader: Async function to load activity pages
|
74
|
+
start: Start datetime
|
75
|
+
end: End datetime
|
76
|
+
actions: Optional list of actions to filter
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
List of Activity objects sorted by time
|
80
|
+
"""
|
81
|
+
results = []
|
82
|
+
actions_to_check: list[ActivityAction | None] = [None]
|
83
|
+
|
84
|
+
if actions:
|
85
|
+
actions_to_check = actions # type: ignore[assignment]
|
86
|
+
|
87
|
+
for action in actions_to_check:
|
88
|
+
page = 1
|
89
|
+
is_last_page = False
|
90
|
+
|
91
|
+
while not is_last_page:
|
92
|
+
# Call async page loader
|
93
|
+
response = await page_loader(action, page=page)
|
94
|
+
data = response.get("data", [])
|
95
|
+
|
96
|
+
if not data:
|
97
|
+
break
|
98
|
+
|
99
|
+
for activity_data in data:
|
100
|
+
activity = cls.from_api_data(activity_data)
|
101
|
+
|
102
|
+
if activity.time > end:
|
103
|
+
continue
|
104
|
+
|
105
|
+
if activity.time < start:
|
106
|
+
is_last_page = True
|
107
|
+
break
|
108
|
+
|
109
|
+
results.append(activity)
|
110
|
+
|
111
|
+
page += 1
|
112
|
+
|
113
|
+
return sorted(results, key=lambda x: x.time)
|
114
|
+
|
115
|
+
def __str__(self) -> str:
|
116
|
+
"""String representation."""
|
117
|
+
return f"Activity({self.time}, {self.action.value})"
|
118
|
+
|
119
|
+
def __repr__(self) -> str:
|
120
|
+
"""Detailed representation."""
|
121
|
+
return f"<Activity(action={self.action}, time={self.time})>"
|
122
|
+
|
123
|
+
def __eq__(self, other) -> bool:
|
124
|
+
"""Check equality."""
|
125
|
+
if not isinstance(other, Activity):
|
126
|
+
return False
|
127
|
+
return self.action == other.action and self.time == other.time
|
@@ -0,0 +1,141 @@
|
|
1
|
+
"""Async Attachment model."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import mimetypes
|
6
|
+
import os
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from io import BufferedReader, BytesIO
|
9
|
+
from os import PathLike
|
10
|
+
from os.path import basename
|
11
|
+
from typing import BinaryIO
|
12
|
+
|
13
|
+
import aiofiles
|
14
|
+
|
15
|
+
__all__ = ("Attachment",)
|
16
|
+
|
17
|
+
|
18
|
+
def guess_mime_type(filename: str | PathLike) -> str:
|
19
|
+
"""Guess MIME type from filename.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
filename: File name or path
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
MIME type string
|
26
|
+
"""
|
27
|
+
if not mimetypes.inited:
|
28
|
+
mimetypes.init(files=os.environ.get("PARARAMIO_MIME_TYPES_PATH", None))
|
29
|
+
return mimetypes.guess_type(str(filename))[0] or "application/octet-stream"
|
30
|
+
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class Attachment:
|
34
|
+
"""File attachment representation.
|
35
|
+
|
36
|
+
This is a utility class for handling file attachments before upload.
|
37
|
+
It can handle various file input types and provides helpers for
|
38
|
+
filename and content type detection.
|
39
|
+
"""
|
40
|
+
|
41
|
+
file: str | bytes | PathLike | BytesIO | BinaryIO
|
42
|
+
filename: str | None = None
|
43
|
+
content_type: str | None = None
|
44
|
+
|
45
|
+
@property
|
46
|
+
def guess_filename(self) -> str:
|
47
|
+
"""Guess filename from file object.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Guessed filename or 'unknown'
|
51
|
+
"""
|
52
|
+
if self.filename:
|
53
|
+
return self.filename
|
54
|
+
|
55
|
+
if isinstance(self.file, (str, PathLike)):
|
56
|
+
return basename(str(self.file))
|
57
|
+
|
58
|
+
if isinstance(self.file, (BytesIO, BinaryIO, BufferedReader)):
|
59
|
+
try:
|
60
|
+
name = getattr(self.file, "name", None)
|
61
|
+
if name:
|
62
|
+
return basename(name)
|
63
|
+
except AttributeError:
|
64
|
+
pass
|
65
|
+
|
66
|
+
return "unknown"
|
67
|
+
|
68
|
+
@property
|
69
|
+
def guess_content_type(self) -> str:
|
70
|
+
"""Guess content type from file.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
MIME type string
|
74
|
+
"""
|
75
|
+
if self.content_type:
|
76
|
+
return self.content_type
|
77
|
+
|
78
|
+
if isinstance(self.file, (str, PathLike)):
|
79
|
+
return guess_mime_type(self.file)
|
80
|
+
|
81
|
+
if isinstance(self.file, (BinaryIO, BufferedReader)):
|
82
|
+
name = getattr(self.file, "name", None)
|
83
|
+
if name:
|
84
|
+
return guess_mime_type(name)
|
85
|
+
|
86
|
+
return "application/octet-stream"
|
87
|
+
|
88
|
+
async def get_fp(self) -> BytesIO | BinaryIO:
|
89
|
+
"""Get file pointer asynchronously.
|
90
|
+
|
91
|
+
This method handles async file reading for path-based files.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
File-like object (BytesIO or BinaryIO)
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
TypeError: If file type is not supported
|
98
|
+
"""
|
99
|
+
if isinstance(self.file, bytes):
|
100
|
+
return BytesIO(self.file)
|
101
|
+
|
102
|
+
if isinstance(self.file, (str, PathLike)):
|
103
|
+
# Read file asynchronously
|
104
|
+
async with aiofiles.open(self.file, "rb") as f:
|
105
|
+
content = await f.read()
|
106
|
+
return BytesIO(content)
|
107
|
+
|
108
|
+
if isinstance(self.file, (BytesIO, BinaryIO, BufferedReader)):
|
109
|
+
return self.file
|
110
|
+
|
111
|
+
raise TypeError(f"Unsupported type {type(self.file)}")
|
112
|
+
|
113
|
+
@property
|
114
|
+
def fp(self) -> BytesIO | BinaryIO:
|
115
|
+
"""Get file pointer.
|
116
|
+
|
117
|
+
Note: This is a sync property. For async file reading,
|
118
|
+
use get_fp() method instead.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
File-like object
|
122
|
+
|
123
|
+
Raises:
|
124
|
+
TypeError: If file type is not supported
|
125
|
+
"""
|
126
|
+
if isinstance(self.file, bytes):
|
127
|
+
return BytesIO(self.file)
|
128
|
+
|
129
|
+
if isinstance(self.file, (str, PathLike)):
|
130
|
+
# Sync file read - not recommended in async context
|
131
|
+
with open(self.file, "rb") as f:
|
132
|
+
return BytesIO(f.read())
|
133
|
+
|
134
|
+
if isinstance(self.file, (BytesIO, BinaryIO, BufferedReader)):
|
135
|
+
return self.file
|
136
|
+
|
137
|
+
raise TypeError(f"Unsupported type {type(self.file)}")
|
138
|
+
|
139
|
+
def __str__(self) -> str:
|
140
|
+
"""String representation."""
|
141
|
+
return f"Attachment({self.guess_filename})"
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""Base classes for async models."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from abc import ABC
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
|
9
|
+
# Imports from core
|
10
|
+
from pararamio_aio._core.models.base import CoreBaseModel
|
11
|
+
from pararamio_aio._core.utils.helpers import parse_iso_datetime
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from ..client import AsyncPararamio
|
15
|
+
|
16
|
+
__all__ = ("BaseModel",)
|
17
|
+
|
18
|
+
|
19
|
+
class BaseModel(CoreBaseModel, ABC):
|
20
|
+
"""Base async model class.
|
21
|
+
|
22
|
+
Unlike sync models, async models don't use lazy loading.
|
23
|
+
All data must be explicitly loaded via async methods.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, client: AsyncPararamio, **kwargs):
|
27
|
+
"""Initialize async model.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
client: AsyncPararamio client instance
|
31
|
+
**kwargs: Model data
|
32
|
+
"""
|
33
|
+
super().__init__(**kwargs)
|
34
|
+
self._client = client
|
35
|
+
|
36
|
+
@property
|
37
|
+
def client(self) -> AsyncPararamio:
|
38
|
+
"""Get the client instance."""
|
39
|
+
return self._client
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def from_dict(cls, client: AsyncPararamio, data: dict[str, Any]) -> BaseModel:
|
43
|
+
"""Create model instance from dict data.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
client: AsyncPararamio client instance
|
47
|
+
data: Raw API data
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Model instance
|
51
|
+
"""
|
52
|
+
# Apply any formatting transformations
|
53
|
+
formatted_data = {}
|
54
|
+
for key, value in data.items():
|
55
|
+
if key.endswith("_created") or key.endswith("_updated") or key.endswith("_edited"):
|
56
|
+
if isinstance(value, str):
|
57
|
+
formatted_data[key] = parse_iso_datetime(data, key)
|
58
|
+
else:
|
59
|
+
formatted_data[key] = value
|
60
|
+
else:
|
61
|
+
formatted_data[key] = value
|
62
|
+
|
63
|
+
return cls(client, **formatted_data)
|
64
|
+
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
66
|
+
"""Convert model to dictionary.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Model data as dict
|
70
|
+
"""
|
71
|
+
result = {}
|
72
|
+
for key, value in self._data.items():
|
73
|
+
if isinstance(value, datetime):
|
74
|
+
result[key] = value.isoformat()
|
75
|
+
else:
|
76
|
+
result[key] = value
|
77
|
+
return result
|
78
|
+
|
79
|
+
def __repr__(self) -> str:
|
80
|
+
"""String representation of the model."""
|
81
|
+
model_name = self.__class__.__name__
|
82
|
+
id_value = getattr(self, "id", None)
|
83
|
+
return f"<{model_name}(id={id_value})>"
|