marqetive-lib 0.1.2__py3-none-any.whl → 0.1.3__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 (41) hide show
  1. marqetive/__init__.py +13 -13
  2. marqetive/core/__init__.py +1 -1
  3. marqetive/core/account_factory.py +2 -2
  4. marqetive/core/base_manager.py +4 -4
  5. marqetive/core/registry.py +3 -3
  6. marqetive/platforms/__init__.py +6 -6
  7. marqetive/platforms/base.py +3 -3
  8. marqetive/platforms/instagram/__init__.py +3 -3
  9. marqetive/platforms/instagram/client.py +4 -4
  10. marqetive/platforms/instagram/exceptions.py +1 -1
  11. marqetive/platforms/instagram/factory.py +5 -5
  12. marqetive/platforms/instagram/manager.py +4 -4
  13. marqetive/platforms/instagram/media.py +2 -2
  14. marqetive/platforms/linkedin/__init__.py +3 -3
  15. marqetive/platforms/linkedin/client.py +4 -4
  16. marqetive/platforms/linkedin/exceptions.py +1 -1
  17. marqetive/platforms/linkedin/factory.py +5 -5
  18. marqetive/platforms/linkedin/manager.py +4 -4
  19. marqetive/platforms/linkedin/media.py +4 -4
  20. marqetive/platforms/tiktok/__init__.py +3 -3
  21. marqetive/platforms/tiktok/client.py +319 -104
  22. marqetive/platforms/tiktok/exceptions.py +170 -66
  23. marqetive/platforms/tiktok/factory.py +5 -5
  24. marqetive/platforms/tiktok/manager.py +5 -5
  25. marqetive/platforms/tiktok/media.py +547 -159
  26. marqetive/platforms/twitter/__init__.py +3 -3
  27. marqetive/platforms/twitter/client.py +7 -53
  28. marqetive/platforms/twitter/exceptions.py +1 -1
  29. marqetive/platforms/twitter/factory.py +5 -6
  30. marqetive/platforms/twitter/manager.py +4 -4
  31. marqetive/platforms/twitter/media.py +4 -4
  32. marqetive/registry_init.py +10 -8
  33. marqetive/utils/__init__.py +3 -3
  34. marqetive/utils/file_handlers.py +1 -1
  35. marqetive/utils/oauth.py +2 -2
  36. marqetive/utils/token_validator.py +1 -1
  37. {marqetive_lib-0.1.2.dist-info → marqetive_lib-0.1.3.dist-info}/METADATA +1 -1
  38. marqetive_lib-0.1.3.dist-info/RECORD +47 -0
  39. marqetive/platforms/twitter/threads.py +0 -442
  40. marqetive_lib-0.1.2.dist-info/RECORD +0 -48
  41. {marqetive_lib-0.1.2.dist-info → marqetive_lib-0.1.3.dist-info}/WHEEL +0 -0
@@ -2,11 +2,14 @@
2
2
 
3
3
  This module provides error handling for the TikTok API, including HTTP status
4
4
  code mapping, TikTok-specific error codes, and user-friendly messages.
5
+
6
+ TikTok API uses string error codes in the response format:
7
+ {"error": {"code": "error_code_string", "message": "...", "log_id": "..."}}
5
8
  """
6
9
 
7
10
  from typing import Any
8
11
 
9
- from src.marqetive.platforms.exceptions import (
12
+ from marqetive.platforms.exceptions import (
10
13
  MediaUploadError,
11
14
  PlatformAuthError,
12
15
  PlatformError,
@@ -16,82 +19,103 @@ from src.marqetive.platforms.exceptions import (
16
19
  )
17
20
 
18
21
 
19
- # TikTok API error codes (hypothetical, based on common API patterns)
20
- # Source: TikTok Developer documentation (assumed)
21
22
  class TikTokErrorCode:
22
- """TikTok API error codes."""
23
+ """TikTok API error codes (string-based).
24
+
25
+ Reference: https://developers.tiktok.com/doc/content-posting-api-reference-direct-post
26
+ """
27
+
28
+ # Success
29
+ OK = "ok"
23
30
 
24
31
  # 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
32
+ ACCESS_TOKEN_INVALID = "access_token_invalid"
33
+ ACCESS_TOKEN_EXPIRED = "access_token_expired"
34
+ SCOPE_NOT_AUTHORIZED = "scope_not_authorized"
35
+ TOKEN_NOT_AUTHORIZED_FOR_OPEN_ID = "token_not_authorized_for_this_open_id"
30
36
 
31
- # Rate Limiting
32
- RATE_LIMIT_EXCEEDED = 20001
33
- TOO_MANY_REQUESTS = 20002
37
+ # Rate Limiting & Spam
38
+ RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
39
+ SPAM_RISK_TOO_MANY_POSTS = "spam_risk_too_many_posts"
40
+ SPAM_RISK_USER_BANNED = "spam_risk_user_banned_from_posting"
34
41
 
35
42
  # 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
43
+ VIDEO_NOT_FOUND = "video_not_found"
44
+ USER_NOT_FOUND = "user_not_found"
45
+ PUBLISH_ID_NOT_FOUND = "publish_id_not_found"
46
+
47
+ # Validation & Privacy Errors
48
+ INVALID_PARAMS = "invalid_params"
49
+ INVALID_FILE_FORMAT = "invalid_file_format"
50
+ VIDEO_DURATION_TOO_LONG = "video_duration_too_long"
51
+ VIDEO_DURATION_TOO_SHORT = "video_duration_too_short"
52
+ PICTURE_NUMBER_EXCEEDS_LIMIT = "picture_number_exceeds_limit"
53
+ PRIVACY_LEVEL_OPTION_MISMATCH = "privacy_level_option_mismatch"
54
+ TITLE_LENGTH_EXCEEDS_LIMIT = "title_length_exceeds_limit"
55
+
56
+ # Upload & Processing
57
+ FILE_FORMAT_CHECK_FAILED = "file_format_check_failed"
58
+ UPLOAD_FAILED = "upload_failed"
59
+ PROCESSING_FAILED = "processing_failed"
60
+
61
+ # Unaudited Client Restrictions
62
+ UNAUDITED_CLIENT_PRIVATE_ONLY = "unaudited_client_can_only_post_to_private_accounts"
63
+
64
+ # Publish Status Values (not errors, but statuses)
65
+ STATUS_PROCESSING_UPLOAD = "PROCESSING_UPLOAD"
66
+ STATUS_PROCESSING_DOWNLOAD = "PROCESSING_DOWNLOAD"
67
+ STATUS_PUBLISH_COMPLETE = "PUBLISH_COMPLETE"
68
+ STATUS_FAILED = "FAILED"
54
69
 
55
70
 
56
71
  # Mapping of error codes to user-friendly messages
57
- ERROR_MESSAGES: dict[int, str] = {
72
+ ERROR_MESSAGES: dict[str, str] = {
73
+ # Success
74
+ TikTokErrorCode.OK: "Request completed successfully.",
58
75
  # 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.",
76
+ TikTokErrorCode.ACCESS_TOKEN_INVALID: "Invalid access token. Please re-authenticate.",
77
+ TikTokErrorCode.ACCESS_TOKEN_EXPIRED: "Access token has expired. Please refresh the token.",
78
+ TikTokErrorCode.SCOPE_NOT_AUTHORIZED: "The application is not authorized for the requested scope.",
79
+ TikTokErrorCode.TOKEN_NOT_AUTHORIZED_FOR_OPEN_ID: "Token is not authorized for this user.",
64
80
  # Rate Limiting
65
- 20001: "Rate limit exceeded. Please wait before making more requests.",
66
- 20002: "Too many requests. You have hit a rate limit.",
81
+ TikTokErrorCode.RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please wait before making more requests.",
82
+ TikTokErrorCode.SPAM_RISK_TOO_MANY_POSTS: "Daily posting limit reached (~15 posts/day).",
83
+ TikTokErrorCode.SPAM_RISK_USER_BANNED: "User has been temporarily banned from posting.",
67
84
  # 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.",
85
+ TikTokErrorCode.VIDEO_NOT_FOUND: "The requested video could not be found.",
86
+ TikTokErrorCode.USER_NOT_FOUND: "The specified user does not exist.",
87
+ TikTokErrorCode.PUBLISH_ID_NOT_FOUND: "The publish ID was not found.",
71
88
  # 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.",
89
+ TikTokErrorCode.INVALID_PARAMS: "Invalid parameter in request.",
90
+ TikTokErrorCode.INVALID_FILE_FORMAT: "Invalid file format. Use MP4 or MOV.",
91
+ TikTokErrorCode.VIDEO_DURATION_TOO_LONG: "Video duration exceeds the maximum allowed.",
92
+ TikTokErrorCode.VIDEO_DURATION_TOO_SHORT: "Video duration is below the minimum (3 seconds).",
93
+ TikTokErrorCode.PICTURE_NUMBER_EXCEEDS_LIMIT: "Too many images in photo post.",
94
+ TikTokErrorCode.PRIVACY_LEVEL_OPTION_MISMATCH: "Privacy level not available for this creator.",
95
+ TikTokErrorCode.TITLE_LENGTH_EXCEEDS_LIMIT: "Video title/description is too long.",
96
+ # Upload
97
+ TikTokErrorCode.FILE_FORMAT_CHECK_FAILED: "File format validation failed.",
98
+ TikTokErrorCode.UPLOAD_FAILED: "Media upload failed.",
99
+ TikTokErrorCode.PROCESSING_FAILED: "Media processing failed after upload.",
100
+ # Unaudited
101
+ TikTokErrorCode.UNAUDITED_CLIENT_PRIVATE_ONLY: "Unaudited apps can only post to private accounts.",
81
102
  }
82
103
 
83
104
 
84
105
  def map_tiktok_error(
85
106
  status_code: int | None,
86
- error_code: int | None = None,
107
+ error_code: str | None = None,
87
108
  error_message: str | None = None,
88
109
  response_data: dict[str, Any] | None = None,
89
110
  ) -> PlatformError:
90
111
  """Map TikTok API error to the appropriate exception class.
91
112
 
113
+ TikTok API returns errors in the format:
114
+ {"error": {"code": "error_code_string", "message": "...", "log_id": "..."}}
115
+
92
116
  Args:
93
117
  status_code: HTTP status code.
94
- error_code: TikTok-specific error code from the response body.
118
+ error_code: TikTok-specific error code string from the response body.
95
119
  error_message: Error message from the API.
96
120
  response_data: Full response data from the API.
97
121
 
@@ -106,43 +130,75 @@ def map_tiktok_error(
106
130
  error_message = error_data.get("message")
107
131
 
108
132
  friendly_message = ERROR_MESSAGES.get(
109
- error_code or 0, error_message or "An unknown TikTok API error occurred"
133
+ error_code or "", error_message or "An unknown TikTok API error occurred"
110
134
  )
111
135
 
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):
136
+ # Authentication errors
137
+ auth_error_codes = {
138
+ TikTokErrorCode.ACCESS_TOKEN_INVALID,
139
+ TikTokErrorCode.ACCESS_TOKEN_EXPIRED,
140
+ TikTokErrorCode.SCOPE_NOT_AUTHORIZED,
141
+ TikTokErrorCode.TOKEN_NOT_AUTHORIZED_FOR_OPEN_ID,
142
+ }
143
+ if error_code in auth_error_codes or status_code in (401, 403):
114
144
  return PlatformAuthError(
115
145
  friendly_message,
116
146
  platform="tiktok",
117
147
  status_code=status_code,
118
148
  )
119
149
 
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
150
+ # Rate limiting errors
151
+ rate_limit_codes = {
152
+ TikTokErrorCode.RATE_LIMIT_EXCEEDED,
153
+ TikTokErrorCode.SPAM_RISK_TOO_MANY_POSTS,
154
+ TikTokErrorCode.SPAM_RISK_USER_BANNED,
155
+ }
156
+ if error_code in rate_limit_codes or status_code == 429:
124
157
  return RateLimitError(
125
158
  friendly_message,
126
159
  platform="tiktok",
127
160
  status_code=status_code or 429,
128
- retry_after=retry_after,
161
+ retry_after=None,
129
162
  )
130
163
 
131
- if error_code in (30001,):
164
+ # Not found errors
165
+ not_found_codes = {
166
+ TikTokErrorCode.VIDEO_NOT_FOUND,
167
+ TikTokErrorCode.USER_NOT_FOUND,
168
+ TikTokErrorCode.PUBLISH_ID_NOT_FOUND,
169
+ }
170
+ if error_code in not_found_codes or status_code == 404:
132
171
  return PostNotFoundError(
133
- post_id="", # TikTok might use a different identifier
172
+ post_id="",
134
173
  platform="tiktok",
135
174
  status_code=status_code,
136
175
  message=friendly_message,
137
176
  )
138
177
 
139
- if error_code in (40001, 40002, 40003, 40004, 40005, 40006):
178
+ # Validation errors
179
+ validation_codes = {
180
+ TikTokErrorCode.INVALID_PARAMS,
181
+ TikTokErrorCode.INVALID_FILE_FORMAT,
182
+ TikTokErrorCode.VIDEO_DURATION_TOO_LONG,
183
+ TikTokErrorCode.VIDEO_DURATION_TOO_SHORT,
184
+ TikTokErrorCode.PICTURE_NUMBER_EXCEEDS_LIMIT,
185
+ TikTokErrorCode.PRIVACY_LEVEL_OPTION_MISMATCH,
186
+ TikTokErrorCode.TITLE_LENGTH_EXCEEDS_LIMIT,
187
+ TikTokErrorCode.UNAUDITED_CLIENT_PRIVATE_ONLY,
188
+ }
189
+ if error_code in validation_codes:
140
190
  return ValidationError(
141
191
  friendly_message,
142
192
  platform="tiktok",
143
193
  )
144
194
 
145
- if error_code in (50001, 50002, 50003, 50004, 50005):
195
+ # Media upload errors
196
+ media_error_codes = {
197
+ TikTokErrorCode.FILE_FORMAT_CHECK_FAILED,
198
+ TikTokErrorCode.UPLOAD_FAILED,
199
+ TikTokErrorCode.PROCESSING_FAILED,
200
+ }
201
+ if error_code in media_error_codes:
146
202
  return MediaUploadError(
147
203
  friendly_message,
148
204
  platform="tiktok",
@@ -165,16 +221,64 @@ class TikTokAPIError(PlatformError):
165
221
  message: str,
166
222
  *,
167
223
  status_code: int | None = None,
168
- error_code: int | None = None,
224
+ error_code: str | None = None,
225
+ log_id: str | None = None,
169
226
  response_data: dict[str, Any] | None = None,
170
227
  ) -> None:
171
228
  super().__init__(message, platform="tiktok", status_code=status_code)
172
229
  self.error_code = error_code
230
+ self.log_id = log_id
173
231
  self.response_data = response_data
174
232
 
175
233
  def __repr__(self) -> str:
176
234
  return (
177
235
  f"TikTokAPIError(message={self.message!r}, "
178
236
  f"status_code={self.status_code}, "
179
- f"error_code={self.error_code})"
237
+ f"error_code={self.error_code!r}, "
238
+ f"log_id={self.log_id!r})"
180
239
  )
240
+
241
+
242
+ def is_retryable_error(error_code: str | None, status_code: int | None = None) -> bool:
243
+ """Check if an error is retryable.
244
+
245
+ Args:
246
+ error_code: TikTok error code string.
247
+ status_code: HTTP status code.
248
+
249
+ Returns:
250
+ True if the error should be retried, False otherwise.
251
+ """
252
+ # Server errors are retryable
253
+ if status_code and 500 <= status_code < 600:
254
+ return True
255
+
256
+ # Rate limits should wait and retry
257
+ if error_code == TikTokErrorCode.RATE_LIMIT_EXCEEDED:
258
+ return True
259
+
260
+ # Upload failures may be transient
261
+ return error_code == TikTokErrorCode.UPLOAD_FAILED
262
+
263
+
264
+ def get_retry_delay(error_code: str | None, attempt: int = 1) -> float:
265
+ """Get recommended retry delay for an error.
266
+
267
+ Args:
268
+ error_code: TikTok error code string.
269
+ attempt: Current attempt number (1-based).
270
+
271
+ Returns:
272
+ Recommended delay in seconds before retry.
273
+ """
274
+ # Rate limits - wait longer
275
+ if error_code == TikTokErrorCode.RATE_LIMIT_EXCEEDED:
276
+ return 60.0 # 1 minute
277
+
278
+ # Daily post limit - wait much longer
279
+ if error_code == TikTokErrorCode.SPAM_RISK_TOO_MANY_POSTS:
280
+ return 3600.0 # 1 hour (user should wait until next day)
281
+
282
+ # Default exponential backoff: 5s, 10s, 20s, max 60s
283
+ delay = min(5 * (2 ** (attempt - 1)), 60)
284
+ return float(delay)
@@ -5,11 +5,11 @@ import os
5
5
  from collections.abc import Callable
6
6
  from datetime import datetime, timedelta
7
7
 
8
- from src.marqetive.core.account_factory import BaseAccountFactory
9
- from src.marqetive.platforms.exceptions import PlatformAuthError
10
- from src.marqetive.platforms.models import AccountStatus, AuthCredentials
11
- from src.marqetive.platforms.tiktok.client import TikTokClient
12
- from src.marqetive.utils.oauth import fetch_tiktok_token, refresh_tiktok_token
8
+ from marqetive.core.account_factory import BaseAccountFactory
9
+ from marqetive.platforms.exceptions import PlatformAuthError
10
+ from marqetive.platforms.models import AccountStatus, AuthCredentials
11
+ from marqetive.platforms.tiktok.client import TikTokClient
12
+ from marqetive.utils.oauth import fetch_tiktok_token, refresh_tiktok_token
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -3,10 +3,10 @@
3
3
  import logging
4
4
  from typing import Any
5
5
 
6
- from src.marqetive.core.base_manager import BasePostManager
7
- from src.marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
8
- from src.marqetive.platforms.tiktok.client import TikTokClient
9
- from src.marqetive.platforms.tiktok.factory import TikTokAccountFactory
6
+ from marqetive.core.base_manager import BasePostManager
7
+ from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
8
+ from marqetive.platforms.tiktok.client import TikTokClient
9
+ from marqetive.platforms.tiktok.factory import TikTokAccountFactory
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -49,7 +49,7 @@ class TikTokPostManager(BasePostManager):
49
49
  self,
50
50
  client: Any,
51
51
  request: PostCreateRequest,
52
- credentials: AuthCredentials,
52
+ credentials: AuthCredentials, # noqa: ARG002
53
53
  ) -> Post:
54
54
  """Execute the TikTok video post creation process.
55
55