marqetive-lib 0.1.3__tar.gz → 0.1.5__tar.gz
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.
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/PKG-INFO +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/pyproject.toml +1 -1
- marqetive_lib-0.1.5/src/marqetive/__init__.py +111 -0
- marqetive_lib-0.1.5/src/marqetive/core/__init__.py +54 -0
- {marqetive_lib-0.1.3/src/marqetive/platforms → marqetive_lib-0.1.5/src/marqetive/core}/base.py +35 -2
- marqetive_lib-0.1.5/src/marqetive/factory.py +380 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/__init__.py +3 -3
- marqetive_lib-0.1.5/src/marqetive/platforms/instagram/__init__.py +5 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/instagram/client.py +11 -7
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/instagram/exceptions.py +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/instagram/media.py +1 -1
- marqetive_lib-0.1.5/src/marqetive/platforms/linkedin/__init__.py +5 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/linkedin/client.py +8 -4
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/linkedin/exceptions.py +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/linkedin/media.py +1 -1
- marqetive_lib-0.1.5/src/marqetive/platforms/tiktok/__init__.py +5 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/tiktok/client.py +18 -9
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/tiktok/exceptions.py +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/tiktok/media.py +2 -4
- marqetive_lib-0.1.5/src/marqetive/platforms/twitter/__init__.py +5 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/twitter/client.py +7 -3
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/twitter/exceptions.py +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/platforms/twitter/media.py +1 -1
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/oauth.py +2 -2
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/token_validator.py +1 -1
- marqetive_lib-0.1.3/src/marqetive/__init__.py +0 -113
- marqetive_lib-0.1.3/src/marqetive/core/__init__.py +0 -5
- marqetive_lib-0.1.3/src/marqetive/core/account_factory.py +0 -212
- marqetive_lib-0.1.3/src/marqetive/core/base_manager.py +0 -303
- marqetive_lib-0.1.3/src/marqetive/core/progress.py +0 -291
- marqetive_lib-0.1.3/src/marqetive/core/registry.py +0 -257
- marqetive_lib-0.1.3/src/marqetive/platforms/instagram/__init__.py +0 -7
- marqetive_lib-0.1.3/src/marqetive/platforms/instagram/factory.py +0 -106
- marqetive_lib-0.1.3/src/marqetive/platforms/instagram/manager.py +0 -112
- marqetive_lib-0.1.3/src/marqetive/platforms/linkedin/__init__.py +0 -7
- marqetive_lib-0.1.3/src/marqetive/platforms/linkedin/factory.py +0 -130
- marqetive_lib-0.1.3/src/marqetive/platforms/linkedin/manager.py +0 -119
- marqetive_lib-0.1.3/src/marqetive/platforms/tiktok/__init__.py +0 -7
- marqetive_lib-0.1.3/src/marqetive/platforms/tiktok/factory.py +0 -188
- marqetive_lib-0.1.3/src/marqetive/platforms/tiktok/manager.py +0 -115
- marqetive_lib-0.1.3/src/marqetive/platforms/twitter/__init__.py +0 -7
- marqetive_lib-0.1.3/src/marqetive/platforms/twitter/factory.py +0 -150
- marqetive_lib-0.1.3/src/marqetive/platforms/twitter/manager.py +0 -121
- marqetive_lib-0.1.3/src/marqetive/registry_init.py +0 -68
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/README.md +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/core/client.py +0 -0
- {marqetive_lib-0.1.3/src/marqetive/platforms → marqetive_lib-0.1.5/src/marqetive/core}/exceptions.py +0 -0
- {marqetive_lib-0.1.3/src/marqetive/platforms → marqetive_lib-0.1.5/src/marqetive/core}/models.py +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/py.typed +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/__init__.py +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/file_handlers.py +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/helpers.py +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/media.py +0 -0
- {marqetive_lib-0.1.3 → marqetive_lib-0.1.5}/src/marqetive/utils/retry.py +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""MarqetiveLib: Modern Python utilities for web APIs and social media platforms.
|
|
2
|
+
|
|
3
|
+
A comprehensive library providing utilities for working with web APIs,
|
|
4
|
+
HTTP requests, data processing, and social media platform integrations.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
Simple usage with factory:
|
|
8
|
+
>>> from marqetive import get_client, AuthCredentials, PostCreateRequest
|
|
9
|
+
>>> credentials = AuthCredentials(
|
|
10
|
+
... platform="twitter",
|
|
11
|
+
... access_token="...",
|
|
12
|
+
... refresh_token="..."
|
|
13
|
+
... )
|
|
14
|
+
>>> client = await get_client(credentials)
|
|
15
|
+
>>> async with client:
|
|
16
|
+
... post = await client.create_post(PostCreateRequest(content="Hello!"))
|
|
17
|
+
|
|
18
|
+
Factory with custom OAuth credentials:
|
|
19
|
+
>>> from marqetive import PlatformFactory, AuthCredentials
|
|
20
|
+
>>> factory = PlatformFactory(
|
|
21
|
+
... twitter_client_id="your_client_id",
|
|
22
|
+
... twitter_client_secret="your_client_secret"
|
|
23
|
+
... )
|
|
24
|
+
>>> client = await factory.get_client(credentials)
|
|
25
|
+
|
|
26
|
+
Direct client usage:
|
|
27
|
+
>>> from marqetive.platforms.twitter import TwitterClient
|
|
28
|
+
>>> async with TwitterClient(credentials) as client:
|
|
29
|
+
... post = await client.create_post(request)
|
|
30
|
+
|
|
31
|
+
Basic API client:
|
|
32
|
+
>>> from marqetive import APIClient
|
|
33
|
+
>>> async with APIClient(base_url="https://api.example.com") as client:
|
|
34
|
+
... response = await client.get("/endpoint")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Core API client
|
|
38
|
+
# Progress callback type
|
|
39
|
+
from marqetive.core.base import ProgressCallback
|
|
40
|
+
from marqetive.core.client import APIClient
|
|
41
|
+
|
|
42
|
+
# Exceptions
|
|
43
|
+
from marqetive.core.exceptions import (
|
|
44
|
+
InvalidFileTypeError,
|
|
45
|
+
MediaUploadError,
|
|
46
|
+
PlatformAuthError,
|
|
47
|
+
PlatformError,
|
|
48
|
+
PostNotFoundError,
|
|
49
|
+
RateLimitError,
|
|
50
|
+
ValidationError,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Models
|
|
54
|
+
from marqetive.core.models import (
|
|
55
|
+
AccountStatus,
|
|
56
|
+
AuthCredentials,
|
|
57
|
+
Comment,
|
|
58
|
+
CommentStatus,
|
|
59
|
+
MediaAttachment,
|
|
60
|
+
MediaType,
|
|
61
|
+
Post,
|
|
62
|
+
PostCreateRequest,
|
|
63
|
+
PostStatus,
|
|
64
|
+
PostUpdateRequest,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Factory
|
|
68
|
+
from marqetive.factory import PlatformFactory, get_client
|
|
69
|
+
|
|
70
|
+
# Utilities
|
|
71
|
+
from marqetive.utils.helpers import format_response
|
|
72
|
+
|
|
73
|
+
# Retry utilities
|
|
74
|
+
from marqetive.utils.retry import STANDARD_BACKOFF, BackoffConfig, retry_async
|
|
75
|
+
|
|
76
|
+
__version__ = "0.2.0"
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
# Core
|
|
80
|
+
"APIClient",
|
|
81
|
+
# Factory
|
|
82
|
+
"PlatformFactory",
|
|
83
|
+
"get_client",
|
|
84
|
+
# Retry
|
|
85
|
+
"BackoffConfig",
|
|
86
|
+
"STANDARD_BACKOFF",
|
|
87
|
+
"retry_async",
|
|
88
|
+
# Models
|
|
89
|
+
"AuthCredentials",
|
|
90
|
+
"AccountStatus",
|
|
91
|
+
"Post",
|
|
92
|
+
"PostStatus",
|
|
93
|
+
"PostCreateRequest",
|
|
94
|
+
"PostUpdateRequest",
|
|
95
|
+
"Comment",
|
|
96
|
+
"CommentStatus",
|
|
97
|
+
"MediaAttachment",
|
|
98
|
+
"MediaType",
|
|
99
|
+
# Exceptions
|
|
100
|
+
"PlatformError",
|
|
101
|
+
"PlatformAuthError",
|
|
102
|
+
"RateLimitError",
|
|
103
|
+
"PostNotFoundError",
|
|
104
|
+
"MediaUploadError",
|
|
105
|
+
"ValidationError",
|
|
106
|
+
"InvalidFileTypeError",
|
|
107
|
+
# Types
|
|
108
|
+
"ProgressCallback",
|
|
109
|
+
# Utilities
|
|
110
|
+
"format_response",
|
|
111
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Core functionality for MarqetiveLib."""
|
|
2
|
+
|
|
3
|
+
from marqetive.core.base import ProgressCallback, SocialMediaPlatform
|
|
4
|
+
from marqetive.core.client import APIClient
|
|
5
|
+
from marqetive.core.exceptions import (
|
|
6
|
+
InvalidFileTypeError,
|
|
7
|
+
MediaUploadError,
|
|
8
|
+
PlatformAuthError,
|
|
9
|
+
PlatformError,
|
|
10
|
+
PostNotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from marqetive.core.models import (
|
|
15
|
+
AccountStatus,
|
|
16
|
+
AuthCredentials,
|
|
17
|
+
Comment,
|
|
18
|
+
CommentStatus,
|
|
19
|
+
MediaAttachment,
|
|
20
|
+
MediaType,
|
|
21
|
+
PlatformResponse,
|
|
22
|
+
Post,
|
|
23
|
+
PostCreateRequest,
|
|
24
|
+
PostStatus,
|
|
25
|
+
PostUpdateRequest,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Client
|
|
30
|
+
"APIClient",
|
|
31
|
+
# Base class
|
|
32
|
+
"SocialMediaPlatform",
|
|
33
|
+
"ProgressCallback",
|
|
34
|
+
# Models
|
|
35
|
+
"AccountStatus",
|
|
36
|
+
"AuthCredentials",
|
|
37
|
+
"Comment",
|
|
38
|
+
"CommentStatus",
|
|
39
|
+
"MediaAttachment",
|
|
40
|
+
"MediaType",
|
|
41
|
+
"PlatformResponse",
|
|
42
|
+
"Post",
|
|
43
|
+
"PostCreateRequest",
|
|
44
|
+
"PostStatus",
|
|
45
|
+
"PostUpdateRequest",
|
|
46
|
+
# Exceptions
|
|
47
|
+
"InvalidFileTypeError",
|
|
48
|
+
"MediaUploadError",
|
|
49
|
+
"PlatformAuthError",
|
|
50
|
+
"PlatformError",
|
|
51
|
+
"PostNotFoundError",
|
|
52
|
+
"RateLimitError",
|
|
53
|
+
"ValidationError",
|
|
54
|
+
]
|
{marqetive_lib-0.1.3/src/marqetive/platforms → marqetive_lib-0.1.5/src/marqetive/core}/base.py
RENAMED
|
@@ -6,16 +6,17 @@ All concrete implementations must implement the abstract methods defined here.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from traceback import TracebackException
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
from marqetive.core.client import APIClient
|
|
14
|
-
from marqetive.
|
|
15
|
+
from marqetive.core.exceptions import (
|
|
15
16
|
PlatformAuthError,
|
|
16
17
|
RateLimitError,
|
|
17
18
|
)
|
|
18
|
-
from marqetive.
|
|
19
|
+
from marqetive.core.models import (
|
|
19
20
|
AuthCredentials,
|
|
20
21
|
Comment,
|
|
21
22
|
MediaAttachment,
|
|
@@ -25,6 +26,10 @@ from marqetive.platforms.models import (
|
|
|
25
26
|
PostUpdateRequest,
|
|
26
27
|
)
|
|
27
28
|
|
|
29
|
+
# Type alias for progress callback
|
|
30
|
+
# (operation: str, progress: int, total: int, message: str | None) -> None
|
|
31
|
+
type ProgressCallback = Callable[[str, int, int, str | None], None]
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
class SocialMediaPlatform(ABC):
|
|
30
35
|
"""Abstract base class for social media platform integrations.
|
|
@@ -57,6 +62,7 @@ class SocialMediaPlatform(ABC):
|
|
|
57
62
|
credentials: AuthCredentials,
|
|
58
63
|
base_url: str,
|
|
59
64
|
timeout: float = 30.0,
|
|
65
|
+
progress_callback: ProgressCallback | None = None,
|
|
60
66
|
) -> None:
|
|
61
67
|
"""Initialize the platform client.
|
|
62
68
|
|
|
@@ -65,6 +71,9 @@ class SocialMediaPlatform(ABC):
|
|
|
65
71
|
credentials: Authentication credentials
|
|
66
72
|
base_url: Base URL for the platform API
|
|
67
73
|
timeout: Request timeout in seconds
|
|
74
|
+
progress_callback: Optional callback for progress updates during
|
|
75
|
+
long-running operations (e.g., media uploads). The callback
|
|
76
|
+
receives (operation, progress, total, message).
|
|
68
77
|
|
|
69
78
|
Raises:
|
|
70
79
|
PlatformAuthError: If credentials are invalid or expired
|
|
@@ -73,6 +82,7 @@ class SocialMediaPlatform(ABC):
|
|
|
73
82
|
self.credentials = credentials
|
|
74
83
|
self.base_url = base_url
|
|
75
84
|
self.timeout = timeout
|
|
85
|
+
self._progress_callback = progress_callback
|
|
76
86
|
self.api_client: APIClient | None = None
|
|
77
87
|
self._rate_limit_remaining: int | None = None
|
|
78
88
|
self._rate_limit_reset: datetime | None = None
|
|
@@ -152,6 +162,29 @@ class SocialMediaPlatform(ABC):
|
|
|
152
162
|
self._rate_limit_remaining = remaining
|
|
153
163
|
self._rate_limit_reset = reset_time
|
|
154
164
|
|
|
165
|
+
def _emit_progress(
|
|
166
|
+
self,
|
|
167
|
+
operation: str,
|
|
168
|
+
progress: int,
|
|
169
|
+
total: int,
|
|
170
|
+
message: str | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Emit a progress update if a callback is registered.
|
|
173
|
+
|
|
174
|
+
This method is safe to call even if no callback is registered.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
operation: Name of the operation (e.g., "upload_media", "create_post")
|
|
178
|
+
progress: Current progress value
|
|
179
|
+
total: Total value for completion
|
|
180
|
+
message: Optional human-readable message
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> self._emit_progress("upload_media", 1, 3, "Uploading image 1 of 3")
|
|
184
|
+
"""
|
|
185
|
+
if self._progress_callback is not None:
|
|
186
|
+
self._progress_callback(operation, progress, total, message)
|
|
187
|
+
|
|
155
188
|
# ==================== Abstract Authentication Methods ====================
|
|
156
189
|
|
|
157
190
|
@abstractmethod
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Platform factory for creating social media clients.
|
|
2
|
+
|
|
3
|
+
This module provides a simple factory for creating authenticated platform clients
|
|
4
|
+
with automatic token refresh and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from marqetive.core.exceptions import PlatformAuthError
|
|
12
|
+
from marqetive.core.models import AuthCredentials
|
|
13
|
+
from marqetive.utils.oauth import (
|
|
14
|
+
refresh_instagram_token,
|
|
15
|
+
refresh_linkedin_token,
|
|
16
|
+
refresh_tiktok_token,
|
|
17
|
+
refresh_twitter_token,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from marqetive.core.base import SocialMediaPlatform
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Supported platforms
|
|
26
|
+
SUPPORTED_PLATFORMS = frozenset({"twitter", "linkedin", "instagram", "tiktok"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _create_client(
|
|
30
|
+
platform: str, credentials: "AuthCredentials"
|
|
31
|
+
) -> "SocialMediaPlatform":
|
|
32
|
+
"""Create a client for a platform (with lazy imports).
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
platform: Platform name (twitter, linkedin, instagram, tiktok).
|
|
36
|
+
credentials: Authentication credentials for the platform.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The platform client instance.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If platform is unknown.
|
|
43
|
+
"""
|
|
44
|
+
if platform == "twitter":
|
|
45
|
+
from marqetive.platforms.twitter.client import TwitterClient
|
|
46
|
+
|
|
47
|
+
return TwitterClient(credentials=credentials)
|
|
48
|
+
elif platform == "linkedin":
|
|
49
|
+
from marqetive.platforms.linkedin.client import LinkedInClient
|
|
50
|
+
|
|
51
|
+
return LinkedInClient(credentials=credentials)
|
|
52
|
+
elif platform == "instagram":
|
|
53
|
+
from marqetive.platforms.instagram.client import InstagramClient
|
|
54
|
+
|
|
55
|
+
return InstagramClient(credentials=credentials)
|
|
56
|
+
elif platform == "tiktok":
|
|
57
|
+
from marqetive.platforms.tiktok.client import TikTokClient
|
|
58
|
+
|
|
59
|
+
return TikTokClient(credentials=credentials)
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown platform: {platform}. "
|
|
63
|
+
f"Supported platforms: {', '.join(sorted(SUPPORTED_PLATFORMS))}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PlatformFactory:
|
|
68
|
+
"""Factory for creating authenticated social media platform clients.
|
|
69
|
+
|
|
70
|
+
This factory handles:
|
|
71
|
+
- Token refresh before client creation (if expired)
|
|
72
|
+
- Platform-specific credential validation
|
|
73
|
+
- OAuth client credentials from environment variables
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> factory = PlatformFactory()
|
|
77
|
+
>>> credentials = AuthCredentials(
|
|
78
|
+
... platform="twitter",
|
|
79
|
+
... access_token="token",
|
|
80
|
+
... refresh_token="refresh"
|
|
81
|
+
... )
|
|
82
|
+
>>> client = await factory.get_client(credentials)
|
|
83
|
+
>>> async with client:
|
|
84
|
+
... post = await client.create_post(request)
|
|
85
|
+
|
|
86
|
+
Example with custom OAuth credentials:
|
|
87
|
+
>>> factory = PlatformFactory(
|
|
88
|
+
... twitter_client_id="your_client_id",
|
|
89
|
+
... twitter_client_secret="your_client_secret"
|
|
90
|
+
... )
|
|
91
|
+
>>> client = await factory.get_client(credentials)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
*,
|
|
97
|
+
twitter_client_id: str | None = None,
|
|
98
|
+
twitter_client_secret: str | None = None,
|
|
99
|
+
twitter_api_key: str | None = None,
|
|
100
|
+
twitter_api_secret: str | None = None,
|
|
101
|
+
linkedin_client_id: str | None = None,
|
|
102
|
+
linkedin_client_secret: str | None = None,
|
|
103
|
+
tiktok_client_id: str | None = None,
|
|
104
|
+
tiktok_client_secret: str | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Initialize platform factory with OAuth credentials.
|
|
107
|
+
|
|
108
|
+
OAuth credentials can be provided directly or via environment variables:
|
|
109
|
+
- TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET, TWITTER_API_KEY, TWITTER_API_SECRET
|
|
110
|
+
- LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET
|
|
111
|
+
- TIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET
|
|
112
|
+
|
|
113
|
+
Note: Instagram uses long-lived tokens that don't require client credentials
|
|
114
|
+
for refresh (only the current access token is needed).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
twitter_client_id: Twitter OAuth client ID.
|
|
118
|
+
twitter_client_secret: Twitter OAuth client secret.
|
|
119
|
+
twitter_api_key: Twitter API key (for media operations).
|
|
120
|
+
twitter_api_secret: Twitter API secret (for media operations).
|
|
121
|
+
linkedin_client_id: LinkedIn OAuth client ID.
|
|
122
|
+
linkedin_client_secret: LinkedIn OAuth client secret.
|
|
123
|
+
tiktok_client_id: TikTok OAuth client ID.
|
|
124
|
+
tiktok_client_secret: TikTok OAuth client secret.
|
|
125
|
+
"""
|
|
126
|
+
self._oauth_credentials = {
|
|
127
|
+
"twitter": {
|
|
128
|
+
"client_id": twitter_client_id or os.getenv("TWITTER_CLIENT_ID"),
|
|
129
|
+
"client_secret": twitter_client_secret
|
|
130
|
+
or os.getenv("TWITTER_CLIENT_SECRET"),
|
|
131
|
+
"api_key": twitter_api_key or os.getenv("TWITTER_API_KEY"),
|
|
132
|
+
"api_secret": twitter_api_secret or os.getenv("TWITTER_API_SECRET"),
|
|
133
|
+
},
|
|
134
|
+
"linkedin": {
|
|
135
|
+
"client_id": linkedin_client_id or os.getenv("LINKEDIN_CLIENT_ID"),
|
|
136
|
+
"client_secret": linkedin_client_secret
|
|
137
|
+
or os.getenv("LINKEDIN_CLIENT_SECRET"),
|
|
138
|
+
},
|
|
139
|
+
"tiktok": {
|
|
140
|
+
"client_id": tiktok_client_id or os.getenv("TIKTOK_CLIENT_ID"),
|
|
141
|
+
"client_secret": tiktok_client_secret
|
|
142
|
+
or os.getenv("TIKTOK_CLIENT_SECRET"),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async def get_client(
|
|
147
|
+
self,
|
|
148
|
+
credentials: AuthCredentials,
|
|
149
|
+
*,
|
|
150
|
+
auto_refresh: bool = True,
|
|
151
|
+
) -> "SocialMediaPlatform":
|
|
152
|
+
"""Create a platform client with the given credentials.
|
|
153
|
+
|
|
154
|
+
If credentials are expired and auto_refresh is True, attempts to
|
|
155
|
+
refresh the token before creating the client.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
credentials: Authentication credentials for the platform.
|
|
159
|
+
auto_refresh: Whether to automatically refresh expired tokens.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Platform client (TwitterClient, LinkedInClient, etc.).
|
|
163
|
+
The client must be used as an async context manager.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
PlatformAuthError: If credentials are invalid or refresh fails.
|
|
167
|
+
ValueError: If platform is unknown.
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> factory = PlatformFactory()
|
|
171
|
+
>>> client = await factory.get_client(credentials)
|
|
172
|
+
>>> async with client:
|
|
173
|
+
... post = await client.create_post(request)
|
|
174
|
+
"""
|
|
175
|
+
platform = credentials.platform.lower()
|
|
176
|
+
|
|
177
|
+
# Validate platform-specific requirements
|
|
178
|
+
self._validate_credentials(credentials)
|
|
179
|
+
|
|
180
|
+
# Refresh token if needed
|
|
181
|
+
if auto_refresh and credentials.needs_refresh():
|
|
182
|
+
logger.info(f"Refreshing expired token for {platform}")
|
|
183
|
+
credentials = await self._refresh_token(credentials)
|
|
184
|
+
credentials.mark_valid()
|
|
185
|
+
|
|
186
|
+
# Enrich credentials with API keys for Twitter (needed for media operations)
|
|
187
|
+
if platform == "twitter":
|
|
188
|
+
credentials = self._enrich_twitter_credentials(credentials)
|
|
189
|
+
|
|
190
|
+
# Create and return client
|
|
191
|
+
return _create_client(platform, credentials)
|
|
192
|
+
|
|
193
|
+
def _validate_credentials(self, credentials: AuthCredentials) -> None:
|
|
194
|
+
"""Validate platform-specific credential requirements.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
credentials: The credentials to validate.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
PlatformAuthError: If credentials are missing required fields.
|
|
201
|
+
"""
|
|
202
|
+
platform = credentials.platform.lower()
|
|
203
|
+
|
|
204
|
+
if platform not in SUPPORTED_PLATFORMS:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"Unknown platform: {platform}. "
|
|
207
|
+
f"Supported platforms: {', '.join(sorted(SUPPORTED_PLATFORMS))}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if not credentials.access_token:
|
|
211
|
+
raise PlatformAuthError(
|
|
212
|
+
"Access token is required",
|
|
213
|
+
platform=platform,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if platform == "instagram":
|
|
217
|
+
# Instagram requires user_id or business_account_id
|
|
218
|
+
business_account_id = credentials.additional_data.get(
|
|
219
|
+
"instagram_business_account_id"
|
|
220
|
+
)
|
|
221
|
+
if not credentials.user_id and not business_account_id:
|
|
222
|
+
raise PlatformAuthError(
|
|
223
|
+
"Instagram requires user_id or 'instagram_business_account_id' "
|
|
224
|
+
"in additional_data",
|
|
225
|
+
platform=platform,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
elif platform == "tiktok":
|
|
229
|
+
# TikTok requires open_id
|
|
230
|
+
if not credentials.additional_data.get("open_id"):
|
|
231
|
+
raise PlatformAuthError(
|
|
232
|
+
"TikTok requires 'open_id' in additional_data",
|
|
233
|
+
platform=platform,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
elif platform == "linkedin":
|
|
237
|
+
# LinkedIn requires user_id (person URN)
|
|
238
|
+
if not credentials.user_id:
|
|
239
|
+
raise PlatformAuthError(
|
|
240
|
+
"LinkedIn requires user_id (person URN, e.g., 'urn:li:person:xxx')",
|
|
241
|
+
platform=platform,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def _refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
|
|
245
|
+
"""Refresh the access token for the given credentials.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
credentials: The credentials with expired token.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Updated credentials with new access token.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
PlatformAuthError: If refresh fails or required OAuth credentials are missing.
|
|
255
|
+
"""
|
|
256
|
+
platform = credentials.platform.lower()
|
|
257
|
+
|
|
258
|
+
if platform == "twitter":
|
|
259
|
+
oauth_creds = self._oauth_credentials.get("twitter", {})
|
|
260
|
+
client_id = oauth_creds.get("client_id")
|
|
261
|
+
client_secret = oauth_creds.get("client_secret")
|
|
262
|
+
|
|
263
|
+
if not client_id or not client_secret:
|
|
264
|
+
raise PlatformAuthError(
|
|
265
|
+
"Twitter client_id and client_secret required for token refresh. "
|
|
266
|
+
"Provide them via PlatformFactory constructor or environment variables "
|
|
267
|
+
"(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET).",
|
|
268
|
+
platform=platform,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return await refresh_twitter_token(credentials, client_id, client_secret)
|
|
272
|
+
|
|
273
|
+
elif platform == "linkedin":
|
|
274
|
+
oauth_creds = self._oauth_credentials.get("linkedin", {})
|
|
275
|
+
client_id = oauth_creds.get("client_id")
|
|
276
|
+
client_secret = oauth_creds.get("client_secret")
|
|
277
|
+
|
|
278
|
+
if not client_id or not client_secret:
|
|
279
|
+
raise PlatformAuthError(
|
|
280
|
+
"LinkedIn client_id and client_secret required for token refresh. "
|
|
281
|
+
"Provide them via PlatformFactory constructor or environment variables "
|
|
282
|
+
"(LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET).",
|
|
283
|
+
platform=platform,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return await refresh_linkedin_token(credentials, client_id, client_secret)
|
|
287
|
+
|
|
288
|
+
elif platform == "instagram":
|
|
289
|
+
# Instagram doesn't need OAuth credentials for refresh
|
|
290
|
+
return await refresh_instagram_token(credentials)
|
|
291
|
+
|
|
292
|
+
elif platform == "tiktok":
|
|
293
|
+
oauth_creds = self._oauth_credentials.get("tiktok", {})
|
|
294
|
+
client_id = oauth_creds.get("client_id")
|
|
295
|
+
client_secret = oauth_creds.get("client_secret")
|
|
296
|
+
|
|
297
|
+
if not client_id or not client_secret:
|
|
298
|
+
raise PlatformAuthError(
|
|
299
|
+
"TikTok client_id and client_secret required for token refresh. "
|
|
300
|
+
"Provide them via PlatformFactory constructor or environment variables "
|
|
301
|
+
"(TIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET).",
|
|
302
|
+
platform=platform,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return await refresh_tiktok_token(credentials, client_id, client_secret)
|
|
306
|
+
|
|
307
|
+
else:
|
|
308
|
+
raise ValueError(f"Unknown platform: {platform}")
|
|
309
|
+
|
|
310
|
+
def _enrich_twitter_credentials(
|
|
311
|
+
self, credentials: AuthCredentials
|
|
312
|
+
) -> AuthCredentials:
|
|
313
|
+
"""Enrich Twitter credentials with API keys for media operations.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
credentials: The Twitter credentials.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Credentials with api_key and api_secret in additional_data.
|
|
320
|
+
"""
|
|
321
|
+
oauth_creds = self._oauth_credentials.get("twitter", {})
|
|
322
|
+
|
|
323
|
+
# Only add if not already present
|
|
324
|
+
if "api_key" not in credentials.additional_data:
|
|
325
|
+
api_key = oauth_creds.get("api_key")
|
|
326
|
+
if api_key:
|
|
327
|
+
credentials.additional_data["api_key"] = api_key
|
|
328
|
+
|
|
329
|
+
if "api_secret" not in credentials.additional_data:
|
|
330
|
+
api_secret = oauth_creds.get("api_secret")
|
|
331
|
+
if api_secret:
|
|
332
|
+
credentials.additional_data["api_secret"] = api_secret
|
|
333
|
+
|
|
334
|
+
return credentials
|
|
335
|
+
|
|
336
|
+
def get_supported_platforms(self) -> frozenset[str]:
|
|
337
|
+
"""Get the set of supported platform names.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Frozenset of supported platform names.
|
|
341
|
+
"""
|
|
342
|
+
return SUPPORTED_PLATFORMS
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def get_client(
|
|
346
|
+
credentials: AuthCredentials,
|
|
347
|
+
*,
|
|
348
|
+
auto_refresh: bool = True,
|
|
349
|
+
) -> "SocialMediaPlatform":
|
|
350
|
+
"""Create a platform client with the given credentials.
|
|
351
|
+
|
|
352
|
+
This is a convenience function that creates a PlatformFactory with
|
|
353
|
+
environment-based OAuth credentials and returns a client.
|
|
354
|
+
|
|
355
|
+
For more control over OAuth credentials, create a PlatformFactory directly.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
credentials: Authentication credentials for the platform.
|
|
359
|
+
auto_refresh: Whether to automatically refresh expired tokens.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Platform client ready to be used as async context manager.
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
PlatformAuthError: If credentials are invalid or refresh fails.
|
|
366
|
+
ValueError: If platform is unknown.
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> from marqetive import get_client, AuthCredentials, PostCreateRequest
|
|
370
|
+
>>> credentials = AuthCredentials(
|
|
371
|
+
... platform="twitter",
|
|
372
|
+
... access_token="...",
|
|
373
|
+
... refresh_token="..."
|
|
374
|
+
... )
|
|
375
|
+
>>> client = await get_client(credentials)
|
|
376
|
+
>>> async with client:
|
|
377
|
+
... post = await client.create_post(PostCreateRequest(content="Hello!"))
|
|
378
|
+
"""
|
|
379
|
+
factory = PlatformFactory()
|
|
380
|
+
return await factory.get_client(credentials, auto_refresh=auto_refresh)
|
|
@@ -9,8 +9,8 @@ Platform clients are available via their respective subpackages:
|
|
|
9
9
|
- from marqetive.platforms.instagram import InstagramClient
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from marqetive.
|
|
13
|
-
from marqetive.
|
|
12
|
+
from marqetive.core.base import SocialMediaPlatform
|
|
13
|
+
from marqetive.core.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.
|
|
21
|
+
from marqetive.core.models import (
|
|
22
22
|
AuthCredentials,
|
|
23
23
|
Comment,
|
|
24
24
|
CommentStatus,
|