marqetive-lib 0.1.20__py3-none-any.whl → 0.1.21__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.
@@ -47,15 +47,44 @@ class PlatformAuthError(PlatformError):
47
47
  - OAuth flow encounters errors
48
48
  - Insufficient permissions for requested operation
49
49
 
50
+ Args:
51
+ message: Human-readable error message
52
+ platform: Name of the platform where error occurred
53
+ status_code: HTTP status code if applicable
54
+ requires_reconnection: If True, user must re-authenticate (token permanently invalid)
55
+
50
56
  Example:
51
57
  >>> raise PlatformAuthError(
52
58
  ... "Access token expired",
53
59
  ... platform="twitter",
54
60
  ... status_code=401
55
61
  ... )
62
+ >>> # For invalid refresh token requiring user re-auth:
63
+ >>> raise PlatformAuthError(
64
+ ... "Refresh token invalid",
65
+ ... platform="twitter",
66
+ ... status_code=400,
67
+ ... requires_reconnection=True
68
+ ... )
56
69
  """
57
70
 
58
- pass
71
+ def __init__(
72
+ self,
73
+ message: str,
74
+ platform: str | None = None,
75
+ status_code: int | None = None,
76
+ *,
77
+ requires_reconnection: bool = False,
78
+ ) -> None:
79
+ self.requires_reconnection = requires_reconnection
80
+ super().__init__(message, platform, status_code)
81
+
82
+ def _format_message(self) -> str:
83
+ """Format the error message with reconnection info."""
84
+ base_message = super()._format_message()
85
+ if self.requires_reconnection:
86
+ return f"{base_message} | Reconnection required"
87
+ return base_message
59
88
 
60
89
 
61
90
  class RateLimitError(PlatformError):
marqetive/factory.py CHANGED
@@ -25,6 +25,24 @@ logger = logging.getLogger(__name__)
25
25
  # Supported platforms
26
26
  SUPPORTED_PLATFORMS = frozenset({"twitter", "linkedin", "instagram", "tiktok"})
27
27
 
28
+ # Platform aliases (alternative names that map to canonical names)
29
+ PLATFORM_ALIASES: dict[str, str] = {
30
+ "x": "twitter",
31
+ }
32
+
33
+
34
+ def normalize_platform(platform: str) -> str:
35
+ """Normalize platform name to canonical form.
36
+
37
+ Args:
38
+ platform: Platform name (may be an alias like 'x').
39
+
40
+ Returns:
41
+ Canonical platform name (e.g., 'twitter').
42
+ """
43
+ platform = platform.lower()
44
+ return PLATFORM_ALIASES.get(platform, platform)
45
+
28
46
 
29
47
  def _create_client(
30
48
  platform: str, credentials: "AuthCredentials"
@@ -172,7 +190,12 @@ class PlatformFactory:
172
190
  >>> async with client:
173
191
  ... post = await client.create_post(request)
174
192
  """
175
- platform = credentials.platform.lower()
193
+ # Normalize platform name (handle aliases like 'x' -> 'twitter')
194
+ platform = normalize_platform(credentials.platform)
195
+
196
+ # Update credentials with normalized platform name
197
+ if platform != credentials.platform.lower():
198
+ credentials.platform = platform
176
199
 
177
200
  # Validate platform-specific requirements
178
201
  self._validate_credentials(credentials)
@@ -180,8 +203,17 @@ class PlatformFactory:
180
203
  # Refresh token if needed
181
204
  if auto_refresh and credentials.needs_refresh():
182
205
  logger.info(f"Refreshing expired token for {platform}")
183
- credentials = await self._refresh_token(credentials)
184
- credentials.mark_valid()
206
+ try:
207
+ credentials = await self._refresh_token(credentials)
208
+ credentials.mark_valid()
209
+ except PlatformAuthError as e:
210
+ # If refresh failed and requires reconnection, update credentials status
211
+ if e.requires_reconnection:
212
+ credentials.mark_reconnection_required()
213
+ logger.warning(
214
+ f"Token refresh for {platform} requires user reconnection"
215
+ )
216
+ raise
185
217
 
186
218
  # Enrich credentials with API keys for Twitter (needed for media operations)
187
219
  if platform == "twitter":
@@ -337,9 +369,9 @@ class PlatformFactory:
337
369
  """Get the set of supported platform names.
338
370
 
339
371
  Returns:
340
- Frozenset of supported platform names.
372
+ Frozenset of supported platform names (includes aliases).
341
373
  """
342
- return SUPPORTED_PLATFORMS
374
+ return SUPPORTED_PLATFORMS | frozenset(PLATFORM_ALIASES.keys())
343
375
 
344
376
 
345
377
  async def get_client(
marqetive/utils/oauth.py CHANGED
@@ -180,10 +180,32 @@ async def refresh_twitter_token(
180
180
 
181
181
  except httpx.HTTPStatusError as e:
182
182
  logger.error(f"HTTP error refreshing Twitter token: {e.response.status_code}")
183
+
184
+ # Determine if this is a permanent failure requiring user re-authentication
185
+ # Twitter returns 400 with "invalid_request" or "invalid_grant" when:
186
+ # - Refresh token was already used (single-use tokens)
187
+ # - Refresh token expired
188
+ # - User revoked access
189
+ requires_reconnection = False
190
+ if e.response.status_code == 400:
191
+ try:
192
+ error_data = e.response.json()
193
+ error_code = error_data.get("error", "")
194
+ if error_code in ("invalid_request", "invalid_grant"):
195
+ requires_reconnection = True
196
+ logger.warning(
197
+ f"Twitter refresh token is invalid ({error_code}), "
198
+ "user needs to reconnect their account"
199
+ )
200
+ except Exception:
201
+ # If we can't parse the response, assume reconnection is needed for 400
202
+ requires_reconnection = True
203
+
183
204
  raise PlatformAuthError(
184
205
  f"Failed to refresh token: {_sanitize_response_text(e.response.text)}",
185
206
  platform="twitter",
186
207
  status_code=e.response.status_code,
208
+ requires_reconnection=requires_reconnection,
187
209
  ) from e
188
210
 
189
211
  except httpx.HTTPError as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marqetive-lib
3
- Version: 0.1.20
3
+ Version: 0.1.21
4
4
  Summary: Modern Python utilities for web APIs
5
5
  Keywords: api,utilities,web,http,marqetive
6
6
  Requires-Python: >=3.12
@@ -2,9 +2,9 @@ marqetive/__init__.py,sha256=pW77CUnzOQ0X1pb-GTcRgrrvsSaJBdVhGZLnvCD_4q4,3032
2
2
  marqetive/core/__init__.py,sha256=0_0vzxJ619YIJkz1yzSvhnGDJRkrErs_QSg2q3Bloss,1172
3
3
  marqetive/core/base.py,sha256=A1RgZZLX5aMkFVmjNdJak9O0WFWOl1PPMIU8-ngEjxA,17225
4
4
  marqetive/core/client.py,sha256=eCtvL100dkxYQHC_TJzHbs3dGjgsa_me9VTHo-CUN2M,3900
5
- marqetive/core/exceptions.py,sha256=Xyj0bzNiZm5VTErmzXgVW8T6IQnOpF92-HJiKPKjIio,7076
5
+ marqetive/core/exceptions.py,sha256=CQXGPjzWJU2NZ0a7E007PxrG_orOOJNmvcsZ0L4HTiQ,8195
6
6
  marqetive/core/models.py,sha256=HmUSKZgJFw4o6Orjn4SGaCwLN6s8_FCsFkK1ptvnad8,14819
7
- marqetive/factory.py,sha256=irZ5oN8a__kXZH70UN2uI7TzqTXu66d4QZ1FoxSoiK8,14092
7
+ marqetive/factory.py,sha256=SfHsp6mdIZpDBtryUvHSwljBRkCyAHhtwV7qDfgL9k4,15268
8
8
  marqetive/platforms/__init__.py,sha256=RBxlQSGyELsulSnwf5uaE1ohxFc7jC61OO9CrKaZp48,1312
9
9
  marqetive/platforms/instagram/__init__.py,sha256=c1Gs0ozG6D7Z-Uz_UQ7S3joL0qUTT9eUZPWcePyESk8,229
10
10
  marqetive/platforms/instagram/client.py,sha256=fyVDAcee28rbINa0jPB9W9GF745UHQ8BbKeOj3UXRoo,35239
@@ -31,8 +31,8 @@ marqetive/utils/__init__.py,sha256=bSrNajbxYBSKQayrPviLz8JeGjplnyK8y_NGDtgb7yQ,9
31
31
  marqetive/utils/file_handlers.py,sha256=mDBGLQpCBYD-AN3W_qysZn0JmXxQE017_lnURdkkucA,17224
32
32
  marqetive/utils/helpers.py,sha256=Sh5HZD6AOJig_6T84n6JsKLosIkKIkpkiYTl69rnOOw,1321
33
33
  marqetive/utils/media.py,sha256=reVousdueG-h5jeI6uLGqVCfjYxlsMiWhx6XZwg-iHY,14664
34
- marqetive/utils/oauth.py,sha256=x30XAW5VlND6TAPBsw9kZShko_Jsmn_NE-KOZjnBxGo,14359
34
+ marqetive/utils/oauth.py,sha256=3TtbUCVuGxtOBxIUvVJH_DUMIHrP76XDpabPYaLXhTU,15392
35
35
  marqetive/utils/retry.py,sha256=UcgrmVBVG5zd30_11mZnRnTaSFrbUYXBO1DrXPR0f8E,7627
36
- marqetive_lib-0.1.20.dist-info/METADATA,sha256=p-ktKzsiPP2g2pP4nQtFEeqRJv3e7UCV0vfk2aSfUCY,7876
37
- marqetive_lib-0.1.20.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
- marqetive_lib-0.1.20.dist-info/RECORD,,
36
+ marqetive_lib-0.1.21.dist-info/METADATA,sha256=bYGdoaoexmFhLH4yVmG5CxkTKyP0fnztXVGa-msRi7M,7876
37
+ marqetive_lib-0.1.21.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
38
+ marqetive_lib-0.1.21.dist-info/RECORD,,