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,786 @@
1
+ """Instagram Graph API client implementation.
2
+
3
+ This module provides a concrete implementation of the SocialMediaPlatform
4
+ ABC for Instagram, using the Instagram Graph API.
5
+
6
+ API Documentation: https://developers.facebook.com/docs/instagram-api
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any, Literal, cast
11
+
12
+ import httpx
13
+ from pydantic import HttpUrl
14
+
15
+ from marqetive.platforms.base import SocialMediaPlatform
16
+ from marqetive.platforms.exceptions import (
17
+ MediaUploadError,
18
+ PlatformAuthError,
19
+ PlatformError,
20
+ PostNotFoundError,
21
+ ValidationError,
22
+ )
23
+ from marqetive.platforms.instagram.media import (
24
+ InstagramMediaManager,
25
+ MediaItem,
26
+ )
27
+ from marqetive.platforms.models import (
28
+ AuthCredentials,
29
+ Comment,
30
+ CommentStatus,
31
+ MediaAttachment,
32
+ MediaType,
33
+ Post,
34
+ PostCreateRequest,
35
+ PostStatus,
36
+ PostUpdateRequest,
37
+ )
38
+
39
+
40
+ class InstagramClient(SocialMediaPlatform):
41
+ """Instagram Graph API client.
42
+
43
+ This client implements the SocialMediaPlatform interface for Instagram,
44
+ using the Instagram Graph API. It supports posts (feed posts), stories,
45
+ and reels, along with comments and media management.
46
+
47
+ Note:
48
+ - Requires a Facebook App with Instagram Graph API permissions
49
+ - Requires an Instagram Business or Creator account
50
+ - Access tokens must have appropriate scopes (instagram_basic,
51
+ instagram_content_publish, etc.)
52
+
53
+ Example:
54
+ >>> credentials = AuthCredentials(
55
+ ... platform="instagram",
56
+ ... access_token="your_token",
57
+ ... user_id="instagram_business_account_id"
58
+ ... )
59
+ >>> async with InstagramClient(credentials) as client:
60
+ ... request = PostCreateRequest(
61
+ ... content="Check out our new product!",
62
+ ... media_urls=["https://example.com/image.jpg"]
63
+ ... )
64
+ ... post = await client.create_post(request)
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ credentials: AuthCredentials,
70
+ timeout: float = 30.0,
71
+ api_version: str = "v21.0",
72
+ ) -> None:
73
+ """Initialize Instagram client.
74
+
75
+ Args:
76
+ credentials: Instagram authentication credentials
77
+ timeout: Request timeout in seconds
78
+ api_version: Instagram Graph API version
79
+
80
+ Raises:
81
+ PlatformAuthError: If credentials are invalid
82
+ """
83
+ base_url = f"https://graph.instagram.com/{api_version}"
84
+ super().__init__(
85
+ platform_name="instagram",
86
+ credentials=credentials,
87
+ base_url=base_url,
88
+ timeout=timeout,
89
+ )
90
+ self.instagram_account_id = credentials.user_id
91
+ self.api_version = api_version
92
+
93
+ # Media manager (initialized in __aenter__)
94
+ self._media_manager: InstagramMediaManager | None = None
95
+
96
+ async def __aenter__(self) -> "InstagramClient":
97
+ """Async context manager entry."""
98
+ await super().__aenter__()
99
+
100
+ # Initialize media manager
101
+ if not self.instagram_account_id:
102
+ raise PlatformAuthError(
103
+ "Instagram account ID (user_id) is required in credentials",
104
+ platform=self.platform_name,
105
+ )
106
+
107
+ self._media_manager = InstagramMediaManager(
108
+ ig_user_id=self.instagram_account_id,
109
+ access_token=self.credentials.access_token,
110
+ api_version=self.api_version,
111
+ timeout=self.timeout,
112
+ )
113
+
114
+ return self
115
+
116
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
117
+ """Async context manager exit."""
118
+ # Cleanup media manager
119
+ if self._media_manager:
120
+ await self._media_manager.__aexit__(exc_type, exc_val, exc_tb)
121
+ self._media_manager = None
122
+
123
+ await super().__aexit__(exc_type, exc_val, exc_tb)
124
+
125
+ # ==================== Authentication Methods ====================
126
+
127
+ async def authenticate(self) -> AuthCredentials:
128
+ """Perform Instagram authentication flow.
129
+
130
+ Note: Instagram uses Facebook OAuth. This method assumes you already
131
+ have a long-lived access token. For the full OAuth flow, use Facebook's
132
+ OAuth implementation.
133
+
134
+ Returns:
135
+ Current credentials if valid.
136
+
137
+ Raises:
138
+ PlatformAuthError: If authentication fails.
139
+ """
140
+ if await self.is_authenticated():
141
+ return self.credentials
142
+
143
+ raise PlatformAuthError(
144
+ "Invalid or expired credentials. Please re-authenticate via Facebook OAuth.",
145
+ platform=self.platform_name,
146
+ )
147
+
148
+ async def refresh_token(self) -> AuthCredentials:
149
+ """Refresh Instagram access token.
150
+
151
+ Instagram long-lived tokens can be refreshed to extend their validity
152
+ from 60 days to another 60 days.
153
+
154
+ Returns:
155
+ Updated credentials with new access token.
156
+
157
+ Raises:
158
+ PlatformAuthError: If token refresh fails.
159
+ """
160
+ if not self.api_client:
161
+ raise RuntimeError("Client must be used as async context manager")
162
+
163
+ try:
164
+ response = await self.api_client.get(
165
+ "/refresh_access_token",
166
+ params={
167
+ "grant_type": "ig_refresh_token",
168
+ "access_token": self.credentials.access_token,
169
+ },
170
+ )
171
+
172
+ data = response.data
173
+ self.credentials.access_token = data["access_token"]
174
+ # Instagram tokens typically expire in 60 days
175
+ self.credentials.expires_at = datetime.fromtimestamp(
176
+ datetime.now().timestamp() + data.get("expires_in", 5184000)
177
+ )
178
+
179
+ return self.credentials
180
+
181
+ except httpx.HTTPError as e:
182
+ raise PlatformAuthError(
183
+ f"Token refresh failed: {e}",
184
+ platform=self.platform_name,
185
+ ) from e
186
+
187
+ async def is_authenticated(self) -> bool:
188
+ """Check if Instagram credentials are valid.
189
+
190
+ Returns:
191
+ True if authenticated and token is valid.
192
+ """
193
+ if not self.api_client:
194
+ raise RuntimeError("Client must be used as async context manager")
195
+
196
+ try:
197
+ # Verify credentials by fetching account info
198
+ await self.api_client.get(
199
+ f"/{self.instagram_account_id}",
200
+ params={
201
+ "fields": "id,username",
202
+ "access_token": self.credentials.access_token,
203
+ },
204
+ )
205
+ return True
206
+ except httpx.HTTPError:
207
+ return False
208
+
209
+ # ==================== Post CRUD Methods ====================
210
+
211
+ async def create_post(self, request: PostCreateRequest) -> Post:
212
+ """Create and publish an Instagram post.
213
+
214
+ Instagram requires a two-step process:
215
+ 1. Create a media container
216
+ 2. Publish the container
217
+
218
+ Args:
219
+ request: Post creation request.
220
+
221
+ Returns:
222
+ Created Post object.
223
+
224
+ Raises:
225
+ ValidationError: If request is invalid.
226
+ MediaUploadError: If media upload fails.
227
+ """
228
+ if not self.api_client:
229
+ raise RuntimeError("Client must be used as async context manager")
230
+
231
+ if not request.media_urls and not request.media_ids:
232
+ raise ValidationError(
233
+ "Instagram posts require at least one media attachment",
234
+ platform=self.platform_name,
235
+ field="media",
236
+ )
237
+
238
+ # Step 1: Create media container
239
+ container_params: dict[str, Any] = {
240
+ "access_token": self.credentials.access_token,
241
+ }
242
+
243
+ if request.media_urls:
244
+ container_params["image_url"] = request.media_urls[0]
245
+
246
+ if request.content:
247
+ container_params["caption"] = request.content
248
+
249
+ try:
250
+ # Create container
251
+ container_response = await self.api_client.post(
252
+ f"/{self.instagram_account_id}/media",
253
+ data=container_params,
254
+ )
255
+ container_id = container_response.data["id"]
256
+
257
+ # Step 2: Publish container
258
+ publish_response = await self.api_client.post(
259
+ f"/{self.instagram_account_id}/media_publish",
260
+ data={
261
+ "creation_id": container_id,
262
+ "access_token": self.credentials.access_token,
263
+ },
264
+ )
265
+ post_id = publish_response.data["id"]
266
+
267
+ # Fetch full post details
268
+ return await self.get_post(post_id)
269
+
270
+ except httpx.HTTPError as e:
271
+ raise MediaUploadError(
272
+ f"Failed to create Instagram post: {e}",
273
+ platform=self.platform_name,
274
+ ) from e
275
+
276
+ async def get_post(self, post_id: str) -> Post:
277
+ """Retrieve an Instagram post by ID.
278
+
279
+ Args:
280
+ post_id: Instagram media ID.
281
+
282
+ Returns:
283
+ Post object with current data.
284
+
285
+ Raises:
286
+ PostNotFoundError: If post doesn't exist.
287
+ """
288
+ if not self.api_client:
289
+ raise RuntimeError("Client must be used as async context manager")
290
+
291
+ try:
292
+ response = await self.api_client.get(
293
+ f"/{post_id}",
294
+ params={
295
+ "fields": "id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count",
296
+ "access_token": self.credentials.access_token,
297
+ },
298
+ )
299
+
300
+ data = response.data
301
+ return self._parse_post(data)
302
+
303
+ except httpx.HTTPStatusError as e:
304
+ if e.response.status_code == 404:
305
+ raise PostNotFoundError(
306
+ post_id=post_id,
307
+ platform=self.platform_name,
308
+ status_code=404,
309
+ ) from e
310
+ raise PlatformError(
311
+ f"Failed to fetch post: {e}",
312
+ platform=self.platform_name,
313
+ ) from e
314
+ except httpx.HTTPError as e:
315
+ raise PlatformError(
316
+ f"Failed to fetch post: {e}",
317
+ platform=self.platform_name,
318
+ ) from e
319
+
320
+ async def update_post(
321
+ self,
322
+ post_id: str, # noqa: ARG002
323
+ request: PostUpdateRequest, # noqa: ARG002
324
+ ) -> Post:
325
+ """Update an Instagram post.
326
+
327
+ Note: Instagram does not support editing published posts. This method
328
+ will raise an error.
329
+
330
+ Args:
331
+ post_id: Instagram media ID.
332
+ request: Post update request.
333
+
334
+ Raises:
335
+ PlatformError: Instagram doesn't support post editing.
336
+ """
337
+ raise PlatformError(
338
+ "Instagram does not support editing published posts",
339
+ platform=self.platform_name,
340
+ )
341
+
342
+ async def delete_post(self, post_id: str) -> bool:
343
+ """Delete an Instagram post.
344
+
345
+ Args:
346
+ post_id: Instagram media ID.
347
+
348
+ Returns:
349
+ True if deletion was successful.
350
+
351
+ Raises:
352
+ PostNotFoundError: If post doesn't exist.
353
+ """
354
+ if not self.api_client:
355
+ raise RuntimeError("Client must be used as async context manager")
356
+
357
+ try:
358
+ await self.api_client.post(
359
+ f"/{post_id}",
360
+ data={
361
+ "access_token": self.credentials.access_token,
362
+ },
363
+ )
364
+ return True
365
+
366
+ except httpx.HTTPStatusError as e:
367
+ if e.response.status_code == 404:
368
+ raise PostNotFoundError(
369
+ post_id=post_id,
370
+ platform=self.platform_name,
371
+ status_code=404,
372
+ ) from e
373
+ raise PlatformError(
374
+ f"Failed to delete post: {e}",
375
+ platform=self.platform_name,
376
+ ) from e
377
+ except httpx.HTTPError as e:
378
+ raise PlatformError(
379
+ f"Failed to delete post: {e}",
380
+ platform=self.platform_name,
381
+ ) from e
382
+
383
+ # ==================== Comment Methods ====================
384
+
385
+ async def get_comments(
386
+ self,
387
+ post_id: str,
388
+ limit: int = 50,
389
+ offset: int = 0, # noqa: ARG002
390
+ ) -> list[Comment]:
391
+ """Retrieve comments for an Instagram post.
392
+
393
+ Args:
394
+ post_id: Instagram media ID.
395
+ limit: Maximum number of comments to retrieve.
396
+ offset: Number of comments to skip.
397
+
398
+ Returns:
399
+ List of Comment objects.
400
+ """
401
+ if not self.api_client:
402
+ raise RuntimeError("Client must be used as async context manager")
403
+
404
+ try:
405
+ response = await self.api_client.get(
406
+ f"/{post_id}/comments",
407
+ params={
408
+ "fields": "id,text,username,timestamp,like_count",
409
+ "access_token": self.credentials.access_token,
410
+ "limit": limit,
411
+ },
412
+ )
413
+
414
+ comments = []
415
+ for comment_data in response.data.get("data", []):
416
+ comments.append(self._parse_comment(comment_data, post_id))
417
+
418
+ return comments
419
+
420
+ except httpx.HTTPError as e:
421
+ raise PlatformError(
422
+ f"Failed to fetch comments: {e}",
423
+ platform=self.platform_name,
424
+ ) from e
425
+
426
+ async def create_comment(self, post_id: str, content: str) -> Comment:
427
+ """Add a comment to an Instagram post.
428
+
429
+ Args:
430
+ post_id: Instagram media ID.
431
+ content: Text content of the comment.
432
+
433
+ Returns:
434
+ Created Comment object.
435
+
436
+ Raises:
437
+ ValidationError: If comment content is invalid.
438
+ """
439
+ if not self.api_client:
440
+ raise RuntimeError("Client must be used as async context manager")
441
+
442
+ if not content or len(content) == 0:
443
+ raise ValidationError(
444
+ "Comment content cannot be empty",
445
+ platform=self.platform_name,
446
+ field="content",
447
+ )
448
+
449
+ try:
450
+ response = await self.api_client.post(
451
+ f"/{post_id}/comments",
452
+ data={
453
+ "message": content,
454
+ "access_token": self.credentials.access_token,
455
+ },
456
+ )
457
+
458
+ comment_id = response.data["id"]
459
+
460
+ # Fetch full comment details
461
+ comment_response = await self.api_client.get(
462
+ f"/{comment_id}",
463
+ params={
464
+ "fields": "id,text,username,timestamp,like_count",
465
+ "access_token": self.credentials.access_token,
466
+ },
467
+ )
468
+
469
+ return self._parse_comment(comment_response.data, post_id)
470
+
471
+ except httpx.HTTPError as e:
472
+ raise PlatformError(
473
+ f"Failed to create comment: {e}",
474
+ platform=self.platform_name,
475
+ ) from e
476
+
477
+ async def delete_comment(self, comment_id: str) -> bool:
478
+ """Delete an Instagram comment.
479
+
480
+ Args:
481
+ comment_id: Instagram comment ID.
482
+
483
+ Returns:
484
+ True if deletion was successful.
485
+ """
486
+ if not self.api_client:
487
+ raise RuntimeError("Client must be used as async context manager")
488
+
489
+ try:
490
+ await self.api_client.post(
491
+ f"/{comment_id}",
492
+ data={
493
+ "access_token": self.credentials.access_token,
494
+ },
495
+ )
496
+ return True
497
+
498
+ except httpx.HTTPError as e:
499
+ raise PlatformError(
500
+ f"Failed to delete comment: {e}",
501
+ platform=self.platform_name,
502
+ ) from e
503
+
504
+ # ==================== Media Methods ====================
505
+
506
+ async def upload_media(
507
+ self,
508
+ media_url: str,
509
+ media_type: str,
510
+ alt_text: str | None = None,
511
+ ) -> MediaAttachment:
512
+ """Upload media to Instagram.
513
+
514
+ Note: Instagram requires media to be hosted on a publicly accessible
515
+ URL. This method creates a media container that can be used for
516
+ publishing.
517
+
518
+ Args:
519
+ media_url: Public URL of the media file.
520
+ media_type: Type of media (image or video).
521
+ alt_text: Alternative text for accessibility.
522
+
523
+ Returns:
524
+ MediaAttachment object with container ID.
525
+
526
+ Raises:
527
+ MediaUploadError: If upload fails.
528
+ """
529
+ if not self.api_client:
530
+ raise RuntimeError("Client must be used as async context manager")
531
+
532
+ params: dict[str, Any] = {
533
+ "access_token": self.credentials.access_token,
534
+ }
535
+
536
+ if media_type.lower() == "image":
537
+ params["image_url"] = media_url
538
+ elif media_type.lower() == "video":
539
+ params["video_url"] = media_url
540
+ params["media_type"] = "VIDEO"
541
+ else:
542
+ raise ValidationError(
543
+ f"Unsupported media type: {media_type}",
544
+ platform=self.platform_name,
545
+ field="media_type",
546
+ )
547
+
548
+ try:
549
+ response = await self.api_client.post(
550
+ f"/{self.instagram_account_id}/media",
551
+ data=params,
552
+ )
553
+
554
+ container_id = response.data["id"]
555
+
556
+ return MediaAttachment(
557
+ media_id=container_id,
558
+ media_type=(
559
+ MediaType.IMAGE
560
+ if media_type.lower() == "image"
561
+ else MediaType.VIDEO
562
+ ),
563
+ url=cast(HttpUrl, media_url),
564
+ alt_text=alt_text,
565
+ )
566
+
567
+ except httpx.HTTPError as e:
568
+ raise MediaUploadError(
569
+ f"Failed to upload media: {e}",
570
+ platform=self.platform_name,
571
+ media_type=media_type,
572
+ ) from e
573
+
574
+ async def create_carousel(
575
+ self,
576
+ media_urls: list[str],
577
+ caption: str | None = None,
578
+ *,
579
+ alt_texts: list[str] | None = None,
580
+ location_id: str | None = None,
581
+ ) -> Post:
582
+ """Create an Instagram carousel post (2-10 images).
583
+
584
+ Args:
585
+ media_urls: List of image URLs (2-10 items).
586
+ caption: Post caption.
587
+ alt_texts: Optional alt texts for each image.
588
+ location_id: Optional location ID.
589
+
590
+ Returns:
591
+ Published Post object.
592
+
593
+ Raises:
594
+ ValidationError: If inputs are invalid.
595
+ MediaUploadError: If creation fails.
596
+ RuntimeError: If client not used as context manager.
597
+
598
+ Example:
599
+ >>> async with InstagramClient(credentials) as client:
600
+ ... post = await client.create_carousel(
601
+ ... media_urls=[
602
+ ... "https://example.com/img1.jpg",
603
+ ... "https://example.com/img2.jpg",
604
+ ... ],
605
+ ... caption="Beautiful carousel post!"
606
+ ... )
607
+ """
608
+ if not self._media_manager:
609
+ raise RuntimeError("Client must be used as async context manager")
610
+
611
+ # Convert to MediaItem objects
612
+ media_items = []
613
+ for idx, url in enumerate(media_urls):
614
+ alt_text = None
615
+ if alt_texts and idx < len(alt_texts):
616
+ alt_text = alt_texts[idx]
617
+ media_items.append(MediaItem(url=url, type="image", alt_text=alt_text))
618
+
619
+ # Create containers
620
+ container_ids = await self._media_manager.create_feed_containers(
621
+ media_items,
622
+ caption=caption,
623
+ location_id=location_id,
624
+ )
625
+
626
+ # Publish
627
+ result = await self._media_manager.publish_container(container_ids[0])
628
+
629
+ # Fetch and return post
630
+ return await self.get_post(result.media_id)
631
+
632
+ async def create_reel(
633
+ self,
634
+ video_url: str,
635
+ caption: str | None = None,
636
+ *,
637
+ cover_url: str | None = None,
638
+ share_to_feed: bool = True,
639
+ ) -> Post:
640
+ """Create an Instagram Reel (video).
641
+
642
+ Args:
643
+ video_url: URL of video file.
644
+ caption: Reel caption.
645
+ cover_url: Optional thumbnail image URL.
646
+ share_to_feed: Share reel to main feed.
647
+
648
+ Returns:
649
+ Published Post object.
650
+
651
+ Raises:
652
+ MediaUploadError: If creation fails.
653
+ RuntimeError: If client not used as context manager.
654
+
655
+ Example:
656
+ >>> async with InstagramClient(credentials) as client:
657
+ ... reel = await client.create_reel(
658
+ ... video_url="https://example.com/video.mp4",
659
+ ... caption="Check out this reel!",
660
+ ... share_to_feed=True
661
+ ... )
662
+ """
663
+ if not self._media_manager:
664
+ raise RuntimeError("Client must be used as async context manager")
665
+
666
+ # Create reel container
667
+ container_id = await self._media_manager.create_reel_container(
668
+ video_url,
669
+ caption=caption,
670
+ cover_url=cover_url,
671
+ share_to_feed=share_to_feed,
672
+ wait_for_processing=True,
673
+ )
674
+
675
+ # Publish
676
+ result = await self._media_manager.publish_container(container_id)
677
+
678
+ # Fetch and return post
679
+ return await self.get_post(result.media_id)
680
+
681
+ async def create_story(
682
+ self,
683
+ media_url: str,
684
+ media_type: Literal["image", "video"],
685
+ ) -> Post:
686
+ """Create an Instagram Story.
687
+
688
+ Args:
689
+ media_url: URL of media file.
690
+ media_type: Type of media ("image" or "video").
691
+
692
+ Returns:
693
+ Published Post object.
694
+
695
+ Raises:
696
+ MediaUploadError: If creation fails.
697
+ RuntimeError: If client not used as context manager.
698
+
699
+ Example:
700
+ >>> async with InstagramClient(credentials) as client:
701
+ ... story = await client.create_story(
702
+ ... media_url="https://example.com/story.jpg",
703
+ ... media_type="image"
704
+ ... )
705
+ """
706
+ if not self._media_manager:
707
+ raise RuntimeError("Client must be used as async context manager")
708
+
709
+ # Create story container
710
+ container_id = await self._media_manager.create_story_container(
711
+ media_url,
712
+ media_type,
713
+ wait_for_processing=(media_type == "video"),
714
+ )
715
+
716
+ # Publish
717
+ result = await self._media_manager.publish_container(container_id)
718
+
719
+ # Fetch and return post
720
+ return await self.get_post(result.media_id)
721
+
722
+ # ==================== Helper Methods ====================
723
+
724
+ def _parse_post(self, data: dict[str, Any]) -> Post:
725
+ """Parse Instagram API response into Post model.
726
+
727
+ Args:
728
+ data: Raw API response data.
729
+
730
+ Returns:
731
+ Post object.
732
+ """
733
+ media_type_map = {
734
+ "IMAGE": MediaType.IMAGE,
735
+ "VIDEO": MediaType.VIDEO,
736
+ "CAROUSEL_ALBUM": MediaType.CAROUSEL,
737
+ }
738
+
739
+ media = []
740
+ if data.get("media_url"):
741
+ media.append(
742
+ MediaAttachment(
743
+ media_id=data["id"],
744
+ media_type=media_type_map.get(
745
+ data.get("media_type", "IMAGE"),
746
+ MediaType.IMAGE,
747
+ ),
748
+ url=data["media_url"],
749
+ )
750
+ )
751
+
752
+ return Post(
753
+ post_id=data["id"],
754
+ platform=self.platform_name,
755
+ content=data.get("caption"),
756
+ media=media,
757
+ status=PostStatus.PUBLISHED,
758
+ url=data.get("permalink"),
759
+ created_at=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
760
+ likes_count=data.get("like_count", 0),
761
+ comments_count=data.get("comments_count", 0),
762
+ raw_data=data,
763
+ )
764
+
765
+ def _parse_comment(self, data: dict[str, Any], post_id: str) -> Comment:
766
+ """Parse Instagram API response into Comment model.
767
+
768
+ Args:
769
+ data: Raw API response data.
770
+ post_id: ID of the post this comment belongs to.
771
+
772
+ Returns:
773
+ Comment object.
774
+ """
775
+ return Comment(
776
+ comment_id=data["id"],
777
+ post_id=post_id,
778
+ platform=self.platform_name,
779
+ content=data.get("text", ""),
780
+ author_username=data.get("username"),
781
+ author_id=data.get("user", {}).get("id", ""),
782
+ created_at=datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")),
783
+ likes_count=data.get("like_count", 0),
784
+ status=CommentStatus.VISIBLE,
785
+ raw_data=data,
786
+ )