Skip to content

Commit 2af946e

Browse files
SDK regeneration (#65)
Co-authored-by: fern-api[bot] <115122769+fern-api[bot]@users.noreply.github.com>
1 parent 17334ec commit 2af946e

File tree

6 files changed

+92
-23
lines changed

6 files changed

+92
-23
lines changed

.fern/metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"cliVersion": "1.9.1",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "4.37.0"
4+
"generatorVersion": "4.41.1"
55
}

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
- name: Set up python
1010
uses: actions/setup-python@v4
1111
with:
12-
python-version: 3.8
12+
python-version: 3.9
1313
- name: Bootstrap poetry
1414
run: |
1515
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
@@ -25,15 +25,15 @@ jobs:
2525
- name: Set up python
2626
uses: actions/setup-python@v4
2727
with:
28-
python-version: 3.8
28+
python-version: 3.9
2929
- name: Bootstrap poetry
3030
run: |
3131
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
3232
- name: Install dependencies
3333
run: poetry install
3434

3535
- name: Test
36-
run: poetry run pytest -rP .
36+
run: poetry run pytest -rP -n auto .
3737

3838
publish:
3939
needs: [compile, test]
@@ -45,7 +45,7 @@ jobs:
4545
- name: Set up python
4646
uses: actions/setup-python@v4
4747
with:
48-
python-version: 3.8
48+
python-version: 3.9
4949
- name: Bootstrap poetry
5050
run: |
5151
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1

poetry.lock

Lines changed: 38 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "credal"
33

44
[tool.poetry]
55
name = "credal"
6-
version = "0.1.12"
6+
version = "0.1.13"
77
description = ""
88
readme = "README.md"
99
authors = []
@@ -44,6 +44,7 @@ typing_extensions = ">= 4.0.0"
4444
mypy = "==1.13.0"
4545
pytest = "^7.4.0"
4646
pytest-asyncio = "^0.23.5"
47+
pytest-xdist = "^3.6.1"
4748
python-dateutil = "^2.9.0"
4849
types-python-dateutil = "^2.9.0.20240316"
4950
ruff = "==0.11.5"

src/credal/core/client_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ def __init__(
2222

2323
def get_headers(self) -> typing.Dict[str, str]:
2424
headers: typing.Dict[str, str] = {
25-
"User-Agent": "credal/0.1.12",
25+
"User-Agent": "credal/0.1.13",
2626
"X-Fern-Language": "Python",
2727
"X-Fern-SDK-Name": "credal",
28-
"X-Fern-SDK-Version": "0.1.12",
28+
"X-Fern-SDK-Version": "0.1.13",
2929
**(self.get_custom_headers() or {}),
3030
}
3131
headers["Authorization"] = f"Bearer {self._get_api_key()}"

src/credal/core/http_client.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from .request_options import RequestOptions
1919
from httpx._types import RequestFiles
2020

21-
INITIAL_RETRY_DELAY_SECONDS = 0.5
22-
MAX_RETRY_DELAY_SECONDS = 10
23-
MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30
21+
INITIAL_RETRY_DELAY_SECONDS = 1.0
22+
MAX_RETRY_DELAY_SECONDS = 60.0
23+
JITTER_FACTOR = 0.2 # 20% random jitter
2424

2525

2626
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
@@ -64,24 +64,58 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float
6464
return seconds
6565

6666

67+
def _add_positive_jitter(delay: float) -> float:
68+
"""Add positive jitter (0-20%) to prevent thundering herd."""
69+
jitter_multiplier = 1 + random() * JITTER_FACTOR
70+
return delay * jitter_multiplier
71+
72+
73+
def _add_symmetric_jitter(delay: float) -> float:
74+
"""Add symmetric jitter (±10%) for exponential backoff."""
75+
jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
76+
return delay * jitter_multiplier
77+
78+
79+
def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
80+
"""
81+
Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
82+
Returns seconds to wait, or None if header is missing/invalid.
83+
"""
84+
reset_time_str = response_headers.get("x-ratelimit-reset")
85+
if reset_time_str is None:
86+
return None
87+
88+
try:
89+
reset_time = int(reset_time_str)
90+
delay = reset_time - time.time()
91+
if delay > 0:
92+
return delay
93+
except (ValueError, TypeError):
94+
pass
95+
96+
return None
97+
98+
6799
def _retry_timeout(response: httpx.Response, retries: int) -> float:
68100
"""
69101
Determine the amount of time to wait before retrying a request.
70102
This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
71103
with a jitter to determine the number of seconds to wait.
72104
"""
73105

74-
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
106+
# 1. Check Retry-After header first
75107
retry_after = _parse_retry_after(response.headers)
76-
if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER:
77-
return retry_after
108+
if retry_after is not None and retry_after > 0:
109+
return min(retry_after, MAX_RETRY_DELAY_SECONDS)
78110

79-
# Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS.
80-
retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
111+
# 2. Check X-RateLimit-Reset header (with positive jitter)
112+
ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
113+
if ratelimit_reset is not None:
114+
return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
81115

82-
# Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries.
83-
timeout = retry_delay * (1 - 0.25 * random())
84-
return timeout if timeout >= 0 else 0
116+
# 3. Fall back to exponential backoff (with symmetric jitter)
117+
backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
118+
return _add_symmetric_jitter(backoff)
85119

86120

87121
def _should_retry(response: httpx.Response) -> bool:

0 commit comments

Comments
 (0)