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.
- marqetive/__init__.py +113 -0
- marqetive/core/__init__.py +5 -0
- marqetive/core/account_factory.py +212 -0
- marqetive/core/base_manager.py +303 -0
- marqetive/core/client.py +108 -0
- marqetive/core/progress.py +291 -0
- marqetive/core/registry.py +257 -0
- marqetive/platforms/__init__.py +55 -0
- marqetive/platforms/base.py +390 -0
- marqetive/platforms/exceptions.py +238 -0
- marqetive/platforms/instagram/__init__.py +7 -0
- marqetive/platforms/instagram/client.py +786 -0
- marqetive/platforms/instagram/exceptions.py +311 -0
- marqetive/platforms/instagram/factory.py +106 -0
- marqetive/platforms/instagram/manager.py +112 -0
- marqetive/platforms/instagram/media.py +669 -0
- marqetive/platforms/linkedin/__init__.py +7 -0
- marqetive/platforms/linkedin/client.py +733 -0
- marqetive/platforms/linkedin/exceptions.py +335 -0
- marqetive/platforms/linkedin/factory.py +130 -0
- marqetive/platforms/linkedin/manager.py +119 -0
- marqetive/platforms/linkedin/media.py +549 -0
- marqetive/platforms/models.py +345 -0
- marqetive/platforms/tiktok/__init__.py +0 -0
- marqetive/platforms/twitter/__init__.py +7 -0
- marqetive/platforms/twitter/client.py +647 -0
- marqetive/platforms/twitter/exceptions.py +311 -0
- marqetive/platforms/twitter/factory.py +151 -0
- marqetive/platforms/twitter/manager.py +121 -0
- marqetive/platforms/twitter/media.py +779 -0
- marqetive/platforms/twitter/threads.py +442 -0
- marqetive/py.typed +0 -0
- marqetive/registry_init.py +66 -0
- marqetive/utils/__init__.py +45 -0
- marqetive/utils/file_handlers.py +438 -0
- marqetive/utils/helpers.py +99 -0
- marqetive/utils/media.py +399 -0
- marqetive/utils/oauth.py +265 -0
- marqetive/utils/retry.py +239 -0
- marqetive/utils/token_validator.py +240 -0
- marqetive_lib-0.1.0.dist-info/METADATA +261 -0
- marqetive_lib-0.1.0.dist-info/RECORD +43 -0
- marqetive_lib-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""LinkedIn-specific exception handling and error code mapping.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive error handling for LinkedIn API errors including:
|
|
4
|
+
- HTTP status code mapping
|
|
5
|
+
- LinkedIn-specific service 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
|
+
PostNotFoundError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# LinkedIn API error codes
|
|
23
|
+
# Source: https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/error-handling
|
|
24
|
+
class LinkedInErrorCode:
|
|
25
|
+
"""LinkedIn API error codes."""
|
|
26
|
+
|
|
27
|
+
# Authentication errors (401)
|
|
28
|
+
INVALID_TOKEN = 401
|
|
29
|
+
EXPIRED_TOKEN = 401
|
|
30
|
+
REVOKED_ACCESS = 401
|
|
31
|
+
|
|
32
|
+
# Authorization errors (403)
|
|
33
|
+
ACCESS_DENIED = 403
|
|
34
|
+
INSUFFICIENT_PERMISSIONS = 403
|
|
35
|
+
|
|
36
|
+
# Resource errors (404)
|
|
37
|
+
RESOURCE_NOT_FOUND = 404
|
|
38
|
+
ENTITY_NOT_FOUND = 404
|
|
39
|
+
|
|
40
|
+
# Validation errors (400)
|
|
41
|
+
BAD_REQUEST = 400
|
|
42
|
+
MALFORMED_REQUEST = 400
|
|
43
|
+
INVALID_PARAMETERS = 400
|
|
44
|
+
|
|
45
|
+
# Rate limiting (429)
|
|
46
|
+
RATE_LIMIT_EXCEEDED = 429
|
|
47
|
+
THROTTLE_LIMIT_REACHED = 429
|
|
48
|
+
|
|
49
|
+
# Server errors (500+)
|
|
50
|
+
INTERNAL_SERVER_ERROR = 500
|
|
51
|
+
SERVICE_UNAVAILABLE = 503
|
|
52
|
+
GATEWAY_TIMEOUT = 504
|
|
53
|
+
|
|
54
|
+
# Protocol errors
|
|
55
|
+
METHOD_NOT_ALLOWED = 405
|
|
56
|
+
LENGTH_REQUIRED = 411
|
|
57
|
+
VERSION_DEPRECATED = 426
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Mapping of status codes to user-friendly messages
|
|
61
|
+
ERROR_MESSAGES: dict[int, str] = {
|
|
62
|
+
# Authentication
|
|
63
|
+
401: "Invalid or expired access token. Please re-authenticate.",
|
|
64
|
+
# Authorization
|
|
65
|
+
403: "Access denied. Insufficient permissions to access this resource.",
|
|
66
|
+
# Resources
|
|
67
|
+
404: "The requested resource or entity does not exist.",
|
|
68
|
+
405: "HTTP method not allowed for this endpoint.",
|
|
69
|
+
# Validation
|
|
70
|
+
400: "Bad request. Please check your request parameters.",
|
|
71
|
+
411: "Content-Length header is required for this request.",
|
|
72
|
+
426: "API version header is deprecated. Please update to latest version.",
|
|
73
|
+
# Rate limiting
|
|
74
|
+
429: "Rate limit exceeded. Please reduce request frequency.",
|
|
75
|
+
# Server errors
|
|
76
|
+
500: "LinkedIn server error. Please try again later.",
|
|
77
|
+
503: "LinkedIn service temporarily unavailable. Please try again later.",
|
|
78
|
+
504: "Gateway timeout. LinkedIn servers took too long to respond.",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Retryable HTTP status codes
|
|
83
|
+
RETRYABLE_STATUS_CODES = {
|
|
84
|
+
408, # Request timeout
|
|
85
|
+
429, # Too many requests
|
|
86
|
+
500, # Internal server error
|
|
87
|
+
502, # Bad gateway
|
|
88
|
+
503, # Service unavailable
|
|
89
|
+
504, # Gateway timeout
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Non-retryable HTTP status codes
|
|
93
|
+
NON_RETRYABLE_STATUS_CODES = {
|
|
94
|
+
400, # Bad request
|
|
95
|
+
401, # Unauthorized
|
|
96
|
+
403, # Forbidden
|
|
97
|
+
404, # Not found
|
|
98
|
+
405, # Method not allowed
|
|
99
|
+
411, # Length required
|
|
100
|
+
426, # Version deprecated
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def map_linkedin_error(
|
|
105
|
+
status_code: int | None,
|
|
106
|
+
service_error_code: int | None = None,
|
|
107
|
+
error_message: str | None = None,
|
|
108
|
+
response_data: dict[str, Any] | None = None,
|
|
109
|
+
) -> PlatformError:
|
|
110
|
+
"""Map LinkedIn API error to appropriate exception.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
status_code: HTTP status code.
|
|
114
|
+
service_error_code: LinkedIn-specific service error code.
|
|
115
|
+
error_message: Error message from API.
|
|
116
|
+
response_data: Full response data from API.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Appropriate PlatformError subclass.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> error = map_linkedin_error(401, error_message="Invalid token")
|
|
123
|
+
>>> print(type(error).__name__)
|
|
124
|
+
PlatformAuthError
|
|
125
|
+
"""
|
|
126
|
+
# Extract error details from response if provided
|
|
127
|
+
if response_data:
|
|
128
|
+
if not service_error_code:
|
|
129
|
+
service_error_code = response_data.get("serviceErrorCode")
|
|
130
|
+
if not error_message:
|
|
131
|
+
error_message = response_data.get("message")
|
|
132
|
+
if not status_code:
|
|
133
|
+
status_code = response_data.get("status")
|
|
134
|
+
|
|
135
|
+
# Get user-friendly message
|
|
136
|
+
friendly_message = ERROR_MESSAGES.get(
|
|
137
|
+
status_code or 0, error_message or "Unknown error"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Determine retry-after for rate limits
|
|
141
|
+
retry_after = None
|
|
142
|
+
if status_code == 429:
|
|
143
|
+
# LinkedIn typically uses 1-hour windows
|
|
144
|
+
retry_after = 3600 # 1 hour in seconds
|
|
145
|
+
|
|
146
|
+
# Map to appropriate exception type
|
|
147
|
+
# Authentication errors
|
|
148
|
+
if status_code == 401:
|
|
149
|
+
return PlatformAuthError(
|
|
150
|
+
friendly_message,
|
|
151
|
+
platform="linkedin",
|
|
152
|
+
status_code=status_code,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Authorization errors
|
|
156
|
+
if status_code == 403:
|
|
157
|
+
return PlatformAuthError(
|
|
158
|
+
friendly_message,
|
|
159
|
+
platform="linkedin",
|
|
160
|
+
status_code=status_code,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Rate limit errors
|
|
164
|
+
if status_code == 429:
|
|
165
|
+
return RateLimitError(
|
|
166
|
+
friendly_message,
|
|
167
|
+
platform="linkedin",
|
|
168
|
+
status_code=status_code,
|
|
169
|
+
retry_after=retry_after,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Resource not found
|
|
173
|
+
if status_code == 404:
|
|
174
|
+
# Try to extract post/resource ID if available
|
|
175
|
+
resource_id = None
|
|
176
|
+
if response_data and isinstance(response_data, dict):
|
|
177
|
+
# Common patterns for resource IDs in LinkedIn responses
|
|
178
|
+
resource_id = response_data.get("id") or response_data.get("entityUrn")
|
|
179
|
+
|
|
180
|
+
if resource_id:
|
|
181
|
+
return PostNotFoundError(
|
|
182
|
+
post_id=str(resource_id),
|
|
183
|
+
platform="linkedin",
|
|
184
|
+
status_code=status_code,
|
|
185
|
+
)
|
|
186
|
+
return PlatformError(
|
|
187
|
+
friendly_message,
|
|
188
|
+
platform="linkedin",
|
|
189
|
+
status_code=status_code,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Validation errors
|
|
193
|
+
if status_code in (400, 411, 426):
|
|
194
|
+
return ValidationError(
|
|
195
|
+
friendly_message,
|
|
196
|
+
platform="linkedin",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Media upload errors (detected by context or specific codes)
|
|
200
|
+
# LinkedIn doesn't have specific codes, but we can detect from message
|
|
201
|
+
if error_message and any(
|
|
202
|
+
keyword in error_message.lower()
|
|
203
|
+
for keyword in ["upload", "media", "asset", "file", "video", "image"]
|
|
204
|
+
):
|
|
205
|
+
return MediaUploadError(
|
|
206
|
+
friendly_message,
|
|
207
|
+
platform="linkedin",
|
|
208
|
+
status_code=status_code,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Generic platform error
|
|
212
|
+
return PlatformError(
|
|
213
|
+
friendly_message,
|
|
214
|
+
platform="linkedin",
|
|
215
|
+
status_code=status_code,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def is_retryable_linkedin_error(
|
|
220
|
+
status_code: int | None,
|
|
221
|
+
service_error_code: int | None = None, # noqa: ARG001
|
|
222
|
+
) -> bool:
|
|
223
|
+
"""Determine if a LinkedIn error is retryable.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
status_code: HTTP status code.
|
|
227
|
+
service_error_code: LinkedIn-specific service error code.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if error is retryable, False otherwise.
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
>>> is_retryable_linkedin_error(503)
|
|
234
|
+
True
|
|
235
|
+
>>> is_retryable_linkedin_error(401)
|
|
236
|
+
False
|
|
237
|
+
"""
|
|
238
|
+
# Check explicit non-retryable codes first
|
|
239
|
+
if status_code in NON_RETRYABLE_STATUS_CODES:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Check retryable codes
|
|
243
|
+
if status_code in RETRYABLE_STATUS_CODES:
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
# Check HTTP status codes
|
|
247
|
+
if status_code:
|
|
248
|
+
# 5xx errors are generally retryable
|
|
249
|
+
if 500 <= status_code < 600:
|
|
250
|
+
return True
|
|
251
|
+
# 4xx errors (except 429, 408) are generally not retryable
|
|
252
|
+
if 400 <= status_code < 500:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# Default to not retryable for safety
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_retry_delay(
|
|
260
|
+
status_code: int | None,
|
|
261
|
+
service_error_code: int | None = None, # noqa: ARG001
|
|
262
|
+
attempt: int = 1,
|
|
263
|
+
) -> float:
|
|
264
|
+
"""Get recommended retry delay for LinkedIn error.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
status_code: HTTP status code.
|
|
268
|
+
service_error_code: LinkedIn-specific service error code.
|
|
269
|
+
attempt: Current retry attempt number.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Recommended delay in seconds.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> get_retry_delay(503, attempt=1)
|
|
276
|
+
5.0
|
|
277
|
+
>>> get_retry_delay(429)
|
|
278
|
+
3600.0
|
|
279
|
+
"""
|
|
280
|
+
# Rate limit errors - wait full window
|
|
281
|
+
if status_code == 429:
|
|
282
|
+
return 3600.0 # 1 hour
|
|
283
|
+
|
|
284
|
+
# Server errors - exponential backoff
|
|
285
|
+
if status_code and 500 <= status_code < 600:
|
|
286
|
+
base_delay = 5.0
|
|
287
|
+
return min(base_delay * (2 ** (attempt - 1)), 120.0)
|
|
288
|
+
|
|
289
|
+
# Gateway timeout - longer delay
|
|
290
|
+
if status_code == 504:
|
|
291
|
+
return 30.0
|
|
292
|
+
|
|
293
|
+
# Default delay
|
|
294
|
+
return 10.0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class LinkedInAPIError(PlatformError):
|
|
298
|
+
"""LinkedIn API specific error with detailed information.
|
|
299
|
+
|
|
300
|
+
Attributes:
|
|
301
|
+
service_error_code: LinkedIn-specific service error code.
|
|
302
|
+
is_retryable: Whether the error is retryable.
|
|
303
|
+
retry_delay: Recommended retry delay in seconds.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(
|
|
307
|
+
self,
|
|
308
|
+
message: str,
|
|
309
|
+
*,
|
|
310
|
+
status_code: int | None = None,
|
|
311
|
+
service_error_code: int | None = None,
|
|
312
|
+
response_data: dict[str, Any] | None = None,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Initialize LinkedIn API error.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
message: Error message.
|
|
318
|
+
status_code: HTTP status code.
|
|
319
|
+
service_error_code: LinkedIn-specific service error code.
|
|
320
|
+
response_data: Full response data from API.
|
|
321
|
+
"""
|
|
322
|
+
super().__init__(message, platform="linkedin", status_code=status_code)
|
|
323
|
+
self.service_error_code = service_error_code
|
|
324
|
+
self.response_data = response_data
|
|
325
|
+
self.is_retryable = is_retryable_linkedin_error(status_code, service_error_code)
|
|
326
|
+
self.retry_delay = get_retry_delay(status_code, service_error_code)
|
|
327
|
+
|
|
328
|
+
def __repr__(self) -> str:
|
|
329
|
+
"""String representation of error."""
|
|
330
|
+
return (
|
|
331
|
+
f"LinkedInAPIError(message={self.message!r}, "
|
|
332
|
+
f"status_code={self.status_code}, "
|
|
333
|
+
f"service_error_code={self.service_error_code}, "
|
|
334
|
+
f"retryable={self.is_retryable})"
|
|
335
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""LinkedIn 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.linkedin.client import LinkedInClient
|
|
10
|
+
from marqetive.platforms.models import AccountStatus, AuthCredentials
|
|
11
|
+
from marqetive.utils.oauth import refresh_linkedin_token
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LinkedInAccountFactory(BaseAccountFactory):
|
|
17
|
+
"""Factory for creating and managing LinkedIn accounts and clients.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> factory = LinkedInAccountFactory(
|
|
21
|
+
... client_id="your_client_id",
|
|
22
|
+
... client_secret="your_client_secret"
|
|
23
|
+
... )
|
|
24
|
+
>>> credentials = AuthCredentials(
|
|
25
|
+
... platform="linkedin",
|
|
26
|
+
... access_token="token",
|
|
27
|
+
... refresh_token="refresh"
|
|
28
|
+
... )
|
|
29
|
+
>>> client = await factory.create_authenticated_client(credentials)
|
|
30
|
+
>>> async with client:
|
|
31
|
+
... post = 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 LinkedIn account factory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
client_id: LinkedIn OAuth client ID (uses LINKEDIN_CLIENT_ID env if None).
|
|
44
|
+
client_secret: LinkedIn OAuth client secret (uses LINKEDIN_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("LINKEDIN_CLIENT_ID")
|
|
49
|
+
self.client_secret = client_secret or os.getenv("LINKEDIN_CLIENT_SECRET")
|
|
50
|
+
|
|
51
|
+
if not self.client_id or not self.client_secret:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"LinkedIn 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 "linkedin"
|
|
61
|
+
|
|
62
|
+
async def refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
|
|
63
|
+
"""Refresh LinkedIn 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
|
+
"LinkedIn 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 LinkedIn access token...")
|
|
87
|
+
return await refresh_linkedin_token(
|
|
88
|
+
credentials,
|
|
89
|
+
self.client_id,
|
|
90
|
+
self.client_secret,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def create_client(self, credentials: AuthCredentials) -> LinkedInClient:
|
|
94
|
+
"""Create LinkedIn API client.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
credentials: Valid LinkedIn credentials.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
LinkedInClient 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
|
+
return LinkedInClient(credentials=credentials)
|
|
112
|
+
|
|
113
|
+
async def validate_credentials(self, credentials: AuthCredentials) -> bool:
|
|
114
|
+
"""Validate LinkedIn credentials by making a test API call.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
credentials: Credentials to validate.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if credentials are valid, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
client = await self.create_client(credentials)
|
|
124
|
+
async with client:
|
|
125
|
+
# Try to verify credentials by getting current user
|
|
126
|
+
# This would need to be implemented in the client
|
|
127
|
+
return await client.is_authenticated()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Error validating LinkedIn credentials: {e}")
|
|
130
|
+
return False
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""LinkedIn 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.linkedin.client import LinkedInClient
|
|
8
|
+
from marqetive.platforms.linkedin.factory import LinkedInAccountFactory
|
|
9
|
+
from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LinkedInPostManager(BasePostManager):
|
|
15
|
+
"""Manager for LinkedIn post operations.
|
|
16
|
+
|
|
17
|
+
Coordinates post creation, media uploads, and progress tracking for LinkedIn.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> manager = LinkedInPostManager()
|
|
21
|
+
>>> credentials = AuthCredentials(
|
|
22
|
+
... platform="linkedin",
|
|
23
|
+
... access_token="token",
|
|
24
|
+
... refresh_token="refresh"
|
|
25
|
+
... )
|
|
26
|
+
>>> request = PostCreateRequest(content="Hello LinkedIn!")
|
|
27
|
+
>>> post = await manager.execute_post(credentials, request)
|
|
28
|
+
>>> print(f"Post URN: {post.post_id}")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
account_factory: LinkedInAccountFactory | None = None,
|
|
34
|
+
client_id: str | None = None,
|
|
35
|
+
client_secret: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize LinkedIn post manager.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
account_factory: LinkedIn account factory (creates default if None).
|
|
41
|
+
client_id: LinkedIn OAuth client ID (for default factory).
|
|
42
|
+
client_secret: LinkedIn OAuth client secret (for default factory).
|
|
43
|
+
"""
|
|
44
|
+
if account_factory is None:
|
|
45
|
+
account_factory = LinkedInAccountFactory(
|
|
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 "linkedin"
|
|
56
|
+
|
|
57
|
+
async def _execute_post_impl(
|
|
58
|
+
self,
|
|
59
|
+
client: Any,
|
|
60
|
+
request: PostCreateRequest,
|
|
61
|
+
credentials: AuthCredentials, # noqa: ARG002
|
|
62
|
+
) -> Post:
|
|
63
|
+
"""Execute LinkedIn post creation.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
client: LinkedInClient instance.
|
|
67
|
+
request: Post creation request.
|
|
68
|
+
credentials: LinkedIn credentials.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Created Post object.
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(client, LinkedInClient):
|
|
74
|
+
raise TypeError(f"Expected LinkedInClient, 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
|
+
media_attachment = await client.upload_media(
|
|
97
|
+
media_url=media_url,
|
|
98
|
+
media_type="image", # Default to image
|
|
99
|
+
alt_text=None,
|
|
100
|
+
)
|
|
101
|
+
media_ids.append(media_attachment.media_id)
|
|
102
|
+
|
|
103
|
+
self._progress_tracker.emit_complete(
|
|
104
|
+
"upload_media",
|
|
105
|
+
message="All media uploaded successfully",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Create post with progress tracking
|
|
109
|
+
self._progress_tracker.emit_progress(
|
|
110
|
+
"execute_post",
|
|
111
|
+
progress=50,
|
|
112
|
+
total=100,
|
|
113
|
+
message="Creating LinkedIn post...",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Use the client to create the post
|
|
117
|
+
post = await client.create_post(request)
|
|
118
|
+
|
|
119
|
+
return post
|