marqetive-lib 0.1.8__py3-none-any.whl → 0.1.10__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/platforms/instagram/client.py +197 -36
- marqetive/platforms/tiktok/client.py +19 -8
- marqetive/platforms/tiktok/media.py +33 -4
- {marqetive_lib-0.1.8.dist-info → marqetive_lib-0.1.10.dist-info}/METADATA +1 -1
- {marqetive_lib-0.1.8.dist-info → marqetive_lib-0.1.10.dist-info}/RECORD +6 -6
- {marqetive_lib-0.1.8.dist-info → marqetive_lib-0.1.10.dist-info}/WHEEL +0 -0
|
@@ -216,12 +216,19 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
216
216
|
async def create_post(self, request: PostCreateRequest) -> Post:
|
|
217
217
|
"""Create and publish an Instagram post.
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
Automatically routes to the appropriate handler based on content type:
|
|
220
|
+
- IMAGE: Single image feed post
|
|
221
|
+
- CAROUSEL: Multiple images (2-10)
|
|
222
|
+
- REEL/VIDEO: Video content as Reel
|
|
223
|
+
- STORY: Story (image or video)
|
|
224
|
+
|
|
225
|
+
Content type can be specified via:
|
|
226
|
+
1. InstagramPostRequest.media_type field
|
|
227
|
+
2. PostCreateRequest.additional_data["media_type"]
|
|
228
|
+
3. Auto-detected from media_urls count (1 = single, 2+ = carousel)
|
|
222
229
|
|
|
223
230
|
Args:
|
|
224
|
-
request: Post creation request.
|
|
231
|
+
request: Post creation request (PostCreateRequest or InstagramPostRequest).
|
|
225
232
|
|
|
226
233
|
Returns:
|
|
227
234
|
Created Post object.
|
|
@@ -229,6 +236,22 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
229
236
|
Raises:
|
|
230
237
|
ValidationError: If request is invalid.
|
|
231
238
|
MediaUploadError: If media upload fails.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> # Single image post
|
|
242
|
+
>>> request = PostCreateRequest(
|
|
243
|
+
... content="Hello Instagram!",
|
|
244
|
+
... media_urls=["https://example.com/image.jpg"]
|
|
245
|
+
... )
|
|
246
|
+
>>> post = await client.create_post(request)
|
|
247
|
+
|
|
248
|
+
>>> # Reel via additional_data
|
|
249
|
+
>>> request = PostCreateRequest(
|
|
250
|
+
... content="Check out this video!",
|
|
251
|
+
... media_urls=["https://example.com/video.mp4"],
|
|
252
|
+
... additional_data={"media_type": "reel"}
|
|
253
|
+
... )
|
|
254
|
+
>>> post = await client.create_post(request)
|
|
232
255
|
"""
|
|
233
256
|
if not self.api_client:
|
|
234
257
|
raise RuntimeError("Client must be used as async context manager")
|
|
@@ -240,47 +263,185 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
240
263
|
field="media",
|
|
241
264
|
)
|
|
242
265
|
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
"access_token": self.credentials.access_token,
|
|
246
|
-
}
|
|
266
|
+
# Determine content type from request
|
|
267
|
+
media_type = self._get_media_type(request)
|
|
247
268
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
253
|
-
|
|
269
|
+
# Route to appropriate handler based on content type
|
|
270
|
+
if media_type == MediaType.STORY:
|
|
271
|
+
return await self._create_story_post(request)
|
|
272
|
+
elif media_type in (MediaType.REEL, MediaType.VIDEO):
|
|
273
|
+
return await self._create_reel_post(request)
|
|
274
|
+
elif media_type == MediaType.CAROUSEL or len(request.media_urls) > 1:
|
|
275
|
+
return await self._create_carousel_post(request)
|
|
276
|
+
else:
|
|
277
|
+
return await self._create_single_image_post(request)
|
|
254
278
|
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
def _get_media_type(self, request: PostCreateRequest) -> MediaType:
|
|
280
|
+
"""Extract media type from request.
|
|
257
281
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
Checks in order:
|
|
283
|
+
1. InstagramPostRequest.media_type (if using platform-specific model)
|
|
284
|
+
2. PostCreateRequest.additional_data["media_type"]
|
|
285
|
+
3. Auto-detect from media count
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
request: Post creation request.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
MediaType enum value.
|
|
292
|
+
"""
|
|
293
|
+
# Check if it's an InstagramPostRequest with media_type
|
|
294
|
+
# Use getattr to avoid type checker issues with duck typing
|
|
295
|
+
media_type_attr = getattr(request, "media_type", None)
|
|
296
|
+
if media_type_attr is not None and isinstance(media_type_attr, MediaType):
|
|
297
|
+
return media_type_attr
|
|
298
|
+
|
|
299
|
+
# Check additional_data for media_type
|
|
300
|
+
if request.additional_data:
|
|
301
|
+
media_type_str = request.additional_data.get("media_type")
|
|
302
|
+
if media_type_str:
|
|
303
|
+
# Normalize and convert to enum
|
|
304
|
+
media_type_str = media_type_str.lower()
|
|
305
|
+
type_map = {
|
|
306
|
+
"image": MediaType.IMAGE,
|
|
307
|
+
"video": MediaType.VIDEO,
|
|
308
|
+
"reel": MediaType.REEL,
|
|
309
|
+
"reels": MediaType.REEL,
|
|
310
|
+
"story": MediaType.STORY,
|
|
311
|
+
"stories": MediaType.STORY,
|
|
312
|
+
"carousel": MediaType.CAROUSEL,
|
|
313
|
+
}
|
|
314
|
+
if media_type_str in type_map:
|
|
315
|
+
return type_map[media_type_str]
|
|
316
|
+
|
|
317
|
+
# Auto-detect: multiple URLs = carousel, single = image
|
|
318
|
+
if len(request.media_urls) > 1:
|
|
319
|
+
return MediaType.CAROUSEL
|
|
320
|
+
|
|
321
|
+
return MediaType.IMAGE
|
|
322
|
+
|
|
323
|
+
async def _create_single_image_post(self, request: PostCreateRequest) -> Post:
|
|
324
|
+
"""Create a single image feed post.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
request: Post creation request.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Created Post object.
|
|
331
|
+
"""
|
|
332
|
+
if not self._media_manager:
|
|
333
|
+
raise RuntimeError("Client must be used as async context manager")
|
|
334
|
+
|
|
335
|
+
# Extract options from additional_data
|
|
336
|
+
location_id = request.additional_data.get("location_id") or request.location
|
|
337
|
+
share_to_feed = request.additional_data.get("share_to_feed", True)
|
|
338
|
+
|
|
339
|
+
# Validate URL
|
|
340
|
+
validated_url = validate_media_url(
|
|
341
|
+
request.media_urls[0], platform=self.platform_name
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Create media item
|
|
345
|
+
media_item = MediaItem(url=validated_url, type="image")
|
|
346
|
+
|
|
347
|
+
# Get alt_texts if provided
|
|
348
|
+
alt_texts = request.additional_data.get("alt_texts", [])
|
|
349
|
+
if alt_texts:
|
|
350
|
+
media_item = MediaItem(
|
|
351
|
+
url=validated_url, type="image", alt_text=alt_texts[0]
|
|
263
352
|
)
|
|
264
|
-
container_id = container_response.data["id"]
|
|
265
353
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
354
|
+
# Create container and publish
|
|
355
|
+
container_ids = await self._media_manager.create_feed_containers(
|
|
356
|
+
[media_item],
|
|
357
|
+
caption=request.content,
|
|
358
|
+
location_id=location_id,
|
|
359
|
+
share_to_feed=share_to_feed,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
result = await self._media_manager.publish_container(container_ids[0])
|
|
363
|
+
return await self.get_post(result.media_id)
|
|
364
|
+
|
|
365
|
+
async def _create_carousel_post(self, request: PostCreateRequest) -> Post:
|
|
366
|
+
"""Create a carousel post (2-10 images).
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
request: Post creation request.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Created Post object.
|
|
373
|
+
"""
|
|
374
|
+
# Extract options from additional_data
|
|
375
|
+
alt_texts = request.additional_data.get("alt_texts")
|
|
376
|
+
location_id = request.additional_data.get("location_id") or request.location
|
|
377
|
+
|
|
378
|
+
return await self.create_carousel(
|
|
379
|
+
media_urls=request.media_urls,
|
|
380
|
+
caption=request.content,
|
|
381
|
+
alt_texts=alt_texts,
|
|
382
|
+
location_id=location_id,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
async def _create_reel_post(self, request: PostCreateRequest) -> Post:
|
|
386
|
+
"""Create a Reel (video post).
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
request: Post creation request.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Created Post object.
|
|
393
|
+
"""
|
|
394
|
+
if not request.media_urls:
|
|
395
|
+
raise ValidationError(
|
|
396
|
+
"Reel requires a video URL",
|
|
397
|
+
platform=self.platform_name,
|
|
398
|
+
field="media_urls",
|
|
273
399
|
)
|
|
274
|
-
post_id = publish_response.data["id"]
|
|
275
400
|
|
|
276
|
-
|
|
277
|
-
|
|
401
|
+
# Extract reel-specific options from additional_data
|
|
402
|
+
cover_url = request.additional_data.get("cover_url")
|
|
403
|
+
share_to_feed = request.additional_data.get("share_to_feed", True)
|
|
278
404
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
405
|
+
return await self.create_reel(
|
|
406
|
+
video_url=request.media_urls[0],
|
|
407
|
+
caption=request.content,
|
|
408
|
+
cover_url=cover_url,
|
|
409
|
+
share_to_feed=share_to_feed,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def _create_story_post(self, request: PostCreateRequest) -> Post:
|
|
413
|
+
"""Create an Instagram Story.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
request: Post creation request.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Created Post object.
|
|
420
|
+
"""
|
|
421
|
+
if not request.media_urls:
|
|
422
|
+
raise ValidationError(
|
|
423
|
+
"Story requires a media URL",
|
|
282
424
|
platform=self.platform_name,
|
|
283
|
-
|
|
425
|
+
field="media_urls",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Determine if it's image or video from additional_data or file extension
|
|
429
|
+
story_media_type: Literal["image", "video"] = "image"
|
|
430
|
+
|
|
431
|
+
# Check additional_data for explicit type
|
|
432
|
+
if request.additional_data.get("story_media_type"):
|
|
433
|
+
story_media_type = request.additional_data["story_media_type"]
|
|
434
|
+
else:
|
|
435
|
+
# Auto-detect from URL extension
|
|
436
|
+
url_lower = request.media_urls[0].lower()
|
|
437
|
+
video_extensions = (".mp4", ".mov", ".avi", ".webm")
|
|
438
|
+
if any(url_lower.endswith(ext) for ext in video_extensions):
|
|
439
|
+
story_media_type = "video"
|
|
440
|
+
|
|
441
|
+
return await self.create_story(
|
|
442
|
+
media_url=request.media_urls[0],
|
|
443
|
+
media_type=story_media_type,
|
|
444
|
+
)
|
|
284
445
|
|
|
285
446
|
async def get_post(self, post_id: str) -> Post:
|
|
286
447
|
"""Retrieve an Instagram post by ID.
|
|
@@ -231,15 +231,26 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
231
231
|
wait_for_publish=True,
|
|
232
232
|
)
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
platform=self.platform_name,
|
|
239
|
-
)
|
|
234
|
+
# 4. Return post - either fetch full details or create minimal Post
|
|
235
|
+
if upload_result.video_id:
|
|
236
|
+
# Fetch the created post to return full Post object
|
|
237
|
+
return await self.get_post(upload_result.video_id)
|
|
240
238
|
|
|
241
|
-
#
|
|
242
|
-
|
|
239
|
+
# For private/SELF_ONLY posts, TikTok may not return video_id
|
|
240
|
+
# Return a minimal Post object with publish_id
|
|
241
|
+
return Post(
|
|
242
|
+
post_id=upload_result.publish_id,
|
|
243
|
+
platform=self.platform_name,
|
|
244
|
+
content=request.content,
|
|
245
|
+
status=PostStatus.PUBLISHED,
|
|
246
|
+
created_at=datetime.now(),
|
|
247
|
+
raw_data={
|
|
248
|
+
"publish_id": upload_result.publish_id,
|
|
249
|
+
"upload_status": upload_result.status,
|
|
250
|
+
"privacy_level": privacy_level.value,
|
|
251
|
+
"note": "Video published but video_id not returned (common for private posts)",
|
|
252
|
+
},
|
|
253
|
+
)
|
|
243
254
|
|
|
244
255
|
async def get_post(self, post_id: str) -> Post:
|
|
245
256
|
"""Retrieve a TikTok video by its ID.
|
|
@@ -531,12 +531,22 @@ class TikTokMediaManager:
|
|
|
531
531
|
result_data = data.get("data", {})
|
|
532
532
|
status = result_data.get("status", PublishStatus.FAILED)
|
|
533
533
|
|
|
534
|
+
logger.debug(f"TikTok publish status response: {data}")
|
|
535
|
+
|
|
534
536
|
video_id = None
|
|
535
537
|
if status == PublishStatus.PUBLISH_COMPLETE:
|
|
536
|
-
# Video IDs are in publicaly_available_post_id array
|
|
538
|
+
# Video IDs are in publicaly_available_post_id array (note TikTok's typo)
|
|
539
|
+
# For private/SELF_ONLY posts, this may be empty
|
|
537
540
|
video_ids = result_data.get("publicaly_available_post_id", [])
|
|
538
541
|
if video_ids:
|
|
539
542
|
video_id = str(video_ids[0])
|
|
543
|
+
else:
|
|
544
|
+
# Try alternative field names that TikTok might use
|
|
545
|
+
video_id = result_data.get("video_id") or result_data.get("item_id")
|
|
546
|
+
logger.warning(
|
|
547
|
+
f"No video ID in publicaly_available_post_id, "
|
|
548
|
+
f"tried alternatives: video_id={video_id}"
|
|
549
|
+
)
|
|
540
550
|
|
|
541
551
|
return MediaUploadResult(
|
|
542
552
|
publish_id=publish_id,
|
|
@@ -713,6 +723,11 @@ class TikTokMediaManager:
|
|
|
713
723
|
def _normalize_chunk_size(self, chunk_size: int, file_size: int) -> int:
|
|
714
724
|
"""Normalize chunk size to TikTok's requirements.
|
|
715
725
|
|
|
726
|
+
TikTok chunk requirements:
|
|
727
|
+
- Minimum chunk size: 5MB (except for files smaller than 5MB)
|
|
728
|
+
- Maximum chunk size: 64MB (final chunk can be up to 128MB)
|
|
729
|
+
- All non-final chunks must be at least MIN_CHUNK_SIZE
|
|
730
|
+
|
|
716
731
|
Args:
|
|
717
732
|
chunk_size: Requested chunk size.
|
|
718
733
|
file_size: Total file size.
|
|
@@ -720,13 +735,27 @@ class TikTokMediaManager:
|
|
|
720
735
|
Returns:
|
|
721
736
|
Normalized chunk size within TikTok limits.
|
|
722
737
|
"""
|
|
723
|
-
# Files smaller than
|
|
724
|
-
|
|
738
|
+
# Files smaller than MAX_CHUNK_SIZE (64MB) should be uploaded as single chunk
|
|
739
|
+
# This avoids issues with the final chunk being smaller than MIN_CHUNK_SIZE
|
|
740
|
+
if file_size <= MAX_CHUNK_SIZE:
|
|
725
741
|
return file_size
|
|
726
742
|
|
|
727
|
-
#
|
|
743
|
+
# For larger files, ensure chunk size is within limits
|
|
728
744
|
chunk_size = max(MIN_CHUNK_SIZE, min(chunk_size, MAX_CHUNK_SIZE))
|
|
729
745
|
|
|
746
|
+
# Ensure the last chunk won't be smaller than MIN_CHUNK_SIZE
|
|
747
|
+
# If it would be, increase chunk size to make fewer, larger chunks
|
|
748
|
+
total_chunks = math.ceil(file_size / chunk_size)
|
|
749
|
+
last_chunk_size = file_size - (chunk_size * (total_chunks - 1))
|
|
750
|
+
|
|
751
|
+
if last_chunk_size < MIN_CHUNK_SIZE and total_chunks > 1:
|
|
752
|
+
# Recalculate to have fewer chunks with larger size
|
|
753
|
+
# Use ceiling division to ensure last chunk is large enough
|
|
754
|
+
total_chunks = math.ceil(file_size / MAX_CHUNK_SIZE)
|
|
755
|
+
chunk_size = math.ceil(file_size / total_chunks)
|
|
756
|
+
# Ensure still within limits
|
|
757
|
+
chunk_size = max(MIN_CHUNK_SIZE, min(chunk_size, MAX_CHUNK_SIZE))
|
|
758
|
+
|
|
730
759
|
return chunk_size
|
|
731
760
|
|
|
732
761
|
def _validate_media(self, mime_type: str, file_size: int) -> None:
|
|
@@ -7,7 +7,7 @@ marqetive/core/models.py,sha256=L2gA4FhW0feAXQFsz2ce1ttd0vScMRhatoTclhDGCU0,1472
|
|
|
7
7
|
marqetive/factory.py,sha256=irZ5oN8a__kXZH70UN2uI7TzqTXu66d4QZ1FoxSoiK8,14092
|
|
8
8
|
marqetive/platforms/__init__.py,sha256=RBxlQSGyELsulSnwf5uaE1ohxFc7jC61OO9CrKaZp48,1312
|
|
9
9
|
marqetive/platforms/instagram/__init__.py,sha256=c1Gs0ozG6D7Z-Uz_UQ7S3joL0qUTT9eUZPWcePyESk8,229
|
|
10
|
-
marqetive/platforms/instagram/client.py,sha256=
|
|
10
|
+
marqetive/platforms/instagram/client.py,sha256=_ccxn3RUZDooR2w-vQocsxhctRB_MAhn3ZKQroJMv8c,32217
|
|
11
11
|
marqetive/platforms/instagram/exceptions.py,sha256=TcD_pX4eSx_k4yW8DgfA6SGPiAz3VW7cMqM8DmiXIhg,8978
|
|
12
12
|
marqetive/platforms/instagram/media.py,sha256=0ZbUbpwJ025_hccL9X8qced_-LJGoL_-NdS84Op97VE,23228
|
|
13
13
|
marqetive/platforms/instagram/models.py,sha256=20v3m1037y3b_WlsKF8zAOgV23nFu63tfmmUN1CefOI,2769
|
|
@@ -17,9 +17,9 @@ marqetive/platforms/linkedin/exceptions.py,sha256=i5fARUkZik46bS3htZBwUInVzetsZx
|
|
|
17
17
|
marqetive/platforms/linkedin/media.py,sha256=iWXUfqDYGsrTqeM6CGZ7a8xjpbdJ5qESolQL8Fv2PIg,20341
|
|
18
18
|
marqetive/platforms/linkedin/models.py,sha256=n7DqwVxYSbGYBmeEJ1woCZ6XhUIHcLx8Gpm8uCBACzI,12620
|
|
19
19
|
marqetive/platforms/tiktok/__init__.py,sha256=BqjkXTZDyBlcY3lvREy13yP9h3RcDga8E6Rl6f5KPp8,238
|
|
20
|
-
marqetive/platforms/tiktok/client.py,sha256=
|
|
20
|
+
marqetive/platforms/tiktok/client.py,sha256=ErhJOskyWVJP1nfLeJl09fGEyVh3QEhjYCgRlVLV-JY,17704
|
|
21
21
|
marqetive/platforms/tiktok/exceptions.py,sha256=vxwyAKujMGZJh0LetG1QsLF95QfUs_kR6ujsWSHGqL0,10124
|
|
22
|
-
marqetive/platforms/tiktok/media.py,sha256=
|
|
22
|
+
marqetive/platforms/tiktok/media.py,sha256=aa47EDRA7woKGqKZkl_5XWu7kcRp2nT93Ol2skEQJpY,28592
|
|
23
23
|
marqetive/platforms/tiktok/models.py,sha256=WWdjuFqhTIR8SnHkz-8UaNc5Mm2PrGomwQ3W7pJcQFg,2962
|
|
24
24
|
marqetive/platforms/twitter/__init__.py,sha256=dvcgVT-v-JOtjSz-OUvxGrn_43OI6w_ep42Wx_nHTSM,217
|
|
25
25
|
marqetive/platforms/twitter/client.py,sha256=08jV2hQVmGOpnG3C05u7bCqL7KapWn7bSsG0wbN_t5M,23270
|
|
@@ -34,6 +34,6 @@ marqetive/utils/media.py,sha256=O1rISYdaP3CuuPxso7kqvxWXNfe2jjioNkaBc4cpwkY,1466
|
|
|
34
34
|
marqetive/utils/oauth.py,sha256=1SkYCE6dcyPvcDqbjRFSSBcKTwLJy8u3jAANPdftVmo,13108
|
|
35
35
|
marqetive/utils/retry.py,sha256=lAniJLMNWp9XsHrvU0XBNifpNEjfde4MGfd5hlFTPfA,7636
|
|
36
36
|
marqetive/utils/token_validator.py,sha256=dNvDeHs2Du5UyMMH2ZOW6ydR7OwOEKA4c9e-rG0f9-0,6698
|
|
37
|
-
marqetive_lib-0.1.
|
|
38
|
-
marqetive_lib-0.1.
|
|
39
|
-
marqetive_lib-0.1.
|
|
37
|
+
marqetive_lib-0.1.10.dist-info/METADATA,sha256=oFcLMxRxsYYnE1CQDOh2LZ0OCY3GY_n5FNgDFJ9h9_8,7876
|
|
38
|
+
marqetive_lib-0.1.10.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
39
|
+
marqetive_lib-0.1.10.dist-info/RECORD,,
|
|
File without changes
|