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,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})>"