marqetive-lib 0.1.11__tar.gz → 0.1.13__tar.gz

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 (39) hide show
  1. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/PKG-INFO +1 -1
  2. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/pyproject.toml +1 -1
  3. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/twitter/client.py +21 -18
  4. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/twitter/media.py +50 -42
  5. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/README.md +0 -0
  6. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/__init__.py +0 -0
  7. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/core/__init__.py +0 -0
  8. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/core/base.py +0 -0
  9. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/core/client.py +0 -0
  10. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/core/exceptions.py +0 -0
  11. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/core/models.py +0 -0
  12. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/factory.py +0 -0
  13. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/__init__.py +0 -0
  14. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/instagram/__init__.py +0 -0
  15. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/instagram/client.py +0 -0
  16. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/instagram/exceptions.py +0 -0
  17. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/instagram/media.py +0 -0
  18. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/instagram/models.py +0 -0
  19. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/linkedin/__init__.py +0 -0
  20. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/linkedin/client.py +0 -0
  21. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/linkedin/exceptions.py +0 -0
  22. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/linkedin/media.py +0 -0
  23. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/linkedin/models.py +0 -0
  24. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/tiktok/__init__.py +0 -0
  25. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/tiktok/client.py +0 -0
  26. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/tiktok/exceptions.py +0 -0
  27. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/tiktok/media.py +0 -0
  28. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/tiktok/models.py +0 -0
  29. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/twitter/__init__.py +0 -0
  30. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/twitter/exceptions.py +0 -0
  31. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/platforms/twitter/models.py +0 -0
  32. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/py.typed +0 -0
  33. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/__init__.py +0 -0
  34. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/file_handlers.py +0 -0
  35. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/helpers.py +0 -0
  36. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/media.py +0 -0
  37. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/oauth.py +0 -0
  38. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/retry.py +0 -0
  39. {marqetive_lib-0.1.11 → marqetive_lib-0.1.13}/src/marqetive/utils/token_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Modern Python utilities for web APIs
5
5
  Keywords: api,utilities,web,http,marqetive
6
6
  Requires-Python: >=3.12
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "marqetive-lib"
7
- version = "0.1.11"
7
+ version = "0.1.13"
8
8
  description = "Modern Python utilities for web APIs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -207,14 +207,6 @@ class TwitterClient(SocialMediaPlatform):
207
207
  platform=self.platform_name,
208
208
  )
209
209
 
210
- # Validate tweet length (280 characters)
211
- if request.content and len(request.content) > 280:
212
- raise ValidationError(
213
- f"Tweet exceeds 280 characters ({len(request.content)} characters)",
214
- platform=self.platform_name,
215
- field="content",
216
- )
217
-
218
210
  try:
219
211
  media_ids = []
220
212
 
@@ -234,11 +226,17 @@ class TwitterClient(SocialMediaPlatform):
234
226
  if media_ids:
235
227
  tweet_params["media_ids"] = media_ids
236
228
 
237
- response = self._tweepy_client.create_tweet(**tweet_params)
229
+ response = self._tweepy_client.create_tweet(**tweet_params, user_auth=False)
238
230
  tweet_id = response.data["id"] # type: ignore[index]
239
231
 
240
- # Fetch full tweet details
241
- return await self.get_post(tweet_id)
232
+ # Return minimal Post object without fetching details
233
+ return Post(
234
+ post_id=tweet_id,
235
+ platform=self.platform_name,
236
+ content=request.content or "",
237
+ status=PostStatus.PUBLISHED,
238
+ created_at=datetime.now(),
239
+ )
242
240
 
243
241
  except tweepy.TweepyException as e:
244
242
  if "429" in str(e):
@@ -278,6 +276,7 @@ class TwitterClient(SocialMediaPlatform):
278
276
  ],
279
277
  expansions=["attachments.media_keys"],
280
278
  media_fields=["url", "type", "width", "height"],
279
+ user_auth=False,
281
280
  )
282
281
 
283
282
  if not response.data: # type: ignore[attr-defined]
@@ -339,7 +338,7 @@ class TwitterClient(SocialMediaPlatform):
339
338
  raise RuntimeError("Client must be used as async context manager")
340
339
 
341
340
  try:
342
- self._tweepy_client.delete_tweet(post_id)
341
+ self._tweepy_client.delete_tweet(post_id, user_auth=False)
343
342
  return True
344
343
 
345
344
  except tweepy.errors.NotFound as e: # type: ignore[attr-defined]
@@ -432,18 +431,22 @@ class TwitterClient(SocialMediaPlatform):
432
431
  response = self._tweepy_client.create_tweet(
433
432
  text=content,
434
433
  in_reply_to_tweet_id=post_id,
434
+ user_auth=False,
435
435
  )
436
436
 
437
437
  reply_id = response.data["id"] # type: ignore[index]
438
438
 
439
- # Fetch full reply details
440
- reply_response = self._tweepy_client.get_tweet(
441
- reply_id,
442
- tweet_fields=["created_at", "author_id", "public_metrics"],
439
+ # Return minimal Comment object without fetching details
440
+ return Comment(
441
+ comment_id=reply_id,
442
+ post_id=post_id,
443
+ platform=self.platform_name,
444
+ content=content,
445
+ author_id="",
446
+ created_at=datetime.now(),
447
+ status=CommentStatus.VISIBLE,
443
448
  )
444
449
 
445
- return self._parse_reply(reply_response.data, post_id) # type: ignore[attr-defined]
446
-
447
450
  except tweepy.TweepyException as e:
448
451
  raise PlatformError(
449
452
  f"Failed to create reply: {e}",
@@ -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://api.x.com/2/media"
55
+ MEDIA_UPLOAD_BASE_URL = "https://api.x.com/2/media/upload"
56
56
 
57
57
 
58
58
  class MediaCategory(str, Enum):
@@ -342,9 +342,9 @@ class TwitterMediaManager:
342
342
  async with aiofiles.open(file_path, "rb") as f:
343
343
  file_data = await f.read()
344
344
 
345
- # Prepare form data
345
+ # Prepare multipart form data
346
346
  files = {"media": (os.path.basename(file_path), file_data)}
347
- data = {}
347
+ data: dict[str, str] = {}
348
348
 
349
349
  if media_category:
350
350
  data["media_category"] = media_category.value
@@ -362,22 +362,24 @@ class TwitterMediaManager:
362
362
  total_bytes=file_size,
363
363
  )
364
364
 
365
- # Upload
365
+ # Upload - v2 API uses base URL directly
366
366
  response = await self.client.post(
367
- f"{self.base_url}/upload",
367
+ self.base_url,
368
368
  files=files,
369
369
  data=data,
370
370
  )
371
371
  response.raise_for_status()
372
372
  result_data = response.json()
373
+ logger.debug(f"Simple upload response: {result_data}")
373
374
 
374
- # Parse result
375
- media_id = str(result_data["media_id"])
375
+ # Parse result - v2 API returns {"data": {"id": "..."}}
376
+ media_data = result_data["data"]
377
+ media_id = str(media_data["id"])
376
378
  result = MediaUploadResult(
377
379
  media_id=media_id,
378
- media_key=result_data.get("media_key"),
379
- size=result_data.get("size"),
380
- expires_after_secs=result_data.get("expires_after_secs"),
380
+ media_key=media_data.get("media_key"),
381
+ size=media_data.get("size"),
382
+ expires_after_secs=media_data.get("expires_after_secs"),
381
383
  )
382
384
 
383
385
  # Notify completion
@@ -544,23 +546,27 @@ class TwitterMediaManager:
544
546
 
545
547
  @retry_async(config=STANDARD_BACKOFF)
546
548
  async def _do_init() -> str:
547
- params = {
548
- "command": "INIT",
549
+ # v2 API uses JSON body for INIT
550
+ json_data: dict[str, Any] = {
549
551
  "total_bytes": total_bytes,
550
552
  "media_type": media_type,
551
553
  "media_category": media_category.value,
554
+ "shared": False,
552
555
  }
553
556
 
554
557
  if additional_owners:
555
- params["additional_owners"] = ",".join(additional_owners)
558
+ json_data["additional_owners"] = ",".join(additional_owners)
556
559
 
560
+ logger.debug(f"INIT request: {json_data}")
557
561
  response = await self.client.post(
558
- f"{self.base_url}/upload",
559
- params=params,
562
+ f"{self.base_url}/initialize",
563
+ json=json_data,
560
564
  )
561
565
  response.raise_for_status()
562
566
  result = response.json()
563
- return str(result["media_id"])
567
+ logger.debug(f"INIT response: {result}")
568
+ # v2 API returns {"data": {"id": "..."}}
569
+ return str(result["data"]["id"])
564
570
 
565
571
  return await _do_init()
566
572
 
@@ -582,19 +588,18 @@ class TwitterMediaManager:
582
588
 
583
589
  @retry_async(config=STANDARD_BACKOFF)
584
590
  async def _do_append() -> None:
591
+ # v2 API uses /{media_id}/append endpoint with form data
585
592
  files = {"media": (filename, chunk_data)}
586
- params = {
587
- "command": "APPEND",
588
- "media_id": media_id,
589
- "segment_index": segment_index,
590
- }
593
+ data = {"segment_index": str(segment_index)}
591
594
 
592
595
  response = await self.client.post(
593
- f"{self.base_url}/upload",
594
- params=params,
596
+ f"{self.base_url}/{media_id}/append",
595
597
  files=files,
598
+ data=data,
596
599
  )
600
+ logger.debug(f"APPEND response status: {response.status_code}")
597
601
  response.raise_for_status()
602
+ # APPEND returns empty body on success
598
603
 
599
604
  await _do_append()
600
605
 
@@ -610,24 +615,23 @@ class TwitterMediaManager:
610
615
 
611
616
  @retry_async(config=STANDARD_BACKOFF)
612
617
  async def _do_finalize() -> MediaUploadResult:
613
- params = {
614
- "command": "FINALIZE",
615
- "media_id": media_id,
616
- }
617
-
618
+ # v2 API uses /{media_id}/finalize endpoint
619
+ logger.debug(f"Finalizing chunked upload for media_id: {media_id}")
618
620
  response = await self.client.post(
619
- f"{self.base_url}/upload",
620
- params=params,
621
+ f"{self.base_url}/{media_id}/finalize",
621
622
  )
622
623
  response.raise_for_status()
623
624
  result = response.json()
625
+ logger.debug(f"FINALIZE response: {result}")
624
626
 
627
+ # v2 API returns {"data": {...}}
628
+ media_data = result["data"]
625
629
  return MediaUploadResult(
626
- media_id=str(result["media_id"]),
627
- media_key=result.get("media_key"),
628
- size=result.get("size"),
629
- expires_after_secs=result.get("expires_after_secs"),
630
- processing_info=result.get("processing_info"),
630
+ media_id=str(media_data["id"]),
631
+ media_key=media_data.get("media_key"),
632
+ size=media_data.get("size"),
633
+ expires_after_secs=media_data.get("expires_after_secs"),
634
+ processing_info=media_data.get("processing_info"),
631
635
  )
632
636
 
633
637
  return await _do_finalize()
@@ -644,24 +648,28 @@ class TwitterMediaManager:
644
648
 
645
649
  @retry_async(config=STANDARD_BACKOFF)
646
650
  async def _do_status() -> MediaUploadResult:
651
+ # v2 API uses GET with media_id and command params
647
652
  params = {
648
- "command": "STATUS",
649
653
  "media_id": media_id,
654
+ "command": "STATUS",
650
655
  }
651
656
 
652
657
  response = await self.client.get(
653
- f"{self.base_url}/upload",
658
+ self.base_url,
654
659
  params=params,
655
660
  )
656
661
  response.raise_for_status()
657
662
  result = response.json()
663
+ logger.debug(f"STATUS response: {result}")
658
664
 
665
+ # v2 API returns {"data": {...}}
666
+ media_data = result["data"]
659
667
  return MediaUploadResult(
660
- media_id=str(result["media_id"]),
661
- media_key=result.get("media_key"),
662
- size=result.get("size"),
663
- expires_after_secs=result.get("expires_after_secs"),
664
- processing_info=result.get("processing_info"),
668
+ media_id=str(media_data["id"]),
669
+ media_key=media_data.get("media_key"),
670
+ size=media_data.get("size"),
671
+ expires_after_secs=media_data.get("expires_after_secs"),
672
+ processing_info=media_data.get("processing_info"),
665
673
  )
666
674
 
667
675
  return await _do_status()
File without changes