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
+ """Instagram-specific exception handling and error code mapping.
2
+
3
+ This module provides comprehensive error handling for Instagram Graph API errors.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from marqetive.platforms.exceptions import (
9
+ MediaUploadError,
10
+ PlatformAuthError,
11
+ PlatformError,
12
+ RateLimitError,
13
+ ValidationError,
14
+ )
15
+
16
+
17
+ # Instagram Graph API error codes
18
+ # Source: https://developers.facebook.com/docs/graph-api/guides/error-handling
19
+ class InstagramErrorCode:
20
+ """Instagram Graph API error codes."""
21
+
22
+ # Authentication & Authorization (1-99)
23
+ API_UNKNOWN = 1
24
+ API_SERVICE = 2
25
+ API_TOO_MANY_CALLS = 4
26
+ API_USER_TOO_MANY_CALLS = 17
27
+ API_PERMISSION_DENIED = 10
28
+ OAuthException = 190
29
+ ACCESS_TOKEN_EXPIRED = 190
30
+
31
+ # Resource errors (100-199)
32
+ UNSUPPORTED_GET_REQUEST = 100
33
+ INVALID_PARAMETER = 100
34
+ USER_NOT_VISIBLE = 190
35
+
36
+ # Rate limiting (200-299)
37
+ APPLICATION_LIMIT = 4
38
+ USER_REQUEST_LIMIT = 17
39
+
40
+ # Media errors (300-399)
41
+ MEDIA_UPLOAD_ERROR = 324
42
+ INVALID_MEDIA_TYPE = 352
43
+ MEDIA_PROCESSING_ERROR = 368
44
+
45
+
46
+ # Error type constants
47
+ ERROR_TYPE_OAUTH = "OAuthException"
48
+ ERROR_TYPE_API = "FacebookApiException"
49
+ ERROR_TYPE_GRAPH_METHOD = "GraphMethodException"
50
+
51
+
52
+ # Mapping of error codes to user-friendly messages
53
+ ERROR_MESSAGES: dict[int, str] = {
54
+ 1: "An unknown error occurred.",
55
+ 2: "A service error occurred. Please try again later.",
56
+ 4: "Application request limit reached. Please retry later.",
57
+ 17: "User request limit reached. Please retry later.",
58
+ 10: "Permission denied. Check your app permissions.",
59
+ 100: "Invalid or unsupported parameter in request.",
60
+ 190: "Access token is invalid or expired. Please re-authenticate.",
61
+ 324: "Media upload failed. Please check file format and size.",
62
+ 352: "Invalid media type. Instagram only supports specific formats.",
63
+ 368: "Media processing failed. Please try a different file.",
64
+ }
65
+
66
+
67
+ # Retryable error codes
68
+ RETRYABLE_ERROR_CODES = {
69
+ 1, # Unknown error
70
+ 2, # Service error
71
+ 4, # Too many calls
72
+ 17, # User too many calls
73
+ }
74
+
75
+
76
+ # Non-retryable error codes
77
+ NON_RETRYABLE_ERROR_CODES = {
78
+ 10, # Permission denied
79
+ 100, # Invalid parameter
80
+ 190, # OAuth exception
81
+ 352, # Invalid media type
82
+ }
83
+
84
+
85
+ def map_instagram_error(
86
+ status_code: int | None,
87
+ error_code: int | None = None,
88
+ error_type: str | None = None,
89
+ error_message: str | None = None,
90
+ response_data: dict[str, Any] | None = None,
91
+ ) -> PlatformError:
92
+ """Map Instagram Graph API error to appropriate exception.
93
+
94
+ Args:
95
+ status_code: HTTP status code.
96
+ error_code: Instagram-specific error code.
97
+ error_type: Instagram error type.
98
+ error_message: Error message from API.
99
+ response_data: Full response data from API.
100
+
101
+ Returns:
102
+ Appropriate PlatformError subclass.
103
+
104
+ Example:
105
+ >>> error = map_instagram_error(401, 190, "OAuthException")
106
+ >>> print(type(error).__name__)
107
+ PlatformAuthError
108
+ """
109
+ # Extract error details from response if provided
110
+ if response_data and "error" in response_data:
111
+ error_obj = response_data["error"]
112
+ if not error_code:
113
+ error_code = error_obj.get("code")
114
+ if not error_type:
115
+ error_type = error_obj.get("type")
116
+ if not error_message:
117
+ error_message = error_obj.get("message")
118
+
119
+ # Get user-friendly message
120
+ friendly_message = ERROR_MESSAGES.get(
121
+ error_code or 0, error_message or "Unknown error"
122
+ )
123
+
124
+ # Determine retry-after for rate limits
125
+ retry_after = None
126
+ if error_code in (4, 17):
127
+ # Instagram uses 1-hour windows typically
128
+ retry_after = 3600 # 1 hour in seconds
129
+
130
+ # Map to appropriate exception type
131
+ # OAuth / Authentication errors
132
+ if error_type == ERROR_TYPE_OAUTH or error_code == 190:
133
+ return PlatformAuthError(
134
+ friendly_message,
135
+ platform="instagram",
136
+ status_code=status_code or 401,
137
+ )
138
+
139
+ # Permission errors
140
+ if error_code == 10 or status_code == 403:
141
+ return PlatformAuthError(
142
+ friendly_message,
143
+ platform="instagram",
144
+ status_code=status_code or 403,
145
+ )
146
+
147
+ # Rate limit errors
148
+ if error_code in (4, 17) or status_code == 429:
149
+ return RateLimitError(
150
+ friendly_message,
151
+ platform="instagram",
152
+ status_code=status_code or 429,
153
+ retry_after=retry_after,
154
+ )
155
+
156
+ # Validation errors
157
+ if error_code == 100:
158
+ return ValidationError(
159
+ friendly_message,
160
+ platform="instagram",
161
+ )
162
+
163
+ # Media errors
164
+ if error_code in (324, 352, 368):
165
+ return MediaUploadError(
166
+ friendly_message,
167
+ platform="instagram",
168
+ status_code=status_code,
169
+ )
170
+
171
+ # Generic platform error
172
+ return PlatformError(
173
+ friendly_message,
174
+ platform="instagram",
175
+ status_code=status_code,
176
+ )
177
+
178
+
179
+ def is_retryable_instagram_error(
180
+ status_code: int | None,
181
+ error_code: int | None = None,
182
+ error_type: str | None = None,
183
+ ) -> bool:
184
+ """Determine if an Instagram error is retryable.
185
+
186
+ Args:
187
+ status_code: HTTP status code.
188
+ error_code: Instagram-specific error code.
189
+ error_type: Instagram error type.
190
+
191
+ Returns:
192
+ True if error is retryable, False otherwise.
193
+
194
+ Example:
195
+ >>> is_retryable_instagram_error(500)
196
+ True
197
+ >>> is_retryable_instagram_error(400, 190, "OAuthException")
198
+ False
199
+ """
200
+ # Check explicit non-retryable codes first
201
+ if error_code in NON_RETRYABLE_ERROR_CODES:
202
+ return False
203
+
204
+ # OAuth errors are not retryable
205
+ if error_type == ERROR_TYPE_OAUTH:
206
+ return False
207
+
208
+ # Check retryable codes
209
+ if error_code in RETRYABLE_ERROR_CODES:
210
+ return True
211
+
212
+ # Check HTTP status codes
213
+ if status_code:
214
+ # 5xx errors are generally retryable
215
+ if 500 <= status_code < 600:
216
+ return True
217
+ # 429 is retryable
218
+ if status_code == 429:
219
+ return True
220
+ # 4xx errors (except 429) are generally not retryable
221
+ if 400 <= status_code < 500:
222
+ return False
223
+
224
+ # Default to not retryable for safety
225
+ return False
226
+
227
+
228
+ def get_retry_delay(
229
+ status_code: int | None,
230
+ error_code: int | None = None,
231
+ attempt: int = 1,
232
+ ) -> float:
233
+ """Get recommended retry delay for Instagram error.
234
+
235
+ Args:
236
+ status_code: HTTP status code.
237
+ error_code: Instagram-specific error code.
238
+ attempt: Current retry attempt number.
239
+
240
+ Returns:
241
+ Recommended delay in seconds.
242
+
243
+ Example:
244
+ >>> get_retry_delay(503, attempt=1)
245
+ 5.0
246
+ >>> get_retry_delay(None, 4)
247
+ 3600.0
248
+ """
249
+ # Rate limit errors - wait full window
250
+ if error_code in (4, 17):
251
+ return 3600.0 # 1 hour
252
+
253
+ # Server errors - exponential backoff
254
+ if status_code and 500 <= status_code < 600:
255
+ base_delay = 5.0
256
+ return min(base_delay * (2 ** (attempt - 1)), 120.0)
257
+
258
+ # Default delay
259
+ return 10.0
260
+
261
+
262
+ class InstagramAPIError(PlatformError):
263
+ """Instagram Graph API specific error with detailed information.
264
+
265
+ Attributes:
266
+ error_code: Instagram-specific error code.
267
+ error_type: Instagram error type.
268
+ error_subcode: Instagram error subcode (if available).
269
+ is_retryable: Whether the error is retryable.
270
+ retry_delay: Recommended retry delay in seconds.
271
+ """
272
+
273
+ def __init__(
274
+ self,
275
+ message: str,
276
+ *,
277
+ status_code: int | None = None,
278
+ error_code: int | None = None,
279
+ error_type: str | None = None,
280
+ error_subcode: int | None = None,
281
+ response_data: dict[str, Any] | None = None,
282
+ ) -> None:
283
+ """Initialize Instagram API error.
284
+
285
+ Args:
286
+ message: Error message.
287
+ status_code: HTTP status code.
288
+ error_code: Instagram-specific error code.
289
+ error_type: Instagram error type.
290
+ error_subcode: Instagram error subcode.
291
+ response_data: Full response data from API.
292
+ """
293
+ super().__init__(message, platform="instagram", status_code=status_code)
294
+ self.error_code = error_code
295
+ self.error_type = error_type
296
+ self.error_subcode = error_subcode
297
+ self.response_data = response_data
298
+ self.is_retryable = is_retryable_instagram_error(
299
+ status_code, error_code, error_type
300
+ )
301
+ self.retry_delay = get_retry_delay(status_code, error_code)
302
+
303
+ def __repr__(self) -> str:
304
+ """String representation of error."""
305
+ return (
306
+ f"InstagramAPIError(message={self.message!r}, "
307
+ f"status_code={self.status_code}, "
308
+ f"error_code={self.error_code}, "
309
+ f"error_type={self.error_type!r}, "
310
+ f"retryable={self.is_retryable})"
311
+ )
@@ -0,0 +1,106 @@
1
+ """Instagram account factory for managing credentials and client creation."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+
6
+ from marqetive.core.account_factory import BaseAccountFactory
7
+ from marqetive.platforms.exceptions import PlatformAuthError
8
+ from marqetive.platforms.instagram.client import InstagramClient
9
+ from marqetive.platforms.models import AccountStatus, AuthCredentials
10
+ from marqetive.utils.oauth import refresh_instagram_token
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class InstagramAccountFactory(BaseAccountFactory):
16
+ """Factory for creating and managing Instagram accounts and clients.
17
+
18
+ Example:
19
+ >>> factory = InstagramAccountFactory()
20
+ >>> credentials = AuthCredentials(
21
+ ... platform="instagram",
22
+ ... access_token="token"
23
+ ... )
24
+ >>> client = await factory.create_authenticated_client(credentials)
25
+ >>> async with client:
26
+ ... post = await client.create_post(request)
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ on_status_update: Callable[[str, AccountStatus], None] | None = None,
32
+ ) -> None:
33
+ """Initialize Instagram account factory.
34
+
35
+ Args:
36
+ on_status_update: Optional callback when account status changes.
37
+ """
38
+ super().__init__(on_status_update=on_status_update)
39
+
40
+ @property
41
+ def platform_name(self) -> str:
42
+ """Get platform name."""
43
+ return "instagram"
44
+
45
+ async def refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
46
+ """Refresh Instagram long-lived access token.
47
+
48
+ Args:
49
+ credentials: Current credentials.
50
+
51
+ Returns:
52
+ Updated credentials with refreshed token.
53
+
54
+ Raises:
55
+ PlatformAuthError: If refresh fails.
56
+ """
57
+ logger.info("Refreshing Instagram access token...")
58
+ return await refresh_instagram_token(credentials)
59
+
60
+ async def create_client(self, credentials: AuthCredentials) -> InstagramClient:
61
+ """Create Instagram API client.
62
+
63
+ Args:
64
+ credentials: Valid Instagram credentials.
65
+
66
+ Returns:
67
+ InstagramClient instance.
68
+
69
+ Raises:
70
+ PlatformAuthError: If credentials are invalid.
71
+ """
72
+ if not credentials.access_token:
73
+ raise PlatformAuthError(
74
+ "Access token is required",
75
+ platform=self.platform_name,
76
+ )
77
+
78
+ # Instagram needs instagram_business_account_id in additional_data
79
+ instagram_business_account_id = credentials.additional_data.get(
80
+ "instagram_business_account_id"
81
+ )
82
+ if not instagram_business_account_id:
83
+ raise PlatformAuthError(
84
+ "instagram_business_account_id is required in additional_data",
85
+ platform=self.platform_name,
86
+ )
87
+
88
+ return InstagramClient(credentials=credentials)
89
+
90
+ async def validate_credentials(self, credentials: AuthCredentials) -> bool:
91
+ """Validate Instagram credentials by making a test API call.
92
+
93
+ Args:
94
+ credentials: Credentials to validate.
95
+
96
+ Returns:
97
+ True if credentials are valid, False otherwise.
98
+ """
99
+ try:
100
+ client = await self.create_client(credentials)
101
+ async with client:
102
+ # Try to verify credentials
103
+ return await client.is_authenticated()
104
+ except Exception as e:
105
+ logger.error(f"Error validating Instagram credentials: {e}")
106
+ return False
@@ -0,0 +1,112 @@
1
+ """Instagram 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.instagram.client import InstagramClient
8
+ from marqetive.platforms.instagram.factory import InstagramAccountFactory
9
+ from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class InstagramPostManager(BasePostManager):
15
+ """Manager for Instagram post operations.
16
+
17
+ Coordinates post creation, media uploads, and progress tracking for Instagram.
18
+
19
+ Example:
20
+ >>> manager = InstagramPostManager()
21
+ >>> credentials = AuthCredentials(
22
+ ... platform="instagram",
23
+ ... access_token="token",
24
+ ... additional_data={"instagram_business_account_id": "123"}
25
+ ... )
26
+ >>> request = PostCreateRequest(content="Hello Instagram!")
27
+ >>> post = await manager.execute_post(credentials, request)
28
+ >>> print(f"Media ID: {post.post_id}")
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ account_factory: InstagramAccountFactory | None = None,
34
+ ) -> None:
35
+ """Initialize Instagram post manager.
36
+
37
+ Args:
38
+ account_factory: Instagram account factory (creates default if None).
39
+ """
40
+ if account_factory is None:
41
+ account_factory = InstagramAccountFactory()
42
+
43
+ super().__init__(account_factory=account_factory)
44
+
45
+ @property
46
+ def platform_name(self) -> str:
47
+ """Get platform name."""
48
+ return "instagram"
49
+
50
+ async def _execute_post_impl(
51
+ self,
52
+ client: Any,
53
+ request: PostCreateRequest,
54
+ credentials: AuthCredentials, # noqa: ARG002
55
+ ) -> Post:
56
+ """Execute Instagram post creation.
57
+
58
+ Args:
59
+ client: InstagramClient instance.
60
+ request: Post creation request.
61
+ credentials: Instagram credentials.
62
+
63
+ Returns:
64
+ Created Post object.
65
+ """
66
+ if not isinstance(client, InstagramClient):
67
+ raise TypeError(f"Expected InstagramClient, got {type(client)}")
68
+
69
+ # Handle media uploads with progress tracking
70
+ media_ids: list[str] = []
71
+ if request.media_urls:
72
+ self._progress_tracker.emit_start(
73
+ "upload_media",
74
+ total=len(request.media_urls),
75
+ message=f"Uploading {len(request.media_urls)} media files...",
76
+ )
77
+
78
+ for idx, media_url in enumerate(request.media_urls):
79
+ if self.is_cancelled():
80
+ raise InterruptedError("Post creation was cancelled")
81
+
82
+ self._progress_tracker.emit_progress(
83
+ "upload_media",
84
+ progress=idx,
85
+ total=len(request.media_urls),
86
+ message=f"Uploading media {idx + 1}/{len(request.media_urls)}...",
87
+ )
88
+
89
+ media_attachment = await client.upload_media(
90
+ media_url=media_url,
91
+ media_type="image", # Default to image
92
+ alt_text=None,
93
+ )
94
+ media_ids.append(media_attachment.media_id)
95
+
96
+ self._progress_tracker.emit_complete(
97
+ "upload_media",
98
+ message="All media uploaded successfully",
99
+ )
100
+
101
+ # Create post with progress tracking
102
+ self._progress_tracker.emit_progress(
103
+ "execute_post",
104
+ progress=50,
105
+ total=100,
106
+ message="Creating Instagram post...",
107
+ )
108
+
109
+ # Use the client to create the post
110
+ post = await client.create_post(request)
111
+
112
+ return post