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,390 @@
1
+ """Abstract base class for social media platform integrations.
2
+
3
+ This module defines the SocialMediaPlatform ABC that serves as a blueprint
4
+ for implementing platform-specific clients (Instagram, Twitter, LinkedIn, etc.).
5
+ All concrete implementations must implement the abstract methods defined here.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from datetime import datetime
10
+ from traceback import TracebackException
11
+ from typing import Any
12
+
13
+ from marqetive.core.client import APIClient
14
+ from marqetive.platforms.exceptions import (
15
+ PlatformAuthError,
16
+ RateLimitError,
17
+ )
18
+ from marqetive.platforms.models import (
19
+ AuthCredentials,
20
+ Comment,
21
+ MediaAttachment,
22
+ PlatformResponse,
23
+ Post,
24
+ PostCreateRequest,
25
+ PostUpdateRequest,
26
+ )
27
+
28
+
29
+ class SocialMediaPlatform(ABC):
30
+ """Abstract base class for social media platform integrations.
31
+
32
+ This class defines the common interface that all platform-specific clients
33
+ must implement. It uses composition to include an APIClient instance and
34
+ provides both abstract methods (must be implemented) and concrete utility
35
+ methods (can be used as-is or overridden).
36
+
37
+ Attributes:
38
+ platform_name: Name of the platform (e.g., "instagram", "twitter")
39
+ credentials: Authentication credentials for the platform
40
+ api_client: HTTPx-based API client for making requests
41
+ base_url: Base URL for the platform's API
42
+
43
+ Example:
44
+ >>> class TwitterClient(SocialMediaPlatform):
45
+ ... def __init__(self, credentials: AuthCredentials):
46
+ ... super().__init__(
47
+ ... platform_name="twitter",
48
+ ... credentials=credentials,
49
+ ... base_url="https://api.x.com/2"
50
+ ... )
51
+ ... # Implement abstract methods...
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ platform_name: str,
57
+ credentials: AuthCredentials,
58
+ base_url: str,
59
+ timeout: float = 30.0,
60
+ ) -> None:
61
+ """Initialize the platform client.
62
+
63
+ Args:
64
+ platform_name: Name of the platform
65
+ credentials: Authentication credentials
66
+ base_url: Base URL for the platform API
67
+ timeout: Request timeout in seconds
68
+
69
+ Raises:
70
+ PlatformAuthError: If credentials are invalid or expired
71
+ """
72
+ self.platform_name = platform_name
73
+ self.credentials = credentials
74
+ self.base_url = base_url
75
+ self.timeout = timeout
76
+ self.api_client: APIClient | None = None
77
+ self._rate_limit_remaining: int | None = None
78
+ self._rate_limit_reset: datetime | None = None
79
+
80
+ # Validate credentials on initialization
81
+ if self.credentials.is_expired():
82
+ raise PlatformAuthError(
83
+ "Access token has expired",
84
+ platform=platform_name,
85
+ )
86
+
87
+ async def __aenter__(self) -> "SocialMediaPlatform":
88
+ """Async context manager entry.
89
+
90
+ Returns:
91
+ Self for use in context manager.
92
+
93
+ Example:
94
+ >>> async with TwitterClient(creds) as client:
95
+ ... post = await client.get_post("12345")
96
+ """
97
+ headers = self._build_auth_headers()
98
+ self.api_client = APIClient(
99
+ base_url=self.base_url,
100
+ timeout=self.timeout,
101
+ headers=headers,
102
+ )
103
+ await self.api_client.__aenter__()
104
+ return self
105
+
106
+ async def __aexit__(
107
+ self, exc_type: type[Exception], exc_val: Any, exc_tb: TracebackException
108
+ ) -> None:
109
+ """Async context manager exit."""
110
+ if self.api_client:
111
+ await self.api_client.__aexit__(exc_type, exc_val, exc_tb)
112
+
113
+ def _build_auth_headers(self) -> dict[str, str]:
114
+ """Build authentication headers for API requests.
115
+
116
+ Returns:
117
+ Dictionary of headers including authorization.
118
+ """
119
+ return {
120
+ "Authorization": f"{self.credentials.token_type} {self.credentials.access_token}",
121
+ "Content-Type": "application/json",
122
+ }
123
+
124
+ def _check_rate_limit(self) -> None:
125
+ """Check if rate limit has been exceeded.
126
+
127
+ Raises:
128
+ RateLimitError: If rate limit is exceeded.
129
+ """
130
+ if self._rate_limit_remaining is not None and self._rate_limit_remaining <= 0:
131
+ retry_after = None
132
+ if self._rate_limit_reset:
133
+ retry_after = int(
134
+ (self._rate_limit_reset - datetime.now()).total_seconds()
135
+ )
136
+ raise RateLimitError(
137
+ "Rate limit exceeded",
138
+ platform=self.platform_name,
139
+ status_code=429,
140
+ retry_after=retry_after,
141
+ )
142
+
143
+ def _update_rate_limit_info(
144
+ self, remaining: int | None, reset_time: datetime | None
145
+ ) -> None:
146
+ """Update rate limit information from API response.
147
+
148
+ Args:
149
+ remaining: Number of remaining requests in current window
150
+ reset_time: Timestamp when rate limit resets
151
+ """
152
+ self._rate_limit_remaining = remaining
153
+ self._rate_limit_reset = reset_time
154
+
155
+ # ==================== Abstract Authentication Methods ====================
156
+
157
+ @abstractmethod
158
+ async def authenticate(self) -> AuthCredentials:
159
+ """Perform platform-specific authentication flow.
160
+
161
+ Each platform implements its own authentication mechanism (OAuth2,
162
+ API keys, etc.). This method should handle the complete auth flow
163
+ and return valid credentials.
164
+
165
+ Returns:
166
+ AuthCredentials object with access tokens.
167
+
168
+ Raises:
169
+ PlatformAuthError: If authentication fails.
170
+
171
+ Example:
172
+ >>> async with TwitterClient(creds) as client:
173
+ ... new_creds = await client.authenticate()
174
+ """
175
+ pass
176
+
177
+ @abstractmethod
178
+ async def refresh_token(self) -> AuthCredentials:
179
+ """Refresh the access token using refresh token.
180
+
181
+ Returns:
182
+ Updated AuthCredentials with new access token.
183
+
184
+ Raises:
185
+ PlatformAuthError: If token refresh fails.
186
+ """
187
+ pass
188
+
189
+ @abstractmethod
190
+ async def is_authenticated(self) -> bool:
191
+ """Check if current credentials are valid.
192
+
193
+ Returns:
194
+ True if authenticated and token is valid, False otherwise.
195
+ """
196
+ pass
197
+
198
+ # ==================== Abstract Post CRUD Methods ====================
199
+
200
+ @abstractmethod
201
+ async def create_post(self, request: PostCreateRequest) -> Post:
202
+ """Create and publish a new post.
203
+
204
+ Args:
205
+ request: Post creation request with content and media.
206
+
207
+ Returns:
208
+ Created Post object with platform-assigned ID.
209
+
210
+ Raises:
211
+ PlatformAuthError: If not authenticated.
212
+ ValidationError: If request data is invalid.
213
+ MediaUploadError: If media upload fails.
214
+
215
+ Example:
216
+ >>> request = PostCreateRequest(
217
+ ... content="Hello world!",
218
+ ... media_urls=["https://example.com/image.jpg"]
219
+ ... )
220
+ >>> post = await client.create_post(request)
221
+ """
222
+ pass
223
+
224
+ @abstractmethod
225
+ async def get_post(self, post_id: str) -> Post:
226
+ """Retrieve a post by its ID.
227
+
228
+ Args:
229
+ post_id: Platform-specific post identifier.
230
+
231
+ Returns:
232
+ Post object with current data.
233
+
234
+ Raises:
235
+ PostNotFoundError: If post doesn't exist.
236
+ PlatformAuthError: If not authenticated.
237
+ """
238
+ pass
239
+
240
+ @abstractmethod
241
+ async def update_post(self, post_id: str, request: PostUpdateRequest) -> Post:
242
+ """Update an existing post.
243
+
244
+ Note: Not all platforms support editing posts. Implementation should
245
+ raise an appropriate error if editing is not supported.
246
+
247
+ Args:
248
+ post_id: Platform-specific post identifier.
249
+ request: Post update request with new content.
250
+
251
+ Returns:
252
+ Updated Post object.
253
+
254
+ Raises:
255
+ PostNotFoundError: If post doesn't exist.
256
+ ValidationError: If update data is invalid.
257
+ PlatformError: If platform doesn't support editing.
258
+ """
259
+ pass
260
+
261
+ @abstractmethod
262
+ async def delete_post(self, post_id: str) -> bool:
263
+ """Delete a post.
264
+
265
+ Args:
266
+ post_id: Platform-specific post identifier.
267
+
268
+ Returns:
269
+ True if deletion was successful.
270
+
271
+ Raises:
272
+ PostNotFoundError: If post doesn't exist.
273
+ PlatformAuthError: If not authenticated or authorized.
274
+ """
275
+ pass
276
+
277
+ # ==================== Abstract Comment Methods ====================
278
+
279
+ @abstractmethod
280
+ async def get_comments(
281
+ self,
282
+ post_id: str,
283
+ limit: int = 50,
284
+ offset: int = 0,
285
+ ) -> list[Comment]:
286
+ """Retrieve comments for a post.
287
+
288
+ Args:
289
+ post_id: Platform-specific post identifier.
290
+ limit: Maximum number of comments to retrieve.
291
+ offset: Number of comments to skip (for pagination).
292
+
293
+ Returns:
294
+ List of Comment objects.
295
+
296
+ Raises:
297
+ PostNotFoundError: If post doesn't exist.
298
+ """
299
+ pass
300
+
301
+ @abstractmethod
302
+ async def create_comment(self, post_id: str, content: str) -> Comment:
303
+ """Add a comment to a post.
304
+
305
+ Args:
306
+ post_id: Platform-specific post identifier.
307
+ content: Text content of the comment.
308
+
309
+ Returns:
310
+ Created Comment object.
311
+
312
+ Raises:
313
+ PostNotFoundError: If post doesn't exist.
314
+ ValidationError: If comment content is invalid.
315
+ """
316
+ pass
317
+
318
+ @abstractmethod
319
+ async def delete_comment(self, comment_id: str) -> bool:
320
+ """Delete a comment.
321
+
322
+ Args:
323
+ comment_id: Platform-specific comment identifier.
324
+
325
+ Returns:
326
+ True if deletion was successful.
327
+
328
+ Raises:
329
+ PlatformError: If comment doesn't exist or can't be deleted.
330
+ """
331
+ pass
332
+
333
+ # ==================== Abstract Media Methods ====================
334
+
335
+ @abstractmethod
336
+ async def upload_media(
337
+ self,
338
+ media_url: str,
339
+ media_type: str,
340
+ alt_text: str | None = None,
341
+ ) -> MediaAttachment:
342
+ """Upload media to the platform.
343
+
344
+ Args:
345
+ media_url: URL of the media file to upload.
346
+ media_type: Type of media (image, video, etc.).
347
+ alt_text: Alternative text for accessibility.
348
+
349
+ Returns:
350
+ MediaAttachment object with platform-assigned media ID.
351
+
352
+ Raises:
353
+ MediaUploadError: If upload fails.
354
+ ValidationError: If media type or format is not supported.
355
+ """
356
+ pass
357
+
358
+ # ==================== Concrete Utility Methods ====================
359
+
360
+ def get_rate_limit_info(self) -> dict[str, Any]:
361
+ """Get current rate limit information.
362
+
363
+ Returns:
364
+ Dictionary with rate limit details.
365
+ """
366
+ return {
367
+ "remaining": self._rate_limit_remaining,
368
+ "reset_time": self._rate_limit_reset,
369
+ "platform": self.platform_name,
370
+ }
371
+
372
+ async def validate_credentials(self) -> PlatformResponse:
373
+ """Validate current credentials by checking authentication status.
374
+
375
+ Returns:
376
+ PlatformResponse indicating if credentials are valid.
377
+ """
378
+ try:
379
+ is_valid = await self.is_authenticated()
380
+ return PlatformResponse(
381
+ success=is_valid,
382
+ platform=self.platform_name,
383
+ data={"valid": is_valid},
384
+ )
385
+ except Exception as e:
386
+ return PlatformResponse(
387
+ success=False,
388
+ platform=self.platform_name,
389
+ error_message=str(e),
390
+ )
@@ -0,0 +1,238 @@
1
+ """Custom exceptions for social media platform integrations.
2
+
3
+ This module defines platform-specific exceptions for handling errors
4
+ that may occur during API interactions with various social media platforms.
5
+ """
6
+
7
+
8
+ class PlatformError(Exception):
9
+ """Base exception for all platform-related errors.
10
+
11
+ Args:
12
+ message: Human-readable error message
13
+ platform: Name of the platform where error occurred
14
+ status_code: HTTP status code if applicable
15
+
16
+ Example:
17
+ >>> raise PlatformError("API request failed", platform="instagram")
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ message: str,
23
+ platform: str | None = None,
24
+ status_code: int | None = None,
25
+ ) -> None:
26
+ self.message = message
27
+ self.platform = platform
28
+ self.status_code = status_code
29
+ super().__init__(self._format_message())
30
+
31
+ def _format_message(self) -> str:
32
+ """Format the error message with platform and status code."""
33
+ parts = [self.message]
34
+ if self.platform:
35
+ parts.append(f"Platform: {self.platform}")
36
+ if self.status_code:
37
+ parts.append(f"Status: {self.status_code}")
38
+ return " | ".join(parts)
39
+
40
+
41
+ class PlatformAuthError(PlatformError):
42
+ """Raised when authentication or authorization fails.
43
+
44
+ This exception is raised when:
45
+ - Authentication credentials are invalid or expired
46
+ - Access token refresh fails
47
+ - OAuth flow encounters errors
48
+ - Insufficient permissions for requested operation
49
+
50
+ Example:
51
+ >>> raise PlatformAuthError(
52
+ ... "Access token expired",
53
+ ... platform="twitter",
54
+ ... status_code=401
55
+ ... )
56
+ """
57
+
58
+ pass
59
+
60
+
61
+ class RateLimitError(PlatformError):
62
+ """Raised when API rate limit is exceeded.
63
+
64
+ Args:
65
+ message: Human-readable error message
66
+ platform: Name of the platform where error occurred
67
+ status_code: HTTP status code (typically 429)
68
+ retry_after: Seconds until rate limit resets
69
+
70
+ Example:
71
+ >>> raise RateLimitError(
72
+ ... "Rate limit exceeded",
73
+ ... platform="instagram",
74
+ ... status_code=429,
75
+ ... retry_after=3600
76
+ ... )
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ message: str,
82
+ platform: str | None = None,
83
+ status_code: int | None = None,
84
+ retry_after: int | None = None,
85
+ ) -> None:
86
+ self.retry_after = retry_after
87
+ super().__init__(message, platform, status_code)
88
+
89
+ def _format_message(self) -> str:
90
+ """Format the error message with retry information."""
91
+ base_message = super()._format_message()
92
+ if self.retry_after:
93
+ return f"{base_message} | Retry after: {self.retry_after}s"
94
+ return base_message
95
+
96
+
97
+ class PostNotFoundError(PlatformError):
98
+ """Raised when a requested post does not exist.
99
+
100
+ Args:
101
+ post_id: ID of the post that was not found
102
+ platform: Name of the platform where error occurred
103
+ status_code: HTTP status code (typically 404)
104
+
105
+ Example:
106
+ >>> raise PostNotFoundError(
107
+ ... post_id="12345",
108
+ ... platform="linkedin",
109
+ ... status_code=404
110
+ ... )
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ post_id: str,
116
+ platform: str | None = None,
117
+ status_code: int | None = None,
118
+ ) -> None:
119
+ self.post_id = post_id
120
+ message = f"Post not found: {post_id}"
121
+ super().__init__(message, platform, status_code)
122
+
123
+
124
+ class MediaUploadError(PlatformError):
125
+ """Raised when media upload fails.
126
+
127
+ This exception is raised when:
128
+ - Media file format is not supported
129
+ - Media file size exceeds platform limits
130
+ - Network error during upload
131
+ - Platform-specific upload validation fails
132
+
133
+ Args:
134
+ message: Human-readable error message
135
+ platform: Name of the platform where error occurred
136
+ status_code: HTTP status code if applicable
137
+ media_type: Type of media that failed to upload (image, video, etc.)
138
+
139
+ Example:
140
+ >>> raise MediaUploadError(
141
+ ... "File size exceeds limit",
142
+ ... platform="twitter",
143
+ ... media_type="video"
144
+ ... )
145
+ """
146
+
147
+ def __init__(
148
+ self,
149
+ message: str,
150
+ platform: str | None = None,
151
+ status_code: int | None = None,
152
+ media_type: str | None = None,
153
+ ) -> None:
154
+ self.media_type = media_type
155
+ super().__init__(message, platform, status_code)
156
+
157
+ def _format_message(self) -> str:
158
+ """Format the error message with media type information."""
159
+ base_message = super()._format_message()
160
+ if self.media_type:
161
+ return f"{base_message} | Media type: {self.media_type}"
162
+ return base_message
163
+
164
+
165
+ class ValidationError(PlatformError):
166
+ """Raised when input validation fails.
167
+
168
+ This exception is raised when:
169
+ - Required fields are missing
170
+ - Field values are invalid or out of range
171
+ - Data format doesn't match platform requirements
172
+
173
+ Args:
174
+ message: Human-readable error message
175
+ platform: Name of the platform where error occurred
176
+ field: Name of the field that failed validation
177
+
178
+ Example:
179
+ >>> raise ValidationError(
180
+ ... "Caption exceeds maximum length",
181
+ ... platform="instagram",
182
+ ... field="caption"
183
+ ... )
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ message: str,
189
+ platform: str | None = None,
190
+ field: str | None = None,
191
+ ) -> None:
192
+ self.field = field
193
+ super().__init__(message, platform)
194
+
195
+ def _format_message(self) -> str:
196
+ """Format the error message with field information."""
197
+ base_message = super()._format_message()
198
+ if self.field:
199
+ return f"{base_message} | Field: {self.field}"
200
+ return base_message
201
+
202
+
203
+ class InvalidFileTypeError(PlatformError):
204
+ """Raised when file type is not supported by the platform.
205
+
206
+ This exception is raised when:
207
+ - File MIME type is not supported
208
+ - File extension doesn't match content
209
+ - Platform doesn't accept the file format
210
+
211
+ Args:
212
+ message: Human-readable error message
213
+ platform: Name of the platform where error occurred
214
+ file_type: The invalid file type/MIME type
215
+
216
+ Example:
217
+ >>> raise InvalidFileTypeError(
218
+ ... "BMP images not supported",
219
+ ... platform="instagram",
220
+ ... file_type="image/bmp"
221
+ ... )
222
+ """
223
+
224
+ def __init__(
225
+ self,
226
+ message: str,
227
+ platform: str | None = None,
228
+ file_type: str | None = None,
229
+ ) -> None:
230
+ self.file_type = file_type
231
+ super().__init__(message, platform)
232
+
233
+ def _format_message(self) -> str:
234
+ """Format the error message with file type information."""
235
+ base_message = super()._format_message()
236
+ if self.file_type:
237
+ return f"{base_message} | File type: {self.file_type}"
238
+ return base_message
@@ -0,0 +1,7 @@
1
+ """Instagram platform integration."""
2
+
3
+ from marqetive.platforms.instagram.client import InstagramClient
4
+ from marqetive.platforms.instagram.factory import InstagramAccountFactory
5
+ from marqetive.platforms.instagram.manager import InstagramPostManager
6
+
7
+ __all__ = ["InstagramClient", "InstagramAccountFactory", "InstagramPostManager"]