lixinger-python 0.3.13__py3-none-any.whl → 0.3.14__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.
- lixinger/api/base.py +20 -2
- lixinger/utils/retry.py +15 -4
- {lixinger_python-0.3.13.dist-info → lixinger_python-0.3.14.dist-info}/METADATA +1 -1
- {lixinger_python-0.3.13.dist-info → lixinger_python-0.3.14.dist-info}/RECORD +6 -6
- {lixinger_python-0.3.13.dist-info → lixinger_python-0.3.14.dist-info}/WHEEL +0 -0
- {lixinger_python-0.3.13.dist-info → lixinger_python-0.3.14.dist-info}/licenses/LICENSE +0 -0
lixinger/api/base.py
CHANGED
|
@@ -15,6 +15,12 @@ _global_rate_limiter = AsyncRateLimiter(max_requests=1000)
|
|
|
15
15
|
# Lazy initialization of global http client to avoid issues with proxy/config changes
|
|
16
16
|
_global_http_client: httpx.AsyncClient | None = None
|
|
17
17
|
|
|
18
|
+
# Fallback delay in seconds when a 429 response omits Retry-After. The Lixinger
|
|
19
|
+
# rate-limit window is 60s, so short exponential backoff (1/2/4s) almost always
|
|
20
|
+
# fires inside the same window and gets 429ed again. 15s gives the window room
|
|
21
|
+
# to age and burns fewer retry attempts on doomed requests.
|
|
22
|
+
DEFAULT_RATE_LIMIT_RETRY_AFTER = 15.0
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
def get_global_client() -> httpx.AsyncClient:
|
|
20
26
|
"""Get or create the global HTTP client."""
|
|
@@ -113,6 +119,10 @@ class BaseAPI:
|
|
|
113
119
|
|
|
114
120
|
if response.status_code == 429:
|
|
115
121
|
retry_after = _parse_retry_after(response.headers.get("Retry-After"))
|
|
122
|
+
# Server didn't tell us how long to wait — pick a delay long enough
|
|
123
|
+
# for the 60s rate-limit window to actually age.
|
|
124
|
+
if retry_after is None:
|
|
125
|
+
retry_after = DEFAULT_RATE_LIMIT_RETRY_AFTER
|
|
116
126
|
raise RateLimitError(
|
|
117
127
|
"Rate limit exceeded",
|
|
118
128
|
status_code=429,
|
|
@@ -133,7 +143,15 @@ class BaseAPI:
|
|
|
133
143
|
code = data.get("code")
|
|
134
144
|
message = data.get("message")
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
# code is the machine-readable success signal; message is human text.
|
|
147
|
+
# Lixinger returns code=1 for success even when there's no data
|
|
148
|
+
# (e.g. message="no data" on non-trading days or empty ranges) —
|
|
149
|
+
# treat any code=1 as success and normalize a missing/null payload
|
|
150
|
+
# to [] so downstream iteration and get_response_df both work.
|
|
151
|
+
if code != 1:
|
|
137
152
|
raise APIError(f"API returned error code {code}: {message}")
|
|
138
153
|
|
|
139
|
-
|
|
154
|
+
payload = data.get("data")
|
|
155
|
+
if payload is None:
|
|
156
|
+
return []
|
|
157
|
+
return payload
|
lixinger/utils/retry.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
|
3
3
|
from functools import wraps
|
|
4
|
+
import random
|
|
4
5
|
from typing import Any, TypeVar
|
|
5
6
|
|
|
6
7
|
T = TypeVar("T")
|
|
@@ -11,8 +12,9 @@ def async_retry_on_failure(
|
|
|
11
12
|
backoff: float = 1.0,
|
|
12
13
|
retry_on: tuple[type[Exception], ...] = (Exception,),
|
|
13
14
|
no_retry_on: tuple[type[Exception], ...] = (),
|
|
15
|
+
jitter: float = 0.2,
|
|
14
16
|
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
|
|
15
|
-
"""Retry failed async requests with exponential backoff.
|
|
17
|
+
"""Retry failed async requests with exponential backoff and jitter.
|
|
16
18
|
|
|
17
19
|
Args:
|
|
18
20
|
max_retries: Maximum number of retry attempts (total attempts = max_retries + 1).
|
|
@@ -21,6 +23,11 @@ def async_retry_on_failure(
|
|
|
21
23
|
no_retry_on: Exception types that must NOT be retried even if matched by
|
|
22
24
|
``retry_on``. Use this to short-circuit on non-transient errors such
|
|
23
25
|
as authentication failures.
|
|
26
|
+
jitter: Multiplicative jitter fraction applied to every sleep. The actual
|
|
27
|
+
sleep is drawn from ``uniform(base * (1 - jitter), base * (1 + jitter))``
|
|
28
|
+
where ``base`` is either the ``retry_after`` hint or the exponential
|
|
29
|
+
backoff. Pass ``0`` to disable (mostly useful in tests). Values are
|
|
30
|
+
clamped so the sleep never goes negative.
|
|
24
31
|
|
|
25
32
|
If the caught exception exposes a ``retry_after`` attribute (float seconds,
|
|
26
33
|
typically parsed from a ``Retry-After`` HTTP header on 429 responses), that
|
|
@@ -41,9 +48,13 @@ def async_retry_on_failure(
|
|
|
41
48
|
raise
|
|
42
49
|
# Prefer server-provided retry hint when available.
|
|
43
50
|
hint = getattr(e, "retry_after", None)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
base = float(hint) if isinstance(hint, int | float) and hint > 0 else backoff * (2**attempt)
|
|
52
|
+
if jitter > 0:
|
|
53
|
+
low = max(0.0, base * (1.0 - jitter))
|
|
54
|
+
high = base * (1.0 + jitter)
|
|
55
|
+
wait_time = random.uniform(low, high) # noqa: S311 (non-crypto jitter)
|
|
56
|
+
else:
|
|
57
|
+
wait_time = base
|
|
47
58
|
await asyncio.sleep(wait_time)
|
|
48
59
|
raise RuntimeError("Unreachable")
|
|
49
60
|
|
|
@@ -4,7 +4,7 @@ lixinger/config.py,sha256=JPz8EOrf1kP4Do-Z5-MsnOM0pMrNE1ZsP-Qoarh9y4c,2008
|
|
|
4
4
|
lixinger/exceptions.py,sha256=Gkv1z-LC09paYGVs430-gDgDs43dLyRHbglB3Frehd4,1069
|
|
5
5
|
lixinger/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
lixinger/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
lixinger/api/base.py,sha256=
|
|
7
|
+
lixinger/api/base.py,sha256=6YQK-2oPFTPeLzw8WaaG-rajFL1Vv8dxrpS1jkxF2ew,5685
|
|
8
8
|
lixinger/api/cn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
lixinger/api/cn/company/__init__.py,sha256=K8aNp2gGj4iQOvc8KjXA5668QUAhwyTfMBPhP3tgtMg,961
|
|
10
10
|
lixinger/api/cn/company/announcement.py,sha256=lYt4AWOV5b9OTKfvJ7ZOqWP61pcPcyIQzb5N2DqFT6k,1920
|
|
@@ -120,8 +120,8 @@ lixinger/utils/api.py,sha256=BktR40JtKCMe9excFWo4ujsq3GiQK4ypJLG3MpJgo2s,402
|
|
|
120
120
|
lixinger/utils/dataframe.py,sha256=tYBrNdmJ4poyuwD-9XgzHt3A_A8bBo0cdHiu8Yy9e9A,2208
|
|
121
121
|
lixinger/utils/dict.py,sha256=yvbUtv8QpRmy0d_o_7gCuEwwiEfBji5_xB490ANxilw,589
|
|
122
122
|
lixinger/utils/rate_limiter.py,sha256=ZHlRTTL60L62uRA_f--LD26qkAfk-pHBllRqPapAXQM,1141
|
|
123
|
-
lixinger/utils/retry.py,sha256=
|
|
124
|
-
lixinger_python-0.3.
|
|
125
|
-
lixinger_python-0.3.
|
|
126
|
-
lixinger_python-0.3.
|
|
127
|
-
lixinger_python-0.3.
|
|
123
|
+
lixinger/utils/retry.py,sha256=C9QBcrpD7WYAuT8R4SJfWNUZFj1mZsv-qyTRIFQDmwc,2862
|
|
124
|
+
lixinger_python-0.3.14.dist-info/METADATA,sha256=mXD9Q3vhJMy-p0Exnj0knq71zUNpengedXCygEMcHyg,9207
|
|
125
|
+
lixinger_python-0.3.14.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
126
|
+
lixinger_python-0.3.14.dist-info/licenses/LICENSE,sha256=5oOwRq1lHSOScbNGCHr2feuNnhNYdGcArj6fSUfsC5U,1064
|
|
127
|
+
lixinger_python-0.3.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|