marqetive-lib 0.1.17__py3-none-any.whl → 0.1.20__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.
@@ -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
- if not request.media_urls:
207
- raise ValidationError(
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=upload_result.video_id or upload_result.publish_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 - use placeholder if empty
493
+ # Handle share_url - construct URL from username if share_url not available
462
494
  share_url = video_data.get("share_url", "")
463
- media_url = (
464
- HttpUrl(share_url) if share_url else HttpUrl("https://www.tiktok.com/")
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
- Created Post object.
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
- if not request.content and not request.media_urls:
207
- raise ValidationError(
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=str(tweet.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(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.17
3
+ Version: 0.1.20
4
4
  Summary: Modern Python utilities for web APIs
5
5
  Keywords: api,utilities,web,http,marqetive
6
6
  Requires-Python: >=3.12
@@ -1,28 +1,28 @@
1
1
  marqetive/__init__.py,sha256=pW77CUnzOQ0X1pb-GTcRgrrvsSaJBdVhGZLnvCD_4q4,3032
2
2
  marqetive/core/__init__.py,sha256=0_0vzxJ619YIJkz1yzSvhnGDJRkrErs_QSg2q3Bloss,1172
3
- marqetive/core/base.py,sha256=EJBW2HMc611SqYvSq7sXCIooENmwOGDGGiNVk_Xo2rg,16160
3
+ marqetive/core/base.py,sha256=A1RgZZLX5aMkFVmjNdJak9O0WFWOl1PPMIU8-ngEjxA,17225
4
4
  marqetive/core/client.py,sha256=eCtvL100dkxYQHC_TJzHbs3dGjgsa_me9VTHo-CUN2M,3900
5
5
  marqetive/core/exceptions.py,sha256=Xyj0bzNiZm5VTErmzXgVW8T6IQnOpF92-HJiKPKjIio,7076
6
- marqetive/core/models.py,sha256=L2gA4FhW0feAXQFsz2ce1ttd0vScMRhatoTclhDGCU0,14727
6
+ marqetive/core/models.py,sha256=HmUSKZgJFw4o6Orjn4SGaCwLN6s8_FCsFkK1ptvnad8,14819
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=6jnoIt4CbdIQdVoVuZ_2DI2tQKj6dKBF_-RbFUljVps,33744
10
+ marqetive/platforms/instagram/client.py,sha256=fyVDAcee28rbINa0jPB9W9GF745UHQ8BbKeOj3UXRoo,35239
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
14
14
  marqetive/platforms/linkedin/__init__.py,sha256=_FrdZpqcXjcUW6C-25zGV7poGih9yzs6y1AFnuizTUQ,1384
15
- marqetive/platforms/linkedin/client.py,sha256=8M34VJXFe_34QQZzhR7SVK8ee3iE6UOGsSVTv5Hf31U,58712
15
+ marqetive/platforms/linkedin/client.py,sha256=lMHvb5dpDozHDKu6g27tSZPDHgZuQzrNCWZhZIzWmpc,74249
16
16
  marqetive/platforms/linkedin/exceptions.py,sha256=i5fARUkZik46bS3htZBwUInVzetsZx1APpKEXLrCKf0,9762
17
- marqetive/platforms/linkedin/media.py,sha256=iWXUfqDYGsrTqeM6CGZ7a8xjpbdJ5qESolQL8Fv2PIg,20341
17
+ marqetive/platforms/linkedin/media.py,sha256=-kpkL5brYhx8cZlFS5ueDgY4eSZWqHoB6xp8jZU2cyI,25931
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=c-XYV3XsLU04jv61mBEYivP-oO-aZhuEq6g7J55OEY8,17387
20
+ marqetive/platforms/tiktok/client.py,sha256=IhLCphzu_qxTSP6CDFqLBQqM3zJ7bLy7uHDSErc-qA8,18968
21
21
  marqetive/platforms/tiktok/exceptions.py,sha256=vxwyAKujMGZJh0LetG1QsLF95QfUs_kR6ujsWSHGqL0,10124
22
22
  marqetive/platforms/tiktok/media.py,sha256=NmvzvzfaZMmzIx88wkXI5tWLd4vYN1VJXN-IkaEAO2c,28638
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
- marqetive/platforms/twitter/client.py,sha256=FzCfkduM2haeQ8--9U3O0cJ-wzZqazrwkU2hkOmTi9I,26629
25
+ marqetive/platforms/twitter/client.py,sha256=x4czyl1TnP-JmxRKl19fEE0gcYCppqK2Xdwpyf7G2E8,29035
26
26
  marqetive/platforms/twitter/exceptions.py,sha256=eZ-dJKOXH_-bAMg29zWKbEqMFud29piEJ5IWfC9wFts,8926
27
27
  marqetive/platforms/twitter/media.py,sha256=KpPxnLCas8NhnsEvaXSJZ7To4wW4FY0YqQvUkJIIr7g,28010
28
28
  marqetive/platforms/twitter/models.py,sha256=yPQlx40SlNmz7YGasXUqdx7rEDEgrQ64aYovlPKo6oc,2126
@@ -33,6 +33,6 @@ marqetive/utils/helpers.py,sha256=Sh5HZD6AOJig_6T84n6JsKLosIkKIkpkiYTl69rnOOw,13
33
33
  marqetive/utils/media.py,sha256=reVousdueG-h5jeI6uLGqVCfjYxlsMiWhx6XZwg-iHY,14664
34
34
  marqetive/utils/oauth.py,sha256=x30XAW5VlND6TAPBsw9kZShko_Jsmn_NE-KOZjnBxGo,14359
35
35
  marqetive/utils/retry.py,sha256=UcgrmVBVG5zd30_11mZnRnTaSFrbUYXBO1DrXPR0f8E,7627
36
- marqetive_lib-0.1.17.dist-info/METADATA,sha256=mXa8m82FUduoiT9f6hCpeoNe-ygPA8TCqI7EOon__a8,7876
37
- marqetive_lib-0.1.17.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
- marqetive_lib-0.1.17.dist-info/RECORD,,
36
+ marqetive_lib-0.1.20.dist-info/METADATA,sha256=p-ktKzsiPP2g2pP4nQtFEeqRJv3e7UCV0vfk2aSfUCY,7876
37
+ marqetive_lib-0.1.20.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
+ marqetive_lib-0.1.20.dist-info/RECORD,,