marqetive-lib 0.1.1__tar.gz → 0.1.2__tar.gz

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 (49) hide show
  1. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/PKG-INFO +1 -2
  2. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/pyproject.toml +1 -2
  3. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/client.py +1 -1
  4. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/exceptions.py +2 -1
  5. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/models.py +2 -0
  6. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/__init__.py +7 -0
  7. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/client.py +277 -0
  8. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/exceptions.py +180 -0
  9. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/factory.py +188 -0
  10. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/manager.py +115 -0
  11. marqetive_lib-0.1.2/src/marqetive/platforms/tiktok/media.py +305 -0
  12. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/client.py +1 -1
  13. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/oauth.py +135 -0
  14. marqetive_lib-0.1.1/src/marqetive/platforms/tiktok/__init__.py +0 -0
  15. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/README.md +0 -0
  16. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/__init__.py +0 -0
  17. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/__init__.py +0 -0
  18. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/account_factory.py +0 -0
  19. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/base_manager.py +0 -0
  20. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/progress.py +0 -0
  21. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/core/registry.py +0 -0
  22. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/__init__.py +0 -0
  23. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/base.py +0 -0
  24. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/__init__.py +0 -0
  25. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/client.py +0 -0
  26. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/exceptions.py +0 -0
  27. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/factory.py +0 -0
  28. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/manager.py +0 -0
  29. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/instagram/media.py +0 -0
  30. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/__init__.py +0 -0
  31. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/client.py +0 -0
  32. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/exceptions.py +0 -0
  33. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/factory.py +0 -0
  34. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/manager.py +0 -0
  35. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/linkedin/media.py +0 -0
  36. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/__init__.py +0 -0
  37. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/exceptions.py +0 -0
  38. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/factory.py +0 -0
  39. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/manager.py +0 -0
  40. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/media.py +0 -0
  41. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/platforms/twitter/threads.py +0 -0
  42. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/py.typed +0 -0
  43. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/registry_init.py +0 -0
  44. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/__init__.py +0 -0
  45. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/file_handlers.py +0 -0
  46. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/helpers.py +0 -0
  47. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/media.py +0 -0
  48. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/retry.py +0 -0
  49. {marqetive_lib-0.1.1 → marqetive_lib-0.1.2}/src/marqetive/utils/token_validator.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Modern Python utilities for web APIs
5
5
  Keywords: api,utilities,web,http,marqetive
6
6
  Requires-Python: >=3.12
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
9
- Classifier: License :: OSI Approved :: MIT License
10
9
  Classifier: Programming Language :: Python :: 3
11
10
  Classifier: Programming Language :: Python :: 3.12
12
11
  Classifier: Programming Language :: Python :: 3.13
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "marqetive-lib"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Modern Python utilities for web APIs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -12,7 +12,6 @@ keywords = ["api", "utilities", "web", "http", "marqetive"]
12
12
  classifiers = [
13
13
  "Development Status :: 3 - Alpha",
14
14
  "Intended Audience :: Developers",
15
- "License :: OSI Approved :: MIT License",
16
15
  "Programming Language :: Python :: 3",
17
16
  "Programming Language :: Python :: 3.12",
18
17
  "Programming Language :: Python :: 3.13",
@@ -73,7 +73,7 @@ class APIClient:
73
73
  if not self._client:
74
74
  raise RuntimeError("Client not initialized. Use async context manager.")
75
75
 
76
- response: httpx.Response = await self._client.get(path, params=params)
76
+ response = await self._client.get(path, params=params)
77
77
  response.raise_for_status()
78
78
 
79
79
  return APIResponse(
@@ -115,9 +115,10 @@ class PostNotFoundError(PlatformError):
115
115
  post_id: str,
116
116
  platform: str | None = None,
117
117
  status_code: int | None = None,
118
+ message: str | None = None,
118
119
  ) -> None:
119
120
  self.post_id = post_id
120
- message = f"Post not found: {post_id}"
121
+ message = message or f"Post not found: {post_id}"
121
122
  super().__init__(message, platform, status_code)
122
123
 
123
124
 
@@ -305,6 +305,7 @@ class PostCreateRequest(BaseModel):
305
305
  link: URL to include in the post
306
306
  tags: List of hashtags or user tags
307
307
  location: Location/place tag for the post
308
+ additional_data: Platform-specific data
308
309
 
309
310
  Example:
310
311
  >>> request = PostCreateRequest(
@@ -321,6 +322,7 @@ class PostCreateRequest(BaseModel):
321
322
  link: str | None = None
322
323
  tags: list[str] = Field(default_factory=list)
323
324
  location: str | None = None
325
+ additional_data: dict[str, Any] = Field(default_factory=dict)
324
326
 
325
327
 
326
328
  class PostUpdateRequest(BaseModel):
@@ -0,0 +1,7 @@
1
+ """TikTok platform integration."""
2
+
3
+ from src.marqetive.platforms.tiktok.client import TikTokClient
4
+ from src.marqetive.platforms.tiktok.factory import TikTokAccountFactory
5
+ from src.marqetive.platforms.tiktok.manager import TikTokPostManager
6
+
7
+ __all__ = ["TikTokClient", "TikTokAccountFactory", "TikTokPostManager"]
@@ -0,0 +1,277 @@
1
+ """TikTok API client implementation.
2
+
3
+ This module provides a concrete implementation of the SocialMediaPlatform
4
+ ABC for TikTok, using the TikTok for Business API (hypothetical).
5
+
6
+ API Documentation: https://developers.tiktok.com/doc/overview
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from pydantic import HttpUrl
13
+
14
+ from src.marqetive.platforms.base import SocialMediaPlatform
15
+ from src.marqetive.platforms.exceptions import (
16
+ PlatformAuthError,
17
+ PlatformError,
18
+ PostNotFoundError,
19
+ ValidationError,
20
+ )
21
+ from src.marqetive.platforms.models import (
22
+ AuthCredentials,
23
+ Comment,
24
+ CommentStatus,
25
+ MediaAttachment,
26
+ MediaType,
27
+ Post,
28
+ PostCreateRequest,
29
+ PostStatus,
30
+ PostUpdateRequest,
31
+ )
32
+ from src.marqetive.platforms.tiktok.media import TikTokMediaManager
33
+
34
+
35
+ class TikTokClient(SocialMediaPlatform):
36
+ """TikTok API client.
37
+
38
+ This client implements the SocialMediaPlatform interface for TikTok,
39
+ focusing on video uploads and management.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ credentials: AuthCredentials,
45
+ timeout: float = 300.0,
46
+ ) -> None:
47
+ """Initialize TikTok client."""
48
+ base_url = "https://open.tiktokapis.com/v2/"
49
+ super().__init__(
50
+ platform_name="tiktok",
51
+ credentials=credentials,
52
+ base_url=base_url,
53
+ timeout=timeout,
54
+ )
55
+ self._media_manager: TikTokMediaManager | None = None
56
+
57
+ async def _setup_managers(self) -> None:
58
+ """Setup media manager."""
59
+ if not self.credentials.access_token or not self.credentials.additional_data:
60
+ raise PlatformAuthError("Access token and open_id are required", "tiktok")
61
+
62
+ self._media_manager = TikTokMediaManager(
63
+ access_token=self.credentials.access_token,
64
+ open_id=self.credentials.additional_data["open_id"],
65
+ timeout=self.timeout,
66
+ )
67
+
68
+ async def _cleanup_managers(self) -> None:
69
+ """Cleanup media manager."""
70
+ if self._media_manager:
71
+ await self._media_manager.__aexit__(None, None, None)
72
+ self._media_manager = None
73
+
74
+ async def __aenter__(self) -> "TikTokClient":
75
+ await super().__aenter__()
76
+ await self._setup_managers()
77
+ return self
78
+
79
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
80
+ await self._cleanup_managers()
81
+ await super().__aexit__(exc_type, exc_val, exc_tb)
82
+
83
+ async def authenticate(self) -> AuthCredentials:
84
+ """Perform TikTok authentication."""
85
+ if await self.is_authenticated():
86
+ return self.credentials
87
+ raise PlatformAuthError(
88
+ "Invalid or expired credentials. Please re-authenticate via TikTok OAuth.",
89
+ platform=self.platform_name,
90
+ )
91
+
92
+ async def refresh_token(self) -> AuthCredentials:
93
+ """Refresh TikTok access token."""
94
+ # The refresh logic should be handled by the AccountFactory
95
+ # For now, we assume it's done and we just return the creds
96
+ return self.credentials
97
+
98
+ async def is_authenticated(self) -> bool:
99
+ """Check if TikTok credentials are valid."""
100
+ if not self.api_client or not self.credentials.additional_data:
101
+ return False
102
+ try:
103
+ # Verify credentials by fetching authenticated user info
104
+ params = {
105
+ "fields": "open_id,union_id,avatar_url,display_name",
106
+ "open_id": self.credentials.additional_data["open_id"],
107
+ }
108
+ response = await self.api_client.get("user/info/", params=params)
109
+ return response.data.get("data", {}).get("user") is not None
110
+ except PlatformError:
111
+ return False
112
+
113
+ async def create_post(self, request: PostCreateRequest) -> Post:
114
+ """Create and publish a TikTok video.
115
+
116
+ Args:
117
+ request: The post creation request, must contain a video URL.
118
+
119
+ Returns:
120
+ The created Post object.
121
+
122
+ Raises:
123
+ ValidationError: If the request is invalid (e.g., no media).
124
+ MediaUploadError: If the video upload fails.
125
+ """
126
+ if not self._media_manager or not self.api_client:
127
+ raise RuntimeError("Client must be used as async context manager")
128
+
129
+ if not request.media_urls:
130
+ raise ValidationError(
131
+ "A video URL is required to create a TikTok post.",
132
+ platform=self.platform_name,
133
+ )
134
+
135
+ # 1. Upload the video
136
+ video_url = request.media_urls[0]
137
+ upload_result = await self._media_manager.upload_media(video_url)
138
+
139
+ if not upload_result.media_id:
140
+ raise PlatformError("Media upload succeeded but did not yield a media ID.")
141
+
142
+ # 2. Create the post with the uploaded video
143
+ payload = {
144
+ "post_info": {
145
+ "title": request.content or "",
146
+ "description": request.additional_data.get("description", ""),
147
+ "privacy_level": request.additional_data.get(
148
+ "privacy_level", "PUBLIC_TO_EVERYONE"
149
+ ),
150
+ },
151
+ "source_info": {
152
+ "source": "FILE_UPLOAD",
153
+ "video_id": upload_result.media_id,
154
+ },
155
+ }
156
+ response = await self.api_client.post("video/publish/", data=payload)
157
+ post_id = response.data.get("data", {}).get("publish_id")
158
+
159
+ if not post_id:
160
+ raise PlatformError("Post creation succeeded but no publish_id returned.")
161
+
162
+ # 3. Fetch the created post to return a full Post object
163
+ return await self.get_post(post_id)
164
+
165
+ async def get_post(self, post_id: str) -> Post:
166
+ """Retrieve a TikTok video by its ID."""
167
+ if not self.api_client or not self.credentials.additional_data:
168
+ raise RuntimeError("Client must be used as async context manager")
169
+ try:
170
+ params = {
171
+ "open_id": self.credentials.additional_data["open_id"],
172
+ "video_ids": [post_id],
173
+ }
174
+ response = await self.api_client.post("video/query/", data=params)
175
+ video_data = response.data.get("data", {}).get("videos", [])
176
+ if not video_data:
177
+ raise PostNotFoundError(post_id, self.platform_name)
178
+ return self._parse_video_post(video_data[0])
179
+ except PlatformError as e:
180
+ raise PostNotFoundError(
181
+ post_id, self.platform_name, status_code=e.status_code
182
+ ) from e
183
+
184
+ async def update_post(self, post_id: str, request: PostUpdateRequest) -> Post:
185
+ """Update a TikTok video. This is not supported by TikTok API."""
186
+ raise PlatformError(
187
+ "TikTok API does not support updating videos.", self.platform_name
188
+ )
189
+
190
+ async def delete_post(self, post_id: str) -> bool:
191
+ """Delete a TikTok video. This is not supported by TikTok API."""
192
+ raise PlatformError(
193
+ "TikTok API does not support deleting videos.", self.platform_name
194
+ )
195
+
196
+ async def get_comments(
197
+ self, post_id: str, limit: int = 20, offset: int = 0
198
+ ) -> list[Comment]:
199
+ """Retrieve comments for a TikTok video."""
200
+ if not self.api_client or not self.credentials.additional_data:
201
+ raise RuntimeError("Client must be used as async context manager")
202
+ # This is a hypothetical endpoint
203
+ params = {
204
+ "open_id": self.credentials.additional_data["open_id"],
205
+ "video_id": post_id,
206
+ "count": limit,
207
+ "cursor": offset,
208
+ }
209
+ response = await self.api_client.get("video/comment/list/", params=params)
210
+ comments_data = response.data.get("data", {}).get("comments", [])
211
+ return [self._parse_comment(c, post_id) for c in comments_data]
212
+
213
+ async def create_comment(self, post_id: str, content: str) -> Comment:
214
+ """Create a comment on a TikTok video."""
215
+ raise PlatformError("Commenting via API is not supported.", self.platform_name)
216
+
217
+ async def delete_comment(self, comment_id: str) -> bool:
218
+ """Delete a comment on a TikTok video."""
219
+ raise PlatformError(
220
+ "Deleting comments via API is not supported.", self.platform_name
221
+ )
222
+
223
+ async def upload_media(
224
+ self, media_url: str, media_type: str, alt_text: str | None = None
225
+ ) -> MediaAttachment:
226
+ """Upload a video to TikTok."""
227
+ if not self._media_manager:
228
+ raise RuntimeError("Client not initialized. Use as async context manager.")
229
+ if media_type != "video":
230
+ raise ValidationError("Only video media type is supported for TikTok.")
231
+
232
+ result = await self._media_manager.upload_media(media_url)
233
+ return MediaAttachment(
234
+ media_id=result.media_id or result.upload_id,
235
+ media_type=MediaType.VIDEO,
236
+ url=HttpUrl(media_url),
237
+ )
238
+
239
+ def _parse_video_post(self, video_data: dict[str, Any]) -> Post:
240
+ """Parse a TikTok API video object into a Post model."""
241
+ return Post(
242
+ post_id=video_data["video_id"],
243
+ platform=self.platform_name,
244
+ content=video_data.get("title", ""),
245
+ media=[
246
+ MediaAttachment(
247
+ media_id=video_data["video_id"],
248
+ media_type=MediaType.VIDEO,
249
+ url=HttpUrl(video_data.get("share_url", "")),
250
+ width=video_data.get("width"),
251
+ height=video_data.get("height"),
252
+ )
253
+ ],
254
+ status=PostStatus.PUBLISHED,
255
+ created_at=datetime.fromtimestamp(video_data.get("create_time", 0)),
256
+ author_id=str(video_data.get("open_id")),
257
+ likes_count=video_data.get("like_count", 0),
258
+ comments_count=video_data.get("comment_count", 0),
259
+ shares_count=video_data.get("share_count", 0),
260
+ views_count=video_data.get("view_count", 0),
261
+ raw_data=video_data,
262
+ )
263
+
264
+ def _parse_comment(self, comment_data: dict[str, Any], post_id: str) -> Comment:
265
+ """Parse a TikTok API comment object into a Comment model."""
266
+ return Comment(
267
+ comment_id=comment_data["comment_id"],
268
+ post_id=post_id,
269
+ platform=self.platform_name,
270
+ content=comment_data.get("text", ""),
271
+ author_id=comment_data.get("open_id", ""),
272
+ created_at=datetime.fromtimestamp(comment_data.get("create_time", 0)),
273
+ likes_count=comment_data.get("like_count", 0),
274
+ replies_count=0,
275
+ status=CommentStatus.VISIBLE,
276
+ raw_data=comment_data,
277
+ )
@@ -0,0 +1,180 @@
1
+ """TikTok-specific exception handling and error code mapping.
2
+
3
+ This module provides error handling for the TikTok API, including HTTP status
4
+ code mapping, TikTok-specific error codes, and user-friendly messages.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from src.marqetive.platforms.exceptions import (
10
+ MediaUploadError,
11
+ PlatformAuthError,
12
+ PlatformError,
13
+ PostNotFoundError,
14
+ RateLimitError,
15
+ ValidationError,
16
+ )
17
+
18
+
19
+ # TikTok API error codes (hypothetical, based on common API patterns)
20
+ # Source: TikTok Developer documentation (assumed)
21
+ class TikTokErrorCode:
22
+ """TikTok API error codes."""
23
+
24
+ # Authentication & Authorization
25
+ INVALID_ACCESS_TOKEN = 10001
26
+ ACCESS_TOKEN_EXPIRED = 10002
27
+ SCOPE_NOT_AUTHORIZED = 10003
28
+ AUTHENTICATION_FAILED = 10004
29
+ ACCOUNT_NOT_AUTHORIZED = 10005
30
+
31
+ # Rate Limiting
32
+ RATE_LIMIT_EXCEEDED = 20001
33
+ TOO_MANY_REQUESTS = 20002
34
+
35
+ # Resource Not Found
36
+ VIDEO_NOT_FOUND = 30001
37
+ USER_NOT_FOUND = 30002
38
+ COMMENT_NOT_FOUND = 30003
39
+
40
+ # Validation Errors
41
+ INVALID_PARAMETER = 40001
42
+ VIDEO_TOO_LARGE = 40002
43
+ VIDEO_DURATION_INVALID = 40003
44
+ DESCRIPTION_TOO_LONG = 40004
45
+ DUPLICATE_VIDEO = 40005
46
+ INVALID_VIDEO_FORMAT = 40006
47
+
48
+ # Media Upload & Processing
49
+ UPLOAD_FAILED = 50001
50
+ INIT_UPLOAD_FAILED = 50002
51
+ APPEND_CHUNK_FAILED = 50003
52
+ FINALIZE_UPLOAD_FAILED = 50004
53
+ MEDIA_PROCESSING_FAILED = 50005
54
+
55
+
56
+ # Mapping of error codes to user-friendly messages
57
+ ERROR_MESSAGES: dict[int, str] = {
58
+ # Authentication
59
+ 10001: "Invalid access token. Please re-authenticate.",
60
+ 10002: "Access token has expired. Please refresh the token.",
61
+ 10003: "The application is not authorized for the requested scope.",
62
+ 10004: "Authentication failed. Please check your credentials.",
63
+ 10005: "The user has not authorized this action for their account.",
64
+ # Rate Limiting
65
+ 20001: "Rate limit exceeded. Please wait before making more requests.",
66
+ 20002: "Too many requests. You have hit a rate limit.",
67
+ # Resource Not Found
68
+ 30001: "The requested video could not be found.",
69
+ 30002: "The specified user does not exist.",
70
+ 30003: "The specified comment could not be found.",
71
+ # Validation
72
+ 40001: "Invalid parameter in request.",
73
+ 40002: "Video file size is too large.",
74
+ 40003: "Video duration is outside the allowed range.",
75
+ 40004: "Video description is too long.",
76
+ 40005: "This video appears to be a duplicate of a previous post.",
77
+ 40006: "Invalid video format. Please use a supported format like MP4 or MOV.",
78
+ # Media
79
+ 50001: "Media upload failed.",
80
+ 50005: "Media processing failed after upload.",
81
+ }
82
+
83
+
84
+ def map_tiktok_error(
85
+ status_code: int | None,
86
+ error_code: int | None = None,
87
+ error_message: str | None = None,
88
+ response_data: dict[str, Any] | None = None,
89
+ ) -> PlatformError:
90
+ """Map TikTok API error to the appropriate exception class.
91
+
92
+ Args:
93
+ status_code: HTTP status code.
94
+ error_code: TikTok-specific error code from the response body.
95
+ error_message: Error message from the API.
96
+ response_data: Full response data from the API.
97
+
98
+ Returns:
99
+ An appropriate subclass of PlatformError.
100
+ """
101
+ if response_data and "error" in response_data:
102
+ error_data = response_data["error"]
103
+ if not error_code:
104
+ error_code = error_data.get("code")
105
+ if not error_message:
106
+ error_message = error_data.get("message")
107
+
108
+ friendly_message = ERROR_MESSAGES.get(
109
+ error_code or 0, error_message or "An unknown TikTok API error occurred"
110
+ )
111
+
112
+ # Map to exception types based on error code categories or status code
113
+ if error_code in (10001, 10002, 10003, 10004, 10005) or status_code in (401, 403):
114
+ return PlatformAuthError(
115
+ friendly_message,
116
+ platform="tiktok",
117
+ status_code=status_code,
118
+ )
119
+
120
+ if error_code in (20001, 20002) or status_code == 429:
121
+ retry_after = None
122
+ if response_data and response_data.get("extra"):
123
+ retry_after = response_data["extra"].get("log_id") # Placeholder
124
+ return RateLimitError(
125
+ friendly_message,
126
+ platform="tiktok",
127
+ status_code=status_code or 429,
128
+ retry_after=retry_after,
129
+ )
130
+
131
+ if error_code in (30001,):
132
+ return PostNotFoundError(
133
+ post_id="", # TikTok might use a different identifier
134
+ platform="tiktok",
135
+ status_code=status_code,
136
+ message=friendly_message,
137
+ )
138
+
139
+ if error_code in (40001, 40002, 40003, 40004, 40005, 40006):
140
+ return ValidationError(
141
+ friendly_message,
142
+ platform="tiktok",
143
+ )
144
+
145
+ if error_code in (50001, 50002, 50003, 50004, 50005):
146
+ return MediaUploadError(
147
+ friendly_message,
148
+ platform="tiktok",
149
+ status_code=status_code,
150
+ )
151
+
152
+ # Generic platform error for everything else
153
+ return PlatformError(
154
+ friendly_message,
155
+ platform="tiktok",
156
+ status_code=status_code,
157
+ )
158
+
159
+
160
+ class TikTokAPIError(PlatformError):
161
+ """TikTok API specific error with detailed information."""
162
+
163
+ def __init__(
164
+ self,
165
+ message: str,
166
+ *,
167
+ status_code: int | None = None,
168
+ error_code: int | None = None,
169
+ response_data: dict[str, Any] | None = None,
170
+ ) -> None:
171
+ super().__init__(message, platform="tiktok", status_code=status_code)
172
+ self.error_code = error_code
173
+ self.response_data = response_data
174
+
175
+ def __repr__(self) -> str:
176
+ return (
177
+ f"TikTokAPIError(message={self.message!r}, "
178
+ f"status_code={self.status_code}, "
179
+ f"error_code={self.error_code})"
180
+ )