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
marqetive/__init__.py ADDED
@@ -0,0 +1,113 @@
1
+ """MarqetiveLib: Modern Python utilities for web APIs and social media platforms.
2
+
3
+ A comprehensive library providing utilities for working with web APIs,
4
+ HTTP requests, data processing, and social media platform integrations.
5
+
6
+ Usage:
7
+ Basic API client:
8
+ >>> from marqetive_lib import APIClient
9
+ >>> async with APIClient(base_url="https://api.example.com") as client:
10
+ ... response = await client.get("/endpoint")
11
+
12
+ Platform managers (high-level):
13
+ >>> from marqetive_lib import initialize_platform_registry, get_manager_for_platform
14
+ >>> from marqetive_lib.platforms.models import AuthCredentials, PostCreateRequest
15
+ >>>
16
+ >>> initialize_platform_registry()
17
+ >>> manager = get_manager_for_platform("twitter")
18
+ >>> credentials = AuthCredentials(platform="twitter", access_token="...")
19
+ >>> request = PostCreateRequest(content="Hello!")
20
+ >>> post = await manager.execute_post(credentials, request)
21
+
22
+ Platform clients (low-level):
23
+ >>> from marqetive_lib.platforms.twitter import TwitterClient
24
+ >>> credentials = AuthCredentials(platform="twitter", access_token="...")
25
+ >>> async with TwitterClient(credentials) as client:
26
+ ... post = await client.create_post(request)
27
+ """
28
+
29
+ # Core API client
30
+ # Account management
31
+ from marqetive.core.account_factory import BaseAccountFactory
32
+
33
+ # Registry and managers
34
+ from marqetive.core.base_manager import BasePostManager
35
+ from marqetive.core.client import APIClient
36
+
37
+ # Progress tracking
38
+ from marqetive.core.progress import (
39
+ ProgressCallback,
40
+ ProgressEvent,
41
+ ProgressStatus,
42
+ ProgressTracker,
43
+ )
44
+ from marqetive.core.registry import (
45
+ PlatformRegistry,
46
+ get_available_platforms,
47
+ get_manager_for_platform,
48
+ get_registry,
49
+ register_platform,
50
+ )
51
+
52
+ # Models
53
+ from marqetive.platforms.models import (
54
+ AccountStatus,
55
+ AuthCredentials,
56
+ Comment,
57
+ CommentStatus,
58
+ MediaAttachment,
59
+ MediaType,
60
+ Post,
61
+ PostCreateRequest,
62
+ PostStatus,
63
+ PostUpdateRequest,
64
+ )
65
+ from marqetive.registry_init import (
66
+ initialize_platform_registry,
67
+ is_registry_initialized,
68
+ )
69
+
70
+ # Utilities
71
+ from marqetive.utils.helpers import format_response
72
+
73
+ # Retry utilities
74
+ from marqetive.utils.retry import STANDARD_BACKOFF, BackoffConfig, retry_async
75
+
76
+ __version__ = "0.1.0"
77
+
78
+ __all__ = [
79
+ # Core
80
+ "APIClient",
81
+ "format_response",
82
+ # Registry
83
+ "PlatformRegistry",
84
+ "initialize_platform_registry",
85
+ "is_registry_initialized",
86
+ "get_registry",
87
+ "register_platform",
88
+ "get_manager_for_platform",
89
+ "get_available_platforms",
90
+ # Managers and factories
91
+ "BasePostManager",
92
+ "BaseAccountFactory",
93
+ # Progress tracking
94
+ "ProgressEvent",
95
+ "ProgressStatus",
96
+ "ProgressTracker",
97
+ "ProgressCallback",
98
+ # Retry
99
+ "BackoffConfig",
100
+ "STANDARD_BACKOFF",
101
+ "retry_async",
102
+ # Models
103
+ "AuthCredentials",
104
+ "AccountStatus",
105
+ "Post",
106
+ "PostStatus",
107
+ "PostCreateRequest",
108
+ "PostUpdateRequest",
109
+ "Comment",
110
+ "CommentStatus",
111
+ "MediaAttachment",
112
+ "MediaType",
113
+ ]
@@ -0,0 +1,5 @@
1
+ """Core functionality for MarqetiveLib."""
2
+
3
+ from marqetive.core.client import APIClient
4
+
5
+ __all__ = ["APIClient"]
@@ -0,0 +1,212 @@
1
+ """Base account factory for managing platform credentials and client creation.
2
+
3
+ This module provides an abstract base class for platform-specific account
4
+ factories that handle credential management, token refresh, and client creation.
5
+ """
6
+
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from marqetive.platforms.exceptions import PlatformAuthError
13
+ from marqetive.platforms.models import AccountStatus, AuthCredentials
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BaseAccountFactory(ABC):
19
+ """Abstract base class for platform account factories.
20
+
21
+ Account factories manage the lifecycle of platform credentials including:
22
+ - Token expiry checking and automatic refresh
23
+ - Account status management
24
+ - Platform-specific client creation
25
+
26
+ Subclasses must implement:
27
+ - refresh_token(): Platform-specific token refresh logic
28
+ - create_client(): Create platform-specific API client
29
+ - validate_credentials(): Check if credentials are valid
30
+
31
+ Example:
32
+ >>> class TwitterAccountFactory(BaseAccountFactory):
33
+ ... async def refresh_token(self, credentials):
34
+ ... # Implement Twitter OAuth token refresh
35
+ ... pass
36
+ ...
37
+ ... async def create_client(self, credentials):
38
+ ... return TwitterClient(credentials=credentials)
39
+ ...
40
+ ... async def validate_credentials(self, credentials):
41
+ ... # Check if credentials work
42
+ ... pass
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ on_status_update: Callable[[str, AccountStatus], None] | None = None,
48
+ ) -> None:
49
+ """Initialize the account factory.
50
+
51
+ Args:
52
+ on_status_update: Optional callback when account status changes.
53
+ Called with (user_id, new_status).
54
+ """
55
+ self.on_status_update = on_status_update
56
+
57
+ @property
58
+ @abstractmethod
59
+ def platform_name(self) -> str:
60
+ """Get the name of the platform this factory manages.
61
+
62
+ Returns:
63
+ Platform name (e.g., "twitter", "linkedin").
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ async def refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
69
+ """Refresh the OAuth access token.
70
+
71
+ Args:
72
+ credentials: Current credentials with expired token.
73
+
74
+ Returns:
75
+ Updated credentials with new token.
76
+
77
+ Raises:
78
+ PlatformAuthError: If token refresh fails.
79
+ """
80
+ pass
81
+
82
+ @abstractmethod
83
+ async def create_client(self, credentials: AuthCredentials) -> Any:
84
+ """Create a platform-specific API client.
85
+
86
+ Args:
87
+ credentials: Valid credentials for the platform.
88
+
89
+ Returns:
90
+ Platform-specific API client instance.
91
+
92
+ Raises:
93
+ PlatformAuthError: If credentials are invalid.
94
+ """
95
+ pass
96
+
97
+ @abstractmethod
98
+ async def validate_credentials(self, credentials: AuthCredentials) -> bool:
99
+ """Validate that credentials are working.
100
+
101
+ Args:
102
+ credentials: Credentials to validate.
103
+
104
+ Returns:
105
+ True if credentials are valid, False otherwise.
106
+ """
107
+ pass
108
+
109
+ async def get_credentials(
110
+ self,
111
+ credentials: AuthCredentials,
112
+ auto_refresh: bool = True,
113
+ ) -> AuthCredentials:
114
+ """Get credentials, refreshing if necessary.
115
+
116
+ This method checks if credentials are expired and automatically
117
+ refreshes them if auto_refresh is True.
118
+
119
+ Args:
120
+ credentials: Current credentials.
121
+ auto_refresh: Whether to automatically refresh expired tokens.
122
+
123
+ Returns:
124
+ Valid credentials (refreshed if necessary).
125
+
126
+ Raises:
127
+ PlatformAuthError: If refresh fails or credentials are invalid.
128
+ """
129
+ # Check if token needs refresh
130
+ if credentials.needs_refresh():
131
+ if not auto_refresh:
132
+ logger.warning(
133
+ f"Credentials for {credentials.platform} are expired "
134
+ "but auto_refresh is disabled"
135
+ )
136
+ return credentials
137
+
138
+ logger.info(
139
+ f"Token expired for {credentials.platform}, attempting refresh..."
140
+ )
141
+ try:
142
+ refreshed_creds = await self.refresh_token(credentials)
143
+ refreshed_creds.mark_valid()
144
+
145
+ # Notify status update
146
+ if self.on_status_update and refreshed_creds.user_id:
147
+ self.on_status_update(refreshed_creds.user_id, AccountStatus.VALID)
148
+
149
+ logger.info(f"Successfully refreshed token for {credentials.platform}")
150
+ return refreshed_creds
151
+
152
+ except PlatformAuthError as e:
153
+ # Determine if this is an OAuth error requiring reconnection
154
+ if "oauth" in str(e).lower() or "authorization" in str(e).lower():
155
+ credentials.mark_reconnection_required()
156
+ if self.on_status_update and credentials.user_id:
157
+ self.on_status_update(
158
+ credentials.user_id, AccountStatus.RECONNECTION_REQUIRED
159
+ )
160
+ logger.error(
161
+ f"OAuth error refreshing token for {credentials.platform}: {e}"
162
+ )
163
+ else:
164
+ credentials.mark_error()
165
+ if self.on_status_update and credentials.user_id:
166
+ self.on_status_update(credentials.user_id, AccountStatus.ERROR)
167
+ logger.error(
168
+ f"Error refreshing token for {credentials.platform}: {e}"
169
+ )
170
+ raise
171
+
172
+ return credentials
173
+
174
+ async def create_authenticated_client(
175
+ self,
176
+ credentials: AuthCredentials,
177
+ auto_refresh: bool = True,
178
+ ) -> Any:
179
+ """Create an authenticated client, refreshing credentials if needed.
180
+
181
+ This is the main method to use for getting a ready-to-use client.
182
+
183
+ Args:
184
+ credentials: Platform credentials.
185
+ auto_refresh: Whether to automatically refresh expired tokens.
186
+
187
+ Returns:
188
+ Authenticated platform client.
189
+
190
+ Raises:
191
+ PlatformAuthError: If authentication fails.
192
+ """
193
+ # Get valid credentials (refresh if needed)
194
+ valid_creds = await self.get_credentials(credentials, auto_refresh=auto_refresh)
195
+
196
+ # Create client
197
+ client = await self.create_client(valid_creds)
198
+
199
+ return client
200
+
201
+ def _update_status(self, user_id: str | None, status: AccountStatus) -> None:
202
+ """Internal method to update account status.
203
+
204
+ Args:
205
+ user_id: User ID for the account.
206
+ status: New status.
207
+ """
208
+ if self.on_status_update and user_id:
209
+ try:
210
+ self.on_status_update(user_id, status)
211
+ except Exception as e:
212
+ logger.error(f"Error calling status update callback: {e}")
@@ -0,0 +1,303 @@
1
+ """Base manager for platform post operations.
2
+
3
+ This module provides an abstract base class for managing post operations
4
+ across different social media platforms with consistent patterns for
5
+ progress tracking, error handling, and state management.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any
12
+
13
+ from marqetive.core.account_factory import BaseAccountFactory
14
+ from marqetive.core.progress import ProgressCallback, ProgressTracker
15
+ from marqetive.platforms.exceptions import PlatformError
16
+ from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class BasePostManager(ABC):
22
+ """Abstract base class for platform post managers.
23
+
24
+ Managers coordinate the posting process including:
25
+ - Creating authenticated clients via account factories
26
+ - Tracking operation progress
27
+ - Handling errors consistently
28
+ - Supporting cancellation
29
+
30
+ Subclasses must implement:
31
+ - _execute_post_impl(): Platform-specific posting logic
32
+ - platform_name: Property returning the platform name
33
+
34
+ Example:
35
+ >>> class TwitterPostManager(BasePostManager):
36
+ ... @property
37
+ ... def platform_name(self) -> str:
38
+ ... return "twitter"
39
+ ...
40
+ ... async def _execute_post_impl(self, client, request):
41
+ ... return await client.create_post(request)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ account_factory: BaseAccountFactory | None = None,
47
+ ) -> None:
48
+ """Initialize the post manager.
49
+
50
+ Args:
51
+ account_factory: Account factory for creating clients.
52
+ If None, subclass must provide default.
53
+ """
54
+ self._account_factory = account_factory
55
+ self._progress_tracker = ProgressTracker()
56
+ self._cancel_event = asyncio.Event()
57
+
58
+ @property
59
+ @abstractmethod
60
+ def platform_name(self) -> str:
61
+ """Get the name of the platform this manager handles.
62
+
63
+ Returns:
64
+ Platform name (e.g., "twitter", "linkedin").
65
+ """
66
+ pass
67
+
68
+ @property
69
+ def account_factory(self) -> BaseAccountFactory:
70
+ """Get the account factory.
71
+
72
+ Returns:
73
+ Account factory instance.
74
+
75
+ Raises:
76
+ RuntimeError: If account factory not set.
77
+ """
78
+ if self._account_factory is None:
79
+ raise RuntimeError(
80
+ f"{self.__class__.__name__} requires an account_factory. "
81
+ "Either pass it to __init__ or override this property."
82
+ )
83
+ return self._account_factory
84
+
85
+ def add_progress_callback(self, callback: ProgressCallback) -> None:
86
+ """Add a callback to receive progress updates.
87
+
88
+ Args:
89
+ callback: Function that receives ProgressEvent objects.
90
+
91
+ Example:
92
+ >>> def my_callback(event):
93
+ ... print(f"{event.operation}: {event.percentage}%")
94
+ >>>
95
+ >>> manager.add_progress_callback(my_callback)
96
+ """
97
+ self._progress_tracker.add_callback(callback)
98
+
99
+ def remove_progress_callback(self, callback: ProgressCallback) -> None:
100
+ """Remove a progress callback.
101
+
102
+ Args:
103
+ callback: Callback to remove.
104
+ """
105
+ self._progress_tracker.remove_callback(callback)
106
+
107
+ def clear_progress_callbacks(self) -> None:
108
+ """Remove all progress callbacks."""
109
+ self._progress_tracker.clear_callbacks()
110
+
111
+ async def execute_post(
112
+ self,
113
+ credentials: AuthCredentials,
114
+ request: PostCreateRequest,
115
+ ) -> Post:
116
+ """Execute post creation with progress tracking and error handling.
117
+
118
+ This is the main entry point for creating posts. It handles:
119
+ - Client creation via account factory
120
+ - Progress tracking
121
+ - Cancellation support
122
+ - Consistent error handling
123
+
124
+ Args:
125
+ credentials: Platform credentials.
126
+ request: Post creation request.
127
+
128
+ Returns:
129
+ Created Post object.
130
+
131
+ Raises:
132
+ PlatformError: If post creation fails.
133
+ asyncio.CancelledError: If operation is cancelled.
134
+
135
+ Example:
136
+ >>> credentials = AuthCredentials(platform="twitter", ...)
137
+ >>> request = PostCreateRequest(content="Hello world!")
138
+ >>> post = await manager.execute_post(credentials, request)
139
+ >>> print(f"Posted: {post.post_id}")
140
+ """
141
+ # Reset cancel event
142
+ self._cancel_event.clear()
143
+
144
+ try:
145
+ # Emit start event
146
+ self._progress_tracker.emit_start(
147
+ "execute_post",
148
+ message=f"Starting post to {self.platform_name}",
149
+ platform=self.platform_name,
150
+ )
151
+
152
+ # Check for cancellation
153
+ if self._cancel_event.is_set():
154
+ self._progress_tracker.emit_cancelled("execute_post")
155
+ raise asyncio.CancelledError("Post execution was cancelled")
156
+
157
+ # Create authenticated client
158
+ logger.info(f"Creating {self.platform_name} client...")
159
+ client = await self.account_factory.create_authenticated_client(credentials)
160
+
161
+ # Check for cancellation
162
+ if self._cancel_event.is_set():
163
+ self._progress_tracker.emit_cancelled("execute_post")
164
+ raise asyncio.CancelledError("Post execution was cancelled")
165
+
166
+ # Execute platform-specific posting logic
167
+ logger.info(f"Creating post on {self.platform_name}...")
168
+ post = await self._execute_post_impl(client, request, credentials)
169
+
170
+ # Emit completion event
171
+ self._progress_tracker.emit_complete(
172
+ "execute_post",
173
+ message=f"Post created successfully on {self.platform_name}",
174
+ post_id=post.post_id,
175
+ platform=self.platform_name,
176
+ )
177
+
178
+ logger.info(
179
+ f"Successfully created post {post.post_id} on {self.platform_name}"
180
+ )
181
+ return post
182
+
183
+ except asyncio.CancelledError:
184
+ self._progress_tracker.emit_cancelled(
185
+ "execute_post",
186
+ message=f"Post creation cancelled on {self.platform_name}",
187
+ )
188
+ raise
189
+
190
+ except Exception as e:
191
+ error_msg = f"Failed to create post on {self.platform_name}: {str(e)}"
192
+ logger.error(error_msg)
193
+ self._progress_tracker.emit_failed("execute_post", e)
194
+
195
+ # Wrap in PlatformError if not already
196
+ if not isinstance(e, PlatformError):
197
+ raise PlatformError(error_msg, platform=self.platform_name) from e
198
+ raise
199
+
200
+ @abstractmethod
201
+ async def _execute_post_impl(
202
+ self,
203
+ client: Any,
204
+ request: PostCreateRequest,
205
+ credentials: AuthCredentials,
206
+ ) -> Post:
207
+ """Platform-specific post creation implementation.
208
+
209
+ Subclasses must implement this to handle the actual post creation
210
+ logic for their specific platform.
211
+
212
+ Args:
213
+ client: Platform-specific authenticated client.
214
+ request: Post creation request.
215
+ credentials: Platform credentials (for context).
216
+
217
+ Returns:
218
+ Created Post object.
219
+
220
+ Raises:
221
+ PlatformError: If post creation fails.
222
+ """
223
+ pass
224
+
225
+ def cancel_post(self) -> None:
226
+ """Cancel the current post operation.
227
+
228
+ Sets a cancellation flag that is checked at key points during
229
+ post execution. The operation may not stop immediately.
230
+
231
+ Example:
232
+ >>> task = asyncio.create_task(manager.execute_post(...))
233
+ >>> # Later...
234
+ >>> manager.cancel_post()
235
+ >>> try:
236
+ ... await task
237
+ ... except asyncio.CancelledError:
238
+ ... print("Operation was cancelled")
239
+ """
240
+ self._cancel_event.set()
241
+ logger.info(f"Cancellation requested for {self.platform_name} post")
242
+
243
+ def is_cancelled(self) -> bool:
244
+ """Check if cancellation has been requested.
245
+
246
+ Returns:
247
+ True if cancel_post() was called, False otherwise.
248
+ """
249
+ return self._cancel_event.is_set()
250
+
251
+ async def get_post_status(
252
+ self,
253
+ credentials: AuthCredentials,
254
+ post_id: str,
255
+ ) -> Post:
256
+ """Get the current status of a post.
257
+
258
+ Args:
259
+ credentials: Platform credentials.
260
+ post_id: Platform-specific post ID.
261
+
262
+ Returns:
263
+ Post object with current status.
264
+
265
+ Raises:
266
+ PlatformError: If fetching post fails.
267
+ """
268
+ try:
269
+ client = await self.account_factory.create_authenticated_client(credentials)
270
+ return await client.get_post(post_id)
271
+ except Exception as e:
272
+ error_msg = f"Failed to get post status from {self.platform_name}: {str(e)}"
273
+ logger.error(error_msg)
274
+ if not isinstance(e, PlatformError):
275
+ raise PlatformError(error_msg, platform=self.platform_name) from e
276
+ raise
277
+
278
+ async def delete_post(
279
+ self,
280
+ credentials: AuthCredentials,
281
+ post_id: str,
282
+ ) -> bool:
283
+ """Delete a post.
284
+
285
+ Args:
286
+ credentials: Platform credentials.
287
+ post_id: Platform-specific post ID.
288
+
289
+ Returns:
290
+ True if deletion was successful.
291
+
292
+ Raises:
293
+ PlatformError: If deletion fails.
294
+ """
295
+ try:
296
+ client = await self.account_factory.create_authenticated_client(credentials)
297
+ return await client.delete_post(post_id)
298
+ except Exception as e:
299
+ error_msg = f"Failed to delete post from {self.platform_name}: {str(e)}"
300
+ logger.error(error_msg)
301
+ if not isinstance(e, PlatformError):
302
+ raise PlatformError(error_msg, platform=self.platform_name) from e
303
+ raise