marqetive-lib 0.1.9__py3-none-any.whl → 0.1.11__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.
@@ -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
- Instagram requires a two-step process:
220
- 1. Create a media container
221
- 2. Publish the container
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
- # Step 1: Create media container
244
- container_params: dict[str, Any] = {
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
- if request.media_urls:
249
- # Validate URL to prevent SSRF attacks
250
- validated_url = validate_media_url(
251
- request.media_urls[0], platform=self.platform_name
252
- )
253
- container_params["image_url"] = validated_url
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
- if request.content:
256
- container_params["caption"] = request.content
279
+ def _get_media_type(self, request: PostCreateRequest) -> MediaType:
280
+ """Extract media type from request.
257
281
 
258
- try:
259
- # Create container
260
- container_response = await self.api_client.post(
261
- f"/{self.instagram_account_id}/media",
262
- data=container_params,
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
- # Step 2: Publish container
267
- publish_response = await self.api_client.post(
268
- f"/{self.instagram_account_id}/media_publish",
269
- data={
270
- "creation_id": container_id,
271
- "access_token": self.credentials.access_token,
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
- # Fetch full post details
277
- return await self.get_post(post_id)
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
- except httpx.HTTPError as e:
280
- raise MediaUploadError(
281
- f"Failed to create Instagram post: {e}",
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
- ) from e
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.
@@ -52,7 +52,7 @@ MAX_VIDEO_SIZE = 512 * 1024 * 1024 # 512MB for videos
52
52
  DEFAULT_REQUEST_TIMEOUT = 120.0 # 2 minutes
53
53
 
54
54
  # Twitter API v2 media upload endpoints
55
- MEDIA_UPLOAD_BASE_URL = "https://upload.x.com/1.1/media"
55
+ MEDIA_UPLOAD_BASE_URL = "https://api.x.com/2/media"
56
56
 
57
57
 
58
58
  class MediaCategory(str, Enum):
@@ -364,7 +364,7 @@ class TwitterMediaManager:
364
364
 
365
365
  # Upload
366
366
  response = await self.client.post(
367
- f"{self.base_url}/upload.json",
367
+ f"{self.base_url}/upload",
368
368
  files=files,
369
369
  data=data,
370
370
  )
@@ -555,7 +555,7 @@ class TwitterMediaManager:
555
555
  params["additional_owners"] = ",".join(additional_owners)
556
556
 
557
557
  response = await self.client.post(
558
- f"{self.base_url}/upload.json",
558
+ f"{self.base_url}/upload",
559
559
  params=params,
560
560
  )
561
561
  response.raise_for_status()
@@ -590,7 +590,7 @@ class TwitterMediaManager:
590
590
  }
591
591
 
592
592
  response = await self.client.post(
593
- f"{self.base_url}/upload.json",
593
+ f"{self.base_url}/upload",
594
594
  params=params,
595
595
  files=files,
596
596
  )
@@ -616,7 +616,7 @@ class TwitterMediaManager:
616
616
  }
617
617
 
618
618
  response = await self.client.post(
619
- f"{self.base_url}/upload.json",
619
+ f"{self.base_url}/upload",
620
620
  params=params,
621
621
  )
622
622
  response.raise_for_status()
@@ -650,7 +650,7 @@ class TwitterMediaManager:
650
650
  }
651
651
 
652
652
  response = await self.client.get(
653
- f"{self.base_url}/upload.json",
653
+ f"{self.base_url}/upload",
654
654
  params=params,
655
655
  )
656
656
  response.raise_for_status()
@@ -684,7 +684,7 @@ class TwitterMediaManager:
684
684
 
685
685
  try:
686
686
  response = await self.client.post(
687
- f"{self.base_url}/metadata/create.json",
687
+ f"{self.base_url}/metadata/create",
688
688
  json={
689
689
  "media_id": media_id,
690
690
  "alt_text": {"text": alt_text},
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.9
3
+ Version: 0.1.11
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 +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=vOx5HpgrxanBIFFC9VgmCNguH-njRGChnyp6Rr1r1Xc,26191
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
@@ -24,7 +24,7 @@ marqetive/platforms/tiktok/models.py,sha256=WWdjuFqhTIR8SnHkz-8UaNc5Mm2PrGomwQ3W
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
26
26
  marqetive/platforms/twitter/exceptions.py,sha256=eZ-dJKOXH_-bAMg29zWKbEqMFud29piEJ5IWfC9wFts,8926
27
- marqetive/platforms/twitter/media.py,sha256=9j7JQpdlOhkMfQkDH0dLpp6HmlYkeB6SvNosRx5Oab8,27152
27
+ marqetive/platforms/twitter/media.py,sha256=UlXKGt1DSz5gkDgV20ezz2rfb3NOvXiLULxthdoPsK0,27117
28
28
  marqetive/platforms/twitter/models.py,sha256=yPQlx40SlNmz7YGasXUqdx7rEDEgrQ64aYovlPKo6oc,2126
29
29
  marqetive/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  marqetive/utils/__init__.py,sha256=bSrNajbxYBSKQayrPviLz8JeGjplnyK8y_NGDtgb7yQ,977
@@ -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.9.dist-info/METADATA,sha256=jdnVbFyp8VBoa2wtc3OMObF0RUDNucFAm6IeezeGYG8,7875
38
- marqetive_lib-0.1.9.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
39
- marqetive_lib-0.1.9.dist-info/RECORD,,
37
+ marqetive_lib-0.1.11.dist-info/METADATA,sha256=RZfg5vQ_7QReP15Xb3j2DScA6e-c5DA2de_xDiPbr4Q,7876
38
+ marqetive_lib-0.1.11.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
39
+ marqetive_lib-0.1.11.dist-info/RECORD,,