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.
- marqetive/__init__.py +13 -7
- marqetive/core/__init__.py +6 -4
- marqetive/core/base.py +92 -13
- marqetive/core/client.py +15 -0
- marqetive/core/models.py +111 -7
- marqetive/platforms/instagram/__init__.py +2 -1
- marqetive/platforms/instagram/client.py +29 -8
- 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/client.py +5 -4
- marqetive/platforms/tiktok/media.py +193 -102
- marqetive/platforms/tiktok/models.py +79 -0
- marqetive/platforms/twitter/__init__.py +2 -1
- marqetive/platforms/twitter/client.py +86 -0
- marqetive/platforms/twitter/media.py +139 -70
- marqetive/platforms/twitter/models.py +58 -0
- marqetive/utils/media.py +86 -0
- marqetive/utils/oauth.py +31 -4
- {marqetive_lib-0.1.6.dist-info → marqetive_lib-0.1.8.dist-info}/METADATA +1 -9
- marqetive_lib-0.1.8.dist-info/RECORD +39 -0
- marqetive_lib-0.1.6.dist-info/RECORD +0 -35
- {marqetive_lib-0.1.6.dist-info → marqetive_lib-0.1.8.dist-info}/WHEEL +0 -0
|
@@ -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:
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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.
|
|
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,,
|
|
File without changes
|