marqetive-lib 0.1.0__py3-none-any.whl → 0.1.2__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 +9 -9
  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 +1 -1
  7. marqetive/platforms/__init__.py +3 -3
  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 +277 -0
  25. marqetive/platforms/tiktok/exceptions.py +180 -0
  26. marqetive/platforms/tiktok/factory.py +188 -0
  27. marqetive/platforms/tiktok/manager.py +115 -0
  28. marqetive/platforms/tiktok/media.py +305 -0
  29. marqetive/platforms/twitter/__init__.py +3 -3
  30. marqetive/platforms/twitter/client.py +6 -6
  31. marqetive/platforms/twitter/exceptions.py +1 -1
  32. marqetive/platforms/twitter/factory.py +5 -5
  33. marqetive/platforms/twitter/manager.py +4 -4
  34. marqetive/platforms/twitter/media.py +4 -4
  35. marqetive/platforms/twitter/threads.py +2 -2
  36. marqetive/registry_init.py +4 -4
  37. marqetive/utils/__init__.py +3 -3
  38. marqetive/utils/file_handlers.py +1 -1
  39. marqetive/utils/oauth.py +137 -2
  40. marqetive/utils/token_validator.py +1 -1
  41. {marqetive_lib-0.1.0.dist-info → marqetive_lib-0.1.2.dist-info}/METADATA +1 -2
  42. marqetive_lib-0.1.2.dist-info/RECORD +48 -0
  43. marqetive_lib-0.1.0.dist-info/RECORD +0 -43
  44. {marqetive_lib-0.1.0.dist-info → marqetive_lib-0.1.2.dist-info}/WHEEL +0 -0
marqetive/__init__.py CHANGED
@@ -28,20 +28,20 @@ Usage:
28
28
 
29
29
  # Core API client
30
30
  # Account management
31
- from marqetive.core.account_factory import BaseAccountFactory
31
+ from src.marqetive.core.account_factory import BaseAccountFactory
32
32
 
33
33
  # Registry and managers
34
- from marqetive.core.base_manager import BasePostManager
35
- from marqetive.core.client import APIClient
34
+ from src.marqetive.core.base_manager import BasePostManager
35
+ from src.marqetive.core.client import APIClient
36
36
 
37
37
  # Progress tracking
38
- from marqetive.core.progress import (
38
+ from src.marqetive.core.progress import (
39
39
  ProgressCallback,
40
40
  ProgressEvent,
41
41
  ProgressStatus,
42
42
  ProgressTracker,
43
43
  )
44
- from marqetive.core.registry import (
44
+ from src.marqetive.core.registry import (
45
45
  PlatformRegistry,
46
46
  get_available_platforms,
47
47
  get_manager_for_platform,
@@ -50,7 +50,7 @@ from marqetive.core.registry import (
50
50
  )
51
51
 
52
52
  # Models
53
- from marqetive.platforms.models import (
53
+ from src.marqetive.platforms.models import (
54
54
  AccountStatus,
55
55
  AuthCredentials,
56
56
  Comment,
@@ -62,16 +62,16 @@ from marqetive.platforms.models import (
62
62
  PostStatus,
63
63
  PostUpdateRequest,
64
64
  )
65
- from marqetive.registry_init import (
65
+ from src.marqetive.registry_init import (
66
66
  initialize_platform_registry,
67
67
  is_registry_initialized,
68
68
  )
69
69
 
70
70
  # Utilities
71
- from marqetive.utils.helpers import format_response
71
+ from src.marqetive.utils.helpers import format_response
72
72
 
73
73
  # Retry utilities
74
- from marqetive.utils.retry import STANDARD_BACKOFF, BackoffConfig, retry_async
74
+ from src.marqetive.utils.retry import STANDARD_BACKOFF, BackoffConfig, retry_async
75
75
 
76
76
  __version__ = "0.1.0"
77
77
 
@@ -1,5 +1,5 @@
1
1
  """Core functionality for MarqetiveLib."""
2
2
 
3
- from marqetive.core.client import APIClient
3
+ from src.marqetive.core.client import APIClient
4
4
 
5
5
  __all__ = ["APIClient"]
@@ -9,8 +9,8 @@ from abc import ABC, abstractmethod
9
9
  from collections.abc import Callable
10
10
  from typing import Any
11
11
 
12
- from marqetive.platforms.exceptions import PlatformAuthError
13
- from marqetive.platforms.models import AccountStatus, AuthCredentials
12
+ from src.marqetive.platforms.exceptions import PlatformAuthError
13
+ from src.marqetive.platforms.models import AccountStatus, AuthCredentials
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -10,10 +10,10 @@ import logging
10
10
  from abc import ABC, abstractmethod
11
11
  from typing import Any
12
12
 
13
- from marqetive.core.account_factory import BaseAccountFactory
14
- from marqetive.core.progress import ProgressCallback, ProgressTracker
15
- from marqetive.platforms.exceptions import PlatformError
16
- from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
13
+ from src.marqetive.core.account_factory import BaseAccountFactory
14
+ from src.marqetive.core.progress import ProgressCallback, ProgressTracker
15
+ from src.marqetive.platforms.exceptions import PlatformError
16
+ from src.marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
marqetive/core/client.py CHANGED
@@ -73,7 +73,7 @@ class APIClient:
73
73
  if not self._client:
74
74
  raise RuntimeError("Client not initialized. Use async context manager.")
75
75
 
76
- response: httpx.Response = await self._client.get(path, params=params)
76
+ response = await self._client.get(path, params=params)
77
77
  response.raise_for_status()
78
78
 
79
79
  return APIResponse(
@@ -7,7 +7,7 @@ platform managers across the application.
7
7
  import threading
8
8
  from typing import Any, TypeVar
9
9
 
10
- from marqetive.platforms.models import AuthCredentials
10
+ from src.marqetive.platforms.models import AuthCredentials
11
11
 
12
12
  # Type variable for manager classes
13
13
  ManagerType = TypeVar("ManagerType")
@@ -9,8 +9,8 @@ Platform clients are available via their respective subpackages:
9
9
  - from marqetive_lib.platforms.instagram import InstagramClient
10
10
  """
11
11
 
12
- from marqetive.platforms.base import SocialMediaPlatform
13
- from marqetive.platforms.exceptions import (
12
+ from src.marqetive.platforms.base import SocialMediaPlatform
13
+ from src.marqetive.platforms.exceptions import (
14
14
  MediaUploadError,
15
15
  PlatformAuthError,
16
16
  PlatformError,
@@ -18,7 +18,7 @@ from marqetive.platforms.exceptions import (
18
18
  RateLimitError,
19
19
  ValidationError,
20
20
  )
21
- from marqetive.platforms.models import (
21
+ from src.marqetive.platforms.models import (
22
22
  AuthCredentials,
23
23
  Comment,
24
24
  CommentStatus,
@@ -10,12 +10,12 @@ from datetime import datetime
10
10
  from traceback import TracebackException
11
11
  from typing import Any
12
12
 
13
- from marqetive.core.client import APIClient
14
- from marqetive.platforms.exceptions import (
13
+ from src.marqetive.core.client import APIClient
14
+ from src.marqetive.platforms.exceptions import (
15
15
  PlatformAuthError,
16
16
  RateLimitError,
17
17
  )
18
- from marqetive.platforms.models import (
18
+ from src.marqetive.platforms.models import (
19
19
  AuthCredentials,
20
20
  Comment,
21
21
  MediaAttachment,
@@ -115,9 +115,10 @@ class PostNotFoundError(PlatformError):
115
115
  post_id: str,
116
116
  platform: str | None = None,
117
117
  status_code: int | None = None,
118
+ message: str | None = None,
118
119
  ) -> None:
119
120
  self.post_id = post_id
120
- message = f"Post not found: {post_id}"
121
+ message = message or f"Post not found: {post_id}"
121
122
  super().__init__(message, platform, status_code)
122
123
 
123
124
 
@@ -1,7 +1,7 @@
1
1
  """Instagram platform integration."""
2
2
 
3
- from marqetive.platforms.instagram.client import InstagramClient
4
- from marqetive.platforms.instagram.factory import InstagramAccountFactory
5
- from marqetive.platforms.instagram.manager import InstagramPostManager
3
+ from src.marqetive.platforms.instagram.client import InstagramClient
4
+ from src.marqetive.platforms.instagram.factory import InstagramAccountFactory
5
+ from src.marqetive.platforms.instagram.manager import InstagramPostManager
6
6
 
7
7
  __all__ = ["InstagramClient", "InstagramAccountFactory", "InstagramPostManager"]
@@ -12,19 +12,19 @@ from typing import Any, Literal, cast
12
12
  import httpx
13
13
  from pydantic import HttpUrl
14
14
 
15
- from marqetive.platforms.base import SocialMediaPlatform
16
- from marqetive.platforms.exceptions import (
15
+ from src.marqetive.platforms.base import SocialMediaPlatform
16
+ from src.marqetive.platforms.exceptions import (
17
17
  MediaUploadError,
18
18
  PlatformAuthError,
19
19
  PlatformError,
20
20
  PostNotFoundError,
21
21
  ValidationError,
22
22
  )
23
- from marqetive.platforms.instagram.media import (
23
+ from src.marqetive.platforms.instagram.media import (
24
24
  InstagramMediaManager,
25
25
  MediaItem,
26
26
  )
27
- from marqetive.platforms.models import (
27
+ from src.marqetive.platforms.models import (
28
28
  AuthCredentials,
29
29
  Comment,
30
30
  CommentStatus,
@@ -5,7 +5,7 @@ This module provides comprehensive error handling for Instagram Graph API errors
5
5
 
6
6
  from typing import Any
7
7
 
8
- from marqetive.platforms.exceptions import (
8
+ from src.marqetive.platforms.exceptions import (
9
9
  MediaUploadError,
10
10
  PlatformAuthError,
11
11
  PlatformError,
@@ -3,11 +3,11 @@
3
3
  import logging
4
4
  from collections.abc import Callable
5
5
 
6
- from marqetive.core.account_factory import BaseAccountFactory
7
- from marqetive.platforms.exceptions import PlatformAuthError
8
- from marqetive.platforms.instagram.client import InstagramClient
9
- from marqetive.platforms.models import AccountStatus, AuthCredentials
10
- from marqetive.utils.oauth import refresh_instagram_token
6
+ from src.marqetive.core.account_factory import BaseAccountFactory
7
+ from src.marqetive.platforms.exceptions import PlatformAuthError
8
+ from src.marqetive.platforms.instagram.client import InstagramClient
9
+ from src.marqetive.platforms.models import AccountStatus, AuthCredentials
10
+ from src.marqetive.utils.oauth import refresh_instagram_token
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -3,10 +3,10 @@
3
3
  import logging
4
4
  from typing import Any
5
5
 
6
- from marqetive.core.base_manager import BasePostManager
7
- from marqetive.platforms.instagram.client import InstagramClient
8
- from marqetive.platforms.instagram.factory import InstagramAccountFactory
9
- from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
6
+ from src.marqetive.core.base_manager import BasePostManager
7
+ from src.marqetive.platforms.instagram.client import InstagramClient
8
+ from src.marqetive.platforms.instagram.factory import InstagramAccountFactory
9
+ from src.marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -19,11 +19,11 @@ from typing import Any, Literal
19
19
 
20
20
  import httpx
21
21
 
22
- from marqetive.platforms.exceptions import (
22
+ from src.marqetive.platforms.exceptions import (
23
23
  MediaUploadError,
24
24
  ValidationError,
25
25
  )
26
- from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
26
+ from src.marqetive.utils.retry import STANDARD_BACKOFF, retry_async
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -1,7 +1,7 @@
1
1
  """LinkedIn platform integration."""
2
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
3
+ from src.marqetive.platforms.linkedin.client import LinkedInClient
4
+ from src.marqetive.platforms.linkedin.factory import LinkedInAccountFactory
5
+ from src.marqetive.platforms.linkedin.manager import LinkedInPostManager
6
6
 
7
7
  __all__ = ["LinkedInClient", "LinkedInAccountFactory", "LinkedInPostManager"]
@@ -12,16 +12,16 @@ from typing import Any, cast
12
12
  import httpx
13
13
  from pydantic import HttpUrl
14
14
 
15
- from marqetive.platforms.base import SocialMediaPlatform
16
- from marqetive.platforms.exceptions import (
15
+ from src.marqetive.platforms.base import SocialMediaPlatform
16
+ from src.marqetive.platforms.exceptions import (
17
17
  MediaUploadError,
18
18
  PlatformAuthError,
19
19
  PlatformError,
20
20
  PostNotFoundError,
21
21
  ValidationError,
22
22
  )
23
- from marqetive.platforms.linkedin.media import LinkedInMediaManager, MediaAsset
24
- from marqetive.platforms.models import (
23
+ from src.marqetive.platforms.linkedin.media import LinkedInMediaManager, MediaAsset
24
+ from src.marqetive.platforms.models import (
25
25
  AuthCredentials,
26
26
  Comment,
27
27
  CommentStatus,
@@ -9,7 +9,7 @@ This module provides comprehensive error handling for LinkedIn API errors includ
9
9
 
10
10
  from typing import Any
11
11
 
12
- from marqetive.platforms.exceptions import (
12
+ from src.marqetive.platforms.exceptions import (
13
13
  MediaUploadError,
14
14
  PlatformAuthError,
15
15
  PlatformError,
@@ -4,11 +4,11 @@ import logging
4
4
  import os
5
5
  from collections.abc import Callable
6
6
 
7
- from marqetive.core.account_factory import BaseAccountFactory
8
- from marqetive.platforms.exceptions import PlatformAuthError
9
- from marqetive.platforms.linkedin.client import LinkedInClient
10
- from marqetive.platforms.models import AccountStatus, AuthCredentials
11
- from marqetive.utils.oauth import refresh_linkedin_token
7
+ from src.marqetive.core.account_factory import BaseAccountFactory
8
+ from src.marqetive.platforms.exceptions import PlatformAuthError
9
+ from src.marqetive.platforms.linkedin.client import LinkedInClient
10
+ from src.marqetive.platforms.models import AccountStatus, AuthCredentials
11
+ from src.marqetive.utils.oauth import refresh_linkedin_token
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -3,10 +3,10 @@
3
3
  import logging
4
4
  from typing import Any
5
5
 
6
- from marqetive.core.base_manager import BasePostManager
7
- from marqetive.platforms.linkedin.client import LinkedInClient
8
- from marqetive.platforms.linkedin.factory import LinkedInAccountFactory
9
- from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
6
+ from src.marqetive.core.base_manager import BasePostManager
7
+ from src.marqetive.platforms.linkedin.client import LinkedInClient
8
+ from src.marqetive.platforms.linkedin.factory import LinkedInAccountFactory
9
+ from src.marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -20,13 +20,13 @@ from typing import Any, Literal
20
20
 
21
21
  import httpx
22
22
 
23
- from marqetive.platforms.exceptions import (
23
+ from src.marqetive.platforms.exceptions import (
24
24
  MediaUploadError,
25
25
  ValidationError,
26
26
  )
27
- from marqetive.utils.file_handlers import download_file, read_file_bytes
28
- from marqetive.utils.media import detect_mime_type, format_file_size
29
- from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
27
+ from src.marqetive.utils.file_handlers import download_file, read_file_bytes
28
+ from src.marqetive.utils.media import detect_mime_type, format_file_size
29
+ from src.marqetive.utils.retry import STANDARD_BACKOFF, retry_async
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -305,6 +305,7 @@ class PostCreateRequest(BaseModel):
305
305
  link: URL to include in the post
306
306
  tags: List of hashtags or user tags
307
307
  location: Location/place tag for the post
308
+ additional_data: Platform-specific data
308
309
 
309
310
  Example:
310
311
  >>> request = PostCreateRequest(
@@ -321,6 +322,7 @@ class PostCreateRequest(BaseModel):
321
322
  link: str | None = None
322
323
  tags: list[str] = Field(default_factory=list)
323
324
  location: str | None = None
325
+ additional_data: dict[str, Any] = Field(default_factory=dict)
324
326
 
325
327
 
326
328
  class PostUpdateRequest(BaseModel):
@@ -0,0 +1,7 @@
1
+ """TikTok platform integration."""
2
+
3
+ from src.marqetive.platforms.tiktok.client import TikTokClient
4
+ from src.marqetive.platforms.tiktok.factory import TikTokAccountFactory
5
+ from src.marqetive.platforms.tiktok.manager import TikTokPostManager
6
+
7
+ __all__ = ["TikTokClient", "TikTokAccountFactory", "TikTokPostManager"]
@@ -0,0 +1,277 @@
1
+ """TikTok API client implementation.
2
+
3
+ This module provides a concrete implementation of the SocialMediaPlatform
4
+ ABC for TikTok, using the TikTok for Business API (hypothetical).
5
+
6
+ API Documentation: https://developers.tiktok.com/doc/overview
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from pydantic import HttpUrl
13
+
14
+ from src.marqetive.platforms.base import SocialMediaPlatform
15
+ from src.marqetive.platforms.exceptions import (
16
+ PlatformAuthError,
17
+ PlatformError,
18
+ PostNotFoundError,
19
+ ValidationError,
20
+ )
21
+ from src.marqetive.platforms.models import (
22
+ AuthCredentials,
23
+ Comment,
24
+ CommentStatus,
25
+ MediaAttachment,
26
+ MediaType,
27
+ Post,
28
+ PostCreateRequest,
29
+ PostStatus,
30
+ PostUpdateRequest,
31
+ )
32
+ from src.marqetive.platforms.tiktok.media import TikTokMediaManager
33
+
34
+
35
+ class TikTokClient(SocialMediaPlatform):
36
+ """TikTok API client.
37
+
38
+ This client implements the SocialMediaPlatform interface for TikTok,
39
+ focusing on video uploads and management.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ credentials: AuthCredentials,
45
+ timeout: float = 300.0,
46
+ ) -> None:
47
+ """Initialize TikTok client."""
48
+ base_url = "https://open.tiktokapis.com/v2/"
49
+ super().__init__(
50
+ platform_name="tiktok",
51
+ credentials=credentials,
52
+ base_url=base_url,
53
+ timeout=timeout,
54
+ )
55
+ self._media_manager: TikTokMediaManager | None = None
56
+
57
+ async def _setup_managers(self) -> None:
58
+ """Setup media manager."""
59
+ if not self.credentials.access_token or not self.credentials.additional_data:
60
+ raise PlatformAuthError("Access token and open_id are required", "tiktok")
61
+
62
+ self._media_manager = TikTokMediaManager(
63
+ access_token=self.credentials.access_token,
64
+ open_id=self.credentials.additional_data["open_id"],
65
+ timeout=self.timeout,
66
+ )
67
+
68
+ async def _cleanup_managers(self) -> None:
69
+ """Cleanup media manager."""
70
+ if self._media_manager:
71
+ await self._media_manager.__aexit__(None, None, None)
72
+ self._media_manager = None
73
+
74
+ async def __aenter__(self) -> "TikTokClient":
75
+ await super().__aenter__()
76
+ await self._setup_managers()
77
+ return self
78
+
79
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
80
+ await self._cleanup_managers()
81
+ await super().__aexit__(exc_type, exc_val, exc_tb)
82
+
83
+ async def authenticate(self) -> AuthCredentials:
84
+ """Perform TikTok authentication."""
85
+ if await self.is_authenticated():
86
+ return self.credentials
87
+ raise PlatformAuthError(
88
+ "Invalid or expired credentials. Please re-authenticate via TikTok OAuth.",
89
+ platform=self.platform_name,
90
+ )
91
+
92
+ async def refresh_token(self) -> AuthCredentials:
93
+ """Refresh TikTok access token."""
94
+ # The refresh logic should be handled by the AccountFactory
95
+ # For now, we assume it's done and we just return the creds
96
+ return self.credentials
97
+
98
+ async def is_authenticated(self) -> bool:
99
+ """Check if TikTok credentials are valid."""
100
+ if not self.api_client or not self.credentials.additional_data:
101
+ return False
102
+ try:
103
+ # Verify credentials by fetching authenticated user info
104
+ params = {
105
+ "fields": "open_id,union_id,avatar_url,display_name",
106
+ "open_id": self.credentials.additional_data["open_id"],
107
+ }
108
+ response = await self.api_client.get("user/info/", params=params)
109
+ return response.data.get("data", {}).get("user") is not None
110
+ except PlatformError:
111
+ return False
112
+
113
+ async def create_post(self, request: PostCreateRequest) -> Post:
114
+ """Create and publish a TikTok video.
115
+
116
+ Args:
117
+ request: The post creation request, must contain a video URL.
118
+
119
+ Returns:
120
+ The created Post object.
121
+
122
+ Raises:
123
+ ValidationError: If the request is invalid (e.g., no media).
124
+ MediaUploadError: If the video upload fails.
125
+ """
126
+ if not self._media_manager or not self.api_client:
127
+ raise RuntimeError("Client must be used as async context manager")
128
+
129
+ if not request.media_urls:
130
+ raise ValidationError(
131
+ "A video URL is required to create a TikTok post.",
132
+ platform=self.platform_name,
133
+ )
134
+
135
+ # 1. Upload the video
136
+ video_url = request.media_urls[0]
137
+ upload_result = await self._media_manager.upload_media(video_url)
138
+
139
+ if not upload_result.media_id:
140
+ raise PlatformError("Media upload succeeded but did not yield a media ID.")
141
+
142
+ # 2. Create the post with the uploaded video
143
+ payload = {
144
+ "post_info": {
145
+ "title": request.content or "",
146
+ "description": request.additional_data.get("description", ""),
147
+ "privacy_level": request.additional_data.get(
148
+ "privacy_level", "PUBLIC_TO_EVERYONE"
149
+ ),
150
+ },
151
+ "source_info": {
152
+ "source": "FILE_UPLOAD",
153
+ "video_id": upload_result.media_id,
154
+ },
155
+ }
156
+ response = await self.api_client.post("video/publish/", data=payload)
157
+ post_id = response.data.get("data", {}).get("publish_id")
158
+
159
+ if not post_id:
160
+ raise PlatformError("Post creation succeeded but no publish_id returned.")
161
+
162
+ # 3. Fetch the created post to return a full Post object
163
+ return await self.get_post(post_id)
164
+
165
+ async def get_post(self, post_id: str) -> Post:
166
+ """Retrieve a TikTok video by its ID."""
167
+ if not self.api_client or not self.credentials.additional_data:
168
+ raise RuntimeError("Client must be used as async context manager")
169
+ try:
170
+ params = {
171
+ "open_id": self.credentials.additional_data["open_id"],
172
+ "video_ids": [post_id],
173
+ }
174
+ response = await self.api_client.post("video/query/", data=params)
175
+ video_data = response.data.get("data", {}).get("videos", [])
176
+ if not video_data:
177
+ raise PostNotFoundError(post_id, self.platform_name)
178
+ return self._parse_video_post(video_data[0])
179
+ except PlatformError as e:
180
+ raise PostNotFoundError(
181
+ post_id, self.platform_name, status_code=e.status_code
182
+ ) from e
183
+
184
+ async def update_post(self, post_id: str, request: PostUpdateRequest) -> Post:
185
+ """Update a TikTok video. This is not supported by TikTok API."""
186
+ raise PlatformError(
187
+ "TikTok API does not support updating videos.", self.platform_name
188
+ )
189
+
190
+ async def delete_post(self, post_id: str) -> bool:
191
+ """Delete a TikTok video. This is not supported by TikTok API."""
192
+ raise PlatformError(
193
+ "TikTok API does not support deleting videos.", self.platform_name
194
+ )
195
+
196
+ async def get_comments(
197
+ self, post_id: str, limit: int = 20, offset: int = 0
198
+ ) -> list[Comment]:
199
+ """Retrieve comments for a TikTok video."""
200
+ if not self.api_client or not self.credentials.additional_data:
201
+ raise RuntimeError("Client must be used as async context manager")
202
+ # This is a hypothetical endpoint
203
+ params = {
204
+ "open_id": self.credentials.additional_data["open_id"],
205
+ "video_id": post_id,
206
+ "count": limit,
207
+ "cursor": offset,
208
+ }
209
+ response = await self.api_client.get("video/comment/list/", params=params)
210
+ comments_data = response.data.get("data", {}).get("comments", [])
211
+ return [self._parse_comment(c, post_id) for c in comments_data]
212
+
213
+ async def create_comment(self, post_id: str, content: str) -> Comment:
214
+ """Create a comment on a TikTok video."""
215
+ raise PlatformError("Commenting via API is not supported.", self.platform_name)
216
+
217
+ async def delete_comment(self, comment_id: str) -> bool:
218
+ """Delete a comment on a TikTok video."""
219
+ raise PlatformError(
220
+ "Deleting comments via API is not supported.", self.platform_name
221
+ )
222
+
223
+ async def upload_media(
224
+ self, media_url: str, media_type: str, alt_text: str | None = None
225
+ ) -> MediaAttachment:
226
+ """Upload a video to TikTok."""
227
+ if not self._media_manager:
228
+ raise RuntimeError("Client not initialized. Use as async context manager.")
229
+ if media_type != "video":
230
+ raise ValidationError("Only video media type is supported for TikTok.")
231
+
232
+ result = await self._media_manager.upload_media(media_url)
233
+ return MediaAttachment(
234
+ media_id=result.media_id or result.upload_id,
235
+ media_type=MediaType.VIDEO,
236
+ url=HttpUrl(media_url),
237
+ )
238
+
239
+ def _parse_video_post(self, video_data: dict[str, Any]) -> Post:
240
+ """Parse a TikTok API video object into a Post model."""
241
+ return Post(
242
+ post_id=video_data["video_id"],
243
+ platform=self.platform_name,
244
+ content=video_data.get("title", ""),
245
+ media=[
246
+ MediaAttachment(
247
+ media_id=video_data["video_id"],
248
+ media_type=MediaType.VIDEO,
249
+ url=HttpUrl(video_data.get("share_url", "")),
250
+ width=video_data.get("width"),
251
+ height=video_data.get("height"),
252
+ )
253
+ ],
254
+ status=PostStatus.PUBLISHED,
255
+ created_at=datetime.fromtimestamp(video_data.get("create_time", 0)),
256
+ author_id=str(video_data.get("open_id")),
257
+ likes_count=video_data.get("like_count", 0),
258
+ comments_count=video_data.get("comment_count", 0),
259
+ shares_count=video_data.get("share_count", 0),
260
+ views_count=video_data.get("view_count", 0),
261
+ raw_data=video_data,
262
+ )
263
+
264
+ def _parse_comment(self, comment_data: dict[str, Any], post_id: str) -> Comment:
265
+ """Parse a TikTok API comment object into a Comment model."""
266
+ return Comment(
267
+ comment_id=comment_data["comment_id"],
268
+ post_id=post_id,
269
+ platform=self.platform_name,
270
+ content=comment_data.get("text", ""),
271
+ author_id=comment_data.get("open_id", ""),
272
+ created_at=datetime.fromtimestamp(comment_data.get("create_time", 0)),
273
+ likes_count=comment_data.get("like_count", 0),
274
+ replies_count=0,
275
+ status=CommentStatus.VISIBLE,
276
+ raw_data=comment_data,
277
+ )