marqetive-lib 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -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
- api_version: str = "v2",
115
+ linkedin_version: str | None = None,
101
116
  timeout: float = 60.0,
102
- progress_callback: Callable[[UploadProgress], None] | None = None,
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 URN (e.g., "urn:li:person:ABC123").
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
- api_version: LinkedIn API version.
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.api_version = api_version
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
- self.base_url = f"https://api.linkedin.com/{api_version}"
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
- if self.progress_callback:
199
- self.progress_callback(
200
- UploadProgress(asset_id, 0, file_size, "registering")
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
- if self.progress_callback:
211
- self.progress_callback(
212
- UploadProgress(asset_id, file_size, file_size, "completed")
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
- if self.progress_callback:
286
- self.progress_callback(
287
- UploadProgress(asset_id, 0, file_size, "registering")
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
- if self.progress_callback:
302
- status = "completed" if wait_for_processing else "processing"
303
- self.progress_callback(
304
- UploadProgress(asset_id, file_size, file_size, status)
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
- if self.progress_callback:
377
- self.progress_callback(
378
- UploadProgress(asset_id, 0, file_size, "registering")
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
- if self.progress_callback:
389
- self.progress_callback(
390
- UploadProgress(asset_id, file_size, file_size, "completed")
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
- if self.progress_callback:
481
- self.progress_callback(
482
- UploadProgress(asset_id, 0, file_size, "uploading")
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
- if self.progress_callback:
494
- self.progress_callback(
495
- UploadProgress(asset_id, file_size, file_size, "uploading")
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
- if self.progress_callback:
537
- progress_pct = min(int((elapsed / timeout) * 90), 90)
538
- self.progress_callback(
539
- UploadProgress(asset_id, progress_pct, 100, "processing")
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