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.
- marqetive/__init__.py +113 -0
- marqetive/core/__init__.py +5 -0
- marqetive/core/account_factory.py +212 -0
- marqetive/core/base_manager.py +303 -0
- marqetive/core/client.py +108 -0
- marqetive/core/progress.py +291 -0
- marqetive/core/registry.py +257 -0
- marqetive/platforms/__init__.py +55 -0
- marqetive/platforms/base.py +390 -0
- marqetive/platforms/exceptions.py +238 -0
- marqetive/platforms/instagram/__init__.py +7 -0
- marqetive/platforms/instagram/client.py +786 -0
- marqetive/platforms/instagram/exceptions.py +311 -0
- marqetive/platforms/instagram/factory.py +106 -0
- marqetive/platforms/instagram/manager.py +112 -0
- marqetive/platforms/instagram/media.py +669 -0
- marqetive/platforms/linkedin/__init__.py +7 -0
- marqetive/platforms/linkedin/client.py +733 -0
- marqetive/platforms/linkedin/exceptions.py +335 -0
- marqetive/platforms/linkedin/factory.py +130 -0
- marqetive/platforms/linkedin/manager.py +119 -0
- marqetive/platforms/linkedin/media.py +549 -0
- marqetive/platforms/models.py +345 -0
- marqetive/platforms/tiktok/__init__.py +0 -0
- marqetive/platforms/twitter/__init__.py +7 -0
- marqetive/platforms/twitter/client.py +647 -0
- marqetive/platforms/twitter/exceptions.py +311 -0
- marqetive/platforms/twitter/factory.py +151 -0
- marqetive/platforms/twitter/manager.py +121 -0
- marqetive/platforms/twitter/media.py +779 -0
- marqetive/platforms/twitter/threads.py +442 -0
- marqetive/py.typed +0 -0
- marqetive/registry_init.py +66 -0
- marqetive/utils/__init__.py +45 -0
- marqetive/utils/file_handlers.py +438 -0
- marqetive/utils/helpers.py +99 -0
- marqetive/utils/media.py +399 -0
- marqetive/utils/oauth.py +265 -0
- marqetive/utils/retry.py +239 -0
- marqetive/utils/token_validator.py +240 -0
- marqetive_lib-0.1.0.dist-info/METADATA +261 -0
- marqetive_lib-0.1.0.dist-info/RECORD +43 -0
- marqetive_lib-0.1.0.dist-info/WHEEL +4 -0
marqetive/core/client.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""API client implementation for MarqetiveLib."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIResponse(BaseModel):
|
|
10
|
+
"""Response model for API calls."""
|
|
11
|
+
|
|
12
|
+
status_code: int
|
|
13
|
+
data: dict[str, Any]
|
|
14
|
+
headers: dict[str, str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIClient:
|
|
18
|
+
"""A simple HTTP API client with type hints.
|
|
19
|
+
|
|
20
|
+
This client provides a clean interface for making HTTP requests
|
|
21
|
+
with automatic response parsing and error handling.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_url: The base URL for API requests
|
|
25
|
+
timeout: Request timeout in seconds (default: 30)
|
|
26
|
+
headers: Optional default headers for all requests
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> client = APIClient(base_url="https://api.example.com")
|
|
30
|
+
>>> response = await client.get("/users/1")
|
|
31
|
+
>>> print(response.data)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
base_url: str,
|
|
37
|
+
timeout: float = 30.0,
|
|
38
|
+
headers: dict[str, str] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize the API client."""
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
42
|
+
self.timeout = timeout
|
|
43
|
+
self.default_headers = headers or {}
|
|
44
|
+
self._client: httpx.AsyncClient | None = None
|
|
45
|
+
|
|
46
|
+
async def __aenter__(self) -> "APIClient":
|
|
47
|
+
"""Async context manager entry."""
|
|
48
|
+
self._client = httpx.AsyncClient(
|
|
49
|
+
base_url=self.base_url,
|
|
50
|
+
timeout=self.timeout,
|
|
51
|
+
headers=self.default_headers,
|
|
52
|
+
)
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
56
|
+
"""Async context manager exit."""
|
|
57
|
+
if self._client:
|
|
58
|
+
await self._client.aclose()
|
|
59
|
+
|
|
60
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> APIResponse:
|
|
61
|
+
"""Make a GET request.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: The endpoint path
|
|
65
|
+
params: Optional query parameters
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
APIResponse object containing status, data, and headers
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
httpx.HTTPError: If the request fails
|
|
72
|
+
"""
|
|
73
|
+
if not self._client:
|
|
74
|
+
raise RuntimeError("Client not initialized. Use async context manager.")
|
|
75
|
+
|
|
76
|
+
response: httpx.Response = await self._client.get(path, params=params)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
|
|
79
|
+
return APIResponse(
|
|
80
|
+
status_code=response.status_code,
|
|
81
|
+
data=response.json() if response.content else {},
|
|
82
|
+
headers=dict(response.headers),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def post(self, path: str, data: dict[str, Any] | None = None) -> APIResponse:
|
|
86
|
+
"""Make a POST request.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path: The endpoint path
|
|
90
|
+
data: Optional request body data
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
APIResponse object containing status, data, and headers
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
httpx.HTTPError: If the request fails
|
|
97
|
+
"""
|
|
98
|
+
if not self._client:
|
|
99
|
+
raise RuntimeError("Client not initialized. Use async context manager.")
|
|
100
|
+
|
|
101
|
+
response = await self._client.post(path, json=data)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
|
|
104
|
+
return APIResponse(
|
|
105
|
+
status_code=response.status_code,
|
|
106
|
+
data=response.json() if response.content else {},
|
|
107
|
+
headers=dict(response.headers),
|
|
108
|
+
)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Progress tracking system for long-running operations.
|
|
2
|
+
|
|
3
|
+
This module provides models and utilities for tracking progress of operations
|
|
4
|
+
like media uploads, post creation, and other long-running tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProgressStatus(str, Enum):
|
|
16
|
+
"""Status of a progress event."""
|
|
17
|
+
|
|
18
|
+
STARTED = "started"
|
|
19
|
+
IN_PROGRESS = "in_progress"
|
|
20
|
+
COMPLETED = "completed"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
CANCELLED = "cancelled"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProgressEvent(BaseModel):
|
|
26
|
+
"""Represents a progress event for an operation.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
operation: Name of the operation (e.g., "upload_media", "create_post").
|
|
30
|
+
progress: Current progress value (e.g., bytes uploaded).
|
|
31
|
+
total: Total expected value (e.g., total bytes).
|
|
32
|
+
status: Current status of the operation.
|
|
33
|
+
message: Optional human-readable message.
|
|
34
|
+
metadata: Additional operation-specific data.
|
|
35
|
+
timestamp: When this event occurred.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> event = ProgressEvent(
|
|
39
|
+
... operation="upload_media",
|
|
40
|
+
... progress=500,
|
|
41
|
+
... total=1000,
|
|
42
|
+
... status=ProgressStatus.IN_PROGRESS,
|
|
43
|
+
... message="Uploading image..."
|
|
44
|
+
... )
|
|
45
|
+
>>> print(f"Progress: {event.percentage}%")
|
|
46
|
+
Progress: 50.0%
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
operation: str
|
|
50
|
+
progress: int | float
|
|
51
|
+
total: int | float | None = None
|
|
52
|
+
status: ProgressStatus
|
|
53
|
+
message: str | None = None
|
|
54
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
timestamp: datetime = Field(default_factory=datetime.now)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def percentage(self) -> float:
|
|
59
|
+
"""Calculate progress as a percentage.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Progress percentage (0-100), or 0 if total is None.
|
|
63
|
+
"""
|
|
64
|
+
if self.total is None or self.total == 0:
|
|
65
|
+
return 0.0
|
|
66
|
+
return (self.progress / self.total) * 100
|
|
67
|
+
|
|
68
|
+
def is_complete(self) -> bool:
|
|
69
|
+
"""Check if operation is complete.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if status is COMPLETED, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
return self.status == ProgressStatus.COMPLETED
|
|
75
|
+
|
|
76
|
+
def is_failed(self) -> bool:
|
|
77
|
+
"""Check if operation failed.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if status is FAILED, False otherwise.
|
|
81
|
+
"""
|
|
82
|
+
return self.status == ProgressStatus.FAILED
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Type alias for progress callback functions
|
|
86
|
+
ProgressCallback = Callable[[ProgressEvent], None]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ProgressTracker:
|
|
90
|
+
"""Tracks and emits progress events for operations.
|
|
91
|
+
|
|
92
|
+
Manages multiple progress callbacks and provides utilities for
|
|
93
|
+
emitting progress events with consistent formatting.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> tracker = ProgressTracker()
|
|
97
|
+
>>> tracker.add_callback(lambda e: print(f"{e.operation}: {e.percentage}%"))
|
|
98
|
+
>>>
|
|
99
|
+
>>> tracker.emit_start("upload_media")
|
|
100
|
+
>>> tracker.emit_progress("upload_media", 500, 1000, "Uploading...")
|
|
101
|
+
>>> tracker.emit_complete("upload_media", "Upload complete")
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
"""Initialize progress tracker."""
|
|
106
|
+
self._callbacks: list[ProgressCallback] = []
|
|
107
|
+
|
|
108
|
+
def add_callback(self, callback: ProgressCallback) -> None:
|
|
109
|
+
"""Add a progress callback.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
callback: Function to call when progress events occur.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> def my_callback(event: ProgressEvent) -> None:
|
|
116
|
+
... print(f"{event.operation}: {event.percentage}%")
|
|
117
|
+
>>>
|
|
118
|
+
>>> tracker.add_callback(my_callback)
|
|
119
|
+
"""
|
|
120
|
+
self._callbacks.append(callback)
|
|
121
|
+
|
|
122
|
+
def remove_callback(self, callback: ProgressCallback) -> None:
|
|
123
|
+
"""Remove a progress callback.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
callback: Callback to remove.
|
|
127
|
+
"""
|
|
128
|
+
if callback in self._callbacks:
|
|
129
|
+
self._callbacks.remove(callback)
|
|
130
|
+
|
|
131
|
+
def clear_callbacks(self) -> None:
|
|
132
|
+
"""Remove all callbacks."""
|
|
133
|
+
self._callbacks.clear()
|
|
134
|
+
|
|
135
|
+
def emit(self, event: ProgressEvent) -> None:
|
|
136
|
+
"""Emit a progress event to all callbacks.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
event: Progress event to emit.
|
|
140
|
+
"""
|
|
141
|
+
import contextlib
|
|
142
|
+
|
|
143
|
+
for callback in self._callbacks:
|
|
144
|
+
with contextlib.suppress(Exception):
|
|
145
|
+
# Silently ignore callback errors to prevent disrupting operations
|
|
146
|
+
callback(event)
|
|
147
|
+
|
|
148
|
+
def emit_start(
|
|
149
|
+
self,
|
|
150
|
+
operation: str,
|
|
151
|
+
total: int | float | None = None,
|
|
152
|
+
message: str | None = None,
|
|
153
|
+
**metadata: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Emit an operation start event.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
operation: Name of the operation.
|
|
159
|
+
total: Total expected value (optional).
|
|
160
|
+
message: Optional message.
|
|
161
|
+
**metadata: Additional metadata.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> tracker.emit_start("upload_media", total=1024000,
|
|
165
|
+
... message="Starting upload...")
|
|
166
|
+
"""
|
|
167
|
+
event = ProgressEvent(
|
|
168
|
+
operation=operation,
|
|
169
|
+
progress=0,
|
|
170
|
+
total=total,
|
|
171
|
+
status=ProgressStatus.STARTED,
|
|
172
|
+
message=message or f"Starting {operation}",
|
|
173
|
+
metadata=metadata,
|
|
174
|
+
)
|
|
175
|
+
self.emit(event)
|
|
176
|
+
|
|
177
|
+
def emit_progress(
|
|
178
|
+
self,
|
|
179
|
+
operation: str,
|
|
180
|
+
progress: int | float,
|
|
181
|
+
total: int | float | None = None,
|
|
182
|
+
message: str | None = None,
|
|
183
|
+
**metadata: Any,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Emit a progress update event.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
operation: Name of the operation.
|
|
189
|
+
progress: Current progress value.
|
|
190
|
+
total: Total expected value (optional).
|
|
191
|
+
message: Optional message.
|
|
192
|
+
**metadata: Additional metadata.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> tracker.emit_progress("upload_media", 512000, 1024000,
|
|
196
|
+
... message="Uploading...")
|
|
197
|
+
"""
|
|
198
|
+
event = ProgressEvent(
|
|
199
|
+
operation=operation,
|
|
200
|
+
progress=progress,
|
|
201
|
+
total=total,
|
|
202
|
+
status=ProgressStatus.IN_PROGRESS,
|
|
203
|
+
message=message,
|
|
204
|
+
metadata=metadata,
|
|
205
|
+
)
|
|
206
|
+
self.emit(event)
|
|
207
|
+
|
|
208
|
+
def emit_complete(
|
|
209
|
+
self,
|
|
210
|
+
operation: str,
|
|
211
|
+
message: str | None = None,
|
|
212
|
+
**metadata: Any,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Emit an operation complete event.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
operation: Name of the operation.
|
|
218
|
+
message: Optional message.
|
|
219
|
+
**metadata: Additional metadata.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> tracker.emit_complete("upload_media",
|
|
223
|
+
... message="Upload successful!")
|
|
224
|
+
"""
|
|
225
|
+
event = ProgressEvent(
|
|
226
|
+
operation=operation,
|
|
227
|
+
progress=100,
|
|
228
|
+
total=100,
|
|
229
|
+
status=ProgressStatus.COMPLETED,
|
|
230
|
+
message=message or f"{operation} completed",
|
|
231
|
+
metadata=metadata,
|
|
232
|
+
)
|
|
233
|
+
self.emit(event)
|
|
234
|
+
|
|
235
|
+
def emit_failed(
|
|
236
|
+
self,
|
|
237
|
+
operation: str,
|
|
238
|
+
error: str | Exception,
|
|
239
|
+
**metadata: Any,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Emit an operation failed event.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
operation: Name of the operation.
|
|
245
|
+
error: Error message or exception.
|
|
246
|
+
**metadata: Additional metadata.
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
>>> try:
|
|
250
|
+
... # Some operation
|
|
251
|
+
... pass
|
|
252
|
+
... except Exception as e:
|
|
253
|
+
... tracker.emit_failed("upload_media", e)
|
|
254
|
+
"""
|
|
255
|
+
message = str(error) if isinstance(error, Exception) else error
|
|
256
|
+
event = ProgressEvent(
|
|
257
|
+
operation=operation,
|
|
258
|
+
progress=0,
|
|
259
|
+
total=100,
|
|
260
|
+
status=ProgressStatus.FAILED,
|
|
261
|
+
message=message,
|
|
262
|
+
metadata=metadata,
|
|
263
|
+
)
|
|
264
|
+
self.emit(event)
|
|
265
|
+
|
|
266
|
+
def emit_cancelled(
|
|
267
|
+
self,
|
|
268
|
+
operation: str,
|
|
269
|
+
message: str | None = None,
|
|
270
|
+
**metadata: Any,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Emit an operation cancelled event.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
operation: Name of the operation.
|
|
276
|
+
message: Optional message.
|
|
277
|
+
**metadata: Additional metadata.
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
>>> tracker.emit_cancelled("upload_media",
|
|
281
|
+
... message="Upload cancelled by user")
|
|
282
|
+
"""
|
|
283
|
+
event = ProgressEvent(
|
|
284
|
+
operation=operation,
|
|
285
|
+
progress=0,
|
|
286
|
+
total=100,
|
|
287
|
+
status=ProgressStatus.CANCELLED,
|
|
288
|
+
message=message or f"{operation} cancelled",
|
|
289
|
+
metadata=metadata,
|
|
290
|
+
)
|
|
291
|
+
self.emit(event)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Platform registry for managing platform manager instances.
|
|
2
|
+
|
|
3
|
+
This module provides a singleton registry pattern for registering and accessing
|
|
4
|
+
platform managers across the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, TypeVar
|
|
9
|
+
|
|
10
|
+
from marqetive.platforms.models import AuthCredentials
|
|
11
|
+
|
|
12
|
+
# Type variable for manager classes
|
|
13
|
+
ManagerType = TypeVar("ManagerType")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlatformRegistry:
|
|
17
|
+
"""Singleton registry for platform managers.
|
|
18
|
+
|
|
19
|
+
The registry maintains a mapping of platform names to their manager classes
|
|
20
|
+
and provides caching of manager instances to avoid unnecessary recreation.
|
|
21
|
+
|
|
22
|
+
Thread-safe implementation using threading.Lock.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> from marqetive_lib.platforms.twitter.manager import TwitterPostManager
|
|
26
|
+
>>> registry = PlatformRegistry()
|
|
27
|
+
>>> registry.register_platform("twitter", TwitterPostManager)
|
|
28
|
+
>>> manager = registry.get_manager("twitter", credentials=creds)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_instance: "PlatformRegistry | None" = None
|
|
32
|
+
_lock: threading.Lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
def __new__(cls) -> "PlatformRegistry":
|
|
35
|
+
"""Ensure only one instance exists (singleton pattern)."""
|
|
36
|
+
if cls._instance is None:
|
|
37
|
+
with cls._lock:
|
|
38
|
+
# Double-check locking pattern
|
|
39
|
+
if cls._instance is None:
|
|
40
|
+
cls._instance = super().__new__(cls)
|
|
41
|
+
cls._instance._initialized = False
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
"""Initialize the registry."""
|
|
46
|
+
# Only initialize once
|
|
47
|
+
if self._initialized:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
self._platforms: dict[str, type] = {}
|
|
51
|
+
self._manager_cache: dict[str, Any] = {}
|
|
52
|
+
self._cache_lock = threading.Lock()
|
|
53
|
+
self._initialized = True
|
|
54
|
+
|
|
55
|
+
def register_platform(self, platform_name: str, manager_class: type) -> None:
|
|
56
|
+
"""Register a platform manager class.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
platform_name: Name of the platform (e.g., "twitter", "linkedin").
|
|
60
|
+
manager_class: The manager class to register.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If platform is already registered.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> registry.register_platform("twitter", TwitterPostManager)
|
|
67
|
+
"""
|
|
68
|
+
if platform_name in self._platforms:
|
|
69
|
+
raise ValueError(f"Platform '{platform_name}' is already registered")
|
|
70
|
+
|
|
71
|
+
self._platforms[platform_name] = manager_class
|
|
72
|
+
|
|
73
|
+
def unregister_platform(self, platform_name: str) -> None:
|
|
74
|
+
"""Unregister a platform and clear its cached instances.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
platform_name: Name of the platform to unregister.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> registry.unregister_platform("twitter")
|
|
81
|
+
"""
|
|
82
|
+
with self._cache_lock:
|
|
83
|
+
self._platforms.pop(platform_name, None)
|
|
84
|
+
# Clear cached instances for this platform
|
|
85
|
+
keys_to_remove = [
|
|
86
|
+
k for k in self._manager_cache if k.startswith(f"{platform_name}:")
|
|
87
|
+
]
|
|
88
|
+
for key in keys_to_remove:
|
|
89
|
+
self._manager_cache.pop(key)
|
|
90
|
+
|
|
91
|
+
def get_manager(
|
|
92
|
+
self,
|
|
93
|
+
platform_name: str,
|
|
94
|
+
use_cache: bool = True,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""Get or create a manager instance for the specified platform.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
platform_name: Name of the platform (e.g., "twitter", "linkedin").
|
|
101
|
+
use_cache: Whether to use cached instance (default: True).
|
|
102
|
+
**kwargs: Arguments to pass to the manager constructor.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Manager instance for the platform.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If platform is not registered.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> manager = registry.get_manager("twitter", credentials=creds)
|
|
112
|
+
>>> post = await manager.execute_post(...)
|
|
113
|
+
"""
|
|
114
|
+
if platform_name not in self._platforms:
|
|
115
|
+
available = ", ".join(self.get_available_platforms())
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Platform '{platform_name}' is not registered. "
|
|
118
|
+
f"Available platforms: {available}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Generate cache key from platform name and kwargs
|
|
122
|
+
cache_key = self._generate_cache_key(platform_name, **kwargs)
|
|
123
|
+
|
|
124
|
+
if use_cache:
|
|
125
|
+
with self._cache_lock:
|
|
126
|
+
if cache_key in self._manager_cache:
|
|
127
|
+
return self._manager_cache[cache_key]
|
|
128
|
+
|
|
129
|
+
# Create new manager instance
|
|
130
|
+
manager_class = self._platforms[platform_name]
|
|
131
|
+
manager = manager_class(**kwargs)
|
|
132
|
+
|
|
133
|
+
if use_cache:
|
|
134
|
+
with self._cache_lock:
|
|
135
|
+
self._manager_cache[cache_key] = manager
|
|
136
|
+
|
|
137
|
+
return manager
|
|
138
|
+
|
|
139
|
+
def get_available_platforms(self) -> list[str]:
|
|
140
|
+
"""Get list of all registered platform names.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of platform names.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> platforms = registry.get_available_platforms()
|
|
147
|
+
>>> print(platforms)
|
|
148
|
+
['twitter', 'linkedin', 'instagram', 'tiktok']
|
|
149
|
+
"""
|
|
150
|
+
return list(self._platforms.keys())
|
|
151
|
+
|
|
152
|
+
def clear_cache(self) -> None:
|
|
153
|
+
"""Clear all cached manager instances.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> registry.clear_cache()
|
|
157
|
+
"""
|
|
158
|
+
with self._cache_lock:
|
|
159
|
+
self._manager_cache.clear()
|
|
160
|
+
|
|
161
|
+
def _generate_cache_key(self, platform_name: str, **kwargs: Any) -> str:
|
|
162
|
+
"""Generate a cache key for manager instance.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
platform_name: Name of the platform.
|
|
166
|
+
**kwargs: Manager constructor arguments.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Cache key string.
|
|
170
|
+
"""
|
|
171
|
+
# For credentials, use account_id if available
|
|
172
|
+
if "credentials" in kwargs:
|
|
173
|
+
creds = kwargs["credentials"]
|
|
174
|
+
if isinstance(creds, AuthCredentials) and creds.user_id:
|
|
175
|
+
return f"{platform_name}:user_id:{creds.user_id}"
|
|
176
|
+
|
|
177
|
+
# Default to platform name only
|
|
178
|
+
return platform_name
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Global registry instance
|
|
182
|
+
_global_registry: PlatformRegistry | None = None
|
|
183
|
+
_global_lock = threading.Lock()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_registry() -> PlatformRegistry:
|
|
187
|
+
"""Get the global platform registry instance.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Global PlatformRegistry instance.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> registry = get_registry()
|
|
194
|
+
>>> manager = registry.get_manager("twitter", credentials=creds)
|
|
195
|
+
"""
|
|
196
|
+
global _global_registry
|
|
197
|
+
|
|
198
|
+
if _global_registry is None:
|
|
199
|
+
with _global_lock:
|
|
200
|
+
if _global_registry is None:
|
|
201
|
+
_global_registry = PlatformRegistry()
|
|
202
|
+
|
|
203
|
+
return _global_registry
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def register_platform(platform_name: str, manager_class: type) -> None:
|
|
207
|
+
"""Register a platform in the global registry.
|
|
208
|
+
|
|
209
|
+
Convenience function that uses the global registry.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
platform_name: Name of the platform.
|
|
213
|
+
manager_class: The manager class to register.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> from marqetive_lib.platforms.twitter.manager import TwitterPostManager
|
|
217
|
+
>>> register_platform("twitter", TwitterPostManager)
|
|
218
|
+
"""
|
|
219
|
+
registry = get_registry()
|
|
220
|
+
registry.register_platform(platform_name, manager_class)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_manager_for_platform(platform_name: str, **kwargs: Any) -> Any:
|
|
224
|
+
"""Get a manager instance for the specified platform.
|
|
225
|
+
|
|
226
|
+
Convenience function that uses the global registry.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
platform_name: Name of the platform.
|
|
230
|
+
**kwargs: Arguments to pass to the manager constructor.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Manager instance for the platform.
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> manager = get_manager_for_platform("twitter", credentials=creds)
|
|
237
|
+
>>> post = await manager.execute_post(...)
|
|
238
|
+
"""
|
|
239
|
+
registry = get_registry()
|
|
240
|
+
return registry.get_manager(platform_name, **kwargs)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_available_platforms() -> list[str]:
|
|
244
|
+
"""Get list of all registered platforms.
|
|
245
|
+
|
|
246
|
+
Convenience function that uses the global registry.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of platform names.
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> platforms = get_available_platforms()
|
|
253
|
+
>>> print(platforms)
|
|
254
|
+
['twitter', 'linkedin', 'instagram']
|
|
255
|
+
"""
|
|
256
|
+
registry = get_registry()
|
|
257
|
+
return registry.get_available_platforms()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Social media platform integrations.
|
|
2
|
+
|
|
3
|
+
This package provides a unified interface for interacting with various social
|
|
4
|
+
media platforms including Instagram, Twitter/X, and LinkedIn.
|
|
5
|
+
|
|
6
|
+
Platform clients are available via their respective subpackages:
|
|
7
|
+
- from marqetive_lib.platforms.twitter import TwitterClient
|
|
8
|
+
- from marqetive_lib.platforms.linkedin import LinkedInClient
|
|
9
|
+
- from marqetive_lib.platforms.instagram import InstagramClient
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from marqetive.platforms.base import SocialMediaPlatform
|
|
13
|
+
from marqetive.platforms.exceptions import (
|
|
14
|
+
MediaUploadError,
|
|
15
|
+
PlatformAuthError,
|
|
16
|
+
PlatformError,
|
|
17
|
+
PostNotFoundError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
from marqetive.platforms.models import (
|
|
22
|
+
AuthCredentials,
|
|
23
|
+
Comment,
|
|
24
|
+
CommentStatus,
|
|
25
|
+
MediaAttachment,
|
|
26
|
+
MediaType,
|
|
27
|
+
PlatformResponse,
|
|
28
|
+
Post,
|
|
29
|
+
PostCreateRequest,
|
|
30
|
+
PostStatus,
|
|
31
|
+
PostUpdateRequest,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Base class
|
|
36
|
+
"SocialMediaPlatform",
|
|
37
|
+
# Models
|
|
38
|
+
"AuthCredentials",
|
|
39
|
+
"Comment",
|
|
40
|
+
"CommentStatus",
|
|
41
|
+
"MediaAttachment",
|
|
42
|
+
"MediaType",
|
|
43
|
+
"PlatformResponse",
|
|
44
|
+
"Post",
|
|
45
|
+
"PostCreateRequest",
|
|
46
|
+
"PostStatus",
|
|
47
|
+
"PostUpdateRequest",
|
|
48
|
+
# Exceptions
|
|
49
|
+
"MediaUploadError",
|
|
50
|
+
"PlatformAuthError",
|
|
51
|
+
"PlatformError",
|
|
52
|
+
"PostNotFoundError",
|
|
53
|
+
"RateLimitError",
|
|
54
|
+
"ValidationError",
|
|
55
|
+
]
|