marqetive-lib 0.1.6__tar.gz → 0.1.8__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.6 → marqetive_lib-0.1.8}/PKG-INFO +1 -9
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/README.md +0 -8
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/pyproject.toml +1 -1
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/__init__.py +13 -7
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/core/__init__.py +6 -4
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/core/base.py +92 -13
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/core/client.py +15 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/core/models.py +111 -7
- marqetive_lib-0.1.8/src/marqetive/platforms/instagram/__init__.py +6 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/instagram/client.py +29 -8
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/instagram/media.py +79 -13
- marqetive_lib-0.1.8/src/marqetive/platforms/instagram/models.py +74 -0
- marqetive_lib-0.1.8/src/marqetive/platforms/linkedin/__init__.py +54 -0
- marqetive_lib-0.1.8/src/marqetive/platforms/linkedin/client.py +1621 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/linkedin/media.py +156 -47
- marqetive_lib-0.1.8/src/marqetive/platforms/linkedin/models.py +413 -0
- marqetive_lib-0.1.8/src/marqetive/platforms/tiktok/__init__.py +6 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/tiktok/client.py +5 -4
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/tiktok/media.py +193 -102
- marqetive_lib-0.1.8/src/marqetive/platforms/tiktok/models.py +79 -0
- marqetive_lib-0.1.8/src/marqetive/platforms/twitter/__init__.py +6 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/twitter/client.py +86 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/twitter/media.py +139 -70
- marqetive_lib-0.1.8/src/marqetive/platforms/twitter/models.py +58 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/media.py +86 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/oauth.py +31 -4
- marqetive_lib-0.1.6/src/marqetive/platforms/instagram/__init__.py +0 -5
- marqetive_lib-0.1.6/src/marqetive/platforms/linkedin/__init__.py +0 -5
- marqetive_lib-0.1.6/src/marqetive/platforms/linkedin/client.py +0 -737
- marqetive_lib-0.1.6/src/marqetive/platforms/tiktok/__init__.py +0 -5
- marqetive_lib-0.1.6/src/marqetive/platforms/twitter/__init__.py +0 -5
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/core/exceptions.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/factory.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/__init__.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/instagram/exceptions.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/linkedin/exceptions.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/tiktok/exceptions.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/platforms/twitter/exceptions.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/py.typed +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/__init__.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/file_handlers.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/helpers.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/retry.py +0 -0
- {marqetive_lib-0.1.6 → marqetive_lib-0.1.8}/src/marqetive/utils/token_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: marqetive-lib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Modern Python utilities for web APIs
|
|
5
5
|
Keywords: api,utilities,web,http,marqetive
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -238,14 +238,6 @@ except PlatformError as e:
|
|
|
238
238
|
print(f"Platform error: {e}")
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
## Requirements
|
|
242
|
-
|
|
243
|
-
- Python 3.12+
|
|
244
|
-
- httpx >= 0.28.1
|
|
245
|
-
- pydantic >= 2.0.0
|
|
246
|
-
- tweepy >= 4.16.0
|
|
247
|
-
- aiofiles >= 24.0.0
|
|
248
|
-
|
|
249
241
|
## Development
|
|
250
242
|
|
|
251
243
|
### Setup Development Environment
|
|
@@ -205,14 +205,6 @@ except PlatformError as e:
|
|
|
205
205
|
print(f"Platform error: {e}")
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
## Requirements
|
|
209
|
-
|
|
210
|
-
- Python 3.12+
|
|
211
|
-
- httpx >= 0.28.1
|
|
212
|
-
- pydantic >= 2.0.0
|
|
213
|
-
- tweepy >= 4.16.0
|
|
214
|
-
- aiofiles >= 24.0.0
|
|
215
|
-
|
|
216
208
|
## Development
|
|
217
209
|
|
|
218
210
|
### Setup Development Environment
|
|
@@ -62,6 +62,8 @@ from marqetive.core.models import (
|
|
|
62
62
|
PostCreateRequest,
|
|
63
63
|
PostStatus,
|
|
64
64
|
PostUpdateRequest,
|
|
65
|
+
ProgressEvent,
|
|
66
|
+
ProgressStatus,
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
# Factory
|
|
@@ -85,17 +87,21 @@ __all__ = [
|
|
|
85
87
|
"BackoffConfig",
|
|
86
88
|
"STANDARD_BACKOFF",
|
|
87
89
|
"retry_async",
|
|
88
|
-
#
|
|
89
|
-
"AuthCredentials",
|
|
90
|
+
# Enums
|
|
90
91
|
"AccountStatus",
|
|
91
|
-
"
|
|
92
|
+
"CommentStatus",
|
|
93
|
+
"MediaType",
|
|
92
94
|
"PostStatus",
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
+
"ProgressStatus",
|
|
96
|
+
# Core Models
|
|
97
|
+
"AuthCredentials",
|
|
95
98
|
"Comment",
|
|
96
|
-
"CommentStatus",
|
|
97
99
|
"MediaAttachment",
|
|
98
|
-
"
|
|
100
|
+
"Post",
|
|
101
|
+
"ProgressEvent",
|
|
102
|
+
# Base Request Models
|
|
103
|
+
"PostCreateRequest",
|
|
104
|
+
"PostUpdateRequest",
|
|
99
105
|
# Exceptions
|
|
100
106
|
"PlatformError",
|
|
101
107
|
"PlatformAuthError",
|
|
@@ -31,17 +31,19 @@ __all__ = [
|
|
|
31
31
|
# Base class
|
|
32
32
|
"SocialMediaPlatform",
|
|
33
33
|
"ProgressCallback",
|
|
34
|
-
#
|
|
34
|
+
# Enums
|
|
35
35
|
"AccountStatus",
|
|
36
|
+
"CommentStatus",
|
|
37
|
+
"MediaType",
|
|
38
|
+
"PostStatus",
|
|
39
|
+
# Core Models
|
|
36
40
|
"AuthCredentials",
|
|
37
41
|
"Comment",
|
|
38
|
-
"CommentStatus",
|
|
39
42
|
"MediaAttachment",
|
|
40
|
-
"MediaType",
|
|
41
43
|
"PlatformResponse",
|
|
42
44
|
"Post",
|
|
45
|
+
# Base Request Models
|
|
43
46
|
"PostCreateRequest",
|
|
44
|
-
"PostStatus",
|
|
45
47
|
"PostUpdateRequest",
|
|
46
48
|
# Exceptions
|
|
47
49
|
"InvalidFileTypeError",
|
|
@@ -5,8 +5,9 @@ for implementing platform-specific clients (Instagram, Twitter, LinkedIn, etc.).
|
|
|
5
5
|
All concrete implementations must implement the abstract methods defined here.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import inspect
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from traceback import TracebackException
|
|
12
13
|
from typing import Any
|
|
@@ -24,11 +25,15 @@ from marqetive.core.models import (
|
|
|
24
25
|
Post,
|
|
25
26
|
PostCreateRequest,
|
|
26
27
|
PostUpdateRequest,
|
|
28
|
+
ProgressEvent,
|
|
29
|
+
ProgressStatus,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
# Type
|
|
30
|
-
#
|
|
31
|
-
type
|
|
32
|
+
# Type aliases for progress callbacks
|
|
33
|
+
# Supports both sync and async callbacks using ProgressEvent
|
|
34
|
+
type SyncProgressCallback = Callable[[ProgressEvent], None]
|
|
35
|
+
type AsyncProgressCallback = Callable[[ProgressEvent], Awaitable[None]]
|
|
36
|
+
type ProgressCallback = SyncProgressCallback | AsyncProgressCallback
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
class SocialMediaPlatform(ABC):
|
|
@@ -162,28 +167,66 @@ class SocialMediaPlatform(ABC):
|
|
|
162
167
|
self._rate_limit_remaining = remaining
|
|
163
168
|
self._rate_limit_reset = reset_time
|
|
164
169
|
|
|
165
|
-
def _emit_progress(
|
|
170
|
+
async def _emit_progress(
|
|
166
171
|
self,
|
|
167
172
|
operation: str,
|
|
173
|
+
status: ProgressStatus,
|
|
168
174
|
progress: int,
|
|
169
175
|
total: int,
|
|
170
176
|
message: str | None = None,
|
|
177
|
+
*,
|
|
178
|
+
entity_id: str | None = None,
|
|
179
|
+
file_path: str | None = None,
|
|
180
|
+
bytes_uploaded: int | None = None,
|
|
181
|
+
total_bytes: int | None = None,
|
|
171
182
|
) -> None:
|
|
172
183
|
"""Emit a progress update if a callback is registered.
|
|
173
184
|
|
|
174
|
-
This method
|
|
185
|
+
This method supports both synchronous and asynchronous callbacks.
|
|
186
|
+
It is safe to call even if no callback is registered.
|
|
175
187
|
|
|
176
188
|
Args:
|
|
177
|
-
operation: Name of the operation (e.g., "upload_media", "create_post")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
operation: Name of the operation (e.g., "upload_media", "create_post", "create_thread")
|
|
190
|
+
status: Current status of the operation
|
|
191
|
+
progress: Current progress value (0-100 for percentage, or bytes)
|
|
192
|
+
total: Total value for completion (100 for percentage, or total bytes)
|
|
193
|
+
message: Optional human-readable status message
|
|
194
|
+
entity_id: Optional platform-specific ID (media_id, post_id, etc.)
|
|
195
|
+
file_path: Optional file path for upload operations
|
|
196
|
+
bytes_uploaded: Optional bytes uploaded so far
|
|
197
|
+
total_bytes: Optional total bytes to upload
|
|
181
198
|
|
|
182
199
|
Example:
|
|
183
|
-
>>> self._emit_progress(
|
|
200
|
+
>>> await self._emit_progress(
|
|
201
|
+
... operation="upload_media",
|
|
202
|
+
... status=ProgressStatus.UPLOADING,
|
|
203
|
+
... progress=50,
|
|
204
|
+
... total=100,
|
|
205
|
+
... message="Uploading image 1 of 3",
|
|
206
|
+
... entity_id="media_123",
|
|
207
|
+
... )
|
|
184
208
|
"""
|
|
185
|
-
if self._progress_callback is
|
|
186
|
-
|
|
209
|
+
if self._progress_callback is None:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
event = ProgressEvent(
|
|
213
|
+
operation=operation,
|
|
214
|
+
platform=self.platform_name,
|
|
215
|
+
status=status,
|
|
216
|
+
progress=progress,
|
|
217
|
+
total=total,
|
|
218
|
+
message=message,
|
|
219
|
+
entity_id=entity_id,
|
|
220
|
+
file_path=file_path,
|
|
221
|
+
bytes_uploaded=bytes_uploaded,
|
|
222
|
+
total_bytes=total_bytes,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result = self._progress_callback(event)
|
|
226
|
+
|
|
227
|
+
# If callback returned a coroutine, await it
|
|
228
|
+
if inspect.iscoroutine(result):
|
|
229
|
+
await result
|
|
187
230
|
|
|
188
231
|
# ==================== Abstract Authentication Methods ====================
|
|
189
232
|
|
|
@@ -307,6 +350,42 @@ class SocialMediaPlatform(ABC):
|
|
|
307
350
|
"""
|
|
308
351
|
pass
|
|
309
352
|
|
|
353
|
+
async def create_thread(
|
|
354
|
+
self,
|
|
355
|
+
posts: list[PostCreateRequest],
|
|
356
|
+
) -> list[Post]:
|
|
357
|
+
"""Create a thread of connected posts.
|
|
358
|
+
|
|
359
|
+
Not all platforms support threads. The default implementation raises
|
|
360
|
+
NotImplementedError. Platforms that support threads (like Twitter)
|
|
361
|
+
should override this method.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
posts: List of post requests to create as a thread.
|
|
365
|
+
Each post can have its own content, media, etc.
|
|
366
|
+
First post is the head of the thread.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
List of Post objects for each post in the thread.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
NotImplementedError: If platform doesn't support threads.
|
|
373
|
+
ValidationError: If posts list is empty.
|
|
374
|
+
PlatformAuthError: If not authenticated.
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
>>> # Twitter thread example
|
|
378
|
+
>>> posts = [
|
|
379
|
+
... TwitterPostRequest(content="Thread start! 1/3"),
|
|
380
|
+
... TwitterPostRequest(content="Middle 2/3", media_urls=[...]),
|
|
381
|
+
... TwitterPostRequest(content="End 3/3"),
|
|
382
|
+
... ]
|
|
383
|
+
>>> thread = await client.create_thread(posts)
|
|
384
|
+
"""
|
|
385
|
+
raise NotImplementedError(
|
|
386
|
+
f"{self.platform_name} does not support thread creation"
|
|
387
|
+
)
|
|
388
|
+
|
|
310
389
|
# ==================== Abstract Comment Methods ====================
|
|
311
390
|
|
|
312
391
|
@abstractmethod
|
|
@@ -5,6 +5,11 @@ from typing import Any
|
|
|
5
5
|
import httpx
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
|
+
# Default connection pool limits for optimal performance
|
|
9
|
+
DEFAULT_MAX_CONNECTIONS = 100
|
|
10
|
+
DEFAULT_MAX_KEEPALIVE_CONNECTIONS = 20
|
|
11
|
+
DEFAULT_KEEPALIVE_EXPIRY = 30.0
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
class APIResponse(BaseModel):
|
|
10
15
|
"""Response model for API calls."""
|
|
@@ -24,6 +29,8 @@ class APIClient:
|
|
|
24
29
|
base_url: The base URL for API requests
|
|
25
30
|
timeout: Request timeout in seconds (default: 30)
|
|
26
31
|
headers: Optional default headers for all requests
|
|
32
|
+
max_connections: Maximum total connections in pool (default: 100)
|
|
33
|
+
max_keepalive_connections: Maximum persistent connections (default: 20)
|
|
27
34
|
|
|
28
35
|
Example:
|
|
29
36
|
>>> client = APIClient(base_url="https://api.example.com")
|
|
@@ -36,12 +43,19 @@ class APIClient:
|
|
|
36
43
|
base_url: str,
|
|
37
44
|
timeout: float = 30.0,
|
|
38
45
|
headers: dict[str, str] | None = None,
|
|
46
|
+
max_connections: int = DEFAULT_MAX_CONNECTIONS,
|
|
47
|
+
max_keepalive_connections: int = DEFAULT_MAX_KEEPALIVE_CONNECTIONS,
|
|
39
48
|
) -> None:
|
|
40
49
|
"""Initialize the API client."""
|
|
41
50
|
self.base_url = base_url.rstrip("/")
|
|
42
51
|
self.timeout = timeout
|
|
43
52
|
self.default_headers = headers or {}
|
|
44
53
|
self._client: httpx.AsyncClient | None = None
|
|
54
|
+
self._limits = httpx.Limits(
|
|
55
|
+
max_connections=max_connections,
|
|
56
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
57
|
+
keepalive_expiry=DEFAULT_KEEPALIVE_EXPIRY,
|
|
58
|
+
)
|
|
45
59
|
|
|
46
60
|
async def __aenter__(self) -> "APIClient":
|
|
47
61
|
"""Async context manager entry."""
|
|
@@ -49,6 +63,7 @@ class APIClient:
|
|
|
49
63
|
base_url=self.base_url,
|
|
50
64
|
timeout=self.timeout,
|
|
51
65
|
headers=self.default_headers,
|
|
66
|
+
limits=self._limits,
|
|
52
67
|
)
|
|
53
68
|
return self
|
|
54
69
|
|
|
@@ -6,10 +6,10 @@ and type safety.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from datetime import UTC, datetime, timedelta
|
|
9
|
-
from enum import Enum
|
|
9
|
+
from enum import Enum, StrEnum
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from pydantic import BaseModel, Field, HttpUrl
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class MediaType(str, Enum):
|
|
@@ -19,6 +19,8 @@ class MediaType(str, Enum):
|
|
|
19
19
|
VIDEO = "video"
|
|
20
20
|
CAROUSEL = "carousel"
|
|
21
21
|
DOCUMENT = "document"
|
|
22
|
+
REEL = "reel"
|
|
23
|
+
STORY = "story"
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class PostStatus(str, Enum):
|
|
@@ -55,6 +57,29 @@ class AccountStatus(str, Enum):
|
|
|
55
57
|
ERROR = "error"
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
class ProgressStatus(StrEnum):
|
|
61
|
+
"""Standard progress status for operations across all platforms.
|
|
62
|
+
|
|
63
|
+
Used with ProgressEvent to provide consistent progress tracking
|
|
64
|
+
for long-running operations like media uploads and post creation.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
INITIALIZING: Operation is starting, preparing resources.
|
|
68
|
+
UPLOADING: Actively transferring data (e.g., uploading media).
|
|
69
|
+
PROCESSING: Server-side processing (e.g., video transcoding).
|
|
70
|
+
FINALIZING: Completing the operation (e.g., publishing post).
|
|
71
|
+
COMPLETED: Operation finished successfully.
|
|
72
|
+
FAILED: Operation failed with an error.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
INITIALIZING = "initializing"
|
|
76
|
+
UPLOADING = "uploading"
|
|
77
|
+
PROCESSING = "processing"
|
|
78
|
+
FINALIZING = "finalizing"
|
|
79
|
+
COMPLETED = "completed"
|
|
80
|
+
FAILED = "failed"
|
|
81
|
+
|
|
82
|
+
|
|
58
83
|
class MediaAttachment(BaseModel):
|
|
59
84
|
"""Represents a media attachment (image, video, etc.).
|
|
60
85
|
|
|
@@ -294,8 +319,82 @@ class PlatformResponse(BaseModel):
|
|
|
294
319
|
rate_limit_reset: datetime | None = None
|
|
295
320
|
|
|
296
321
|
|
|
322
|
+
class ProgressEvent(BaseModel):
|
|
323
|
+
"""Unified progress event for tracking long-running operations.
|
|
324
|
+
|
|
325
|
+
Provides a consistent interface for progress callbacks across all platforms.
|
|
326
|
+
Supports both synchronous and asynchronous callbacks.
|
|
327
|
+
|
|
328
|
+
Attributes:
|
|
329
|
+
operation: Name of the operation (e.g., "upload_media", "create_post", "create_thread").
|
|
330
|
+
platform: Platform name (e.g., "twitter", "linkedin", "instagram", "tiktok").
|
|
331
|
+
status: Current status of the operation.
|
|
332
|
+
progress: Current progress value (0-100 for percentage, or bytes uploaded).
|
|
333
|
+
total: Total value for completion (100 for percentage, or total bytes).
|
|
334
|
+
message: Optional human-readable status message.
|
|
335
|
+
entity_id: Optional platform-specific ID (media_id, post_id, container_id).
|
|
336
|
+
file_path: Optional file path for upload operations.
|
|
337
|
+
bytes_uploaded: Optional bytes uploaded so far.
|
|
338
|
+
total_bytes: Optional total bytes to upload.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> # Progress callback for media upload
|
|
342
|
+
>>> def on_progress(event: ProgressEvent) -> None:
|
|
343
|
+
... print(f"{event.operation}: {event.percentage:.1f}% - {event.message}")
|
|
344
|
+
...
|
|
345
|
+
>>> # Async progress callback
|
|
346
|
+
>>> async def on_progress_async(event: ProgressEvent) -> None:
|
|
347
|
+
... await log_to_database(event)
|
|
348
|
+
|
|
349
|
+
>>> event = ProgressEvent(
|
|
350
|
+
... operation="upload_media",
|
|
351
|
+
... platform="twitter",
|
|
352
|
+
... status=ProgressStatus.UPLOADING,
|
|
353
|
+
... progress=50,
|
|
354
|
+
... total=100,
|
|
355
|
+
... message="Uploading image 1 of 2",
|
|
356
|
+
... bytes_uploaded=524288,
|
|
357
|
+
... total_bytes=1048576,
|
|
358
|
+
... )
|
|
359
|
+
>>> print(event.percentage) # 50.0
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
operation: str
|
|
363
|
+
platform: str
|
|
364
|
+
status: ProgressStatus
|
|
365
|
+
progress: int
|
|
366
|
+
total: int
|
|
367
|
+
message: str | None = None
|
|
368
|
+
|
|
369
|
+
# Optional detailed info
|
|
370
|
+
entity_id: str | None = None
|
|
371
|
+
file_path: str | None = None
|
|
372
|
+
bytes_uploaded: int | None = None
|
|
373
|
+
total_bytes: int | None = None
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def percentage(self) -> float:
|
|
377
|
+
"""Calculate progress as a percentage.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Progress percentage (0.0 to 100.0).
|
|
381
|
+
"""
|
|
382
|
+
if self.total == 0:
|
|
383
|
+
return 0.0
|
|
384
|
+
return (self.progress / self.total) * 100
|
|
385
|
+
|
|
386
|
+
model_config = ConfigDict(frozen=True)
|
|
387
|
+
|
|
388
|
+
|
|
297
389
|
class PostCreateRequest(BaseModel):
|
|
298
|
-
"""
|
|
390
|
+
"""Base request model for creating a new post.
|
|
391
|
+
|
|
392
|
+
This is a minimal base model with universal fields that work across all platforms.
|
|
393
|
+
For platform-specific features, use the dedicated request models:
|
|
394
|
+
- TwitterPostRequest
|
|
395
|
+
- LinkedInPostRequest
|
|
396
|
+
- InstagramPostRequest
|
|
397
|
+
- TikTokPostRequest
|
|
299
398
|
|
|
300
399
|
Attributes:
|
|
301
400
|
content: Text content of the post
|
|
@@ -305,7 +404,7 @@ class PostCreateRequest(BaseModel):
|
|
|
305
404
|
link: URL to include in the post
|
|
306
405
|
tags: List of hashtags or user tags
|
|
307
406
|
location: Location/place tag for the post
|
|
308
|
-
additional_data: Platform-specific data
|
|
407
|
+
additional_data: Platform-specific data for backward compatibility
|
|
309
408
|
|
|
310
409
|
Example:
|
|
311
410
|
>>> request = PostCreateRequest(
|
|
@@ -326,10 +425,15 @@ class PostCreateRequest(BaseModel):
|
|
|
326
425
|
|
|
327
426
|
|
|
328
427
|
class PostUpdateRequest(BaseModel):
|
|
329
|
-
"""
|
|
428
|
+
"""Base request model for updating an existing post.
|
|
429
|
+
|
|
430
|
+
Note: Not all platforms support editing posts:
|
|
431
|
+
- Twitter: No editing support (for most users)
|
|
432
|
+
- Instagram: No editing support
|
|
433
|
+
- TikTok: No editing support
|
|
434
|
+
- LinkedIn: Supports updating content, CTA, and landing page
|
|
330
435
|
|
|
331
|
-
|
|
332
|
-
updated vary by platform.
|
|
436
|
+
For LinkedIn-specific update features, use LinkedInPostUpdateRequest.
|
|
333
437
|
|
|
334
438
|
Attributes:
|
|
335
439
|
content: Updated text content
|
|
@@ -35,6 +35,7 @@ from marqetive.platforms.instagram.media import (
|
|
|
35
35
|
InstagramMediaManager,
|
|
36
36
|
MediaItem,
|
|
37
37
|
)
|
|
38
|
+
from marqetive.utils.media import validate_media_url
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class InstagramClient(SocialMediaPlatform):
|
|
@@ -245,7 +246,11 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
if request.media_urls:
|
|
248
|
-
|
|
249
|
+
# Validate URL to prevent SSRF attacks
|
|
250
|
+
validated_url = validate_media_url(
|
|
251
|
+
request.media_urls[0], platform=self.platform_name
|
|
252
|
+
)
|
|
253
|
+
container_params["image_url"] = validated_url
|
|
249
254
|
|
|
250
255
|
if request.content:
|
|
251
256
|
container_params["caption"] = request.content
|
|
@@ -533,14 +538,17 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
533
538
|
if not self.api_client:
|
|
534
539
|
raise RuntimeError("Client must be used as async context manager")
|
|
535
540
|
|
|
541
|
+
# Validate URL to prevent SSRF attacks
|
|
542
|
+
validated_url = validate_media_url(media_url, platform=self.platform_name)
|
|
543
|
+
|
|
536
544
|
params: dict[str, Any] = {
|
|
537
545
|
"access_token": self.credentials.access_token,
|
|
538
546
|
}
|
|
539
547
|
|
|
540
548
|
if media_type.lower() == "image":
|
|
541
|
-
params["image_url"] =
|
|
549
|
+
params["image_url"] = validated_url
|
|
542
550
|
elif media_type.lower() == "video":
|
|
543
|
-
params["video_url"] =
|
|
551
|
+
params["video_url"] = validated_url
|
|
544
552
|
params["media_type"] = "VIDEO"
|
|
545
553
|
else:
|
|
546
554
|
raise ValidationError(
|
|
@@ -612,13 +620,15 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
612
620
|
if not self._media_manager:
|
|
613
621
|
raise RuntimeError("Client must be used as async context manager")
|
|
614
622
|
|
|
615
|
-
# Convert to MediaItem objects
|
|
623
|
+
# Convert to MediaItem objects with URL validation
|
|
616
624
|
media_items = []
|
|
617
625
|
for idx, url in enumerate(media_urls):
|
|
626
|
+
# Validate each URL to prevent SSRF attacks
|
|
627
|
+
validated_url = validate_media_url(url, platform=self.platform_name)
|
|
618
628
|
alt_text = None
|
|
619
629
|
if alt_texts and idx < len(alt_texts):
|
|
620
630
|
alt_text = alt_texts[idx]
|
|
621
|
-
media_items.append(MediaItem(url=
|
|
631
|
+
media_items.append(MediaItem(url=validated_url, type="image", alt_text=alt_text))
|
|
622
632
|
|
|
623
633
|
# Create containers
|
|
624
634
|
container_ids = await self._media_manager.create_feed_containers(
|
|
@@ -667,11 +677,19 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
667
677
|
if not self._media_manager:
|
|
668
678
|
raise RuntimeError("Client must be used as async context manager")
|
|
669
679
|
|
|
680
|
+
# Validate URLs to prevent SSRF attacks
|
|
681
|
+
validated_video_url = validate_media_url(video_url, platform=self.platform_name)
|
|
682
|
+
validated_cover_url = None
|
|
683
|
+
if cover_url:
|
|
684
|
+
validated_cover_url = validate_media_url(
|
|
685
|
+
cover_url, platform=self.platform_name
|
|
686
|
+
)
|
|
687
|
+
|
|
670
688
|
# Create reel container
|
|
671
689
|
container_id = await self._media_manager.create_reel_container(
|
|
672
|
-
|
|
690
|
+
validated_video_url,
|
|
673
691
|
caption=caption,
|
|
674
|
-
cover_url=
|
|
692
|
+
cover_url=validated_cover_url,
|
|
675
693
|
share_to_feed=share_to_feed,
|
|
676
694
|
wait_for_processing=True,
|
|
677
695
|
)
|
|
@@ -710,9 +728,12 @@ class InstagramClient(SocialMediaPlatform):
|
|
|
710
728
|
if not self._media_manager:
|
|
711
729
|
raise RuntimeError("Client must be used as async context manager")
|
|
712
730
|
|
|
731
|
+
# Validate URL to prevent SSRF attacks
|
|
732
|
+
validated_url = validate_media_url(media_url, platform=self.platform_name)
|
|
733
|
+
|
|
713
734
|
# Create story container
|
|
714
735
|
container_id = await self._media_manager.create_story_container(
|
|
715
|
-
|
|
736
|
+
validated_url,
|
|
716
737
|
media_type,
|
|
717
738
|
wait_for_processing=(media_type == "video"),
|
|
718
739
|
)
|