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.
@@ -9,19 +9,22 @@ This module provides comprehensive media upload functionality for Twitter API v2
9
9
  """
10
10
 
11
11
  import asyncio
12
+ import inspect
12
13
  import logging
13
14
  import os
14
- from collections.abc import Callable
15
+ from collections.abc import Awaitable, Callable
15
16
  from dataclasses import dataclass
16
17
  from enum import Enum
17
18
  from typing import Any, Literal
18
19
 
20
+ import aiofiles
19
21
  import httpx
20
22
 
21
23
  from marqetive.core.exceptions import (
22
24
  InvalidFileTypeError,
23
25
  MediaUploadError,
24
26
  )
27
+ from marqetive.core.models import ProgressEvent, ProgressStatus
25
28
  from marqetive.utils.file_handlers import download_file
26
29
  from marqetive.utils.media import (
27
30
  detect_mime_type,
@@ -30,6 +33,14 @@ from marqetive.utils.media import (
30
33
  )
31
34
  from marqetive.utils.retry import STANDARD_BACKOFF, retry_async
32
35
 
36
+ # Type aliases for progress callbacks
37
+ type SyncProgressCallback = Callable[[ProgressEvent], None]
38
+ type AsyncProgressCallback = Callable[[ProgressEvent], Awaitable[None]]
39
+ type ProgressCallback = SyncProgressCallback | AsyncProgressCallback
40
+
41
+ # Legacy callback type for backward compatibility
42
+ type LegacyProgressCallback = Callable[["UploadProgress"], None]
43
+
33
44
  logger = logging.getLogger(__name__)
34
45
 
35
46
  # Constants
@@ -76,6 +87,10 @@ SUPPORTED_GIF_TYPE = "image/gif"
76
87
  class UploadProgress:
77
88
  """Progress information for media upload.
78
89
 
90
+ .. deprecated:: 0.2.0
91
+ Use :class:`marqetive.core.models.ProgressEvent` instead.
92
+ This class will be removed in a future version.
93
+
79
94
  Attributes:
80
95
  media_id: Twitter media ID (if available).
81
96
  file_path: Path to file being uploaded.
@@ -136,13 +151,21 @@ class TwitterMediaManager:
136
151
  >>> manager = TwitterMediaManager(bearer_token="your_token")
137
152
  >>> result = await manager.upload_media("/path/to/image.jpg")
138
153
  >>> print(f"Media ID: {result.media_id}")
154
+
155
+ >>> # With progress callback
156
+ >>> def on_progress(event: ProgressEvent) -> None:
157
+ ... print(f"{event.operation}: {event.percentage:.1f}%")
158
+ >>> manager = TwitterMediaManager(
159
+ ... bearer_token="your_token",
160
+ ... progress_callback=on_progress,
161
+ ... )
139
162
  """
140
163
 
141
164
  def __init__(
142
165
  self,
143
166
  bearer_token: str,
144
167
  *,
145
- progress_callback: Callable[[UploadProgress], None] | None = None,
168
+ progress_callback: ProgressCallback | None = None,
146
169
  timeout: float = DEFAULT_REQUEST_TIMEOUT,
147
170
  ) -> None:
148
171
  """Initialize Twitter media manager.
@@ -150,6 +173,7 @@ class TwitterMediaManager:
150
173
  Args:
151
174
  bearer_token: Twitter OAuth 2.0 bearer token.
152
175
  progress_callback: Optional callback for progress updates.
176
+ Accepts ProgressEvent and can be sync or async.
153
177
  timeout: Request timeout in seconds.
154
178
  """
155
179
  self.bearer_token = bearer_token
@@ -163,6 +187,44 @@ class TwitterMediaManager:
163
187
  headers={"Authorization": f"Bearer {bearer_token}"},
164
188
  )
165
189
 
190
+ async def _emit_progress(
191
+ self,
192
+ status: ProgressStatus,
193
+ progress: int,
194
+ total: int,
195
+ message: str | None = None,
196
+ *,
197
+ entity_id: str | None = None,
198
+ file_path: str | None = None,
199
+ bytes_uploaded: int | None = None,
200
+ total_bytes: int | None = None,
201
+ ) -> None:
202
+ """Emit a progress update if a callback is registered.
203
+
204
+ Supports both sync and async callbacks.
205
+ """
206
+ if self.progress_callback is None:
207
+ return
208
+
209
+ event = ProgressEvent(
210
+ operation="upload_media",
211
+ platform="twitter",
212
+ status=status,
213
+ progress=progress,
214
+ total=total,
215
+ message=message,
216
+ entity_id=entity_id,
217
+ file_path=file_path,
218
+ bytes_uploaded=bytes_uploaded,
219
+ total_bytes=total_bytes,
220
+ )
221
+
222
+ result = self.progress_callback(event)
223
+
224
+ # If callback returned a coroutine, await it
225
+ if inspect.iscoroutine(result):
226
+ await result
227
+
166
228
  async def __aenter__(self) -> "TwitterMediaManager":
167
229
  """Enter async context."""
168
230
  return self
@@ -264,21 +326,21 @@ class TwitterMediaManager:
264
326
  file_size = os.path.getsize(file_path)
265
327
 
266
328
  # Notify upload start
267
- if self.progress_callback:
268
- progress = UploadProgress(
269
- media_id=None,
270
- file_path=file_path,
271
- bytes_uploaded=0,
272
- total_bytes=file_size,
273
- status="init",
274
- )
275
- self.progress_callback(progress)
329
+ await self._emit_progress(
330
+ status=ProgressStatus.INITIALIZING,
331
+ progress=0,
332
+ total=100,
333
+ message="Initializing upload",
334
+ file_path=file_path,
335
+ bytes_uploaded=0,
336
+ total_bytes=file_size,
337
+ )
276
338
 
277
339
  @retry_async(config=STANDARD_BACKOFF)
278
340
  async def _do_upload() -> MediaUploadResult:
279
- # Read file
280
- with open(file_path, "rb") as f:
281
- file_data = f.read()
341
+ # Read file asynchronously
342
+ async with aiofiles.open(file_path, "rb") as f:
343
+ file_data = await f.read()
282
344
 
283
345
  # Prepare form data
284
346
  files = {"media": (os.path.basename(file_path), file_data)}
@@ -290,15 +352,15 @@ class TwitterMediaManager:
290
352
  data["additional_owners"] = ",".join(additional_owners)
291
353
 
292
354
  # Notify upload in progress
293
- if self.progress_callback:
294
- progress = UploadProgress(
295
- media_id=None,
296
- file_path=file_path,
297
- bytes_uploaded=0,
298
- total_bytes=file_size,
299
- status="uploading",
300
- )
301
- self.progress_callback(progress)
355
+ await self._emit_progress(
356
+ status=ProgressStatus.UPLOADING,
357
+ progress=0,
358
+ total=100,
359
+ message="Uploading file",
360
+ file_path=file_path,
361
+ bytes_uploaded=0,
362
+ total_bytes=file_size,
363
+ )
302
364
 
303
365
  # Upload
304
366
  response = await self.client.post(
@@ -319,15 +381,16 @@ class TwitterMediaManager:
319
381
  )
320
382
 
321
383
  # Notify completion
322
- if self.progress_callback:
323
- progress = UploadProgress(
324
- media_id=media_id,
325
- file_path=file_path,
326
- bytes_uploaded=file_size,
327
- total_bytes=file_size,
328
- status="completed",
329
- )
330
- self.progress_callback(progress)
384
+ await self._emit_progress(
385
+ status=ProgressStatus.COMPLETED,
386
+ progress=100,
387
+ total=100,
388
+ message="Upload completed",
389
+ entity_id=media_id,
390
+ file_path=file_path,
391
+ bytes_uploaded=file_size,
392
+ total_bytes=file_size,
393
+ )
331
394
 
332
395
  logger.info(f"Simple upload completed: {media_id}")
333
396
  return result
@@ -388,23 +451,24 @@ class TwitterMediaManager:
388
451
  )
389
452
 
390
453
  # Notify upload start
391
- if self.progress_callback:
392
- progress = UploadProgress(
393
- media_id=media_id,
394
- file_path=file_path,
395
- bytes_uploaded=0,
396
- total_bytes=file_size,
397
- status="uploading",
398
- )
399
- self.progress_callback(progress)
454
+ await self._emit_progress(
455
+ status=ProgressStatus.UPLOADING,
456
+ progress=0,
457
+ total=100,
458
+ message="Starting chunked upload",
459
+ entity_id=media_id,
460
+ file_path=file_path,
461
+ bytes_uploaded=0,
462
+ total_bytes=file_size,
463
+ )
400
464
 
401
465
  # STEP 2: Upload chunks
402
466
  bytes_uploaded = 0
403
467
  segment_index = 0
404
468
 
405
- with open(file_path, "rb") as f:
469
+ async with aiofiles.open(file_path, "rb") as f:
406
470
  while True:
407
- chunk_data = f.read(chunk_size)
471
+ chunk_data = await f.read(chunk_size)
408
472
  if not chunk_data:
409
473
  break
410
474
 
@@ -419,15 +483,17 @@ class TwitterMediaManager:
419
483
  segment_index += 1
420
484
 
421
485
  # Notify progress
422
- if self.progress_callback:
423
- progress = UploadProgress(
424
- media_id=media_id,
425
- file_path=file_path,
426
- bytes_uploaded=bytes_uploaded,
427
- total_bytes=file_size,
428
- status="uploading",
429
- )
430
- self.progress_callback(progress)
486
+ progress_pct = int((bytes_uploaded / file_size) * 100)
487
+ await self._emit_progress(
488
+ status=ProgressStatus.UPLOADING,
489
+ progress=progress_pct,
490
+ total=100,
491
+ message=f"Uploading chunk {segment_index}",
492
+ entity_id=media_id,
493
+ file_path=file_path,
494
+ bytes_uploaded=bytes_uploaded,
495
+ total_bytes=file_size,
496
+ )
431
497
 
432
498
  logger.debug(
433
499
  f"Uploaded chunk {segment_index}: "
@@ -443,15 +509,16 @@ class TwitterMediaManager:
443
509
  await self._wait_for_processing(result, file_path)
444
510
 
445
511
  # Notify completion
446
- if self.progress_callback:
447
- progress = UploadProgress(
448
- media_id=media_id,
449
- file_path=file_path,
450
- bytes_uploaded=file_size,
451
- total_bytes=file_size,
452
- status="completed",
453
- )
454
- self.progress_callback(progress)
512
+ await self._emit_progress(
513
+ status=ProgressStatus.COMPLETED,
514
+ progress=100,
515
+ total=100,
516
+ message="Chunked upload completed",
517
+ entity_id=media_id,
518
+ file_path=file_path,
519
+ bytes_uploaded=file_size,
520
+ total_bytes=file_size,
521
+ )
455
522
 
456
523
  logger.info(f"Chunked upload completed: {media_id}")
457
524
  return result
@@ -662,15 +729,17 @@ class TwitterMediaManager:
662
729
  )
663
730
 
664
731
  # Notify processing status
665
- if self.progress_callback:
666
- progress = UploadProgress(
667
- media_id=result.media_id,
668
- file_path=file_path,
669
- bytes_uploaded=os.path.getsize(file_path),
670
- total_bytes=os.path.getsize(file_path),
671
- status="processing",
672
- )
673
- self.progress_callback(progress)
732
+ file_size = os.path.getsize(file_path)
733
+ await self._emit_progress(
734
+ status=ProgressStatus.PROCESSING,
735
+ progress=90, # Show 90% during processing
736
+ total=100,
737
+ message=f"Processing media ({state})",
738
+ entity_id=result.media_id,
739
+ file_path=file_path,
740
+ bytes_uploaded=file_size,
741
+ total_bytes=file_size,
742
+ )
674
743
 
675
744
  await asyncio.sleep(check_after)
676
745
 
@@ -0,0 +1,58 @@
1
+ """Twitter/X-specific models for post creation.
2
+
3
+ This module defines Twitter-specific data models for creating tweets,
4
+ replies, quote tweets, and polls.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class TwitterPostRequest(BaseModel):
11
+ """Twitter/X-specific post creation request.
12
+
13
+ Supports tweets, replies, quote tweets, and media attachments.
14
+ Twitter has a 280 character limit for text content.
15
+
16
+ Attributes:
17
+ content: Tweet text (max 280 characters)
18
+ media_urls: List of media URLs to attach (max 4 images or 1 video)
19
+ media_ids: List of pre-uploaded media IDs
20
+ reply_to_post_id: Tweet ID to reply to
21
+ quote_post_id: Tweet ID to quote
22
+ poll_options: List of poll options (2-4 options, each max 25 chars)
23
+ poll_duration_minutes: Poll duration in minutes (5-10080)
24
+ alt_texts: Alt text for each media item (for accessibility)
25
+
26
+ Example:
27
+ >>> # Simple tweet
28
+ >>> request = TwitterPostRequest(content="Hello Twitter!")
29
+
30
+ >>> # Reply to a tweet
31
+ >>> request = TwitterPostRequest(
32
+ ... content="Great point!",
33
+ ... reply_to_post_id="1234567890"
34
+ ... )
35
+
36
+ >>> # Quote tweet with media
37
+ >>> request = TwitterPostRequest(
38
+ ... content="Check this out!",
39
+ ... quote_post_id="1234567890",
40
+ ... media_urls=["https://example.com/image.jpg"]
41
+ ... )
42
+
43
+ >>> # Tweet with poll
44
+ >>> request = TwitterPostRequest(
45
+ ... content="What's your favorite?",
46
+ ... poll_options=["Option A", "Option B", "Option C"],
47
+ ... poll_duration_minutes=1440
48
+ ... )
49
+ """
50
+
51
+ content: str | None = None
52
+ media_urls: list[str] = Field(default_factory=list, max_length=4)
53
+ media_ids: list[str] = Field(default_factory=list, max_length=4)
54
+ reply_to_post_id: str | None = None
55
+ quote_post_id: str | None = None
56
+ poll_options: list[str] = Field(default_factory=list, max_length=4)
57
+ poll_duration_minutes: int | None = Field(default=None, ge=5, le=10080)
58
+ alt_texts: list[str] = Field(default_factory=list)
marqetive/utils/media.py CHANGED
@@ -5,14 +5,19 @@ This module provides utilities for working with media files including:
5
5
  - File validation (size, type, format)
6
6
  - File chunking for large uploads
7
7
  - File hashing for integrity verification
8
+ - URL validation for media URLs
8
9
  """
9
10
 
10
11
  import hashlib
12
+ import ipaddress
11
13
  import mimetypes
12
14
  import os
13
15
  from collections.abc import AsyncGenerator
14
16
  from pathlib import Path
15
17
  from typing import Literal
18
+ from urllib.parse import urlparse
19
+
20
+ from marqetive.core.exceptions import ValidationError
16
21
 
17
22
  # Initialize mimetypes database
18
23
  mimetypes.init()
@@ -397,3 +402,84 @@ def get_chunk_count(file_path: str, chunk_size: int) -> int:
397
402
 
398
403
  file_size = os.path.getsize(file_path)
399
404
  return (file_size + chunk_size - 1) // chunk_size # Ceiling division
405
+
406
+
407
+ def validate_media_url(
408
+ url: str,
409
+ *,
410
+ allowed_schemes: list[str] | None = None,
411
+ block_private_ips: bool = True,
412
+ platform: str = "unknown",
413
+ ) -> str:
414
+ """Validate a media URL for security.
415
+
416
+ Validates that the URL uses an allowed scheme (default: http/https) and
417
+ optionally blocks private/internal IP addresses to prevent SSRF attacks.
418
+
419
+ Args:
420
+ url: The URL to validate.
421
+ allowed_schemes: List of allowed URL schemes (default: ['http', 'https']).
422
+ block_private_ips: If True, block private/loopback IP addresses.
423
+ platform: Platform name for error messages.
424
+
425
+ Returns:
426
+ The validated URL (unchanged if valid).
427
+
428
+ Raises:
429
+ ValidationError: If the URL is invalid or uses a disallowed scheme/IP.
430
+
431
+ Example:
432
+ >>> url = validate_media_url("https://example.com/image.jpg")
433
+ >>> url = validate_media_url("https://cdn.example.com/video.mp4", platform="instagram")
434
+ """
435
+ if allowed_schemes is None:
436
+ allowed_schemes = ["http", "https"]
437
+
438
+ try:
439
+ parsed = urlparse(url)
440
+ except Exception as e:
441
+ raise ValidationError(
442
+ f"Invalid URL format: {e}",
443
+ platform=platform,
444
+ field="media_url",
445
+ ) from e
446
+
447
+ # Validate scheme
448
+ if not parsed.scheme:
449
+ raise ValidationError(
450
+ "URL must include a scheme (e.g., https://)",
451
+ platform=platform,
452
+ field="media_url",
453
+ )
454
+
455
+ if parsed.scheme.lower() not in allowed_schemes:
456
+ raise ValidationError(
457
+ f"URL scheme '{parsed.scheme}' not allowed. "
458
+ f"Allowed schemes: {', '.join(allowed_schemes)}",
459
+ platform=platform,
460
+ field="media_url",
461
+ )
462
+
463
+ # Validate hostname exists
464
+ if not parsed.hostname:
465
+ raise ValidationError(
466
+ "URL must include a hostname",
467
+ platform=platform,
468
+ field="media_url",
469
+ )
470
+
471
+ # Block private/internal IPs if requested
472
+ if block_private_ips:
473
+ try:
474
+ ip = ipaddress.ip_address(parsed.hostname)
475
+ if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local:
476
+ raise ValidationError(
477
+ "Private, loopback, and reserved IP addresses are not allowed",
478
+ platform=platform,
479
+ field="media_url",
480
+ )
481
+ except ValueError:
482
+ # Not an IP address, it's a hostname - that's fine
483
+ pass
484
+
485
+ return url
marqetive/utils/oauth.py CHANGED
@@ -5,6 +5,7 @@ different social media platforms.
5
5
  """
6
6
 
7
7
  import logging
8
+ import re
8
9
  from datetime import datetime, timedelta
9
10
  from typing import Any
10
11
 
@@ -15,6 +16,32 @@ from marqetive.core.models import AuthCredentials
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
19
+ # Patterns for sensitive data that should be redacted from logs
20
+ _SENSITIVE_PATTERNS = [
21
+ re.compile(r'"access_token"\s*:\s*"[^"]*"', re.IGNORECASE),
22
+ re.compile(r'"refresh_token"\s*:\s*"[^"]*"', re.IGNORECASE),
23
+ re.compile(r'"client_secret"\s*:\s*"[^"]*"', re.IGNORECASE),
24
+ re.compile(r'"api_key"\s*:\s*"[^"]*"', re.IGNORECASE),
25
+ re.compile(r'"token"\s*:\s*"[^"]*"', re.IGNORECASE),
26
+ re.compile(r"access_token=[^&\s]+", re.IGNORECASE),
27
+ re.compile(r"refresh_token=[^&\s]+", re.IGNORECASE),
28
+ ]
29
+
30
+
31
+ def _sanitize_response_text(text: str) -> str:
32
+ """Sanitize response text to remove sensitive credentials.
33
+
34
+ Args:
35
+ text: Raw response text that may contain credentials.
36
+
37
+ Returns:
38
+ Sanitized text with sensitive values redacted.
39
+ """
40
+ result = text
41
+ for pattern in _SENSITIVE_PATTERNS:
42
+ result = pattern.sub("[REDACTED]", result)
43
+ return result
44
+
18
45
 
19
46
  async def refresh_oauth2_token(
20
47
  refresh_token: str,
@@ -73,7 +100,7 @@ async def refresh_oauth2_token(
73
100
  except httpx.HTTPStatusError as e:
74
101
  logger.error(f"HTTP error refreshing token: {e.response.status_code}")
75
102
  raise PlatformAuthError(
76
- f"Failed to refresh token: {e.response.text}",
103
+ f"Failed to refresh token: {_sanitize_response_text(e.response.text)}",
77
104
  platform="oauth2",
78
105
  status_code=e.response.status_code,
79
106
  ) from e
@@ -252,7 +279,7 @@ async def refresh_instagram_token(
252
279
  except httpx.HTTPStatusError as e:
253
280
  logger.error(f"HTTP error refreshing Instagram token: {e.response.status_code}")
254
281
  raise PlatformAuthError(
255
- f"Failed to refresh Instagram token: {e.response.text}",
282
+ f"Failed to refresh Instagram token: {_sanitize_response_text(e.response.text)}",
256
283
  platform="instagram",
257
284
  status_code=e.response.status_code,
258
285
  ) from e
@@ -312,7 +339,7 @@ async def refresh_tiktok_token(
312
339
  except httpx.HTTPStatusError as e:
313
340
  logger.error(f"HTTP error refreshing tiktok token: {e.response.status_code}")
314
341
  raise PlatformAuthError(
315
- f"Failed to refresh token: {e.response.text}",
342
+ f"Failed to refresh token: {_sanitize_response_text(e.response.text)}",
316
343
  platform="tiktok",
317
344
  status_code=e.response.status_code,
318
345
  ) from e
@@ -387,7 +414,7 @@ async def fetch_tiktok_token(
387
414
  except httpx.HTTPStatusError as e:
388
415
  logger.error(f"HTTP error fetching tiktok token: {e.response.status_code}")
389
416
  raise PlatformAuthError(
390
- f"Failed to fetch token: {e.response.text}",
417
+ f"Failed to fetch token: {_sanitize_response_text(e.response.text)}",
391
418
  platform="tiktok",
392
419
  status_code=e.response.status_code,
393
420
  ) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.6
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
@@ -0,0 +1,39 @@
1
+ marqetive/__init__.py,sha256=pW77CUnzOQ0X1pb-GTcRgrrvsSaJBdVhGZLnvCD_4q4,3032
2
+ marqetive/core/__init__.py,sha256=0_0vzxJ619YIJkz1yzSvhnGDJRkrErs_QSg2q3Bloss,1172
3
+ marqetive/core/base.py,sha256=J5iYXpa2C371zVLNMx3eFnC0fdLWqTrjbVcqJQdGyrU,16147
4
+ marqetive/core/client.py,sha256=eCtvL100dkxYQHC_TJzHbs3dGjgsa_me9VTHo-CUN2M,3900
5
+ marqetive/core/exceptions.py,sha256=Xyj0bzNiZm5VTErmzXgVW8T6IQnOpF92-HJiKPKjIio,7076
6
+ marqetive/core/models.py,sha256=L2gA4FhW0feAXQFsz2ce1ttd0vScMRhatoTclhDGCU0,14727
7
+ marqetive/factory.py,sha256=irZ5oN8a__kXZH70UN2uI7TzqTXu66d4QZ1FoxSoiK8,14092
8
+ marqetive/platforms/__init__.py,sha256=RBxlQSGyELsulSnwf5uaE1ohxFc7jC61OO9CrKaZp48,1312
9
+ marqetive/platforms/instagram/__init__.py,sha256=c1Gs0ozG6D7Z-Uz_UQ7S3joL0qUTT9eUZPWcePyESk8,229
10
+ marqetive/platforms/instagram/client.py,sha256=vOx5HpgrxanBIFFC9VgmCNguH-njRGChnyp6Rr1r1Xc,26191
11
+ marqetive/platforms/instagram/exceptions.py,sha256=TcD_pX4eSx_k4yW8DgfA6SGPiAz3VW7cMqM8DmiXIhg,8978
12
+ marqetive/platforms/instagram/media.py,sha256=0ZbUbpwJ025_hccL9X8qced_-LJGoL_-NdS84Op97VE,23228
13
+ marqetive/platforms/instagram/models.py,sha256=20v3m1037y3b_WlsKF8zAOgV23nFu63tfmmUN1CefOI,2769
14
+ marqetive/platforms/linkedin/__init__.py,sha256=_FrdZpqcXjcUW6C-25zGV7poGih9yzs6y1AFnuizTUQ,1384
15
+ marqetive/platforms/linkedin/client.py,sha256=_5sNiu0YDYLh8rLKA_dv0Ggrja8z3dLbk9_oLzFQ2yc,56287
16
+ marqetive/platforms/linkedin/exceptions.py,sha256=i5fARUkZik46bS3htZBwUInVzetsZx1APpKEXLrCKf0,9762
17
+ marqetive/platforms/linkedin/media.py,sha256=iWXUfqDYGsrTqeM6CGZ7a8xjpbdJ5qESolQL8Fv2PIg,20341
18
+ marqetive/platforms/linkedin/models.py,sha256=n7DqwVxYSbGYBmeEJ1woCZ6XhUIHcLx8Gpm8uCBACzI,12620
19
+ marqetive/platforms/tiktok/__init__.py,sha256=BqjkXTZDyBlcY3lvREy13yP9h3RcDga8E6Rl6f5KPp8,238
20
+ marqetive/platforms/tiktok/client.py,sha256=wCCCFQ4mGiZrrGYjRUCUngz6_eqf4G6BUxYxw8szpig,17178
21
+ marqetive/platforms/tiktok/exceptions.py,sha256=vxwyAKujMGZJh0LetG1QsLF95QfUs_kR6ujsWSHGqL0,10124
22
+ marqetive/platforms/tiktok/media.py,sha256=bPQmyVL8egb4teXQDzxQvWLwg2EnBh4Ik6lz20ReFvg,27008
23
+ marqetive/platforms/tiktok/models.py,sha256=WWdjuFqhTIR8SnHkz-8UaNc5Mm2PrGomwQ3W7pJcQFg,2962
24
+ marqetive/platforms/twitter/__init__.py,sha256=dvcgVT-v-JOtjSz-OUvxGrn_43OI6w_ep42Wx_nHTSM,217
25
+ marqetive/platforms/twitter/client.py,sha256=08jV2hQVmGOpnG3C05u7bCqL7KapWn7bSsG0wbN_t5M,23270
26
+ marqetive/platforms/twitter/exceptions.py,sha256=eZ-dJKOXH_-bAMg29zWKbEqMFud29piEJ5IWfC9wFts,8926
27
+ marqetive/platforms/twitter/media.py,sha256=9j7JQpdlOhkMfQkDH0dLpp6HmlYkeB6SvNosRx5Oab8,27152
28
+ marqetive/platforms/twitter/models.py,sha256=yPQlx40SlNmz7YGasXUqdx7rEDEgrQ64aYovlPKo6oc,2126
29
+ marqetive/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ marqetive/utils/__init__.py,sha256=bSrNajbxYBSKQayrPviLz8JeGjplnyK8y_NGDtgb7yQ,977
31
+ marqetive/utils/file_handlers.py,sha256=4TP5kmWofNTSZmlS683CM1UYP83WvRd_NubMbqtXv-g,12568
32
+ marqetive/utils/helpers.py,sha256=8-ljhL47SremKcQO2GF8DIHOPODEv1rSioVNuSPCbec,2634
33
+ marqetive/utils/media.py,sha256=O1rISYdaP3CuuPxso7kqvxWXNfe2jjioNkaBc4cpwkY,14668
34
+ marqetive/utils/oauth.py,sha256=1SkYCE6dcyPvcDqbjRFSSBcKTwLJy8u3jAANPdftVmo,13108
35
+ marqetive/utils/retry.py,sha256=lAniJLMNWp9XsHrvU0XBNifpNEjfde4MGfd5hlFTPfA,7636
36
+ marqetive/utils/token_validator.py,sha256=dNvDeHs2Du5UyMMH2ZOW6ydR7OwOEKA4c9e-rG0f9-0,6698
37
+ marqetive_lib-0.1.8.dist-info/METADATA,sha256=q99TsfInwfKVr_zoFnkvM3Wvb5cGZ7Nj6qQikle5EG4,7875
38
+ marqetive_lib-0.1.8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
39
+ marqetive_lib-0.1.8.dist-info/RECORD,,
@@ -1,35 +0,0 @@
1
- marqetive/__init__.py,sha256=TPjbyZ8ZRux2b6GqSG_7w9A7auPaTkxlamcfYIz0XVw,2907
2
- marqetive/core/__init__.py,sha256=8n-Ys3qR4wcGUV2rZTz68ICknD1GrkYz82SxLo-dods,1129
3
- marqetive/core/base.py,sha256=CKNTA6nErZ_Fx8uPtWxgNY054kmSTlGXL81zaF2d0R0,13248
4
- marqetive/core/client.py,sha256=2_FoNpqaRglsWg10i5RTbyDg_kRQKhgWjYs6iDdFxLg,3210
5
- marqetive/core/exceptions.py,sha256=Xyj0bzNiZm5VTErmzXgVW8T6IQnOpF92-HJiKPKjIio,7076
6
- marqetive/core/models.py,sha256=W_yOuRWItWSn82n8vXRNN_ScdNkzY1De2qqXaVN2RGU,10974
7
- marqetive/factory.py,sha256=irZ5oN8a__kXZH70UN2uI7TzqTXu66d4QZ1FoxSoiK8,14092
8
- marqetive/platforms/__init__.py,sha256=RBxlQSGyELsulSnwf5uaE1ohxFc7jC61OO9CrKaZp48,1312
9
- marqetive/platforms/instagram/__init__.py,sha256=7ClfTovAcCHac2DzKS7z1MFuZpy9lcwet7YP5d6MPeY,135
10
- marqetive/platforms/instagram/client.py,sha256=C5T9v1Aina8F3_Dk3d_I_RiVt7VvxTwwqdYeizDr0iQ,25187
11
- marqetive/platforms/instagram/exceptions.py,sha256=TcD_pX4eSx_k4yW8DgfA6SGPiAz3VW7cMqM8DmiXIhg,8978
12
- marqetive/platforms/instagram/media.py,sha256=sZKbBpTac4hIR3OvVV3wL21uHOSQUUpBBKXrvC1zXPc,21161
13
- marqetive/platforms/linkedin/__init__.py,sha256=zCnokoPYs56iA1sBSYIlaZW2J50L3CbnQpJSaOLrzP8,131
14
- marqetive/platforms/linkedin/client.py,sha256=cU22ympiWxDPXSUIB0juAQUV3RCpBg-m2nb4D38N5Go,23806
15
- marqetive/platforms/linkedin/exceptions.py,sha256=i5fARUkZik46bS3htZBwUInVzetsZx1APpKEXLrCKf0,9762
16
- marqetive/platforms/linkedin/media.py,sha256=lYUKJWbI3mOsvdSOZfUytc4BWb_nIf4YjiKNpC7EpT0,16896
17
- marqetive/platforms/tiktok/__init__.py,sha256=BQtxdECd2bW9_vV9W-MY4A1rdXi_xurGWWmzTjTUpMM,123
18
- marqetive/platforms/tiktok/client.py,sha256=i_nyhVywh4feFu5vv4LrMQOkoLsxfxgpH6aKa9JjdOc,17060
19
- marqetive/platforms/tiktok/exceptions.py,sha256=vxwyAKujMGZJh0LetG1QsLF95QfUs_kR6ujsWSHGqL0,10124
20
- marqetive/platforms/tiktok/media.py,sha256=p42E0D3bp9x0RgdK7lqcBl2v5G70ZOpY4JTYS-oCgD4,23766
21
- marqetive/platforms/twitter/__init__.py,sha256=AA5BELRvZyl2WE_7-puSEWArxZjaXcTJ_i8NGOWrv6k,129
22
- marqetive/platforms/twitter/client.py,sha256=nVXFU1nZBimjzxq0uwCzi0hQy7tdkbW42bbV_A2w8jk,19627
23
- marqetive/platforms/twitter/exceptions.py,sha256=eZ-dJKOXH_-bAMg29zWKbEqMFud29piEJ5IWfC9wFts,8926
24
- marqetive/platforms/twitter/media.py,sha256=N8f9UZv1JPJoFDTrPOrvxqdShFU-DQPFBScBoCrZci4,24963
25
- marqetive/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- marqetive/utils/__init__.py,sha256=bSrNajbxYBSKQayrPviLz8JeGjplnyK8y_NGDtgb7yQ,977
27
- marqetive/utils/file_handlers.py,sha256=4TP5kmWofNTSZmlS683CM1UYP83WvRd_NubMbqtXv-g,12568
28
- marqetive/utils/helpers.py,sha256=8-ljhL47SremKcQO2GF8DIHOPODEv1rSioVNuSPCbec,2634
29
- marqetive/utils/media.py,sha256=Rvxw9XKU65n-z4G1bEihG3wXZBmjSDZUqClfjGFrg6k,12013
30
- marqetive/utils/oauth.py,sha256=LQLXpThZUe0XbSpO3dJ5oW3sPRJuKjSk3_f5_3baUzA,12095
31
- marqetive/utils/retry.py,sha256=lAniJLMNWp9XsHrvU0XBNifpNEjfde4MGfd5hlFTPfA,7636
32
- marqetive/utils/token_validator.py,sha256=dNvDeHs2Du5UyMMH2ZOW6ydR7OwOEKA4c9e-rG0f9-0,6698
33
- marqetive_lib-0.1.6.dist-info/METADATA,sha256=Xf_dBnNYwqOSf9GHJuormIVzyb1oRAJI_zQOsebTOxE,7986
34
- marqetive_lib-0.1.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
35
- marqetive_lib-0.1.6.dist-info/RECORD,,