marqetive-lib 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -1,291 +0,0 @@
1
- """Progress tracking system for long-running operations.
2
-
3
- This module provides models and utilities for tracking progress of operations
4
- like media uploads, post creation, and other long-running tasks.
5
- """
6
-
7
- from collections.abc import Callable
8
- from datetime import datetime
9
- from enum import Enum
10
- from typing import Any
11
-
12
- from pydantic import BaseModel, Field
13
-
14
-
15
- class ProgressStatus(str, Enum):
16
- """Status of a progress event."""
17
-
18
- STARTED = "started"
19
- IN_PROGRESS = "in_progress"
20
- COMPLETED = "completed"
21
- FAILED = "failed"
22
- CANCELLED = "cancelled"
23
-
24
-
25
- class ProgressEvent(BaseModel):
26
- """Represents a progress event for an operation.
27
-
28
- Attributes:
29
- operation: Name of the operation (e.g., "upload_media", "create_post").
30
- progress: Current progress value (e.g., bytes uploaded).
31
- total: Total expected value (e.g., total bytes).
32
- status: Current status of the operation.
33
- message: Optional human-readable message.
34
- metadata: Additional operation-specific data.
35
- timestamp: When this event occurred.
36
-
37
- Example:
38
- >>> event = ProgressEvent(
39
- ... operation="upload_media",
40
- ... progress=500,
41
- ... total=1000,
42
- ... status=ProgressStatus.IN_PROGRESS,
43
- ... message="Uploading image..."
44
- ... )
45
- >>> print(f"Progress: {event.percentage}%")
46
- Progress: 50.0%
47
- """
48
-
49
- operation: str
50
- progress: int | float
51
- total: int | float | None = None
52
- status: ProgressStatus
53
- message: str | None = None
54
- metadata: dict[str, Any] = Field(default_factory=dict)
55
- timestamp: datetime = Field(default_factory=datetime.now)
56
-
57
- @property
58
- def percentage(self) -> float:
59
- """Calculate progress as a percentage.
60
-
61
- Returns:
62
- Progress percentage (0-100), or 0 if total is None.
63
- """
64
- if self.total is None or self.total == 0:
65
- return 0.0
66
- return (self.progress / self.total) * 100
67
-
68
- def is_complete(self) -> bool:
69
- """Check if operation is complete.
70
-
71
- Returns:
72
- True if status is COMPLETED, False otherwise.
73
- """
74
- return self.status == ProgressStatus.COMPLETED
75
-
76
- def is_failed(self) -> bool:
77
- """Check if operation failed.
78
-
79
- Returns:
80
- True if status is FAILED, False otherwise.
81
- """
82
- return self.status == ProgressStatus.FAILED
83
-
84
-
85
- # Type alias for progress callback functions
86
- ProgressCallback = Callable[[ProgressEvent], None]
87
-
88
-
89
- class ProgressTracker:
90
- """Tracks and emits progress events for operations.
91
-
92
- Manages multiple progress callbacks and provides utilities for
93
- emitting progress events with consistent formatting.
94
-
95
- Example:
96
- >>> tracker = ProgressTracker()
97
- >>> tracker.add_callback(lambda e: print(f"{e.operation}: {e.percentage}%"))
98
- >>>
99
- >>> tracker.emit_start("upload_media")
100
- >>> tracker.emit_progress("upload_media", 500, 1000, "Uploading...")
101
- >>> tracker.emit_complete("upload_media", "Upload complete")
102
- """
103
-
104
- def __init__(self) -> None:
105
- """Initialize progress tracker."""
106
- self._callbacks: list[ProgressCallback] = []
107
-
108
- def add_callback(self, callback: ProgressCallback) -> None:
109
- """Add a progress callback.
110
-
111
- Args:
112
- callback: Function to call when progress events occur.
113
-
114
- Example:
115
- >>> def my_callback(event: ProgressEvent) -> None:
116
- ... print(f"{event.operation}: {event.percentage}%")
117
- >>>
118
- >>> tracker.add_callback(my_callback)
119
- """
120
- self._callbacks.append(callback)
121
-
122
- def remove_callback(self, callback: ProgressCallback) -> None:
123
- """Remove a progress callback.
124
-
125
- Args:
126
- callback: Callback to remove.
127
- """
128
- if callback in self._callbacks:
129
- self._callbacks.remove(callback)
130
-
131
- def clear_callbacks(self) -> None:
132
- """Remove all callbacks."""
133
- self._callbacks.clear()
134
-
135
- def emit(self, event: ProgressEvent) -> None:
136
- """Emit a progress event to all callbacks.
137
-
138
- Args:
139
- event: Progress event to emit.
140
- """
141
- import contextlib
142
-
143
- for callback in self._callbacks:
144
- with contextlib.suppress(Exception):
145
- # Silently ignore callback errors to prevent disrupting operations
146
- callback(event)
147
-
148
- def emit_start(
149
- self,
150
- operation: str,
151
- total: int | float | None = None,
152
- message: str | None = None,
153
- **metadata: Any,
154
- ) -> None:
155
- """Emit an operation start event.
156
-
157
- Args:
158
- operation: Name of the operation.
159
- total: Total expected value (optional).
160
- message: Optional message.
161
- **metadata: Additional metadata.
162
-
163
- Example:
164
- >>> tracker.emit_start("upload_media", total=1024000,
165
- ... message="Starting upload...")
166
- """
167
- event = ProgressEvent(
168
- operation=operation,
169
- progress=0,
170
- total=total,
171
- status=ProgressStatus.STARTED,
172
- message=message or f"Starting {operation}",
173
- metadata=metadata,
174
- )
175
- self.emit(event)
176
-
177
- def emit_progress(
178
- self,
179
- operation: str,
180
- progress: int | float,
181
- total: int | float | None = None,
182
- message: str | None = None,
183
- **metadata: Any,
184
- ) -> None:
185
- """Emit a progress update event.
186
-
187
- Args:
188
- operation: Name of the operation.
189
- progress: Current progress value.
190
- total: Total expected value (optional).
191
- message: Optional message.
192
- **metadata: Additional metadata.
193
-
194
- Example:
195
- >>> tracker.emit_progress("upload_media", 512000, 1024000,
196
- ... message="Uploading...")
197
- """
198
- event = ProgressEvent(
199
- operation=operation,
200
- progress=progress,
201
- total=total,
202
- status=ProgressStatus.IN_PROGRESS,
203
- message=message,
204
- metadata=metadata,
205
- )
206
- self.emit(event)
207
-
208
- def emit_complete(
209
- self,
210
- operation: str,
211
- message: str | None = None,
212
- **metadata: Any,
213
- ) -> None:
214
- """Emit an operation complete event.
215
-
216
- Args:
217
- operation: Name of the operation.
218
- message: Optional message.
219
- **metadata: Additional metadata.
220
-
221
- Example:
222
- >>> tracker.emit_complete("upload_media",
223
- ... message="Upload successful!")
224
- """
225
- event = ProgressEvent(
226
- operation=operation,
227
- progress=100,
228
- total=100,
229
- status=ProgressStatus.COMPLETED,
230
- message=message or f"{operation} completed",
231
- metadata=metadata,
232
- )
233
- self.emit(event)
234
-
235
- def emit_failed(
236
- self,
237
- operation: str,
238
- error: str | Exception,
239
- **metadata: Any,
240
- ) -> None:
241
- """Emit an operation failed event.
242
-
243
- Args:
244
- operation: Name of the operation.
245
- error: Error message or exception.
246
- **metadata: Additional metadata.
247
-
248
- Example:
249
- >>> try:
250
- ... # Some operation
251
- ... pass
252
- ... except Exception as e:
253
- ... tracker.emit_failed("upload_media", e)
254
- """
255
- message = str(error) if isinstance(error, Exception) else error
256
- event = ProgressEvent(
257
- operation=operation,
258
- progress=0,
259
- total=100,
260
- status=ProgressStatus.FAILED,
261
- message=message,
262
- metadata=metadata,
263
- )
264
- self.emit(event)
265
-
266
- def emit_cancelled(
267
- self,
268
- operation: str,
269
- message: str | None = None,
270
- **metadata: Any,
271
- ) -> None:
272
- """Emit an operation cancelled event.
273
-
274
- Args:
275
- operation: Name of the operation.
276
- message: Optional message.
277
- **metadata: Additional metadata.
278
-
279
- Example:
280
- >>> tracker.emit_cancelled("upload_media",
281
- ... message="Upload cancelled by user")
282
- """
283
- event = ProgressEvent(
284
- operation=operation,
285
- progress=0,
286
- total=100,
287
- status=ProgressStatus.CANCELLED,
288
- message=message or f"{operation} cancelled",
289
- metadata=metadata,
290
- )
291
- self.emit(event)
@@ -1,257 +0,0 @@
1
- """Platform registry for managing platform manager instances.
2
-
3
- This module provides a singleton registry pattern for registering and accessing
4
- platform managers across the application.
5
- """
6
-
7
- import threading
8
- from typing import Any, TypeVar
9
-
10
- from marqetive.platforms.models import AuthCredentials
11
-
12
- # Type variable for manager classes
13
- ManagerType = TypeVar("ManagerType")
14
-
15
-
16
- class PlatformRegistry:
17
- """Singleton registry for platform managers.
18
-
19
- The registry maintains a mapping of platform names to their manager classes
20
- and provides caching of manager instances to avoid unnecessary recreation.
21
-
22
- Thread-safe implementation using threading.Lock.
23
-
24
- Example:
25
- >>> from marqetive.platforms.twitter.manager import TwitterPostManager
26
- >>> registry = PlatformRegistry()
27
- >>> registry.register_platform("twitter", TwitterPostManager)
28
- >>> manager = registry.get_manager("twitter", credentials=creds)
29
- """
30
-
31
- _instance: "PlatformRegistry | None" = None
32
- _lock: threading.Lock = threading.Lock()
33
-
34
- def __new__(cls) -> "PlatformRegistry":
35
- """Ensure only one instance exists (singleton pattern)."""
36
- if cls._instance is None:
37
- with cls._lock:
38
- # Double-check locking pattern
39
- if cls._instance is None:
40
- cls._instance = super().__new__(cls)
41
- cls._instance._initialized = False
42
- return cls._instance
43
-
44
- def __init__(self) -> None:
45
- """Initialize the registry."""
46
- # Only initialize once
47
- if self._initialized:
48
- return
49
-
50
- self._platforms: dict[str, type] = {}
51
- self._manager_cache: dict[str, Any] = {}
52
- self._cache_lock = threading.Lock()
53
- self._initialized = True
54
-
55
- def register_platform(self, platform_name: str, manager_class: type) -> None:
56
- """Register a platform manager class.
57
-
58
- Args:
59
- platform_name: Name of the platform (e.g., "twitter", "linkedin").
60
- manager_class: The manager class to register.
61
-
62
- Raises:
63
- ValueError: If platform is already registered.
64
-
65
- Example:
66
- >>> registry.register_platform("twitter", TwitterPostManager)
67
- """
68
- if platform_name in self._platforms:
69
- raise ValueError(f"Platform '{platform_name}' is already registered")
70
-
71
- self._platforms[platform_name] = manager_class
72
-
73
- def unregister_platform(self, platform_name: str) -> None:
74
- """Unregister a platform and clear its cached instances.
75
-
76
- Args:
77
- platform_name: Name of the platform to unregister.
78
-
79
- Example:
80
- >>> registry.unregister_platform("twitter")
81
- """
82
- with self._cache_lock:
83
- self._platforms.pop(platform_name, None)
84
- # Clear cached instances for this platform
85
- keys_to_remove = [
86
- k for k in self._manager_cache if k.startswith(f"{platform_name}:")
87
- ]
88
- for key in keys_to_remove:
89
- self._manager_cache.pop(key)
90
-
91
- def get_manager(
92
- self,
93
- platform_name: str,
94
- use_cache: bool = True,
95
- **kwargs: Any,
96
- ) -> Any:
97
- """Get or create a manager instance for the specified platform.
98
-
99
- Args:
100
- platform_name: Name of the platform (e.g., "twitter", "linkedin").
101
- use_cache: Whether to use cached instance (default: True).
102
- **kwargs: Arguments to pass to the manager constructor.
103
-
104
- Returns:
105
- Manager instance for the platform.
106
-
107
- Raises:
108
- ValueError: If platform is not registered.
109
-
110
- Example:
111
- >>> manager = registry.get_manager("twitter", credentials=creds)
112
- >>> post = await manager.execute_post(...)
113
- """
114
- if platform_name not in self._platforms:
115
- available = ", ".join(self.get_available_platforms())
116
- raise ValueError(
117
- f"Platform '{platform_name}' is not registered. "
118
- f"Available platforms: {available}"
119
- )
120
-
121
- # Generate cache key from platform name and kwargs
122
- cache_key = self._generate_cache_key(platform_name, **kwargs)
123
-
124
- if use_cache:
125
- with self._cache_lock:
126
- if cache_key in self._manager_cache:
127
- return self._manager_cache[cache_key]
128
-
129
- # Create new manager instance
130
- manager_class = self._platforms[platform_name]
131
- manager = manager_class(**kwargs)
132
-
133
- if use_cache:
134
- with self._cache_lock:
135
- self._manager_cache[cache_key] = manager
136
-
137
- return manager
138
-
139
- def get_available_platforms(self) -> list[str]:
140
- """Get list of all registered platform names.
141
-
142
- Returns:
143
- List of platform names.
144
-
145
- Example:
146
- >>> platforms = registry.get_available_platforms()
147
- >>> print(platforms)
148
- ['twitter', 'linkedin', 'instagram', 'tiktok']
149
- """
150
- return list(self._platforms.keys())
151
-
152
- def clear_cache(self) -> None:
153
- """Clear all cached manager instances.
154
-
155
- Example:
156
- >>> registry.clear_cache()
157
- """
158
- with self._cache_lock:
159
- self._manager_cache.clear()
160
-
161
- def _generate_cache_key(self, platform_name: str, **kwargs: Any) -> str:
162
- """Generate a cache key for manager instance.
163
-
164
- Args:
165
- platform_name: Name of the platform.
166
- **kwargs: Manager constructor arguments.
167
-
168
- Returns:
169
- Cache key string.
170
- """
171
- # For credentials, use account_id if available
172
- if "credentials" in kwargs:
173
- creds = kwargs["credentials"]
174
- if isinstance(creds, AuthCredentials) and creds.user_id:
175
- return f"{platform_name}:user_id:{creds.user_id}"
176
-
177
- # Default to platform name only
178
- return platform_name
179
-
180
-
181
- # Global registry instance
182
- _global_registry: PlatformRegistry | None = None
183
- _global_lock = threading.Lock()
184
-
185
-
186
- def get_registry() -> PlatformRegistry:
187
- """Get the global platform registry instance.
188
-
189
- Returns:
190
- Global PlatformRegistry instance.
191
-
192
- Example:
193
- >>> registry = get_registry()
194
- >>> manager = registry.get_manager("twitter", credentials=creds)
195
- """
196
- global _global_registry
197
-
198
- if _global_registry is None:
199
- with _global_lock:
200
- if _global_registry is None:
201
- _global_registry = PlatformRegistry()
202
-
203
- return _global_registry
204
-
205
-
206
- def register_platform(platform_name: str, manager_class: type) -> None:
207
- """Register a platform in the global registry.
208
-
209
- Convenience function that uses the global registry.
210
-
211
- Args:
212
- platform_name: Name of the platform.
213
- manager_class: The manager class to register.
214
-
215
- Example:
216
- >>> from marqetive.platforms.twitter.manager import TwitterPostManager
217
- >>> register_platform("twitter", TwitterPostManager)
218
- """
219
- registry = get_registry()
220
- registry.register_platform(platform_name, manager_class)
221
-
222
-
223
- def get_manager_for_platform(platform_name: str, **kwargs: Any) -> Any:
224
- """Get a manager instance for the specified platform.
225
-
226
- Convenience function that uses the global registry.
227
-
228
- Args:
229
- platform_name: Name of the platform.
230
- **kwargs: Arguments to pass to the manager constructor.
231
-
232
- Returns:
233
- Manager instance for the platform.
234
-
235
- Example:
236
- >>> manager = get_manager_for_platform("twitter", credentials=creds)
237
- >>> post = await manager.execute_post(...)
238
- """
239
- registry = get_registry()
240
- return registry.get_manager(platform_name, **kwargs)
241
-
242
-
243
- def get_available_platforms() -> list[str]:
244
- """Get list of all registered platforms.
245
-
246
- Convenience function that uses the global registry.
247
-
248
- Returns:
249
- List of platform names.
250
-
251
- Example:
252
- >>> platforms = get_available_platforms()
253
- >>> print(platforms)
254
- ['twitter', 'linkedin', 'instagram']
255
- """
256
- registry = get_registry()
257
- return registry.get_available_platforms()
@@ -1,106 +0,0 @@
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