marqetive-lib 0.1.0__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.
Files changed (43) hide show
  1. marqetive/__init__.py +113 -0
  2. marqetive/core/__init__.py +5 -0
  3. marqetive/core/account_factory.py +212 -0
  4. marqetive/core/base_manager.py +303 -0
  5. marqetive/core/client.py +108 -0
  6. marqetive/core/progress.py +291 -0
  7. marqetive/core/registry.py +257 -0
  8. marqetive/platforms/__init__.py +55 -0
  9. marqetive/platforms/base.py +390 -0
  10. marqetive/platforms/exceptions.py +238 -0
  11. marqetive/platforms/instagram/__init__.py +7 -0
  12. marqetive/platforms/instagram/client.py +786 -0
  13. marqetive/platforms/instagram/exceptions.py +311 -0
  14. marqetive/platforms/instagram/factory.py +106 -0
  15. marqetive/platforms/instagram/manager.py +112 -0
  16. marqetive/platforms/instagram/media.py +669 -0
  17. marqetive/platforms/linkedin/__init__.py +7 -0
  18. marqetive/platforms/linkedin/client.py +733 -0
  19. marqetive/platforms/linkedin/exceptions.py +335 -0
  20. marqetive/platforms/linkedin/factory.py +130 -0
  21. marqetive/platforms/linkedin/manager.py +119 -0
  22. marqetive/platforms/linkedin/media.py +549 -0
  23. marqetive/platforms/models.py +345 -0
  24. marqetive/platforms/tiktok/__init__.py +0 -0
  25. marqetive/platforms/twitter/__init__.py +7 -0
  26. marqetive/platforms/twitter/client.py +647 -0
  27. marqetive/platforms/twitter/exceptions.py +311 -0
  28. marqetive/platforms/twitter/factory.py +151 -0
  29. marqetive/platforms/twitter/manager.py +121 -0
  30. marqetive/platforms/twitter/media.py +779 -0
  31. marqetive/platforms/twitter/threads.py +442 -0
  32. marqetive/py.typed +0 -0
  33. marqetive/registry_init.py +66 -0
  34. marqetive/utils/__init__.py +45 -0
  35. marqetive/utils/file_handlers.py +438 -0
  36. marqetive/utils/helpers.py +99 -0
  37. marqetive/utils/media.py +399 -0
  38. marqetive/utils/oauth.py +265 -0
  39. marqetive/utils/retry.py +239 -0
  40. marqetive/utils/token_validator.py +240 -0
  41. marqetive_lib-0.1.0.dist-info/METADATA +261 -0
  42. marqetive_lib-0.1.0.dist-info/RECORD +43 -0
  43. marqetive_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,311 @@
1
+ """Twitter-specific exception handling and error code mapping.
2
+
3
+ This module provides comprehensive error handling for Twitter API errors including:
4
+ - HTTP status code mapping
5
+ - Twitter-specific error codes
6
+ - Retry strategies
7
+ - User-friendly error messages
8
+ """
9
+
10
+ from typing import Any
11
+
12
+ from marqetive.platforms.exceptions import (
13
+ MediaUploadError,
14
+ PlatformAuthError,
15
+ PlatformError,
16
+ RateLimitError,
17
+ ValidationError,
18
+ )
19
+
20
+
21
+ # Twitter API error codes
22
+ # Source: https://developer.x.com/en/support/twitter-api/error-troubleshooting
23
+ class TwitterErrorCode:
24
+ """Twitter API error codes."""
25
+
26
+ # Authentication errors (200-299)
27
+ COULD_NOT_AUTHENTICATE = 32
28
+ INVALID_OR_EXPIRED_TOKEN = 89
29
+ UNABLE_TO_VERIFY_CREDENTIALS = 99
30
+ BAD_AUTHENTICATION_DATA = 215
31
+
32
+ # Authorization errors (300-399)
33
+ FORBIDDEN = 64
34
+ ACCOUNT_SUSPENDED = 63
35
+ API_ACCESS_REVOKED = 87
36
+
37
+ # Resource errors (400-499)
38
+ PAGE_NOT_EXIST = 34
39
+ USER_NOT_FOUND = 17
40
+ TWEET_NOT_FOUND = 144
41
+ NO_STATUS_FOUND = 34
42
+
43
+ # Rate limiting (500-599)
44
+ RATE_LIMIT_EXCEEDED = 88
45
+ TOO_MANY_REQUESTS = 429
46
+
47
+ # Validation errors (600-699)
48
+ STATUS_TOO_LONG = 186
49
+ DUPLICATE_STATUS = 187
50
+ INVALID_MEDIA = 324
51
+ MEDIA_ID_NOT_FOUND = 325
52
+
53
+ # Media upload errors
54
+ MEDIA_TYPE_UNSUPPORTED = 323
55
+ MEDIA_SIZE_TOO_LARGE = 324
56
+ VIDEO_DURATION_TOO_LONG = 324
57
+ MEDIA_PROCESSING_FAILED = 324
58
+
59
+
60
+ # Mapping of error codes to user-friendly messages
61
+ ERROR_MESSAGES: dict[int, str] = {
62
+ # Authentication
63
+ 32: "Could not authenticate. Please check your API credentials.",
64
+ 89: "Invalid or expired access token. Please re-authenticate.",
65
+ 99: "Unable to verify your credentials. Please check your API keys.",
66
+ 215: "Bad authentication data. Please verify your OAuth credentials.",
67
+ # Authorization
68
+ 64: "Your account is not authorized to access this resource.",
69
+ 63: "Your account is suspended and cannot be accessed.",
70
+ 87: "API access has been revoked for this application.",
71
+ # Resources
72
+ 34: "The requested page or resource does not exist.",
73
+ 17: "User not found.",
74
+ 144: "Tweet not found.",
75
+ # Rate limiting
76
+ 88: "Rate limit exceeded. Please wait before making more requests.",
77
+ 429: "Too many requests. You have hit a rate limit.",
78
+ # Validation
79
+ 186: "Tweet is too long. Maximum length is 280 characters.",
80
+ 187: "Duplicate tweet. You've already posted this content.",
81
+ 324: "Invalid media for upload.",
82
+ 325: "Media ID not found.",
83
+ 323: "Media type not supported.",
84
+ }
85
+
86
+
87
+ # Retryable error codes
88
+ RETRYABLE_ERROR_CODES = {
89
+ 88, # Rate limit exceeded
90
+ 130, # Over capacity
91
+ 131, # Internal error
92
+ 429, # Too many requests
93
+ 500, # Internal server error
94
+ 502, # Bad gateway
95
+ 503, # Service unavailable
96
+ 504, # Gateway timeout
97
+ }
98
+
99
+ # Non-retryable error codes
100
+ NON_RETRYABLE_ERROR_CODES = {
101
+ 32, # Authentication failed
102
+ 89, # Invalid token
103
+ 99, # Unable to verify credentials
104
+ 64, # Forbidden
105
+ 63, # Account suspended
106
+ 186, # Status too long
107
+ 187, # Duplicate status
108
+ 144, # Tweet not found
109
+ }
110
+
111
+
112
+ def map_twitter_error(
113
+ status_code: int | None,
114
+ error_code: int | None = None,
115
+ error_message: str | None = None,
116
+ response_data: dict[str, Any] | None = None,
117
+ ) -> PlatformError:
118
+ """Map Twitter API error to appropriate exception.
119
+
120
+ Args:
121
+ status_code: HTTP status code.
122
+ error_code: Twitter-specific error code.
123
+ error_message: Error message from API.
124
+ response_data: Full response data from API.
125
+
126
+ Returns:
127
+ Appropriate PlatformError subclass.
128
+
129
+ Example:
130
+ >>> error = map_twitter_error(401, 32, "Could not authenticate")
131
+ >>> print(type(error).__name__)
132
+ PlatformAuthError
133
+ """
134
+ # Extract error details from response if provided
135
+ if response_data and "errors" in response_data:
136
+ errors = response_data["errors"]
137
+ if errors and isinstance(errors, list):
138
+ first_error = errors[0]
139
+ if not error_code:
140
+ error_code = first_error.get("code")
141
+ if not error_message:
142
+ error_message = first_error.get("message")
143
+
144
+ # Get user-friendly message
145
+ friendly_message = ERROR_MESSAGES.get(
146
+ error_code or 0, error_message or "Unknown error"
147
+ )
148
+
149
+ # Determine retry-after for rate limits
150
+ retry_after = None
151
+ if status_code == 429:
152
+ # Twitter typically uses 15-minute windows
153
+ retry_after = 900 # 15 minutes in seconds
154
+
155
+ # Map to appropriate exception type
156
+ # Authentication errors
157
+ if error_code in (32, 89, 99, 215) or status_code in (401, 403):
158
+ return PlatformAuthError(
159
+ friendly_message,
160
+ platform="twitter",
161
+ status_code=status_code,
162
+ )
163
+
164
+ # Rate limit errors
165
+ if error_code in (88, 429) or status_code == 429:
166
+ return RateLimitError(
167
+ friendly_message,
168
+ platform="twitter",
169
+ status_code=status_code or 429,
170
+ retry_after=retry_after,
171
+ )
172
+
173
+ # Validation errors
174
+ if error_code in (186, 187, 324, 325, 323):
175
+ return ValidationError(
176
+ friendly_message,
177
+ platform="twitter",
178
+ )
179
+
180
+ # Media upload errors
181
+ if error_code in (323, 324, 325):
182
+ return MediaUploadError(
183
+ friendly_message,
184
+ platform="twitter",
185
+ status_code=status_code,
186
+ )
187
+
188
+ # Generic platform error
189
+ return PlatformError(
190
+ friendly_message,
191
+ platform="twitter",
192
+ status_code=status_code,
193
+ )
194
+
195
+
196
+ def is_retryable_twitter_error(
197
+ status_code: int | None,
198
+ error_code: int | None = None,
199
+ ) -> bool:
200
+ """Determine if a Twitter error is retryable.
201
+
202
+ Args:
203
+ status_code: HTTP status code.
204
+ error_code: Twitter-specific error code.
205
+
206
+ Returns:
207
+ True if error is retryable, False otherwise.
208
+
209
+ Example:
210
+ >>> is_retryable_twitter_error(503)
211
+ True
212
+ >>> is_retryable_twitter_error(401, 32)
213
+ False
214
+ """
215
+ # Check explicit non-retryable codes first
216
+ if error_code in NON_RETRYABLE_ERROR_CODES:
217
+ return False
218
+
219
+ # Check retryable codes
220
+ if error_code in RETRYABLE_ERROR_CODES:
221
+ return True
222
+
223
+ # Check HTTP status codes
224
+ if status_code:
225
+ # 5xx errors are generally retryable
226
+ if 500 <= status_code < 600:
227
+ return True
228
+ # 429 is retryable
229
+ if status_code == 429:
230
+ return True
231
+ # 4xx errors (except 429) are generally not retryable
232
+ if 400 <= status_code < 500:
233
+ return False
234
+
235
+ # Default to not retryable for safety
236
+ return False
237
+
238
+
239
+ def get_retry_delay(
240
+ status_code: int | None,
241
+ error_code: int | None = None,
242
+ attempt: int = 1,
243
+ ) -> float:
244
+ """Get recommended retry delay for Twitter error.
245
+
246
+ Args:
247
+ status_code: HTTP status code.
248
+ error_code: Twitter-specific error code.
249
+ attempt: Current retry attempt number.
250
+
251
+ Returns:
252
+ Recommended delay in seconds.
253
+
254
+ Example:
255
+ >>> get_retry_delay(503, attempt=1)
256
+ 5.0
257
+ >>> get_retry_delay(429)
258
+ 900.0
259
+ """
260
+ # Rate limit errors - wait full window
261
+ if error_code in (88, 429) or status_code == 429:
262
+ return 900.0 # 15 minutes
263
+
264
+ # Server errors - exponential backoff
265
+ if status_code and 500 <= status_code < 600:
266
+ base_delay = 5.0
267
+ return min(base_delay * (2 ** (attempt - 1)), 60.0)
268
+
269
+ # Default delay
270
+ return 5.0
271
+
272
+
273
+ class TwitterAPIError(PlatformError):
274
+ """Twitter API specific error with detailed information.
275
+
276
+ Attributes:
277
+ error_code: Twitter-specific error code.
278
+ is_retryable: Whether the error is retryable.
279
+ retry_delay: Recommended retry delay in seconds.
280
+ """
281
+
282
+ def __init__(
283
+ self,
284
+ message: str,
285
+ *,
286
+ status_code: int | None = None,
287
+ error_code: int | None = None,
288
+ response_data: dict[str, Any] | None = None,
289
+ ) -> None:
290
+ """Initialize Twitter API error.
291
+
292
+ Args:
293
+ message: Error message.
294
+ status_code: HTTP status code.
295
+ error_code: Twitter-specific error code.
296
+ response_data: Full response data from API.
297
+ """
298
+ super().__init__(message, platform="twitter", status_code=status_code)
299
+ self.error_code = error_code
300
+ self.response_data = response_data
301
+ self.is_retryable = is_retryable_twitter_error(status_code, error_code)
302
+ self.retry_delay = get_retry_delay(status_code, error_code)
303
+
304
+ def __repr__(self) -> str:
305
+ """String representation of error."""
306
+ return (
307
+ f"TwitterAPIError(message={self.message!r}, "
308
+ f"status_code={self.status_code}, "
309
+ f"error_code={self.error_code}, "
310
+ f"retryable={self.is_retryable})"
311
+ )
@@ -0,0 +1,151 @@
1
+ """Twitter account factory for managing credentials and client creation."""
2
+
3
+ import logging
4
+ import os
5
+ from collections.abc import Callable
6
+
7
+ from marqetive.core.account_factory import BaseAccountFactory
8
+ from marqetive.platforms.exceptions import PlatformAuthError
9
+ from marqetive.platforms.models import AccountStatus, AuthCredentials
10
+ from marqetive.platforms.twitter.client import TwitterClient
11
+ from marqetive.utils.oauth import refresh_twitter_token
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TwitterAccountFactory(BaseAccountFactory):
17
+ """Factory for creating and managing Twitter/X accounts and clients.
18
+
19
+ Example:
20
+ >>> factory = TwitterAccountFactory(
21
+ ... client_id="your_client_id",
22
+ ... client_secret="your_client_secret"
23
+ ... )
24
+ >>> credentials = AuthCredentials(
25
+ ... platform="twitter",
26
+ ... access_token="token",
27
+ ... refresh_token="refresh"
28
+ ... )
29
+ >>> client = await factory.create_authenticated_client(credentials)
30
+ >>> async with client:
31
+ ... tweet = await client.create_post(request)
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ client_id: str | None = None,
37
+ client_secret: str | None = None,
38
+ on_status_update: Callable[[str, AccountStatus], None] | None = None,
39
+ ) -> None:
40
+ """Initialize Twitter account factory.
41
+
42
+ Args:
43
+ client_id: Twitter OAuth client ID (uses TWITTER_CLIENT_ID env if None).
44
+ client_secret: Twitter OAuth client secret (uses TWITTER_CLIENT_SECRET env if None).
45
+ on_status_update: Optional callback when account status changes.
46
+ """
47
+ super().__init__(on_status_update=on_status_update)
48
+ self.client_id = client_id or os.getenv("TWITTER_CLIENT_ID")
49
+ self.client_secret = client_secret or os.getenv("TWITTER_CLIENT_SECRET")
50
+
51
+ if not self.client_id or not self.client_secret:
52
+ logger.warning(
53
+ "Twitter client_id/client_secret not provided. "
54
+ "Token refresh will not work."
55
+ )
56
+
57
+ @property
58
+ def platform_name(self) -> str:
59
+ """Get platform name."""
60
+ return "twitter"
61
+
62
+ async def refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
63
+ """Refresh Twitter OAuth2 access token.
64
+
65
+ Args:
66
+ credentials: Current credentials with refresh token.
67
+
68
+ Returns:
69
+ Updated credentials with new access token.
70
+
71
+ Raises:
72
+ PlatformAuthError: If refresh fails or credentials missing.
73
+ """
74
+ if not self.client_id or not self.client_secret:
75
+ raise PlatformAuthError(
76
+ "Twitter client_id and client_secret are required for token refresh",
77
+ platform=self.platform_name,
78
+ )
79
+
80
+ if not credentials.refresh_token:
81
+ raise PlatformAuthError(
82
+ "No refresh token available",
83
+ platform=self.platform_name,
84
+ )
85
+
86
+ logger.info("Refreshing Twitter access token...")
87
+ return await refresh_twitter_token(
88
+ credentials,
89
+ self.client_id,
90
+ self.client_secret,
91
+ )
92
+
93
+ async def create_client(self, credentials: AuthCredentials) -> TwitterClient:
94
+ """Create Twitter API client.
95
+
96
+ Args:
97
+ credentials: Valid Twitter credentials.
98
+
99
+ Returns:
100
+ TwitterClient instance.
101
+
102
+ Raises:
103
+ PlatformAuthError: If credentials are invalid.
104
+ """
105
+ if not credentials.access_token:
106
+ raise PlatformAuthError(
107
+ "Access token is required",
108
+ platform=self.platform_name,
109
+ )
110
+
111
+ # Get additional data for Twitter
112
+ additional_data = credentials.additional_data or {}
113
+
114
+ # Twitter needs API key/secret for some operations
115
+ api_key = additional_data.get("api_key") or os.getenv("TWITTER_API_KEY")
116
+ api_secret = additional_data.get("api_secret") or os.getenv(
117
+ "TWITTER_API_SECRET"
118
+ )
119
+ additional_data.get("access_secret")
120
+
121
+ # Update additional_data if we have env values
122
+ if api_key:
123
+ additional_data["api_key"] = api_key
124
+ if api_secret:
125
+ additional_data["api_secret"] = api_secret
126
+
127
+ credentials.additional_data = additional_data
128
+
129
+ return TwitterClient(credentials=credentials)
130
+
131
+ async def validate_credentials(self, credentials: AuthCredentials) -> bool:
132
+ """Validate Twitter credentials by making a test API call.
133
+
134
+ Args:
135
+ credentials: Credentials to validate.
136
+
137
+ Returns:
138
+ True if credentials are valid, False otherwise.
139
+ """
140
+ try:
141
+ client = await self.create_client(credentials)
142
+ async with client:
143
+ # Try to verify credentials by getting current user
144
+ # This is a lightweight call to test authentication
145
+ if client._tweepy_client:
146
+ me = client._tweepy_client.get_me()
147
+ return me is not None
148
+ return False
149
+ except Exception as e:
150
+ logger.error(f"Error validating Twitter credentials: {e}")
151
+ return False
@@ -0,0 +1,121 @@
1
+ """Twitter post manager for handling post operations."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from marqetive.core.base_manager import BasePostManager
7
+ from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
8
+ from marqetive.platforms.twitter.client import TwitterClient
9
+ from marqetive.platforms.twitter.factory import TwitterAccountFactory
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TwitterPostManager(BasePostManager):
15
+ """Manager for Twitter/X post operations.
16
+
17
+ Coordinates post creation, media uploads, and progress tracking for Twitter.
18
+
19
+ Example:
20
+ >>> manager = TwitterPostManager()
21
+ >>> credentials = AuthCredentials(
22
+ ... platform="twitter",
23
+ ... access_token="token",
24
+ ... refresh_token="refresh"
25
+ ... )
26
+ >>> request = PostCreateRequest(content="Hello Twitter!")
27
+ >>> post = await manager.execute_post(credentials, request)
28
+ >>> print(f"Tweet ID: {post.post_id}")
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ account_factory: TwitterAccountFactory | None = None,
34
+ client_id: str | None = None,
35
+ client_secret: str | None = None,
36
+ ) -> None:
37
+ """Initialize Twitter post manager.
38
+
39
+ Args:
40
+ account_factory: Twitter account factory (creates default if None).
41
+ client_id: Twitter OAuth client ID (for default factory).
42
+ client_secret: Twitter OAuth client secret (for default factory).
43
+ """
44
+ if account_factory is None:
45
+ account_factory = TwitterAccountFactory(
46
+ client_id=client_id,
47
+ client_secret=client_secret,
48
+ )
49
+
50
+ super().__init__(account_factory=account_factory)
51
+
52
+ @property
53
+ def platform_name(self) -> str:
54
+ """Get platform name."""
55
+ return "twitter"
56
+
57
+ async def _execute_post_impl(
58
+ self,
59
+ client: Any,
60
+ request: PostCreateRequest,
61
+ credentials: AuthCredentials, # noqa: ARG002
62
+ ) -> Post:
63
+ """Execute Twitter post creation.
64
+
65
+ Args:
66
+ client: TwitterClient instance.
67
+ request: Post creation request.
68
+ credentials: Twitter credentials.
69
+
70
+ Returns:
71
+ Created Post object.
72
+ """
73
+ if not isinstance(client, TwitterClient):
74
+ raise TypeError(f"Expected TwitterClient, got {type(client)}")
75
+
76
+ # Handle media uploads with progress tracking
77
+ media_ids: list[str] = []
78
+ if request.media_urls:
79
+ self._progress_tracker.emit_start(
80
+ "upload_media",
81
+ total=len(request.media_urls),
82
+ message=f"Uploading {len(request.media_urls)} media files...",
83
+ )
84
+
85
+ for idx, media_url in enumerate(request.media_urls):
86
+ if self.is_cancelled():
87
+ raise InterruptedError("Post creation was cancelled")
88
+
89
+ self._progress_tracker.emit_progress(
90
+ "upload_media",
91
+ progress=idx,
92
+ total=len(request.media_urls),
93
+ message=f"Uploading media {idx + 1}/{len(request.media_urls)}...",
94
+ )
95
+
96
+ # Twitter media upload is simplified in the client
97
+ # In production, this would use tweepy's media upload
98
+ media_attachment = await client.upload_media(
99
+ media_url=media_url,
100
+ media_type="image", # Default to image
101
+ alt_text=None,
102
+ )
103
+ media_ids.append(media_attachment.media_id)
104
+
105
+ self._progress_tracker.emit_complete(
106
+ "upload_media",
107
+ message="All media uploaded successfully",
108
+ )
109
+
110
+ # Create post with progress tracking
111
+ self._progress_tracker.emit_progress(
112
+ "execute_post",
113
+ progress=50,
114
+ total=100,
115
+ message="Creating tweet...",
116
+ )
117
+
118
+ # Use the client to create the post
119
+ post = await client.create_post(request)
120
+
121
+ return post