marqetive-lib 0.1.0__py3-none-any.whl → 0.1.2__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 +9 -9
- marqetive/core/__init__.py +1 -1
- marqetive/core/account_factory.py +2 -2
- marqetive/core/base_manager.py +4 -4
- marqetive/core/client.py +1 -1
- marqetive/core/registry.py +1 -1
- marqetive/platforms/__init__.py +3 -3
- marqetive/platforms/base.py +3 -3
- marqetive/platforms/exceptions.py +2 -1
- 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/models.py +2 -0
- marqetive/platforms/tiktok/__init__.py +7 -0
- marqetive/platforms/tiktok/client.py +277 -0
- marqetive/platforms/tiktok/exceptions.py +180 -0
- marqetive/platforms/tiktok/factory.py +188 -0
- marqetive/platforms/tiktok/manager.py +115 -0
- marqetive/platforms/tiktok/media.py +305 -0
- marqetive/platforms/twitter/__init__.py +3 -3
- marqetive/platforms/twitter/client.py +6 -6
- marqetive/platforms/twitter/exceptions.py +1 -1
- marqetive/platforms/twitter/factory.py +5 -5
- marqetive/platforms/twitter/manager.py +4 -4
- marqetive/platforms/twitter/media.py +4 -4
- marqetive/platforms/twitter/threads.py +2 -2
- marqetive/registry_init.py +4 -4
- marqetive/utils/__init__.py +3 -3
- marqetive/utils/file_handlers.py +1 -1
- marqetive/utils/oauth.py +137 -2
- marqetive/utils/token_validator.py +1 -1
- {marqetive_lib-0.1.0.dist-info → marqetive_lib-0.1.2.dist-info}/METADATA +1 -2
- marqetive_lib-0.1.2.dist-info/RECORD +48 -0
- marqetive_lib-0.1.0.dist-info/RECORD +0 -43
- {marqetive_lib-0.1.0.dist-info → marqetive_lib-0.1.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""TikTok media upload manager for handling video uploads.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for uploading videos to TikTok's API,
|
|
4
|
+
focusing on a chunked upload process suitable for large video files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from src.marqetive.platforms.exceptions import (
|
|
18
|
+
InvalidFileTypeError,
|
|
19
|
+
MediaUploadError,
|
|
20
|
+
)
|
|
21
|
+
from src.marqetive.utils.file_handlers import download_file
|
|
22
|
+
from src.marqetive.utils.media import (
|
|
23
|
+
detect_mime_type,
|
|
24
|
+
format_file_size,
|
|
25
|
+
get_chunk_count,
|
|
26
|
+
)
|
|
27
|
+
from src.marqetive.utils.retry import STANDARD_BACKOFF, retry_async
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Constants for TikTok Media Upload
|
|
32
|
+
DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024 # 5MB chunks
|
|
33
|
+
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024 # 10GB, hypothetical
|
|
34
|
+
MIN_VIDEO_DURATION_SECS = 3
|
|
35
|
+
MAX_VIDEO_DURATION_SECS = 600 # 10 minutes
|
|
36
|
+
DEFAULT_REQUEST_TIMEOUT = 300.0
|
|
37
|
+
|
|
38
|
+
# Hypothetical TikTok API endpoints for media
|
|
39
|
+
MEDIA_API_BASE_URL = "https://open.tiktokapis.com/v2/video"
|
|
40
|
+
|
|
41
|
+
# Supported MIME types for TikTok
|
|
42
|
+
SUPPORTED_VIDEO_TYPES = ["video/mp4", "video/quicktime"] # MOV
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProcessingState(str, Enum):
|
|
46
|
+
"""States for async media processing."""
|
|
47
|
+
|
|
48
|
+
PENDING = "pending"
|
|
49
|
+
PROCESSING = "processing"
|
|
50
|
+
READY = "ready"
|
|
51
|
+
FAILED = "failed"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class UploadProgress:
|
|
56
|
+
"""Progress information for a media upload."""
|
|
57
|
+
|
|
58
|
+
upload_id: str | None
|
|
59
|
+
file_path: str
|
|
60
|
+
bytes_uploaded: int
|
|
61
|
+
total_bytes: int
|
|
62
|
+
status: Literal["init", "uploading", "processing", "completed", "failed"]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def percentage(self) -> float:
|
|
66
|
+
if self.total_bytes == 0:
|
|
67
|
+
return 0.0
|
|
68
|
+
return (self.bytes_uploaded / self.total_bytes) * 100
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
return (
|
|
72
|
+
f"Upload Progress: {self.percentage:.1f}% "
|
|
73
|
+
f"({format_file_size(self.bytes_uploaded)} / "
|
|
74
|
+
f"{format_file_size(self.total_bytes)}) - {self.status}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class MediaUploadResult:
|
|
80
|
+
"""Result of a successful media upload."""
|
|
81
|
+
|
|
82
|
+
upload_id: str
|
|
83
|
+
media_id: str | None = None # Available after processing
|
|
84
|
+
size: int | None = None
|
|
85
|
+
processing_info: dict[str, Any] | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TikTokMediaManager:
|
|
89
|
+
"""Manages video uploads to the TikTok API.
|
|
90
|
+
|
|
91
|
+
This class handles the complexities of chunked uploads for large video files
|
|
92
|
+
and monitors the processing status of the uploaded media.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
access_token: str,
|
|
98
|
+
open_id: str,
|
|
99
|
+
*,
|
|
100
|
+
progress_callback: Callable[[UploadProgress], None] | None = None,
|
|
101
|
+
timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize the TikTok media manager."""
|
|
104
|
+
self.access_token = access_token
|
|
105
|
+
self.open_id = open_id
|
|
106
|
+
self.progress_callback = progress_callback
|
|
107
|
+
self.timeout = timeout
|
|
108
|
+
self.base_url = MEDIA_API_BASE_URL
|
|
109
|
+
self.client = httpx.AsyncClient(
|
|
110
|
+
timeout=httpx.Timeout(timeout),
|
|
111
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def __aenter__(self) -> "TikTokMediaManager":
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
118
|
+
await self.client.aclose()
|
|
119
|
+
|
|
120
|
+
async def upload_media(
|
|
121
|
+
self,
|
|
122
|
+
file_path: str,
|
|
123
|
+
*,
|
|
124
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
|
125
|
+
wait_for_processing: bool = True,
|
|
126
|
+
) -> MediaUploadResult:
|
|
127
|
+
"""Upload a video file to TikTok.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
file_path: The local path or URL of the video to upload.
|
|
131
|
+
chunk_size: The size of each chunk to upload in bytes.
|
|
132
|
+
wait_for_processing: If True, wait until the video is processed and ready.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A MediaUploadResult containing the upload and media IDs.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
FileNotFoundError: If the local file does not exist.
|
|
139
|
+
InvalidFileTypeError: If the file is not a supported video format.
|
|
140
|
+
MediaUploadError: If the upload or processing fails.
|
|
141
|
+
"""
|
|
142
|
+
if file_path.startswith(("http://", "https://")):
|
|
143
|
+
logger.info(f"Downloading media from URL: {file_path}")
|
|
144
|
+
file_path = await download_file(file_path)
|
|
145
|
+
|
|
146
|
+
if not os.path.exists(file_path):
|
|
147
|
+
raise FileNotFoundError(f"Media file not found: {file_path}")
|
|
148
|
+
|
|
149
|
+
mime_type = detect_mime_type(file_path)
|
|
150
|
+
file_size = os.path.getsize(file_path)
|
|
151
|
+
self._validate_media(mime_type, file_size)
|
|
152
|
+
|
|
153
|
+
# TikTok uses a one-step chunked upload
|
|
154
|
+
result = await self.chunked_upload(
|
|
155
|
+
file_path,
|
|
156
|
+
chunk_size=chunk_size,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Note: TikTok's real API might have a different flow for processing.
|
|
160
|
+
# This is a hypothetical implementation.
|
|
161
|
+
if wait_for_processing:
|
|
162
|
+
await self._wait_for_processing(result, file_path)
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
async def chunked_upload(
|
|
167
|
+
self, file_path: str, *, chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
168
|
+
) -> MediaUploadResult:
|
|
169
|
+
"""Upload a video using the chunked upload method.
|
|
170
|
+
|
|
171
|
+
This is a simplified model of how a chunked upload to TikTok might work.
|
|
172
|
+
The actual API may differ.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
file_path: Path to the video file.
|
|
176
|
+
chunk_size: Size of each upload chunk.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
MediaUploadResult with upload ID.
|
|
180
|
+
"""
|
|
181
|
+
file_size = os.path.getsize(file_path)
|
|
182
|
+
chunk_count = get_chunk_count(file_path, chunk_size)
|
|
183
|
+
upload_id = None # Hypothetically obtained from an INIT step if needed
|
|
184
|
+
|
|
185
|
+
if self.progress_callback:
|
|
186
|
+
self.progress_callback(
|
|
187
|
+
UploadProgress(
|
|
188
|
+
upload_id=upload_id,
|
|
189
|
+
file_path=file_path,
|
|
190
|
+
bytes_uploaded=0,
|
|
191
|
+
total_bytes=file_size,
|
|
192
|
+
status="init",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@retry_async(config=STANDARD_BACKOFF)
|
|
197
|
+
async def _do_upload() -> MediaUploadResult:
|
|
198
|
+
bytes_uploaded = 0
|
|
199
|
+
|
|
200
|
+
# Hypothetical: The real API may require separate INIT/APPEND/FINALIZE steps.
|
|
201
|
+
# This implementation models a single POST with all chunks.
|
|
202
|
+
async def file_iterator():
|
|
203
|
+
nonlocal bytes_uploaded
|
|
204
|
+
with open(file_path, "rb") as f:
|
|
205
|
+
for i, chunk in enumerate(iter(lambda: f.read(chunk_size), b"")):
|
|
206
|
+
bytes_uploaded += len(chunk)
|
|
207
|
+
if self.progress_callback:
|
|
208
|
+
self.progress_callback(
|
|
209
|
+
UploadProgress(
|
|
210
|
+
upload_id=upload_id,
|
|
211
|
+
file_path=file_path,
|
|
212
|
+
bytes_uploaded=bytes_uploaded,
|
|
213
|
+
total_bytes=file_size,
|
|
214
|
+
status="uploading",
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
logger.debug(f"Uploading chunk {i + 1}/{chunk_count}")
|
|
218
|
+
yield chunk
|
|
219
|
+
|
|
220
|
+
# In a real scenario, you'd likely get an upload URL first.
|
|
221
|
+
upload_url = f"{self.base_url}/upload/"
|
|
222
|
+
response = await self.client.post(
|
|
223
|
+
upload_url,
|
|
224
|
+
params={"open_id": self.open_id},
|
|
225
|
+
content=file_iterator(),
|
|
226
|
+
headers={"Content-Type": "video/mp4"},
|
|
227
|
+
)
|
|
228
|
+
response.raise_for_status()
|
|
229
|
+
result_data = response.json()
|
|
230
|
+
upload_id = result_data.get("data", {}).get("video", {}).get("video_id")
|
|
231
|
+
|
|
232
|
+
if not upload_id:
|
|
233
|
+
raise MediaUploadError(
|
|
234
|
+
"Upload succeeded but did not return a video ID.", platform="tiktok"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
result = MediaUploadResult(
|
|
238
|
+
upload_id=upload_id,
|
|
239
|
+
media_id=upload_id, # Assume upload_id is the media_id
|
|
240
|
+
size=file_size,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if self.progress_callback:
|
|
244
|
+
self.progress_callback(
|
|
245
|
+
UploadProgress(
|
|
246
|
+
upload_id=upload_id,
|
|
247
|
+
file_path=file_path,
|
|
248
|
+
bytes_uploaded=file_size,
|
|
249
|
+
total_bytes=file_size,
|
|
250
|
+
status="completed",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
logger.info(f"Chunked upload completed for TikTok: {upload_id}")
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
return await _do_upload()
|
|
259
|
+
except httpx.HTTPError as e:
|
|
260
|
+
raise MediaUploadError(
|
|
261
|
+
f"TikTok video upload failed: {e}",
|
|
262
|
+
platform="tiktok",
|
|
263
|
+
media_type=detect_mime_type(file_path),
|
|
264
|
+
) from e
|
|
265
|
+
|
|
266
|
+
async def _wait_for_processing(
|
|
267
|
+
self, result: MediaUploadResult, file_path: str
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Wait for async video processing to complete.
|
|
270
|
+
|
|
271
|
+
This is a hypothetical implementation.
|
|
272
|
+
"""
|
|
273
|
+
logger.info(f"Waiting for TikTok video processing: {result.upload_id}")
|
|
274
|
+
# In a real implementation, you would poll a status endpoint.
|
|
275
|
+
# Here, we'll just simulate a delay.
|
|
276
|
+
await asyncio.sleep(5) # Simulate processing time
|
|
277
|
+
if self.progress_callback:
|
|
278
|
+
self.progress_callback(
|
|
279
|
+
UploadProgress(
|
|
280
|
+
upload_id=result.upload_id,
|
|
281
|
+
file_path=file_path,
|
|
282
|
+
bytes_uploaded=os.path.getsize(file_path),
|
|
283
|
+
total_bytes=os.path.getsize(file_path),
|
|
284
|
+
status="processing",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
logger.info(f"TikTok video processing assumed complete: {result.upload_id}")
|
|
288
|
+
|
|
289
|
+
def _validate_media(self, mime_type: str, file_size: int) -> None:
|
|
290
|
+
"""Validate media type and size against TikTok's requirements."""
|
|
291
|
+
if mime_type not in SUPPORTED_VIDEO_TYPES:
|
|
292
|
+
raise InvalidFileTypeError(
|
|
293
|
+
f"Unsupported video type for TikTok: {mime_type}. "
|
|
294
|
+
f"Supported types are: {', '.join(SUPPORTED_VIDEO_TYPES)}",
|
|
295
|
+
platform="tiktok",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if file_size > MAX_VIDEO_SIZE:
|
|
299
|
+
raise MediaUploadError(
|
|
300
|
+
f"Video file size ({format_file_size(file_size)}) exceeds the "
|
|
301
|
+
f"TikTok limit of {format_file_size(MAX_VIDEO_SIZE)}",
|
|
302
|
+
platform="tiktok",
|
|
303
|
+
media_type=mime_type,
|
|
304
|
+
)
|
|
305
|
+
# Add duration validation logic here if possible to check before upload
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Twitter/X platform integration."""
|
|
2
2
|
|
|
3
|
-
from marqetive.platforms.twitter.client import TwitterClient
|
|
4
|
-
from marqetive.platforms.twitter.factory import TwitterAccountFactory
|
|
5
|
-
from marqetive.platforms.twitter.manager import TwitterPostManager
|
|
3
|
+
from src.marqetive.platforms.twitter.client import TwitterClient
|
|
4
|
+
from src.marqetive.platforms.twitter.factory import TwitterAccountFactory
|
|
5
|
+
from src.marqetive.platforms.twitter.manager import TwitterPostManager
|
|
6
6
|
|
|
7
7
|
__all__ = ["TwitterClient", "TwitterAccountFactory", "TwitterPostManager"]
|
|
@@ -13,8 +13,8 @@ import httpx
|
|
|
13
13
|
import tweepy
|
|
14
14
|
from pydantic import HttpUrl
|
|
15
15
|
|
|
16
|
-
from marqetive.platforms.base import SocialMediaPlatform
|
|
17
|
-
from marqetive.platforms.exceptions import (
|
|
16
|
+
from src.marqetive.platforms.base import SocialMediaPlatform
|
|
17
|
+
from src.marqetive.platforms.exceptions import (
|
|
18
18
|
MediaUploadError,
|
|
19
19
|
PlatformAuthError,
|
|
20
20
|
PlatformError,
|
|
@@ -22,7 +22,7 @@ from marqetive.platforms.exceptions import (
|
|
|
22
22
|
RateLimitError,
|
|
23
23
|
ValidationError,
|
|
24
24
|
)
|
|
25
|
-
from marqetive.platforms.models import (
|
|
25
|
+
from src.marqetive.platforms.models import (
|
|
26
26
|
AuthCredentials,
|
|
27
27
|
Comment,
|
|
28
28
|
CommentStatus,
|
|
@@ -33,8 +33,8 @@ from marqetive.platforms.models import (
|
|
|
33
33
|
PostStatus,
|
|
34
34
|
PostUpdateRequest,
|
|
35
35
|
)
|
|
36
|
-
from marqetive.platforms.twitter.media import TwitterMediaManager
|
|
37
|
-
from marqetive.platforms.twitter.threads import (
|
|
36
|
+
from src.marqetive.platforms.twitter.media import TwitterMediaManager
|
|
37
|
+
from src.marqetive.platforms.twitter.threads import (
|
|
38
38
|
ThreadResult,
|
|
39
39
|
TweetData,
|
|
40
40
|
TwitterThreadManager,
|
|
@@ -345,7 +345,7 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
345
345
|
|
|
346
346
|
async def update_post(
|
|
347
347
|
self,
|
|
348
|
-
post_id: str,
|
|
348
|
+
post_id: str, # noqa: ARG002
|
|
349
349
|
request: PostUpdateRequest, # noqa: ARG002
|
|
350
350
|
) -> Post:
|
|
351
351
|
"""Update a tweet.
|
|
@@ -9,7 +9,7 @@ This module provides comprehensive error handling for Twitter API errors includi
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from marqetive.platforms.exceptions import (
|
|
12
|
+
from src.marqetive.platforms.exceptions import (
|
|
13
13
|
MediaUploadError,
|
|
14
14
|
PlatformAuthError,
|
|
15
15
|
PlatformError,
|
|
@@ -4,11 +4,11 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
|
|
7
|
-
from marqetive.core.account_factory import BaseAccountFactory
|
|
8
|
-
from marqetive.platforms.exceptions import PlatformAuthError
|
|
9
|
-
from marqetive.platforms.models import AccountStatus, AuthCredentials
|
|
10
|
-
from marqetive.platforms.twitter.client import TwitterClient
|
|
11
|
-
from marqetive.utils.oauth import refresh_twitter_token
|
|
7
|
+
from src.marqetive.core.account_factory import BaseAccountFactory
|
|
8
|
+
from src.marqetive.platforms.exceptions import PlatformAuthError
|
|
9
|
+
from src.marqetive.platforms.models import AccountStatus, AuthCredentials
|
|
10
|
+
from src.marqetive.platforms.twitter.client import TwitterClient
|
|
11
|
+
from src.marqetive.utils.oauth import refresh_twitter_token
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from marqetive.core.base_manager import BasePostManager
|
|
7
|
-
from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
|
|
8
|
-
from marqetive.platforms.twitter.client import TwitterClient
|
|
9
|
-
from marqetive.platforms.twitter.factory import TwitterAccountFactory
|
|
6
|
+
from src.marqetive.core.base_manager import BasePostManager
|
|
7
|
+
from src.marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
|
|
8
|
+
from src.marqetive.platforms.twitter.client import TwitterClient
|
|
9
|
+
from src.marqetive.platforms.twitter.factory import TwitterAccountFactory
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -18,17 +18,17 @@ from typing import Any, Literal
|
|
|
18
18
|
|
|
19
19
|
import httpx
|
|
20
20
|
|
|
21
|
-
from marqetive.platforms.exceptions import (
|
|
21
|
+
from src.marqetive.platforms.exceptions import (
|
|
22
22
|
InvalidFileTypeError,
|
|
23
23
|
MediaUploadError,
|
|
24
24
|
)
|
|
25
|
-
from marqetive.utils.file_handlers import download_file
|
|
26
|
-
from marqetive.utils.media import (
|
|
25
|
+
from src.marqetive.utils.file_handlers import download_file
|
|
26
|
+
from src.marqetive.utils.media import (
|
|
27
27
|
detect_mime_type,
|
|
28
28
|
format_file_size,
|
|
29
29
|
get_chunk_count,
|
|
30
30
|
)
|
|
31
|
-
from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
|
|
31
|
+
from src.marqetive.utils.retry import STANDARD_BACKOFF, retry_async
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
@@ -9,8 +9,8 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
import tweepy
|
|
11
11
|
|
|
12
|
-
from marqetive.platforms.exceptions import PlatformError, ValidationError
|
|
13
|
-
from marqetive.platforms.models import Post, PostStatus
|
|
12
|
+
from src.marqetive.platforms.exceptions import PlatformError, ValidationError
|
|
13
|
+
from src.marqetive.platforms.models import Post, PostStatus
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
marqetive/registry_init.py
CHANGED
|
@@ -6,10 +6,10 @@ platform managers with the global registry.
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
-
from marqetive.core.registry import register_platform
|
|
10
|
-
from marqetive.platforms.instagram.manager import InstagramPostManager
|
|
11
|
-
from marqetive.platforms.linkedin.manager import LinkedInPostManager
|
|
12
|
-
from marqetive.platforms.twitter.manager import TwitterPostManager
|
|
9
|
+
from src.marqetive.core.registry import register_platform
|
|
10
|
+
from src.marqetive.platforms.instagram.manager import InstagramPostManager
|
|
11
|
+
from src.marqetive.platforms.linkedin.manager import LinkedInPostManager
|
|
12
|
+
from src.marqetive.platforms.twitter.manager import TwitterPostManager
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
marqetive/utils/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Utility functions for MarqetiveLib."""
|
|
2
2
|
|
|
3
|
-
from marqetive.utils.file_handlers import (
|
|
3
|
+
from src.marqetive.utils.file_handlers import (
|
|
4
4
|
TempFileManager,
|
|
5
5
|
download_file,
|
|
6
6
|
download_to_memory,
|
|
@@ -9,8 +9,8 @@ from marqetive.utils.file_handlers import (
|
|
|
9
9
|
stream_file_upload,
|
|
10
10
|
write_file_bytes,
|
|
11
11
|
)
|
|
12
|
-
from marqetive.utils.helpers import format_response, parse_query_params
|
|
13
|
-
from marqetive.utils.media import (
|
|
12
|
+
from src.marqetive.utils.helpers import format_response, parse_query_params
|
|
13
|
+
from src.marqetive.utils.media import (
|
|
14
14
|
MediaValidator,
|
|
15
15
|
chunk_file,
|
|
16
16
|
detect_mime_type,
|
marqetive/utils/file_handlers.py
CHANGED
marqetive/utils/oauth.py
CHANGED
|
@@ -10,8 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
|
-
from marqetive.platforms.exceptions import PlatformAuthError
|
|
14
|
-
from marqetive.platforms.models import AuthCredentials
|
|
13
|
+
from src.marqetive.platforms.exceptions import PlatformAuthError
|
|
14
|
+
from src.marqetive.platforms.models import AuthCredentials
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -263,3 +263,138 @@ async def refresh_instagram_token(
|
|
|
263
263
|
f"Network error refreshing Instagram token: {e}",
|
|
264
264
|
platform="instagram",
|
|
265
265
|
) from e
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def refresh_tiktok_token(
|
|
269
|
+
credentials: AuthCredentials,
|
|
270
|
+
client_id: str,
|
|
271
|
+
client_secret: str,
|
|
272
|
+
) -> AuthCredentials:
|
|
273
|
+
"""Refresh TikTok OAuth2 access token.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
credentials: Current credentials with refresh token.
|
|
277
|
+
client_id: TikTok OAuth client ID (client_key).
|
|
278
|
+
client_secret: TikTok OAuth client secret.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Updated credentials with new access token.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
PlatformAuthError: If refresh fails.
|
|
285
|
+
"""
|
|
286
|
+
if not credentials.refresh_token:
|
|
287
|
+
raise PlatformAuthError(
|
|
288
|
+
"No refresh token available",
|
|
289
|
+
platform="tiktok",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
token_url = "https://open.tiktokapis.com/v2/oauth/token/"
|
|
293
|
+
|
|
294
|
+
params = {
|
|
295
|
+
"client_key": client_id,
|
|
296
|
+
"client_secret": client_secret,
|
|
297
|
+
"grant_type": "refresh_token",
|
|
298
|
+
"refresh_token": credentials.refresh_token,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
async with httpx.AsyncClient() as client:
|
|
303
|
+
response = await client.post(
|
|
304
|
+
token_url,
|
|
305
|
+
data=params,
|
|
306
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
307
|
+
timeout=30.0,
|
|
308
|
+
)
|
|
309
|
+
response.raise_for_status()
|
|
310
|
+
token_data = response.json()
|
|
311
|
+
|
|
312
|
+
except httpx.HTTPStatusError as e:
|
|
313
|
+
logger.error(f"HTTP error refreshing tiktok token: {e.response.status_code}")
|
|
314
|
+
raise PlatformAuthError(
|
|
315
|
+
f"Failed to refresh token: {e.response.text}",
|
|
316
|
+
platform="tiktok",
|
|
317
|
+
status_code=e.response.status_code,
|
|
318
|
+
) from e
|
|
319
|
+
|
|
320
|
+
except httpx.HTTPError as e:
|
|
321
|
+
logger.error(f"Network error refreshing tiktok token: {e}")
|
|
322
|
+
raise PlatformAuthError(
|
|
323
|
+
f"Network error refreshing token: {e}",
|
|
324
|
+
platform="tiktok",
|
|
325
|
+
) from e
|
|
326
|
+
|
|
327
|
+
# Update credentials
|
|
328
|
+
credentials.access_token = token_data["access_token"]
|
|
329
|
+
|
|
330
|
+
# TikTok might provide new refresh token
|
|
331
|
+
if "refresh_token" in token_data:
|
|
332
|
+
credentials.refresh_token = token_data["refresh_token"]
|
|
333
|
+
|
|
334
|
+
# Calculate expiry
|
|
335
|
+
if "expires_in" in token_data:
|
|
336
|
+
expires_in = int(token_data["expires_in"])
|
|
337
|
+
credentials.expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
338
|
+
|
|
339
|
+
return credentials
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async def fetch_tiktok_token(
|
|
343
|
+
code: str,
|
|
344
|
+
client_id: str,
|
|
345
|
+
client_secret: str,
|
|
346
|
+
redirect_uri: str,
|
|
347
|
+
code_verifier: str | None = None,
|
|
348
|
+
) -> dict[str, Any]:
|
|
349
|
+
"""Fetch a TikTok OAuth2 access token using an authorization code.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
code: The authorization code from the callback.
|
|
353
|
+
client_id: TikTok OAuth client ID (client_key).
|
|
354
|
+
client_secret: TikTok OAuth client secret.
|
|
355
|
+
redirect_uri: The redirect URI used for the authorization request.
|
|
356
|
+
code_verifier: PKCE code verifier for mobile/desktop apps.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Token response dictionary with access_token, refresh_token, etc.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
PlatformAuthError: If token fetch fails.
|
|
363
|
+
"""
|
|
364
|
+
token_url = "https://open.tiktokapis.com/v2/oauth/token/"
|
|
365
|
+
|
|
366
|
+
params = {
|
|
367
|
+
"client_key": client_id,
|
|
368
|
+
"client_secret": client_secret,
|
|
369
|
+
"code": code,
|
|
370
|
+
"grant_type": "authorization_code",
|
|
371
|
+
"redirect_uri": redirect_uri,
|
|
372
|
+
}
|
|
373
|
+
if code_verifier:
|
|
374
|
+
params["code_verifier"] = code_verifier
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
async with httpx.AsyncClient() as client:
|
|
378
|
+
response = await client.post(
|
|
379
|
+
token_url,
|
|
380
|
+
data=params,
|
|
381
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
382
|
+
timeout=30.0,
|
|
383
|
+
)
|
|
384
|
+
response.raise_for_status()
|
|
385
|
+
return response.json()
|
|
386
|
+
|
|
387
|
+
except httpx.HTTPStatusError as e:
|
|
388
|
+
logger.error(f"HTTP error fetching tiktok token: {e.response.status_code}")
|
|
389
|
+
raise PlatformAuthError(
|
|
390
|
+
f"Failed to fetch token: {e.response.text}",
|
|
391
|
+
platform="tiktok",
|
|
392
|
+
status_code=e.response.status_code,
|
|
393
|
+
) from e
|
|
394
|
+
|
|
395
|
+
except httpx.HTTPError as e:
|
|
396
|
+
logger.error(f"Network error fetching tiktok token: {e}")
|
|
397
|
+
raise PlatformAuthError(
|
|
398
|
+
f"Network error fetching token: {e}",
|
|
399
|
+
platform="tiktok",
|
|
400
|
+
) from e
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: marqetive-lib
|
|
3
|
-
Version: 0.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
|