marqetive-lib 0.1.0__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 (43) hide show
  1. marqetive/__init__.py +113 -0
  2. marqetive/core/__init__.py +5 -0
  3. marqetive/core/account_factory.py +212 -0
  4. marqetive/core/base_manager.py +303 -0
  5. marqetive/core/client.py +108 -0
  6. marqetive/core/progress.py +291 -0
  7. marqetive/core/registry.py +257 -0
  8. marqetive/platforms/__init__.py +55 -0
  9. marqetive/platforms/base.py +390 -0
  10. marqetive/platforms/exceptions.py +238 -0
  11. marqetive/platforms/instagram/__init__.py +7 -0
  12. marqetive/platforms/instagram/client.py +786 -0
  13. marqetive/platforms/instagram/exceptions.py +311 -0
  14. marqetive/platforms/instagram/factory.py +106 -0
  15. marqetive/platforms/instagram/manager.py +112 -0
  16. marqetive/platforms/instagram/media.py +669 -0
  17. marqetive/platforms/linkedin/__init__.py +7 -0
  18. marqetive/platforms/linkedin/client.py +733 -0
  19. marqetive/platforms/linkedin/exceptions.py +335 -0
  20. marqetive/platforms/linkedin/factory.py +130 -0
  21. marqetive/platforms/linkedin/manager.py +119 -0
  22. marqetive/platforms/linkedin/media.py +549 -0
  23. marqetive/platforms/models.py +345 -0
  24. marqetive/platforms/tiktok/__init__.py +0 -0
  25. marqetive/platforms/twitter/__init__.py +7 -0
  26. marqetive/platforms/twitter/client.py +647 -0
  27. marqetive/platforms/twitter/exceptions.py +311 -0
  28. marqetive/platforms/twitter/factory.py +151 -0
  29. marqetive/platforms/twitter/manager.py +121 -0
  30. marqetive/platforms/twitter/media.py +779 -0
  31. marqetive/platforms/twitter/threads.py +442 -0
  32. marqetive/py.typed +0 -0
  33. marqetive/registry_init.py +66 -0
  34. marqetive/utils/__init__.py +45 -0
  35. marqetive/utils/file_handlers.py +438 -0
  36. marqetive/utils/helpers.py +99 -0
  37. marqetive/utils/media.py +399 -0
  38. marqetive/utils/oauth.py +265 -0
  39. marqetive/utils/retry.py +239 -0
  40. marqetive/utils/token_validator.py +240 -0
  41. marqetive_lib-0.1.0.dist-info/METADATA +261 -0
  42. marqetive_lib-0.1.0.dist-info/RECORD +43 -0
  43. marqetive_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,669 @@
1
+ """Instagram media manager for container-based publishing.
2
+
3
+ Instagram uses a two-step publishing process:
4
+ 1. Create media containers (upload and process media)
5
+ 2. Publish containers to make content live
6
+
7
+ This module provides comprehensive media management for:
8
+ - Feed posts (single image or carousel)
9
+ - Reels (videos)
10
+ - Stories (images and videos)
11
+ """
12
+
13
+ import asyncio
14
+ import logging
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from typing import Any, Literal
19
+
20
+ import httpx
21
+
22
+ from marqetive.platforms.exceptions import (
23
+ MediaUploadError,
24
+ ValidationError,
25
+ )
26
+ from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Instagram API limits
31
+ MAX_CAROUSEL_ITEMS = 10
32
+ MAX_CAPTION_LENGTH = 2200
33
+ MAX_VIDEO_DURATION_FEED = 60 # seconds
34
+ MAX_VIDEO_DURATION_REEL = 90 # seconds
35
+ MAX_VIDEO_DURATION_STORY = 60 # seconds
36
+
37
+ # Processing timeouts
38
+ DEFAULT_VIDEO_TIMEOUT = 300 # 5 minutes
39
+ REEL_VIDEO_TIMEOUT = 420 # 7 minutes
40
+ STORY_VIDEO_TIMEOUT = 180 # 3 minutes
41
+
42
+
43
+ class MediaType(str, Enum):
44
+ """Instagram media types."""
45
+
46
+ IMAGE = "IMAGE"
47
+ VIDEO = "VIDEO"
48
+ CAROUSEL = "CAROUSEL"
49
+
50
+
51
+ class PublishStatus(str, Enum):
52
+ """Container publishing status."""
53
+
54
+ EXPIRED = "EXPIRED"
55
+ ERROR = "ERROR"
56
+ FINISHED = "FINISHED"
57
+ IN_PROGRESS = "IN_PROGRESS"
58
+ PUBLISHED = "PUBLISHED"
59
+
60
+
61
+ @dataclass
62
+ class MediaItem:
63
+ """Media item for Instagram posts.
64
+
65
+ Attributes:
66
+ url: URL of the media file (must be publicly accessible).
67
+ type: Type of media ("image" or "video").
68
+ alt_text: Alternative text for accessibility.
69
+ """
70
+
71
+ url: str
72
+ type: Literal["image", "video"]
73
+ alt_text: str | None = None
74
+
75
+ def __post_init__(self) -> None:
76
+ """Validate media item."""
77
+ if not self.url:
78
+ raise ValidationError("Media URL cannot be empty", platform="instagram")
79
+
80
+ if self.type not in ("image", "video"):
81
+ raise ValidationError(
82
+ f"Invalid media type: {self.type}. Must be 'image' or 'video'",
83
+ platform="instagram",
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class ContainerResult:
89
+ """Result of creating a media container.
90
+
91
+ Attributes:
92
+ container_id: Instagram container ID.
93
+ status: Current status of the container.
94
+ media_type: Type of media in container.
95
+ """
96
+
97
+ container_id: str
98
+ status: str
99
+ media_type: str
100
+
101
+
102
+ @dataclass
103
+ class PublishResult:
104
+ """Result of publishing a container.
105
+
106
+ Attributes:
107
+ media_id: Instagram media ID (post/reel/story ID).
108
+ permalink: Direct link to the published content.
109
+ """
110
+
111
+ media_id: str
112
+ permalink: str | None = None
113
+
114
+
115
+ class InstagramMediaManager:
116
+ """Manager for Instagram container-based media publishing.
117
+
118
+ Instagram requires a two-step process:
119
+ 1. Create container (uploads and processes media)
120
+ 2. Publish container (makes it live)
121
+
122
+ Example:
123
+ >>> manager = InstagramMediaManager(ig_user_id, access_token)
124
+ >>> # Single image post
125
+ >>> media = [MediaItem(url="https://...", type="image")]
126
+ >>> container_ids = await manager.create_feed_containers(
127
+ ... media, caption="Hello Instagram!"
128
+ ... )
129
+ >>> result = await manager.publish_container(container_ids[0])
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ ig_user_id: str,
135
+ access_token: str,
136
+ *,
137
+ api_version: str = "v21.0",
138
+ timeout: float = 30.0,
139
+ progress_callback: Callable[[str, str, int], None] | None = None,
140
+ ) -> None:
141
+ """Initialize Instagram media manager.
142
+
143
+ Args:
144
+ ig_user_id: Instagram Business Account ID.
145
+ access_token: Instagram/Facebook access token.
146
+ api_version: Instagram Graph API version.
147
+ timeout: Request timeout in seconds.
148
+ progress_callback: Optional callback(container_id, status, progress_pct).
149
+ """
150
+ self.ig_user_id = ig_user_id
151
+ self.access_token = access_token
152
+ self.api_version = api_version
153
+ self.timeout = timeout
154
+ self.progress_callback = progress_callback
155
+
156
+ self.base_url = f"https://graph.instagram.com/{api_version}"
157
+ self.client = httpx.AsyncClient(
158
+ timeout=httpx.Timeout(timeout),
159
+ params={"access_token": access_token},
160
+ )
161
+
162
+ async def __aenter__(self) -> "InstagramMediaManager":
163
+ """Enter async context."""
164
+ return self
165
+
166
+ async def __aexit__(self, *args: Any) -> None:
167
+ """Exit async context and cleanup."""
168
+ await self.client.aclose()
169
+
170
+ async def create_feed_containers(
171
+ self,
172
+ media_items: list[MediaItem],
173
+ *,
174
+ caption: str | None = None,
175
+ location_id: str | None = None,
176
+ share_to_feed: bool = True,
177
+ ) -> list[str]:
178
+ """Create containers for Instagram feed post (single or carousel).
179
+
180
+ Note: Instagram deprecated video feed posts. Use create_reel_container instead.
181
+
182
+ Args:
183
+ media_items: List of media items (images only, 1-10 items).
184
+ caption: Post caption (max 2200 characters).
185
+ location_id: Optional location ID.
186
+ share_to_feed: Whether to share to feed (default: True).
187
+
188
+ Returns:
189
+ List of container IDs.
190
+
191
+ Raises:
192
+ ValidationError: If validation fails.
193
+ MediaUploadError: If container creation fails.
194
+
195
+ Example:
196
+ >>> media = [
197
+ ... MediaItem("https://example.com/img1.jpg", "image"),
198
+ ... MediaItem("https://example.com/img2.jpg", "image"),
199
+ ... ]
200
+ >>> containers = await manager.create_feed_containers(
201
+ ... media, caption="Carousel post!"
202
+ ... )
203
+ """
204
+ # Validate inputs
205
+ if not media_items:
206
+ raise ValidationError(
207
+ "At least one media item required",
208
+ platform="instagram",
209
+ field="media_items",
210
+ )
211
+
212
+ if len(media_items) > MAX_CAROUSEL_ITEMS:
213
+ raise ValidationError(
214
+ f"Maximum {MAX_CAROUSEL_ITEMS} media items allowed",
215
+ platform="instagram",
216
+ field="media_items",
217
+ )
218
+
219
+ # Check for videos (not allowed in feed posts)
220
+ video_items = [m for m in media_items if m.type == "video"]
221
+ if video_items:
222
+ raise ValidationError(
223
+ "Instagram feed posts no longer support videos. Use create_reel_container instead.",
224
+ platform="instagram",
225
+ field="media_items",
226
+ )
227
+
228
+ if caption and len(caption) > MAX_CAPTION_LENGTH:
229
+ raise ValidationError(
230
+ f"Caption exceeds {MAX_CAPTION_LENGTH} characters",
231
+ platform="instagram",
232
+ field="caption",
233
+ )
234
+
235
+ is_carousel = len(media_items) > 1
236
+ container_ids: list[str] = []
237
+
238
+ logger.info(
239
+ f"Creating {'carousel' if is_carousel else 'single'} "
240
+ f"feed containers for {len(media_items)} items"
241
+ )
242
+
243
+ # Create individual containers
244
+ for idx, media_item in enumerate(media_items):
245
+ if is_carousel:
246
+ # Carousel item containers
247
+ container_id = await self._create_carousel_item_container(
248
+ media_item.url,
249
+ media_item.alt_text,
250
+ )
251
+ else:
252
+ # Single post container
253
+ container_id = await self._create_single_image_container(
254
+ media_item.url,
255
+ caption=caption,
256
+ location_id=location_id,
257
+ share_to_feed=share_to_feed,
258
+ )
259
+
260
+ container_ids.append(container_id)
261
+
262
+ # Notify progress
263
+ if self.progress_callback:
264
+ progress = int(((idx + 1) / len(media_items)) * 100)
265
+ self.progress_callback(container_id, "created", progress)
266
+
267
+ # If carousel, create parent container
268
+ if is_carousel:
269
+ parent_container = await self._create_carousel_parent_container(
270
+ container_ids,
271
+ caption=caption,
272
+ location_id=location_id,
273
+ share_to_feed=share_to_feed,
274
+ )
275
+ # Return only parent container for publishing
276
+ return [parent_container]
277
+
278
+ return container_ids
279
+
280
+ async def create_reel_container(
281
+ self,
282
+ video_url: str,
283
+ *,
284
+ caption: str | None = None,
285
+ cover_url: str | None = None,
286
+ share_to_feed: bool = True,
287
+ audio_name: str | None = None,
288
+ wait_for_processing: bool = True,
289
+ ) -> str:
290
+ """Create container for Instagram Reel (video).
291
+
292
+ Args:
293
+ video_url: URL of video file (publicly accessible).
294
+ caption: Reel caption (max 2200 characters).
295
+ cover_url: Optional thumbnail image URL.
296
+ share_to_feed: Share reel to main feed.
297
+ audio_name: Optional audio/music name attribution.
298
+ wait_for_processing: Wait for video processing to complete.
299
+
300
+ Returns:
301
+ Container ID ready for publishing.
302
+
303
+ Raises:
304
+ ValidationError: If validation fails.
305
+ MediaUploadError: If container creation or processing fails.
306
+
307
+ Example:
308
+ >>> container_id = await manager.create_reel_container(
309
+ ... "https://example.com/video.mp4",
310
+ ... caption="Check out this reel!",
311
+ ... share_to_feed=True
312
+ ... )
313
+ >>> result = await manager.publish_container(container_id)
314
+ """
315
+ if caption and len(caption) > MAX_CAPTION_LENGTH:
316
+ raise ValidationError(
317
+ f"Caption exceeds {MAX_CAPTION_LENGTH} characters",
318
+ platform="instagram",
319
+ field="caption",
320
+ )
321
+
322
+ @retry_async(config=STANDARD_BACKOFF)
323
+ async def _create() -> str:
324
+ params: dict[str, Any] = {
325
+ "media_type": "REELS",
326
+ "video_url": video_url,
327
+ }
328
+
329
+ if caption:
330
+ params["caption"] = caption
331
+ if cover_url:
332
+ params["cover_url"] = cover_url
333
+ if not share_to_feed:
334
+ params["share_to_feed"] = "false"
335
+ if audio_name:
336
+ params["audio_name"] = audio_name
337
+
338
+ response = await self.client.post(
339
+ f"{self.base_url}/{self.ig_user_id}/media",
340
+ params=params,
341
+ )
342
+ response.raise_for_status()
343
+ result = response.json()
344
+ return result["id"]
345
+
346
+ try:
347
+ container_id = await _create()
348
+ logger.info(f"Created reel container: {container_id}")
349
+
350
+ # Wait for video processing if requested
351
+ if wait_for_processing:
352
+ await self._wait_for_container_ready(
353
+ container_id,
354
+ timeout=REEL_VIDEO_TIMEOUT,
355
+ media_type="reel",
356
+ )
357
+
358
+ if self.progress_callback:
359
+ self.progress_callback(container_id, "ready", 100)
360
+
361
+ return container_id
362
+
363
+ except httpx.HTTPError as e:
364
+ raise MediaUploadError(
365
+ f"Failed to create reel container: {e}",
366
+ platform="instagram",
367
+ media_type="video",
368
+ ) from e
369
+
370
+ async def create_story_container(
371
+ self,
372
+ media_url: str,
373
+ media_type: Literal["image", "video"],
374
+ *,
375
+ wait_for_processing: bool = True,
376
+ ) -> str:
377
+ """Create container for Instagram Story.
378
+
379
+ Args:
380
+ media_url: URL of media file.
381
+ media_type: Type of media ("image" or "video").
382
+ wait_for_processing: Wait for video processing (if video).
383
+
384
+ Returns:
385
+ Container ID ready for publishing.
386
+
387
+ Raises:
388
+ MediaUploadError: If container creation fails.
389
+
390
+ Example:
391
+ >>> container_id = await manager.create_story_container(
392
+ ... "https://example.com/story.jpg",
393
+ ... "image"
394
+ ... )
395
+ >>> result = await manager.publish_container(container_id)
396
+ """
397
+
398
+ @retry_async(config=STANDARD_BACKOFF)
399
+ async def _create() -> str:
400
+ params: dict[str, Any] = {
401
+ "media_type": "STORIES",
402
+ }
403
+
404
+ if media_type == "image":
405
+ params["image_url"] = media_url
406
+ else:
407
+ params["video_url"] = media_url
408
+
409
+ response = await self.client.post(
410
+ f"{self.base_url}/{self.ig_user_id}/media",
411
+ params=params,
412
+ )
413
+ response.raise_for_status()
414
+ result = response.json()
415
+ return result["id"]
416
+
417
+ try:
418
+ container_id = await _create()
419
+ logger.info(f"Created story container: {container_id}")
420
+
421
+ # Wait for video processing if needed
422
+ if media_type == "video" and wait_for_processing:
423
+ await self._wait_for_container_ready(
424
+ container_id,
425
+ timeout=STORY_VIDEO_TIMEOUT,
426
+ media_type="story",
427
+ )
428
+
429
+ if self.progress_callback:
430
+ self.progress_callback(container_id, "ready", 100)
431
+
432
+ return container_id
433
+
434
+ except httpx.HTTPError as e:
435
+ raise MediaUploadError(
436
+ f"Failed to create story container: {e}",
437
+ platform="instagram",
438
+ media_type=media_type,
439
+ ) from e
440
+
441
+ async def publish_container(
442
+ self,
443
+ container_id: str,
444
+ ) -> PublishResult:
445
+ """Publish a media container to make it live.
446
+
447
+ Args:
448
+ container_id: Container ID to publish.
449
+
450
+ Returns:
451
+ PublishResult with media ID and permalink.
452
+
453
+ Raises:
454
+ MediaUploadError: If publishing fails.
455
+
456
+ Example:
457
+ >>> result = await manager.publish_container(container_id)
458
+ >>> print(f"Published: {result.media_id}")
459
+ >>> print(f"Link: {result.permalink}")
460
+ """
461
+
462
+ @retry_async(config=STANDARD_BACKOFF)
463
+ async def _publish() -> PublishResult:
464
+ response = await self.client.post(
465
+ f"{self.base_url}/{self.ig_user_id}/media_publish",
466
+ params={"creation_id": container_id},
467
+ )
468
+ response.raise_for_status()
469
+ result = response.json()
470
+
471
+ media_id = result["id"]
472
+
473
+ # Fetch permalink
474
+ permalink = await self._get_media_permalink(media_id)
475
+
476
+ return PublishResult(media_id=media_id, permalink=permalink)
477
+
478
+ try:
479
+ result = await _publish()
480
+ logger.info(f"Published container {container_id} -> {result.media_id}")
481
+ return result
482
+
483
+ except httpx.HTTPError as e:
484
+ raise MediaUploadError(
485
+ f"Failed to publish container: {e}",
486
+ platform="instagram",
487
+ ) from e
488
+
489
+ async def get_container_status(self, container_id: str) -> dict[str, Any]:
490
+ """Get status of a media container.
491
+
492
+ Args:
493
+ container_id: Container ID to check.
494
+
495
+ Returns:
496
+ Dictionary with container status information.
497
+
498
+ Example:
499
+ >>> status = await manager.get_container_status(container_id)
500
+ >>> print(f"Status: {status['status_code']}")
501
+ """
502
+
503
+ @retry_async(config=STANDARD_BACKOFF)
504
+ async def _get_status() -> dict[str, Any]:
505
+ response = await self.client.get(
506
+ f"{self.base_url}/{container_id}",
507
+ params={"fields": "status_code,status"},
508
+ )
509
+ response.raise_for_status()
510
+ return response.json()
511
+
512
+ return await _get_status()
513
+
514
+ async def _wait_for_container_ready(
515
+ self,
516
+ container_id: str,
517
+ *,
518
+ timeout: int = DEFAULT_VIDEO_TIMEOUT,
519
+ check_interval: int = 5,
520
+ media_type: str = "media",
521
+ ) -> None:
522
+ """Wait for container to finish processing.
523
+
524
+ Args:
525
+ container_id: Container ID to monitor.
526
+ timeout: Maximum wait time in seconds.
527
+ check_interval: Seconds between status checks.
528
+ media_type: Type of media for logging.
529
+
530
+ Raises:
531
+ MediaUploadError: If processing fails or times out.
532
+ """
533
+ elapsed = 0
534
+ logger.info(f"Waiting for {media_type} container {container_id} to process...")
535
+
536
+ while elapsed < timeout:
537
+ status_data = await self.get_container_status(container_id)
538
+ status = status_data.get("status_code")
539
+
540
+ if status == PublishStatus.FINISHED.value:
541
+ logger.info(f"Container {container_id} processing complete")
542
+ return
543
+
544
+ if status in (PublishStatus.ERROR.value, PublishStatus.EXPIRED.value):
545
+ error_msg = status_data.get("status", "Unknown error")
546
+ raise MediaUploadError(
547
+ f"Container processing failed: {error_msg}",
548
+ platform="instagram",
549
+ )
550
+
551
+ # Notify progress
552
+ if self.progress_callback:
553
+ progress = min(int((elapsed / timeout) * 90), 90) # Cap at 90%
554
+ self.progress_callback(container_id, "processing", progress)
555
+
556
+ await asyncio.sleep(check_interval)
557
+ elapsed += check_interval
558
+
559
+ raise MediaUploadError(
560
+ f"Container processing timeout after {timeout}s",
561
+ platform="instagram",
562
+ )
563
+
564
+ async def _create_single_image_container(
565
+ self,
566
+ image_url: str,
567
+ *,
568
+ caption: str | None = None,
569
+ location_id: str | None = None,
570
+ share_to_feed: bool = True,
571
+ ) -> str:
572
+ """Create container for single image post."""
573
+
574
+ @retry_async(config=STANDARD_BACKOFF)
575
+ async def _create() -> str:
576
+ params: dict[str, Any] = {
577
+ "image_url": image_url,
578
+ }
579
+
580
+ if caption:
581
+ params["caption"] = caption
582
+ if location_id:
583
+ params["location_id"] = location_id
584
+ if not share_to_feed:
585
+ params["share_to_feed"] = "false"
586
+
587
+ response = await self.client.post(
588
+ f"{self.base_url}/{self.ig_user_id}/media",
589
+ params=params,
590
+ )
591
+ response.raise_for_status()
592
+ result = response.json()
593
+ return result["id"]
594
+
595
+ return await _create()
596
+
597
+ async def _create_carousel_item_container(
598
+ self,
599
+ image_url: str,
600
+ alt_text: str | None = None,
601
+ ) -> str:
602
+ """Create container for carousel item."""
603
+
604
+ @retry_async(config=STANDARD_BACKOFF)
605
+ async def _create() -> str:
606
+ params: dict[str, Any] = {
607
+ "image_url": image_url,
608
+ "is_carousel_item": "true",
609
+ }
610
+
611
+ if alt_text:
612
+ params["caption"] = alt_text # Alt text goes in caption for items
613
+
614
+ response = await self.client.post(
615
+ f"{self.base_url}/{self.ig_user_id}/media",
616
+ params=params,
617
+ )
618
+ response.raise_for_status()
619
+ result = response.json()
620
+ return result["id"]
621
+
622
+ return await _create()
623
+
624
+ async def _create_carousel_parent_container(
625
+ self,
626
+ children_ids: list[str],
627
+ *,
628
+ caption: str | None = None,
629
+ location_id: str | None = None,
630
+ share_to_feed: bool = True,
631
+ ) -> str:
632
+ """Create parent container for carousel post."""
633
+
634
+ @retry_async(config=STANDARD_BACKOFF)
635
+ async def _create() -> str:
636
+ params: dict[str, Any] = {
637
+ "media_type": "CAROUSEL",
638
+ "children": ",".join(children_ids),
639
+ }
640
+
641
+ if caption:
642
+ params["caption"] = caption
643
+ if location_id:
644
+ params["location_id"] = location_id
645
+ if not share_to_feed:
646
+ params["share_to_feed"] = "false"
647
+
648
+ response = await self.client.post(
649
+ f"{self.base_url}/{self.ig_user_id}/media",
650
+ params=params,
651
+ )
652
+ response.raise_for_status()
653
+ result = response.json()
654
+ return result["id"]
655
+
656
+ return await _create()
657
+
658
+ async def _get_media_permalink(self, media_id: str) -> str | None:
659
+ """Get permalink for published media."""
660
+ try:
661
+ response = await self.client.get(
662
+ f"{self.base_url}/{media_id}",
663
+ params={"fields": "permalink"},
664
+ )
665
+ response.raise_for_status()
666
+ result = response.json()
667
+ return result.get("permalink")
668
+ except httpx.HTTPError:
669
+ return None
@@ -0,0 +1,7 @@
1
+ """LinkedIn platform integration."""
2
+
3
+ from marqetive.platforms.linkedin.client import LinkedInClient
4
+ from marqetive.platforms.linkedin.factory import LinkedInAccountFactory
5
+ from marqetive.platforms.linkedin.manager import LinkedInPostManager
6
+
7
+ __all__ = ["LinkedInClient", "LinkedInAccountFactory", "LinkedInPostManager"]