marqetive-lib 0.1.18__tar.gz → 0.1.20__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.
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/PKG-INFO +1 -1
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/pyproject.toml +1 -1
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/core/base.py +33 -1
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/core/models.py +2 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/client.py +43 -6
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/client.py +45 -14
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/tiktok/client.py +51 -10
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/twitter/client.py +66 -8
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/README.md +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/core/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/core/client.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/core/exceptions.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/factory.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/exceptions.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/media.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/models.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/exceptions.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/media.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/models.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/tiktok/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/tiktok/exceptions.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/tiktok/media.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/tiktok/models.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/twitter/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/twitter/exceptions.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/twitter/media.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/twitter/models.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/py.typed +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/__init__.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/file_handlers.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/helpers.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/media.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/oauth.py +0 -0
- {marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/utils/retry.py +0 -0
|
@@ -119,7 +119,10 @@ class SocialMediaPlatform(ABC):
|
|
|
119
119
|
return self
|
|
120
120
|
|
|
121
121
|
async def __aexit__(
|
|
122
|
-
self,
|
|
122
|
+
self,
|
|
123
|
+
exc_type: type[Exception] | None,
|
|
124
|
+
exc_val: Exception | None,
|
|
125
|
+
exc_tb: TracebackType | None,
|
|
123
126
|
) -> None:
|
|
124
127
|
"""Async context manager exit."""
|
|
125
128
|
if self.api_client:
|
|
@@ -228,6 +231,35 @@ class SocialMediaPlatform(ABC):
|
|
|
228
231
|
if inspect.iscoroutine(result):
|
|
229
232
|
await result
|
|
230
233
|
|
|
234
|
+
# ==================== Validation Helpers ====================
|
|
235
|
+
|
|
236
|
+
@abstractmethod
|
|
237
|
+
def _validate_create_post_request(self, request: PostCreateRequest) -> None:
|
|
238
|
+
"""Validate a post creation request.
|
|
239
|
+
|
|
240
|
+
This base implementation performs no validation. Subclasses should
|
|
241
|
+
override this method to implement platform-specific validation rules.
|
|
242
|
+
|
|
243
|
+
The validation method should raise ValidationError with descriptive
|
|
244
|
+
messages including platform name and field name for consistency.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
request: The post creation request to validate.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ValidationError: If validation fails.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
>>> def _validate_create_post_request(self, request):
|
|
254
|
+
... if not request.content:
|
|
255
|
+
... raise ValidationError(
|
|
256
|
+
... "Content is required",
|
|
257
|
+
... platform=self.platform_name,
|
|
258
|
+
... field="content",
|
|
259
|
+
... )
|
|
260
|
+
"""
|
|
261
|
+
pass
|
|
262
|
+
|
|
231
263
|
# ==================== Abstract Authentication Methods ====================
|
|
232
264
|
|
|
233
265
|
@abstractmethod
|
|
@@ -217,6 +217,7 @@ class AuthCredentials(BaseModel):
|
|
|
217
217
|
expires_at: Timestamp when access token expires
|
|
218
218
|
scope: List of permission scopes granted
|
|
219
219
|
user_id: ID of the authenticated user
|
|
220
|
+
username: Username/handle of the authenticated user
|
|
220
221
|
status: Current status of the account credentials
|
|
221
222
|
additional_data: Platform-specific auth data
|
|
222
223
|
|
|
@@ -237,6 +238,7 @@ class AuthCredentials(BaseModel):
|
|
|
237
238
|
expires_at: datetime | None = None
|
|
238
239
|
scope: list[str] = Field(default_factory=list)
|
|
239
240
|
user_id: str | None = None
|
|
241
|
+
username: str | None = None
|
|
240
242
|
status: AccountStatus = AccountStatus.VALID
|
|
241
243
|
additional_data: dict[str, Any] = Field(default_factory=dict)
|
|
242
244
|
|
|
@@ -211,6 +211,39 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
211
211
|
except httpx.HTTPError:
|
|
212
212
|
return False
|
|
213
213
|
|
|
214
|
+
# ==================== Validation ====================
|
|
215
|
+
|
|
216
|
+
def _validate_create_post_request(self, request: PostCreateRequest) -> None:
|
|
217
|
+
"""Validate Instagram post creation request.
|
|
218
|
+
|
|
219
|
+
Instagram Requirements:
|
|
220
|
+
- Media is ALWAYS required (Instagram is a visual platform)
|
|
221
|
+
- For carousels: 2-10 images
|
|
222
|
+
- For reels: 1 video (3 sec - 15 min)
|
|
223
|
+
- Caption max 2200 characters
|
|
224
|
+
- Media must be publicly accessible URLs
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
request: Post creation request to validate.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValidationError: If validation fails.
|
|
231
|
+
"""
|
|
232
|
+
if not request.media_urls and not request.media_ids:
|
|
233
|
+
raise ValidationError(
|
|
234
|
+
"Instagram posts require at least one media attachment. "
|
|
235
|
+
"Instagram is a visual platform - text-only posts are not supported.",
|
|
236
|
+
platform=self.platform_name,
|
|
237
|
+
field="media",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if request.content and len(request.content) > 2200:
|
|
241
|
+
raise ValidationError(
|
|
242
|
+
f"Caption exceeds 2200 characters ({len(request.content)} characters)",
|
|
243
|
+
platform=self.platform_name,
|
|
244
|
+
field="content",
|
|
245
|
+
)
|
|
246
|
+
|
|
214
247
|
# ==================== Post CRUD Methods ====================
|
|
215
248
|
|
|
216
249
|
async def create_post(self, request: PostCreateRequest) -> Post:
|
|
@@ -256,12 +289,8 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
256
289
|
if not self.api_client:
|
|
257
290
|
raise RuntimeError("Client must be used as async context manager")
|
|
258
291
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"Instagram posts require at least one media attachment",
|
|
262
|
-
platform=self.platform_name,
|
|
263
|
-
field="media",
|
|
264
|
-
)
|
|
292
|
+
# Validate request
|
|
293
|
+
self._validate_create_post_request(request)
|
|
265
294
|
|
|
266
295
|
# Determine content type from request
|
|
267
296
|
media_type = self._get_media_type(request)
|
|
@@ -375,7 +404,9 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
375
404
|
content=request.content,
|
|
376
405
|
status=PostStatus.PUBLISHED,
|
|
377
406
|
created_at=datetime.now(),
|
|
407
|
+
author_id=self.instagram_account_id,
|
|
378
408
|
url=cast(HttpUrl, result.permalink) if result.permalink else None,
|
|
409
|
+
raw_data={"container_id": container_ids[0]},
|
|
379
410
|
)
|
|
380
411
|
|
|
381
412
|
async def _create_carousel_post(self, request: PostCreateRequest) -> Post:
|
|
@@ -826,7 +857,9 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
826
857
|
content=caption,
|
|
827
858
|
status=PostStatus.PUBLISHED,
|
|
828
859
|
created_at=datetime.now(),
|
|
860
|
+
author_id=self.instagram_account_id,
|
|
829
861
|
url=cast(HttpUrl, result.permalink) if result.permalink else None,
|
|
862
|
+
raw_data={"container_id": container_ids[0]},
|
|
830
863
|
)
|
|
831
864
|
|
|
832
865
|
async def create_reel(
|
|
@@ -890,7 +923,9 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
890
923
|
content=caption,
|
|
891
924
|
status=PostStatus.PUBLISHED,
|
|
892
925
|
created_at=datetime.now(),
|
|
926
|
+
author_id=self.instagram_account_id,
|
|
893
927
|
url=cast(HttpUrl, result.permalink) if result.permalink else None,
|
|
928
|
+
raw_data={"container_id": container_id},
|
|
894
929
|
)
|
|
895
930
|
|
|
896
931
|
async def create_story(
|
|
@@ -941,7 +976,9 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
941
976
|
content=None, # Stories don't have captions
|
|
942
977
|
status=PostStatus.PUBLISHED,
|
|
943
978
|
created_at=datetime.now(),
|
|
979
|
+
author_id=self.instagram_account_id,
|
|
944
980
|
url=cast(HttpUrl, result.permalink) if result.permalink else None,
|
|
981
|
+
raw_data={"container_id": container_id},
|
|
945
982
|
)
|
|
946
983
|
|
|
947
984
|
# ==================== Helper Methods ====================
|
|
@@ -319,6 +319,48 @@ class LinkedInClient(SocialMediaPlatform):
|
|
|
319
319
|
except httpx.HTTPError:
|
|
320
320
|
return False
|
|
321
321
|
|
|
322
|
+
# ==================== Validation ====================
|
|
323
|
+
|
|
324
|
+
def _validate_create_post_request(self, request: PostCreateRequest) -> None:
|
|
325
|
+
"""Validate LinkedIn post creation request.
|
|
326
|
+
|
|
327
|
+
LinkedIn Requirements:
|
|
328
|
+
- Content is required (text post content)
|
|
329
|
+
- Content max 3000 characters
|
|
330
|
+
- CTA labels must be from approved list if provided
|
|
331
|
+
- Media: max 20 images, or 1 video, or 1 document
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
request: Post creation request to validate.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValidationError: If validation fails.
|
|
338
|
+
"""
|
|
339
|
+
if not request.content:
|
|
340
|
+
raise ValidationError(
|
|
341
|
+
"LinkedIn posts require content",
|
|
342
|
+
platform=self.platform_name,
|
|
343
|
+
field="content",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if len(request.content) > 3000:
|
|
347
|
+
raise ValidationError(
|
|
348
|
+
f"Post content exceeds 3000 characters ({len(request.content)} characters)",
|
|
349
|
+
platform=self.platform_name,
|
|
350
|
+
field="content",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Validate CTA label if provided
|
|
354
|
+
if cta := request.additional_data.get("call_to_action"):
|
|
355
|
+
cta_upper = cta.upper()
|
|
356
|
+
if cta_upper not in VALID_CTA_LABELS:
|
|
357
|
+
raise ValidationError(
|
|
358
|
+
f"Invalid call_to_action: '{cta}'. "
|
|
359
|
+
f"Valid values: {', '.join(sorted(VALID_CTA_LABELS))}",
|
|
360
|
+
platform=self.platform_name,
|
|
361
|
+
field="call_to_action",
|
|
362
|
+
)
|
|
363
|
+
|
|
322
364
|
# ==================== Post CRUD Methods ====================
|
|
323
365
|
|
|
324
366
|
async def create_post(self, request: PostCreateRequest) -> Post:
|
|
@@ -434,20 +476,8 @@ class LinkedInClient(SocialMediaPlatform):
|
|
|
434
476
|
if not self.api_client:
|
|
435
477
|
raise RuntimeError("Client must be used as async context manager")
|
|
436
478
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"LinkedIn posts require content",
|
|
440
|
-
platform=self.platform_name,
|
|
441
|
-
field="content",
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
# Validate content length (3000 characters for posts)
|
|
445
|
-
if len(request.content) > 3000:
|
|
446
|
-
raise ValidationError(
|
|
447
|
-
f"Post content exceeds 3000 characters ({len(request.content)} characters)",
|
|
448
|
-
platform=self.platform_name,
|
|
449
|
-
field="content",
|
|
450
|
-
)
|
|
479
|
+
# Validate request
|
|
480
|
+
self._validate_create_post_request(request)
|
|
451
481
|
|
|
452
482
|
try:
|
|
453
483
|
# Build REST API payload structure
|
|
@@ -578,6 +608,7 @@ class LinkedInClient(SocialMediaPlatform):
|
|
|
578
608
|
status=PostStatus.PUBLISHED,
|
|
579
609
|
created_at=datetime.now(),
|
|
580
610
|
author_id=self.author_urn,
|
|
611
|
+
url=None, # Not available without separate fetch
|
|
581
612
|
raw_data=response.data,
|
|
582
613
|
)
|
|
583
614
|
|
|
@@ -166,6 +166,32 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
166
166
|
except PlatformError:
|
|
167
167
|
return False
|
|
168
168
|
|
|
169
|
+
# ==================== Validation ====================
|
|
170
|
+
|
|
171
|
+
def _validate_create_post_request(self, request: PostCreateRequest) -> None:
|
|
172
|
+
"""Validate TikTok post creation request.
|
|
173
|
+
|
|
174
|
+
TikTok Requirements:
|
|
175
|
+
- Video URL is ALWAYS required (TikTok is a video-only platform)
|
|
176
|
+
- Video duration: 3 seconds to 10 minutes
|
|
177
|
+
- open_id must be in credentials.additional_data
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
request: Post creation request to validate.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValidationError: If validation fails.
|
|
184
|
+
"""
|
|
185
|
+
if not request.media_urls:
|
|
186
|
+
raise ValidationError(
|
|
187
|
+
"A video URL is required to create a TikTok post. "
|
|
188
|
+
"TikTok is a video platform - image-only or text-only posts are not supported.",
|
|
189
|
+
platform=self.platform_name,
|
|
190
|
+
field="media_urls",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ==================== TikTok-specific Methods ====================
|
|
194
|
+
|
|
169
195
|
async def query_creator_info(self) -> CreatorInfo:
|
|
170
196
|
"""Query creator info before posting.
|
|
171
197
|
|
|
@@ -203,11 +229,8 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
203
229
|
if not self._media_manager or not self.api_client:
|
|
204
230
|
raise RuntimeError("Client must be used as async context manager")
|
|
205
231
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"A video URL is required to create a TikTok post.",
|
|
209
|
-
platform=self.platform_name,
|
|
210
|
-
)
|
|
232
|
+
# Validate request
|
|
233
|
+
self._validate_create_post_request(request)
|
|
211
234
|
|
|
212
235
|
# 1. Query creator info (required before posting)
|
|
213
236
|
if not self._creator_info:
|
|
@@ -232,12 +255,21 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
232
255
|
)
|
|
233
256
|
|
|
234
257
|
# 4. Return minimal Post object without fetching details
|
|
258
|
+
video_id = upload_result.video_id or upload_result.publish_id
|
|
259
|
+
url = (
|
|
260
|
+
HttpUrl(f"https://www.tiktok.com/@{self.credentials.username}/video/{video_id}")
|
|
261
|
+
if self.credentials.username and video_id
|
|
262
|
+
else None
|
|
263
|
+
)
|
|
264
|
+
|
|
235
265
|
return Post(
|
|
236
|
-
post_id=
|
|
266
|
+
post_id=video_id,
|
|
237
267
|
platform=self.platform_name,
|
|
238
268
|
content=request.content,
|
|
239
269
|
status=PostStatus.PUBLISHED,
|
|
240
270
|
created_at=datetime.now(),
|
|
271
|
+
author_id=self.credentials.additional_data.get("open_id"),
|
|
272
|
+
url=url,
|
|
241
273
|
raw_data={
|
|
242
274
|
"publish_id": upload_result.publish_id,
|
|
243
275
|
"video_id": upload_result.video_id,
|
|
@@ -458,16 +490,25 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
458
490
|
# TikTok uses 'id' field for video ID
|
|
459
491
|
video_id = video_data.get("id", video_data.get("video_id", ""))
|
|
460
492
|
|
|
461
|
-
# Handle share_url -
|
|
493
|
+
# Handle share_url - construct URL from username if share_url not available
|
|
462
494
|
share_url = video_data.get("share_url", "")
|
|
463
|
-
|
|
464
|
-
HttpUrl
|
|
465
|
-
|
|
495
|
+
if share_url:
|
|
496
|
+
post_url: HttpUrl | None = HttpUrl(share_url)
|
|
497
|
+
elif self.credentials.username and video_id:
|
|
498
|
+
post_url = HttpUrl(
|
|
499
|
+
f"https://www.tiktok.com/@{self.credentials.username}/video/{video_id}"
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
post_url = None
|
|
503
|
+
|
|
504
|
+
# MediaAttachment.url requires a non-null HttpUrl, use placeholder if needed
|
|
505
|
+
media_url = post_url if post_url else HttpUrl("https://www.tiktok.com/")
|
|
466
506
|
|
|
467
507
|
return Post(
|
|
468
508
|
post_id=video_id,
|
|
469
509
|
platform=self.platform_name,
|
|
470
510
|
content=video_data.get("title", video_data.get("video_description", "")),
|
|
511
|
+
url=post_url,
|
|
471
512
|
media=[
|
|
472
513
|
MediaAttachment(
|
|
473
514
|
media_id=video_id,
|
|
@@ -185,29 +185,68 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
185
185
|
except tweepy.TweepyException:
|
|
186
186
|
return False
|
|
187
187
|
|
|
188
|
+
# ==================== Validation ====================
|
|
189
|
+
|
|
190
|
+
def _validate_create_post_request(self, request: PostCreateRequest) -> None:
|
|
191
|
+
"""Validate Twitter post creation request.
|
|
192
|
+
|
|
193
|
+
Twitter Requirements:
|
|
194
|
+
- Must have content OR media (at least one)
|
|
195
|
+
- Content max 280 characters (enforced by Twitter API)
|
|
196
|
+
- Max 4 images or 1 video per tweet
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
request: Post creation request to validate.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValidationError: If validation fails.
|
|
203
|
+
"""
|
|
204
|
+
if not request.content and not request.media_urls and not request.media_ids:
|
|
205
|
+
raise ValidationError(
|
|
206
|
+
"Tweet must contain content or media",
|
|
207
|
+
platform=self.platform_name,
|
|
208
|
+
field="content",
|
|
209
|
+
)
|
|
210
|
+
|
|
188
211
|
# ==================== Post CRUD Methods ====================
|
|
189
212
|
|
|
190
213
|
async def create_post(self, request: PostCreateRequest) -> Post:
|
|
191
214
|
"""Create and publish a tweet.
|
|
192
215
|
|
|
216
|
+
Twitter Requirements:
|
|
217
|
+
- Must have content OR media (at least one)
|
|
218
|
+
- Content max 280 characters (enforced by Twitter API)
|
|
219
|
+
- Max 4 images or 1 video per tweet
|
|
220
|
+
|
|
193
221
|
Args:
|
|
194
|
-
request: Post creation request.
|
|
222
|
+
request: Post creation request. Supports:
|
|
223
|
+
- content: Tweet text (max 280 chars)
|
|
224
|
+
- media_urls: URLs of media to attach
|
|
225
|
+
- reply_to_post_id: Tweet ID to reply to (for threads)
|
|
226
|
+
- quote_post_id: Tweet ID to quote
|
|
195
227
|
|
|
196
228
|
Returns:
|
|
197
|
-
|
|
229
|
+
Post object with:
|
|
230
|
+
- post_id: Twitter tweet ID
|
|
231
|
+
- platform: "twitter"
|
|
232
|
+
- content: Tweet text
|
|
233
|
+
- status: PostStatus.PUBLISHED
|
|
234
|
+
- created_at: Creation timestamp
|
|
235
|
+
- author_id: None (not available without extra API call)
|
|
236
|
+
- url: None (not returned in create response)
|
|
237
|
+
- raw_data: {"tweet_id": ...}
|
|
198
238
|
|
|
199
239
|
Raises:
|
|
200
240
|
ValidationError: If request is invalid.
|
|
201
241
|
MediaUploadError: If media upload fails.
|
|
242
|
+
RateLimitError: If Twitter rate limit is exceeded.
|
|
243
|
+
PlatformError: For other Twitter API errors.
|
|
202
244
|
"""
|
|
203
245
|
if not self._tweepy_client:
|
|
204
246
|
raise RuntimeError("Client must be used as async context manager")
|
|
205
247
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"Tweet must contain content or media",
|
|
209
|
-
platform=self.platform_name,
|
|
210
|
-
)
|
|
248
|
+
# Validate request
|
|
249
|
+
self._validate_create_post_request(request)
|
|
211
250
|
|
|
212
251
|
try:
|
|
213
252
|
media_ids = []
|
|
@@ -241,6 +280,13 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
241
280
|
response = self._tweepy_client.create_tweet(**tweet_params, user_auth=False)
|
|
242
281
|
tweet_id = response.data["id"] # type: ignore[index]
|
|
243
282
|
|
|
283
|
+
# Construct Twitter URL if username is available
|
|
284
|
+
url = (
|
|
285
|
+
HttpUrl(f"https://x.com/{self.credentials.username}/status/{tweet_id}")
|
|
286
|
+
if self.credentials.username
|
|
287
|
+
else HttpUrl(f"https://x.com/web/status/{tweet_id}")
|
|
288
|
+
)
|
|
289
|
+
|
|
244
290
|
# Return minimal Post object without fetching details
|
|
245
291
|
return Post(
|
|
246
292
|
post_id=tweet_id,
|
|
@@ -248,6 +294,9 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
248
294
|
content=request.content or "",
|
|
249
295
|
status=PostStatus.PUBLISHED,
|
|
250
296
|
created_at=datetime.now(),
|
|
297
|
+
author_id=None, # Not available without extra API call
|
|
298
|
+
url=url,
|
|
299
|
+
raw_data={"tweet_id": tweet_id},
|
|
251
300
|
)
|
|
252
301
|
|
|
253
302
|
except tweepy.TweepyException as e:
|
|
@@ -741,10 +790,19 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
741
790
|
|
|
742
791
|
metrics = tweet.public_metrics or {}
|
|
743
792
|
|
|
793
|
+
# Construct Twitter URL if username is available
|
|
794
|
+
tweet_id = str(tweet.id)
|
|
795
|
+
url = (
|
|
796
|
+
HttpUrl(f"https://x.com/{self.credentials.username}/status/{tweet_id}")
|
|
797
|
+
if self.credentials.username
|
|
798
|
+
else HttpUrl(f"https://x.com/web/status/{tweet_id}")
|
|
799
|
+
)
|
|
800
|
+
|
|
744
801
|
return Post(
|
|
745
|
-
post_id=
|
|
802
|
+
post_id=tweet_id,
|
|
746
803
|
platform=self.platform_name,
|
|
747
804
|
content=tweet.text,
|
|
805
|
+
url=url,
|
|
748
806
|
media=media,
|
|
749
807
|
status=PostStatus.PUBLISHED,
|
|
750
808
|
created_at=tweet.created_at or datetime.now(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/instagram/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{marqetive_lib-0.1.18 → marqetive_lib-0.1.20}/src/marqetive/platforms/linkedin/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|