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.
- marqetive/__init__.py +13 -13
- marqetive/core/__init__.py +1 -1
- marqetive/core/account_factory.py +2 -2
- marqetive/core/base_manager.py +4 -4
- marqetive/core/registry.py +3 -3
- marqetive/platforms/__init__.py +6 -6
- marqetive/platforms/base.py +3 -3
- marqetive/platforms/instagram/__init__.py +3 -3
- marqetive/platforms/instagram/client.py +4 -4
- marqetive/platforms/instagram/exceptions.py +1 -1
- marqetive/platforms/instagram/factory.py +5 -5
- marqetive/platforms/instagram/manager.py +4 -4
- marqetive/platforms/instagram/media.py +2 -2
- marqetive/platforms/linkedin/__init__.py +3 -3
- marqetive/platforms/linkedin/client.py +4 -4
- marqetive/platforms/linkedin/exceptions.py +1 -1
- marqetive/platforms/linkedin/factory.py +5 -5
- marqetive/platforms/linkedin/manager.py +4 -4
- marqetive/platforms/linkedin/media.py +4 -4
- marqetive/platforms/tiktok/__init__.py +3 -3
- marqetive/platforms/tiktok/client.py +319 -104
- marqetive/platforms/tiktok/exceptions.py +170 -66
- marqetive/platforms/tiktok/factory.py +5 -5
- marqetive/platforms/tiktok/manager.py +5 -5
- marqetive/platforms/tiktok/media.py +547 -159
- marqetive/platforms/twitter/__init__.py +3 -3
- marqetive/platforms/twitter/client.py +7 -53
- marqetive/platforms/twitter/exceptions.py +1 -1
- marqetive/platforms/twitter/factory.py +5 -6
- marqetive/platforms/twitter/manager.py +4 -4
- marqetive/platforms/twitter/media.py +4 -4
- marqetive/registry_init.py +10 -8
- marqetive/utils/__init__.py +3 -3
- marqetive/utils/file_handlers.py +1 -1
- marqetive/utils/oauth.py +2 -2
- marqetive/utils/token_validator.py +1 -1
- {marqetive_lib-0.1.2.dist-info → marqetive_lib-0.1.3.dist-info}/METADATA +1 -1
- marqetive_lib-0.1.3.dist-info/RECORD +47 -0
- marqetive/platforms/twitter/threads.py +0 -442
- marqetive_lib-0.1.2.dist-info/RECORD +0 -48
- {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
|
|
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
|
-
|
|
26
|
-
ACCESS_TOKEN_EXPIRED =
|
|
27
|
-
SCOPE_NOT_AUTHORIZED =
|
|
28
|
-
|
|
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 =
|
|
33
|
-
|
|
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 =
|
|
37
|
-
USER_NOT_FOUND =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Validation Errors
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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[
|
|
72
|
+
ERROR_MESSAGES: dict[str, str] = {
|
|
73
|
+
# Success
|
|
74
|
+
TikTokErrorCode.OK: "Request completed successfully.",
|
|
58
75
|
# Authentication
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
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
|
|
133
|
+
error_code or "", error_message or "An unknown TikTok API error occurred"
|
|
110
134
|
)
|
|
111
135
|
|
|
112
|
-
#
|
|
113
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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=
|
|
161
|
+
retry_after=None,
|
|
129
162
|
)
|
|
130
163
|
|
|
131
|
-
|
|
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="",
|
|
172
|
+
post_id="",
|
|
134
173
|
platform="tiktok",
|
|
135
174
|
status_code=status_code,
|
|
136
175
|
message=friendly_message,
|
|
137
176
|
)
|
|
138
177
|
|
|
139
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
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
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
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
|
|