marqetive-lib 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (44) hide show
  1. marqetive/__init__.py +13 -13
  2. marqetive/core/__init__.py +1 -1
  3. marqetive/core/account_factory.py +2 -2
  4. marqetive/core/base_manager.py +4 -4
  5. marqetive/core/client.py +1 -1
  6. marqetive/core/registry.py +3 -3
  7. marqetive/platforms/__init__.py +6 -6
  8. marqetive/platforms/base.py +3 -3
  9. marqetive/platforms/exceptions.py +2 -1
  10. marqetive/platforms/instagram/__init__.py +3 -3
  11. marqetive/platforms/instagram/client.py +4 -4
  12. marqetive/platforms/instagram/exceptions.py +1 -1
  13. marqetive/platforms/instagram/factory.py +5 -5
  14. marqetive/platforms/instagram/manager.py +4 -4
  15. marqetive/platforms/instagram/media.py +2 -2
  16. marqetive/platforms/linkedin/__init__.py +3 -3
  17. marqetive/platforms/linkedin/client.py +4 -4
  18. marqetive/platforms/linkedin/exceptions.py +1 -1
  19. marqetive/platforms/linkedin/factory.py +5 -5
  20. marqetive/platforms/linkedin/manager.py +4 -4
  21. marqetive/platforms/linkedin/media.py +4 -4
  22. marqetive/platforms/models.py +2 -0
  23. marqetive/platforms/tiktok/__init__.py +7 -0
  24. marqetive/platforms/tiktok/client.py +492 -0
  25. marqetive/platforms/tiktok/exceptions.py +284 -0
  26. marqetive/platforms/tiktok/factory.py +188 -0
  27. marqetive/platforms/tiktok/manager.py +115 -0
  28. marqetive/platforms/tiktok/media.py +693 -0
  29. marqetive/platforms/twitter/__init__.py +3 -3
  30. marqetive/platforms/twitter/client.py +8 -54
  31. marqetive/platforms/twitter/exceptions.py +1 -1
  32. marqetive/platforms/twitter/factory.py +5 -6
  33. marqetive/platforms/twitter/manager.py +4 -4
  34. marqetive/platforms/twitter/media.py +4 -4
  35. marqetive/registry_init.py +10 -8
  36. marqetive/utils/__init__.py +3 -3
  37. marqetive/utils/file_handlers.py +1 -1
  38. marqetive/utils/oauth.py +137 -2
  39. marqetive/utils/token_validator.py +1 -1
  40. {marqetive_lib-0.1.1.dist-info → marqetive_lib-0.1.3.dist-info}/METADATA +1 -2
  41. marqetive_lib-0.1.3.dist-info/RECORD +47 -0
  42. marqetive/platforms/twitter/threads.py +0 -442
  43. marqetive_lib-0.1.1.dist-info/RECORD +0 -43
  44. {marqetive_lib-0.1.1.dist-info → marqetive_lib-0.1.3.dist-info}/WHEEL +0 -0
@@ -1,442 +0,0 @@
1
- """Twitter thread creation and management.
2
-
3
- This module provides utilities for creating and managing Twitter threads
4
- (sequences of connected tweets).
5
- """
6
-
7
- from dataclasses import dataclass
8
- from typing import Any
9
-
10
- import tweepy
11
-
12
- from src.marqetive.platforms.exceptions import PlatformError, ValidationError
13
- from src.marqetive.platforms.models import Post, PostStatus
14
-
15
-
16
- @dataclass
17
- class TweetData:
18
- """Data for a single tweet in a thread.
19
-
20
- Attributes:
21
- content: Tweet text content (max 280 characters).
22
- media_ids: List of Twitter media IDs to attach.
23
- alt_texts: Optional alt texts for media (same order as media_ids).
24
- """
25
-
26
- content: str
27
- media_ids: list[str] | None = None
28
- alt_texts: list[str] | None = None
29
-
30
- def __post_init__(self) -> None:
31
- """Validate tweet data after initialization."""
32
- # Validate content length
33
- if not self.content:
34
- raise ValidationError(
35
- "Tweet content cannot be empty",
36
- platform="twitter",
37
- field="content",
38
- )
39
-
40
- if len(self.content) > 280:
41
- raise ValidationError(
42
- f"Tweet exceeds 280 characters ({len(self.content)} characters)",
43
- platform="twitter",
44
- field="content",
45
- )
46
-
47
- # Validate media count (Twitter allows max 4 images or 1 video per tweet)
48
- if self.media_ids and len(self.media_ids) > 4:
49
- raise ValidationError(
50
- f"Too many media attachments ({len(self.media_ids)}). "
51
- "Twitter allows maximum 4 images or 1 video per tweet.",
52
- platform="twitter",
53
- field="media_ids",
54
- )
55
-
56
- # Validate alt texts match media count
57
- if self.alt_texts:
58
- if not self.media_ids:
59
- raise ValidationError(
60
- "Cannot provide alt_texts without media_ids",
61
- platform="twitter",
62
- field="alt_texts",
63
- )
64
- if len(self.alt_texts) != len(self.media_ids):
65
- raise ValidationError(
66
- f"Number of alt_texts ({len(self.alt_texts)}) must match "
67
- f"number of media_ids ({len(self.media_ids)})",
68
- platform="twitter",
69
- field="alt_texts",
70
- )
71
-
72
-
73
- @dataclass
74
- class ThreadResult:
75
- """Result of creating a Twitter thread.
76
-
77
- Attributes:
78
- thread_id: ID of the first tweet in the thread.
79
- tweet_ids: List of all tweet IDs in order.
80
- tweets: List of Post objects for each tweet.
81
- total_tweets: Total number of tweets in the thread.
82
- """
83
-
84
- thread_id: str
85
- tweet_ids: list[str]
86
- tweets: list[Post]
87
-
88
- @property
89
- def total_tweets(self) -> int:
90
- """Get total number of tweets in thread."""
91
- return len(self.tweet_ids)
92
-
93
- def __str__(self) -> str:
94
- """String representation of thread result."""
95
- return (
96
- f"Thread(id={self.thread_id}, "
97
- f"tweets={self.total_tweets}, "
98
- f"ids={self.tweet_ids})"
99
- )
100
-
101
-
102
- class TwitterThreadManager:
103
- """Manager for creating and managing Twitter threads.
104
-
105
- Example:
106
- >>> manager = TwitterThreadManager(tweepy_client)
107
- >>> tweets = [
108
- ... TweetData("First tweet in thread"),
109
- ... TweetData("Second tweet with more info"),
110
- ... TweetData("Final tweet with conclusion"),
111
- ... ]
112
- >>> result = await manager.create_thread(tweets)
113
- >>> print(f"Created thread: {result.thread_id}")
114
- """
115
-
116
- def __init__(self, tweepy_client: tweepy.Client) -> None:
117
- """Initialize thread manager.
118
-
119
- Args:
120
- tweepy_client: Authenticated tweepy Client instance.
121
- """
122
- self.client = tweepy_client
123
-
124
- async def create_thread(
125
- self,
126
- tweets: list[TweetData],
127
- *,
128
- auto_number: bool = False,
129
- number_format: str = "{index}/{total}",
130
- ) -> ThreadResult:
131
- """Create a Twitter thread from multiple tweets.
132
-
133
- Args:
134
- tweets: List of TweetData objects representing the thread.
135
- auto_number: If True, automatically add tweet numbers to content.
136
- number_format: Format string for auto-numbering (supports {index}, {total}).
137
-
138
- Returns:
139
- ThreadResult with thread ID and all tweet information.
140
-
141
- Raises:
142
- ValidationError: If thread data is invalid.
143
- PlatformError: If thread creation fails.
144
-
145
- Example:
146
- >>> tweets = [
147
- ... TweetData("Part 1: Introduction"),
148
- ... TweetData("Part 2: Details"),
149
- ... TweetData("Part 3: Conclusion"),
150
- ... ]
151
- >>> result = await manager.create_thread(tweets, auto_number=True)
152
- """
153
- if not tweets:
154
- raise ValidationError(
155
- "Thread must contain at least one tweet",
156
- platform="twitter",
157
- field="tweets",
158
- )
159
-
160
- if len(tweets) > 25:
161
- raise ValidationError(
162
- f"Thread too long ({len(tweets)} tweets). "
163
- "Consider splitting into multiple threads.",
164
- platform="twitter",
165
- field="tweets",
166
- )
167
-
168
- thread_id: str | None = None
169
- tweet_ids: list[str] = []
170
- post_objects: list[Post] = []
171
- previous_tweet_id: str | None = None
172
-
173
- try:
174
- for index, tweet_data in enumerate(tweets, start=1):
175
- # Prepare tweet content
176
- content = tweet_data.content
177
-
178
- # Add auto-numbering if requested
179
- if auto_number and len(tweets) > 1:
180
- number_prefix = number_format.format(
181
- index=index,
182
- total=len(tweets),
183
- )
184
- content = f"{number_prefix} {content}"
185
-
186
- # Validate length after numbering
187
- if len(content) > 280:
188
- raise ValidationError(
189
- f"Tweet {index} exceeds 280 characters after auto-numbering "
190
- f"({len(content)} characters). Consider shorter content or "
191
- "disable auto_number.",
192
- platform="twitter",
193
- field="content",
194
- )
195
-
196
- # Prepare tweet parameters
197
- tweet_params: dict[str, Any] = {"text": content}
198
-
199
- # Add reply-to for threading
200
- if previous_tweet_id:
201
- tweet_params["in_reply_to_tweet_id"] = previous_tweet_id
202
-
203
- # Add media if provided
204
- if tweet_data.media_ids:
205
- tweet_params["media_ids"] = tweet_data.media_ids
206
-
207
- # Create tweet
208
- response = self.client.create_tweet(**tweet_params)
209
- tweet_id = str(response.data["id"]) # type: ignore[index]
210
-
211
- # Store IDs
212
- if thread_id is None:
213
- thread_id = tweet_id
214
- tweet_ids.append(tweet_id)
215
- previous_tweet_id = tweet_id
216
-
217
- # Fetch full tweet details
218
- tweet_response = self.client.get_tweet(
219
- tweet_id,
220
- tweet_fields=[
221
- "created_at",
222
- "public_metrics",
223
- "attachments",
224
- "author_id",
225
- ],
226
- )
227
-
228
- # Convert to Post object
229
- tweet = tweet_response.data # type: ignore[attr-defined]
230
- post = Post(
231
- post_id=tweet_id,
232
- platform="twitter",
233
- content=content,
234
- status=PostStatus.PUBLISHED,
235
- created_at=tweet.created_at,
236
- author_id=str(tweet.author_id) if tweet.author_id else None,
237
- raw_data=tweet.data,
238
- )
239
- post_objects.append(post)
240
-
241
- except tweepy.TweepyException as e:
242
- # If thread creation fails partway through, we have partial thread
243
- # Log the created tweets for cleanup if needed
244
- if tweet_ids:
245
- raise PlatformError(
246
- f"Thread creation failed at tweet {len(tweet_ids) + 1}. "
247
- f"Partial thread created with IDs: {tweet_ids}. "
248
- f"Error: {e}",
249
- platform="twitter",
250
- ) from e
251
- raise PlatformError(
252
- f"Failed to create thread: {e}",
253
- platform="twitter",
254
- ) from e
255
-
256
- if not thread_id:
257
- raise PlatformError(
258
- "Thread creation completed but no thread ID available",
259
- platform="twitter",
260
- )
261
-
262
- return ThreadResult(
263
- thread_id=thread_id,
264
- tweet_ids=tweet_ids,
265
- tweets=post_objects,
266
- )
267
-
268
- async def delete_thread(
269
- self,
270
- thread_id: str,
271
- tweet_ids: list[str],
272
- *,
273
- continue_on_error: bool = True,
274
- ) -> dict[str, bool]:
275
- """Delete all tweets in a thread.
276
-
277
- Args:
278
- thread_id: ID of the first tweet (for logging).
279
- tweet_ids: List of all tweet IDs to delete.
280
- continue_on_error: If True, continue deleting even if some fail.
281
-
282
- Returns:
283
- Dictionary mapping tweet_id to deletion success status.
284
-
285
- Example:
286
- >>> result = await manager.delete_thread(
287
- ... thread_id="123",
288
- ... tweet_ids=["123", "456", "789"]
289
- ... )
290
- >>> print(f"Deleted: {sum(result.values())} / {len(result)} tweets")
291
- """
292
- results: dict[str, bool] = {}
293
- errors: list[str] = []
294
-
295
- # Delete in reverse order (newest to oldest)
296
- for tweet_id in reversed(tweet_ids):
297
- try:
298
- self.client.delete_tweet(tweet_id)
299
- results[tweet_id] = True
300
- except tweepy.TweepyException as e:
301
- results[tweet_id] = False
302
- errors.append(f"Tweet {tweet_id}: {e}")
303
-
304
- if not continue_on_error:
305
- raise PlatformError(
306
- f"Failed to delete tweet {tweet_id} in thread {thread_id}: {e}",
307
- platform="twitter",
308
- ) from e
309
-
310
- # If there were errors but continue_on_error=True, log them
311
- if errors:
312
- error_summary = "; ".join(errors)
313
- raise PlatformError(
314
- f"Some tweets in thread {thread_id} failed to delete: {error_summary}",
315
- platform="twitter",
316
- )
317
-
318
- return results
319
-
320
-
321
- def split_content_into_tweets(
322
- content: str,
323
- *,
324
- max_length: int = 280,
325
- split_on_sentences: bool = True,
326
- add_continuation: bool = True,
327
- continuation_suffix: str = "...",
328
- ) -> list[str]:
329
- """Split long content into multiple tweet-sized chunks.
330
-
331
- Attempts to split on sentence boundaries when possible.
332
-
333
- Args:
334
- content: Long text content to split.
335
- max_length: Maximum characters per tweet (default: 280).
336
- split_on_sentences: Try to split on sentence boundaries.
337
- add_continuation: Add continuation indicator to split tweets.
338
- continuation_suffix: Suffix to add when splitting mid-content.
339
-
340
- Returns:
341
- List of tweet content strings.
342
-
343
- Example:
344
- >>> long_text = "This is a very long piece of content..." * 20
345
- >>> tweets = split_content_into_tweets(long_text)
346
- >>> print(f"Split into {len(tweets)} tweets")
347
- """
348
- if len(content) <= max_length:
349
- return [content]
350
-
351
- tweets: list[str] = []
352
- remaining = content
353
-
354
- while remaining:
355
- # Calculate available space
356
- available = max_length
357
- if add_continuation and len(remaining) > max_length:
358
- available -= len(continuation_suffix)
359
-
360
- if len(remaining) <= available:
361
- # Last chunk
362
- tweets.append(remaining)
363
- break
364
-
365
- # Find split point
366
- split_point = available
367
-
368
- if split_on_sentences:
369
- # Try to split on sentence boundary
370
- sentence_ends = [". ", "! ", "? ", ".\n", "!\n", "?\n"]
371
- best_split = 0
372
-
373
- for sent_end in sentence_ends:
374
- pos = remaining[:available].rfind(sent_end)
375
- if pos > best_split:
376
- best_split = pos + len(sent_end)
377
-
378
- if best_split > 0:
379
- split_point = best_split
380
- else:
381
- # Try to split on word boundary
382
- last_space = remaining[:available].rfind(" ")
383
- if last_space > 0:
384
- split_point = last_space + 1
385
-
386
- # Extract chunk
387
- chunk = remaining[:split_point].rstrip()
388
-
389
- # Add continuation indicator if not at end
390
- if add_continuation and len(remaining) > split_point:
391
- chunk += continuation_suffix
392
-
393
- tweets.append(chunk)
394
- remaining = remaining[split_point:].lstrip()
395
-
396
- return tweets
397
-
398
-
399
- def create_numbered_thread(
400
- content_parts: list[str],
401
- *,
402
- number_format: str = "{index}/{total}",
403
- media_per_tweet: list[list[str]] | None = None,
404
- ) -> list[TweetData]:
405
- """Create a numbered thread from content parts.
406
-
407
- Convenience function to create TweetData with auto-numbering.
408
-
409
- Args:
410
- content_parts: List of content strings, one per tweet.
411
- number_format: Format string for numbering.
412
- media_per_tweet: Optional list of media ID lists, one per tweet.
413
-
414
- Returns:
415
- List of TweetData objects ready for thread creation.
416
-
417
- Example:
418
- >>> parts = ["Introduction", "Main content", "Conclusion"]
419
- >>> tweets = create_numbered_thread(parts)
420
- >>> # Tweets will be: "1/3 Introduction", "2/3 Main content", "3/3 Conclusion"
421
- """
422
- total = len(content_parts)
423
- tweet_data_list: list[TweetData] = []
424
-
425
- for index, content in enumerate(content_parts, start=1):
426
- # Add number prefix
427
- number_prefix = number_format.format(index=index, total=total)
428
- numbered_content = f"{number_prefix} {content}"
429
-
430
- # Get media for this tweet if provided
431
- media_ids = None
432
- if media_per_tweet and index - 1 < len(media_per_tweet):
433
- media_ids = media_per_tweet[index - 1]
434
-
435
- tweet_data_list.append(
436
- TweetData(
437
- content=numbered_content,
438
- media_ids=media_ids,
439
- )
440
- )
441
-
442
- return tweet_data_list
@@ -1,43 +0,0 @@
1
- marqetive/__init__.py,sha256=XVnYmB817AILAnJYg1WVNnmrd1MvJns3QpPbfdjmwRE,3142
2
- marqetive/core/__init__.py,sha256=5Wgcby-EQkTxrPVD4so69nBmZ66Ng9JGqBov4RQMT4A,117
3
- marqetive/core/account_factory.py,sha256=9fsP6fzLYp3UGU59fkxwURgzn7eBxr8-WJt3aP-Z65U,7153
4
- marqetive/core/base_manager.py,sha256=z28bV4p1FC7s9g_YkXIKW_qkv8bjF4tMoF-cGGksJuA,10047
5
- marqetive/core/client.py,sha256=GBgWzkgdMRMyoCy5dXdEFFzPne6Usp_9i0UxaG7Xd-Y,3226
6
- marqetive/core/progress.py,sha256=5Vyksf8YfQUVRudwDEuegcxoc0js5qbHM2SFZ6VLxHM,8593
7
- marqetive/core/registry.py,sha256=D6qlafpvemYfE9GouWcWZyltK0aNcFLSi_2M-WdTIdE,8084
8
- marqetive/platforms/__init__.py,sha256=6QjetNWodf32pgGQsmLKs7W-v-pG2kKckb5JEPqIPNI,1351
9
- marqetive/platforms/base.py,sha256=zmCda00Szq_LswRdsWPI5V88TysLH3FIZ0mKwH6b8dE,11957
10
- marqetive/platforms/exceptions.py,sha256=C8H0vsICM6amtQTz1iwXjc3duFGq-M71N7QqsLElt1M,7029
11
- marqetive/platforms/instagram/__init__.py,sha256=9V0GPVm9HSTtDdV0bQEfuND8r7PEF2smOL3lb5BZevg,343
12
- marqetive/platforms/instagram/client.py,sha256=PxVsHUW8SSpPe2Kdd5zGezD7LUvJj5tdyNj8In_ChxI,24955
13
- marqetive/platforms/instagram/exceptions.py,sha256=zFG_CkstlZWG47zsFdnObOZviTDIx3Z8yVG9-x7GMVY,8987
14
- marqetive/platforms/instagram/factory.py,sha256=082cF47zs9GvrnrFFaKmftbt_GmvcHZvSNIJsOQcupY,3553
15
- marqetive/platforms/instagram/manager.py,sha256=RhV2QpLp9z2p-MCP3E-UzeYFCmuB0K8PIdDfwSaxdJI,3730
16
- marqetive/platforms/instagram/media.py,sha256=tbFzOf670T8UDoCD9fVeS03Uzhp6HSUFrpPgJVgpdRE,21174
17
- marqetive/platforms/linkedin/__init__.py,sha256=mcHNOKW5niq0MGBMuRpCJfEg2DkE3sCt0J0AqSZUPBs,333
18
- marqetive/platforms/linkedin/client.py,sha256=d7YDd23VAqUMAHw8Oi9Swpq5RwhIIpl4TjbrBI9GkfE,23574
19
- marqetive/platforms/linkedin/exceptions.py,sha256=64J02_99cUcUW5i5xHE_sa0_6V4ibW8PmCEz2aROIE4,9771
20
- marqetive/platforms/linkedin/factory.py,sha256=8JNxAMC-lJcU1trnd0PFge0LQWQMiIVzZORJHwcBhmM,4536
21
- marqetive/platforms/linkedin/manager.py,sha256=XB6HB6-MWsDz2Arij5GuyRH_j5xWkYb8sHCTlKFWzMg,4000
22
- marqetive/platforms/linkedin/media.py,sha256=busrlTA3CisY43qDy9dmiAmbFVz8De4nDsA4CV9PeWk,16917
23
- marqetive/platforms/models.py,sha256=8NSIh-fT5gFHCvs6A5dnnsfhsklpSfNgD_ovDPVv1aY,10860
24
- marqetive/platforms/tiktok/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- marqetive/platforms/twitter/__init__.py,sha256=TRVOejA-wa_tw50_YUwTYnhGyGuOK2IFpZWvVZP94HQ,325
26
- marqetive/platforms/twitter/client.py,sha256=881gM9twIPo81ShTpLXbQMpOzeWad1D4Nu6H0BzCSlY,20840
27
- marqetive/platforms/twitter/exceptions.py,sha256=yRLLcodSouaFV076iwbxcWZzXUuoswAOXuL_-iPvLOk,8935
28
- marqetive/platforms/twitter/factory.py,sha256=nBBl5aFcNCgIwf45PWywjpx_Qp0aOdbOHthvQX-BzI4,5286
29
- marqetive/platforms/twitter/manager.py,sha256=NEqFllDdQ6nlUQMV1M5koX8DMURuvNsa39gKiTpm5A0,4108
30
- marqetive/platforms/twitter/media.py,sha256=szkILg8geuH5wVbGXUBeHr9B-48j08njb-NlClPDcQQ,24984
31
- marqetive/platforms/twitter/threads.py,sha256=F1msBRcmpAb_NKaZvdDn0oA_cQwwzFoXGeiUIdr7rrY,14772
32
- marqetive/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- marqetive/registry_init.py,sha256=ozvGN_lWWx_vvYcU_ONA49EzwN4ymbrB1jtkRchYvAU,2051
34
- marqetive/utils/__init__.py,sha256=qBn_DN-L2YTfuB_G-n_bHJjtuVRiyH3ftl9JZ0JO93M,989
35
- marqetive/utils/file_handlers.py,sha256=IUdRZxr_jxAuwxUypUkUgKqiGDcIHNr1LhCb2Ne2a98,12572
36
- marqetive/utils/helpers.py,sha256=8-ljhL47SremKcQO2GF8DIHOPODEv1rSioVNuSPCbec,2634
37
- marqetive/utils/media.py,sha256=Rvxw9XKU65n-z4G1bEihG3wXZBmjSDZUqClfjGFrg6k,12013
38
- marqetive/utils/oauth.py,sha256=NOQidwZ6vAscUMPUm-Ho16L2G64vGHkopJHZVSNWv88,7921
39
- marqetive/utils/retry.py,sha256=lAniJLMNWp9XsHrvU0XBNifpNEjfde4MGfd5hlFTPfA,7636
40
- marqetive/utils/token_validator.py,sha256=asLMiEgT-BtqEpn_HDX15vJoSBcH7CGW0abervFOXxM,6707
41
- marqetive_lib-0.1.1.dist-info/METADATA,sha256=o7T_GLuQ3UqFCIIGZEsg6gCDOz3sKPxWImBhlaO-XWc,7849
42
- marqetive_lib-0.1.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
43
- marqetive_lib-0.1.1.dist-info/RECORD,,