marqetive-lib 0.1.5__py3-none-any.whl → 0.1.7__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 +13 -7
- marqetive/core/__init__.py +6 -4
- marqetive/core/base.py +92 -13
- marqetive/core/models.py +111 -7
- marqetive/platforms/instagram/__init__.py +2 -1
- marqetive/platforms/instagram/media.py +79 -13
- marqetive/platforms/instagram/models.py +74 -0
- marqetive/platforms/linkedin/__init__.py +51 -2
- marqetive/platforms/linkedin/client.py +978 -94
- marqetive/platforms/linkedin/media.py +156 -47
- marqetive/platforms/linkedin/models.py +413 -0
- marqetive/platforms/tiktok/__init__.py +2 -1
- marqetive/platforms/tiktok/media.py +112 -51
- marqetive/platforms/tiktok/models.py +82 -0
- marqetive/platforms/twitter/__init__.py +2 -1
- marqetive/platforms/twitter/client.py +86 -0
- marqetive/platforms/twitter/media.py +133 -65
- marqetive/platforms/twitter/models.py +58 -0
- marqetive_lib-0.1.7.dist-info/METADATA +289 -0
- marqetive_lib-0.1.7.dist-info/RECORD +39 -0
- marqetive_lib-0.1.5.dist-info/METADATA +0 -260
- marqetive_lib-0.1.5.dist-info/RECORD +0 -35
- {marqetive_lib-0.1.5.dist-info → marqetive_lib-0.1.7.dist-info}/WHEEL +0 -0
|
@@ -12,8 +12,9 @@ This module supports:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import inspect
|
|
15
16
|
import logging
|
|
16
|
-
from collections.abc import Callable
|
|
17
|
+
from collections.abc import Awaitable, Callable
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from enum import Enum
|
|
19
20
|
from typing import Any, Literal
|
|
@@ -24,10 +25,16 @@ from marqetive.core.exceptions import (
|
|
|
24
25
|
MediaUploadError,
|
|
25
26
|
ValidationError,
|
|
26
27
|
)
|
|
28
|
+
from marqetive.core.models import ProgressEvent, ProgressStatus
|
|
27
29
|
from marqetive.utils.file_handlers import download_file, read_file_bytes
|
|
28
30
|
from marqetive.utils.media import detect_mime_type, format_file_size
|
|
29
31
|
from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
|
|
30
32
|
|
|
33
|
+
# Type aliases for progress callbacks
|
|
34
|
+
type SyncProgressCallback = Callable[[ProgressEvent], None]
|
|
35
|
+
type AsyncProgressCallback = Callable[[ProgressEvent], Awaitable[None]]
|
|
36
|
+
type ProgressCallback = SyncProgressCallback | AsyncProgressCallback
|
|
37
|
+
|
|
31
38
|
logger = logging.getLogger(__name__)
|
|
32
39
|
|
|
33
40
|
# LinkedIn limits
|
|
@@ -51,7 +58,12 @@ class VideoProcessingState(str, Enum):
|
|
|
51
58
|
|
|
52
59
|
@dataclass
|
|
53
60
|
class UploadProgress:
|
|
54
|
-
"""Progress information for media upload.
|
|
61
|
+
"""Progress information for media upload.
|
|
62
|
+
|
|
63
|
+
.. deprecated:: 0.2.0
|
|
64
|
+
Use :class:`marqetive.core.models.ProgressEvent` instead.
|
|
65
|
+
This class will be removed in a future version.
|
|
66
|
+
"""
|
|
55
67
|
|
|
56
68
|
asset_id: str
|
|
57
69
|
bytes_uploaded: int
|
|
@@ -82,7 +94,7 @@ class MediaAsset:
|
|
|
82
94
|
|
|
83
95
|
|
|
84
96
|
class LinkedInMediaManager:
|
|
85
|
-
"""Manager for LinkedIn media uploads.
|
|
97
|
+
"""Manager for LinkedIn media uploads using the Community Management API.
|
|
86
98
|
|
|
87
99
|
Supports images, videos, and documents with progress tracking.
|
|
88
100
|
|
|
@@ -92,39 +104,87 @@ class LinkedInMediaManager:
|
|
|
92
104
|
>>> print(f"Uploaded: {asset.asset_id}")
|
|
93
105
|
"""
|
|
94
106
|
|
|
107
|
+
# Default API version in YYYYMM format
|
|
108
|
+
DEFAULT_LINKEDIN_VERSION = "202511"
|
|
109
|
+
|
|
95
110
|
def __init__(
|
|
96
111
|
self,
|
|
97
112
|
person_urn: str,
|
|
98
113
|
access_token: str,
|
|
99
114
|
*,
|
|
100
|
-
|
|
115
|
+
linkedin_version: str | None = None,
|
|
101
116
|
timeout: float = 60.0,
|
|
102
|
-
progress_callback:
|
|
117
|
+
progress_callback: ProgressCallback | None = None,
|
|
103
118
|
) -> None:
|
|
104
119
|
"""Initialize LinkedIn media manager.
|
|
105
120
|
|
|
106
121
|
Args:
|
|
107
|
-
person_urn: LinkedIn person
|
|
122
|
+
person_urn: LinkedIn person or organization URN
|
|
123
|
+
(e.g., "urn:li:person:ABC123" or "urn:li:organization:12345").
|
|
108
124
|
access_token: LinkedIn OAuth access token.
|
|
109
|
-
|
|
125
|
+
linkedin_version: LinkedIn API version in YYYYMM format (e.g., "202511").
|
|
126
|
+
Defaults to the latest supported version.
|
|
110
127
|
timeout: Request timeout in seconds.
|
|
111
128
|
progress_callback: Optional callback for progress updates.
|
|
129
|
+
Accepts ProgressEvent and can be sync or async.
|
|
112
130
|
"""
|
|
113
131
|
self.person_urn = person_urn
|
|
114
132
|
self.access_token = access_token
|
|
115
|
-
self.
|
|
133
|
+
self.linkedin_version = linkedin_version or self.DEFAULT_LINKEDIN_VERSION
|
|
116
134
|
self.timeout = timeout
|
|
117
135
|
self.progress_callback = progress_callback
|
|
118
136
|
|
|
119
|
-
|
|
137
|
+
# Use v2 API for media uploads (still uses assets endpoint)
|
|
138
|
+
# Note: Media registration still uses v2 API, not REST API
|
|
139
|
+
self.base_url = "https://api.linkedin.com/v2"
|
|
120
140
|
self.client = httpx.AsyncClient(
|
|
121
141
|
timeout=httpx.Timeout(timeout),
|
|
122
142
|
headers={
|
|
123
143
|
"Authorization": f"Bearer {access_token}",
|
|
124
144
|
"X-Restli-Protocol-Version": "2.0.0",
|
|
145
|
+
"Linkedin-Version": self.linkedin_version,
|
|
146
|
+
"Content-Type": "application/json",
|
|
125
147
|
},
|
|
126
148
|
)
|
|
127
149
|
|
|
150
|
+
async def _emit_progress(
|
|
151
|
+
self,
|
|
152
|
+
status: ProgressStatus,
|
|
153
|
+
progress: int,
|
|
154
|
+
total: int,
|
|
155
|
+
message: str | None = None,
|
|
156
|
+
*,
|
|
157
|
+
entity_id: str | None = None,
|
|
158
|
+
file_path: str | None = None,
|
|
159
|
+
bytes_uploaded: int | None = None,
|
|
160
|
+
total_bytes: int | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Emit a progress update if a callback is registered.
|
|
163
|
+
|
|
164
|
+
Supports both sync and async callbacks.
|
|
165
|
+
"""
|
|
166
|
+
if self.progress_callback is None:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
event = ProgressEvent(
|
|
170
|
+
operation="upload_media",
|
|
171
|
+
platform="linkedin",
|
|
172
|
+
status=status,
|
|
173
|
+
progress=progress,
|
|
174
|
+
total=total,
|
|
175
|
+
message=message,
|
|
176
|
+
entity_id=entity_id,
|
|
177
|
+
file_path=file_path,
|
|
178
|
+
bytes_uploaded=bytes_uploaded,
|
|
179
|
+
total_bytes=total_bytes,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
result = self.progress_callback(event)
|
|
183
|
+
|
|
184
|
+
# If callback returned a coroutine, await it
|
|
185
|
+
if inspect.iscoroutine(result):
|
|
186
|
+
await result
|
|
187
|
+
|
|
128
188
|
async def __aenter__(self) -> "LinkedInMediaManager":
|
|
129
189
|
"""Enter async context."""
|
|
130
190
|
return self
|
|
@@ -195,10 +255,15 @@ class LinkedInMediaManager:
|
|
|
195
255
|
asset_id = await self._register_upload(register_data)
|
|
196
256
|
|
|
197
257
|
# Notify start
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
258
|
+
await self._emit_progress(
|
|
259
|
+
status=ProgressStatus.INITIALIZING,
|
|
260
|
+
progress=0,
|
|
261
|
+
total=100,
|
|
262
|
+
message="Registering upload",
|
|
263
|
+
entity_id=asset_id,
|
|
264
|
+
bytes_uploaded=0,
|
|
265
|
+
total_bytes=file_size,
|
|
266
|
+
)
|
|
202
267
|
|
|
203
268
|
# Get upload URL
|
|
204
269
|
upload_url = await self._get_upload_url(asset_id)
|
|
@@ -207,10 +272,15 @@ class LinkedInMediaManager:
|
|
|
207
272
|
await self._upload_to_url(upload_url, file_bytes, file_size, asset_id)
|
|
208
273
|
|
|
209
274
|
# Notify completion
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
275
|
+
await self._emit_progress(
|
|
276
|
+
status=ProgressStatus.COMPLETED,
|
|
277
|
+
progress=100,
|
|
278
|
+
total=100,
|
|
279
|
+
message="Upload completed",
|
|
280
|
+
entity_id=asset_id,
|
|
281
|
+
bytes_uploaded=file_size,
|
|
282
|
+
total_bytes=file_size,
|
|
283
|
+
)
|
|
214
284
|
|
|
215
285
|
logger.info(f"Image uploaded successfully: {asset_id}")
|
|
216
286
|
return MediaAsset(asset_id=asset_id, status="READY")
|
|
@@ -282,10 +352,15 @@ class LinkedInMediaManager:
|
|
|
282
352
|
asset_id = await self._register_upload(register_data)
|
|
283
353
|
|
|
284
354
|
# Notify start
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
355
|
+
await self._emit_progress(
|
|
356
|
+
status=ProgressStatus.INITIALIZING,
|
|
357
|
+
progress=0,
|
|
358
|
+
total=100,
|
|
359
|
+
message="Registering video upload",
|
|
360
|
+
entity_id=asset_id,
|
|
361
|
+
bytes_uploaded=0,
|
|
362
|
+
total_bytes=file_size,
|
|
363
|
+
)
|
|
289
364
|
|
|
290
365
|
# Get upload URL
|
|
291
366
|
upload_url = await self._get_upload_url(asset_id)
|
|
@@ -298,11 +373,22 @@ class LinkedInMediaManager:
|
|
|
298
373
|
await self._wait_for_video_processing(asset_id)
|
|
299
374
|
|
|
300
375
|
# Notify completion
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
376
|
+
final_status = (
|
|
377
|
+
ProgressStatus.COMPLETED
|
|
378
|
+
if wait_for_processing
|
|
379
|
+
else ProgressStatus.PROCESSING
|
|
380
|
+
)
|
|
381
|
+
await self._emit_progress(
|
|
382
|
+
status=final_status,
|
|
383
|
+
progress=100,
|
|
384
|
+
total=100,
|
|
385
|
+
message=(
|
|
386
|
+
"Video upload completed" if wait_for_processing else "Video processing"
|
|
387
|
+
),
|
|
388
|
+
entity_id=asset_id,
|
|
389
|
+
bytes_uploaded=file_size,
|
|
390
|
+
total_bytes=file_size,
|
|
391
|
+
)
|
|
306
392
|
|
|
307
393
|
logger.info(f"Video uploaded successfully: {asset_id}")
|
|
308
394
|
return MediaAsset(
|
|
@@ -373,10 +459,15 @@ class LinkedInMediaManager:
|
|
|
373
459
|
asset_id = await self._register_upload(register_data)
|
|
374
460
|
|
|
375
461
|
# Notify start
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
462
|
+
await self._emit_progress(
|
|
463
|
+
status=ProgressStatus.INITIALIZING,
|
|
464
|
+
progress=0,
|
|
465
|
+
total=100,
|
|
466
|
+
message="Registering document upload",
|
|
467
|
+
entity_id=asset_id,
|
|
468
|
+
bytes_uploaded=0,
|
|
469
|
+
total_bytes=file_size,
|
|
470
|
+
)
|
|
380
471
|
|
|
381
472
|
# Get upload URL
|
|
382
473
|
upload_url = await self._get_upload_url(asset_id)
|
|
@@ -385,10 +476,15 @@ class LinkedInMediaManager:
|
|
|
385
476
|
await self._upload_to_url(upload_url, file_bytes, file_size, asset_id)
|
|
386
477
|
|
|
387
478
|
# Notify completion
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
479
|
+
await self._emit_progress(
|
|
480
|
+
status=ProgressStatus.COMPLETED,
|
|
481
|
+
progress=100,
|
|
482
|
+
total=100,
|
|
483
|
+
message="Document upload completed",
|
|
484
|
+
entity_id=asset_id,
|
|
485
|
+
bytes_uploaded=file_size,
|
|
486
|
+
total_bytes=file_size,
|
|
487
|
+
)
|
|
392
488
|
|
|
393
489
|
logger.info(f"Document uploaded successfully: {asset_id}")
|
|
394
490
|
return MediaAsset(asset_id=asset_id, status="READY")
|
|
@@ -477,10 +573,15 @@ class LinkedInMediaManager:
|
|
|
477
573
|
}
|
|
478
574
|
|
|
479
575
|
# Notify upload start
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
576
|
+
await self._emit_progress(
|
|
577
|
+
status=ProgressStatus.UPLOADING,
|
|
578
|
+
progress=0,
|
|
579
|
+
total=100,
|
|
580
|
+
message="Uploading file",
|
|
581
|
+
entity_id=asset_id,
|
|
582
|
+
bytes_uploaded=0,
|
|
583
|
+
total_bytes=file_size,
|
|
584
|
+
)
|
|
484
585
|
|
|
485
586
|
response = await self.client.put(
|
|
486
587
|
upload_url,
|
|
@@ -490,10 +591,15 @@ class LinkedInMediaManager:
|
|
|
490
591
|
response.raise_for_status()
|
|
491
592
|
|
|
492
593
|
# Notify upload complete
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
594
|
+
await self._emit_progress(
|
|
595
|
+
status=ProgressStatus.UPLOADING,
|
|
596
|
+
progress=100,
|
|
597
|
+
total=100,
|
|
598
|
+
message="File uploaded",
|
|
599
|
+
entity_id=asset_id,
|
|
600
|
+
bytes_uploaded=file_size,
|
|
601
|
+
total_bytes=file_size,
|
|
602
|
+
)
|
|
497
603
|
|
|
498
604
|
try:
|
|
499
605
|
await _upload()
|
|
@@ -533,11 +639,14 @@ class LinkedInMediaManager:
|
|
|
533
639
|
)
|
|
534
640
|
|
|
535
641
|
# Notify progress
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
642
|
+
progress_pct = min(int((elapsed / timeout) * 90), 90)
|
|
643
|
+
await self._emit_progress(
|
|
644
|
+
status=ProgressStatus.PROCESSING,
|
|
645
|
+
progress=progress_pct,
|
|
646
|
+
total=100,
|
|
647
|
+
message=f"Processing video ({status})",
|
|
648
|
+
entity_id=asset_id,
|
|
649
|
+
)
|
|
541
650
|
|
|
542
651
|
await asyncio.sleep(check_interval)
|
|
543
652
|
elapsed += check_interval
|