lex-api-mx-client 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/.gitignore +2 -0
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/PKG-INFO +9 -2
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/README.md +8 -1
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/pyproject.toml +1 -1
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/src/lex_api_mx/__init__.py +1 -1
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/src/lex_api_mx/client.py +44 -3
- {lex_api_mx_client-0.2.0 → lex_api_mx_client-0.3.0}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lex-api-mx-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Cliente Python de Lex API MX: legislación federal mexicana vigente con fuente, checksum y vigencia
|
|
5
5
|
Project-URL: Homepage, https://lexapi.mx
|
|
6
6
|
Project-URL: Documentation, https://lexapi.mx/docs
|
|
@@ -50,7 +50,14 @@ with LexApiClient(base_url="https://api.lexapi.mx", api_key="tu_clave") as lex:
|
|
|
50
50
|
|
|
51
51
|
`resolve` y `map` aceptan `idempotency_key`: un reintento con la misma clave no
|
|
52
52
|
se cobra dos veces. Los errores HTTP se lanzan como `LexApiError` con
|
|
53
|
-
`status_code`, `detail`, `request_id` y `
|
|
53
|
+
`status_code`, `detail`, `request_id`, `body` y `retry_after`; `402` significa
|
|
54
|
+
saldo agotado.
|
|
55
|
+
|
|
56
|
+
Un `429` (límite de peticiones) no se cobra: el cliente espera los segundos de
|
|
57
|
+
`Retry-After` y reintenta solo, hasta `max_retries=2` veces, cuando la espera no
|
|
58
|
+
pasa de `max_retry_wait=30.0` segundos. Una espera mayor, como el límite diario
|
|
59
|
+
del plan gratuito, se lanza enseguida como `LexApiError` con `retry_after`.
|
|
60
|
+
`LexApiClient(..., max_retries=0)` desactiva los reintentos.
|
|
54
61
|
|
|
55
62
|
Para obtener una clave, crea tu cuenta en
|
|
56
63
|
[lexapi.mx/#cuenta](https://lexapi.mx/#cuenta). Referencia de la API:
|
|
@@ -26,7 +26,14 @@ with LexApiClient(base_url="https://api.lexapi.mx", api_key="tu_clave") as lex:
|
|
|
26
26
|
|
|
27
27
|
`resolve` y `map` aceptan `idempotency_key`: un reintento con la misma clave no
|
|
28
28
|
se cobra dos veces. Los errores HTTP se lanzan como `LexApiError` con
|
|
29
|
-
`status_code`, `detail`, `request_id` y `
|
|
29
|
+
`status_code`, `detail`, `request_id`, `body` y `retry_after`; `402` significa
|
|
30
|
+
saldo agotado.
|
|
31
|
+
|
|
32
|
+
Un `429` (límite de peticiones) no se cobra: el cliente espera los segundos de
|
|
33
|
+
`Retry-After` y reintenta solo, hasta `max_retries=2` veces, cuando la espera no
|
|
34
|
+
pasa de `max_retry_wait=30.0` segundos. Una espera mayor, como el límite diario
|
|
35
|
+
del plan gratuito, se lanza enseguida como `LexApiError` con `retry_after`.
|
|
36
|
+
`LexApiClient(..., max_retries=0)` desactiva los reintentos.
|
|
30
37
|
|
|
31
38
|
Para obtener una clave, crea tu cuenta en
|
|
32
39
|
[lexapi.mx/#cuenta](https://lexapi.mx/#cuenta). Referencia de la API:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from datetime import date
|
|
4
5
|
from typing import Any
|
|
5
6
|
from urllib.parse import quote
|
|
@@ -14,17 +15,34 @@ class LexApiError(RuntimeError):
|
|
|
14
15
|
message: str,
|
|
15
16
|
request_id: str | None = None,
|
|
16
17
|
body: Any = None,
|
|
18
|
+
retry_after: float | None = None,
|
|
17
19
|
):
|
|
18
20
|
self.status_code = status_code
|
|
19
21
|
self.request_id = request_id
|
|
20
22
|
self.detail = message
|
|
21
23
|
self.body = body
|
|
24
|
+
self.retry_after = retry_after
|
|
22
25
|
suffix = f" request_id={request_id}" if request_id else ""
|
|
23
26
|
super().__init__(f"Lex API MX returned {status_code}: {message}{suffix}")
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
def _retry_after_seconds(response: httpx.Response) -> float | None:
|
|
30
|
+
"""Segundos de `Retry-After`; la API los envía en cada 429 (no usa fechas HTTP)."""
|
|
31
|
+
try:
|
|
32
|
+
seconds = float(response.headers.get("Retry-After", ""))
|
|
33
|
+
except ValueError:
|
|
34
|
+
return None
|
|
35
|
+
return seconds if seconds >= 0 else None
|
|
36
|
+
|
|
37
|
+
|
|
26
38
|
class LexApiClient:
|
|
27
|
-
"""Synchronous client for the stable Lex API MX integration surface.
|
|
39
|
+
"""Synchronous client for the stable Lex API MX integration surface.
|
|
40
|
+
|
|
41
|
+
Un 429 con `Retry-After` de hasta `max_retry_wait` segundos se reintenta solo, hasta
|
|
42
|
+
`max_retries` veces: la API rechaza la petición antes de procesarla y no la cobra. Una
|
|
43
|
+
espera mayor (el límite diario del plan gratuito) se lanza enseguida como `LexApiError`
|
|
44
|
+
con `retry_after`.
|
|
45
|
+
"""
|
|
28
46
|
|
|
29
47
|
def __init__(
|
|
30
48
|
self,
|
|
@@ -34,11 +52,17 @@ class LexApiClient:
|
|
|
34
52
|
timeout: float = 30.0,
|
|
35
53
|
transport: httpx.BaseTransport | None = None,
|
|
36
54
|
user_agent: str | None = None,
|
|
55
|
+
max_retries: int = 2,
|
|
56
|
+
max_retry_wait: float = 30.0,
|
|
37
57
|
) -> None:
|
|
38
58
|
if not base_url.strip():
|
|
39
59
|
raise ValueError("base_url is required")
|
|
40
60
|
if not api_key.strip():
|
|
41
61
|
raise ValueError("api_key is required")
|
|
62
|
+
if max_retries < 0:
|
|
63
|
+
raise ValueError("max_retries must be >= 0")
|
|
64
|
+
self._max_retries = max_retries
|
|
65
|
+
self._max_retry_wait = max_retry_wait
|
|
42
66
|
if user_agent is None:
|
|
43
67
|
from . import __version__ # importación tardía: el paquete importa este módulo
|
|
44
68
|
|
|
@@ -171,12 +195,28 @@ class LexApiClient:
|
|
|
171
195
|
idempotency_key: str | None = None,
|
|
172
196
|
) -> dict[str, Any]:
|
|
173
197
|
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
174
|
-
|
|
198
|
+
retries = 0
|
|
199
|
+
while True:
|
|
200
|
+
response = self._client.request(method, path, json=json, params=params, headers=headers)
|
|
201
|
+
retry_after = _retry_after_seconds(response) if response.status_code == 429 else None
|
|
202
|
+
if (
|
|
203
|
+
retry_after is None
|
|
204
|
+
or retries >= self._max_retries
|
|
205
|
+
or retry_after > self._max_retry_wait
|
|
206
|
+
):
|
|
207
|
+
break
|
|
208
|
+
retries += 1
|
|
209
|
+
time.sleep(retry_after)
|
|
175
210
|
if response.is_error:
|
|
176
211
|
body: Any = None
|
|
177
212
|
try:
|
|
178
213
|
body = response.json()
|
|
179
|
-
|
|
214
|
+
detail = body.get("detail", body) if isinstance(body, dict) else body
|
|
215
|
+
# Los 402 y 403 de plan traen {"code", "message", "upgrade_url"}.
|
|
216
|
+
if isinstance(detail, dict) and detail.get("message"):
|
|
217
|
+
message = str(detail["message"])
|
|
218
|
+
else:
|
|
219
|
+
message = str(detail)
|
|
180
220
|
except ValueError:
|
|
181
221
|
message = response.text or response.reason_phrase
|
|
182
222
|
raise LexApiError(
|
|
@@ -184,6 +224,7 @@ class LexApiClient:
|
|
|
184
224
|
message,
|
|
185
225
|
response.headers.get("X-Request-ID"),
|
|
186
226
|
body,
|
|
227
|
+
retry_after,
|
|
187
228
|
)
|
|
188
229
|
try:
|
|
189
230
|
value = response.json()
|
|
File without changes
|