Skip to content

Commit a3a4df4

Browse files
Release 0.0.318
1 parent 98a3856 commit a3a4df4

9 files changed

Lines changed: 169 additions & 267 deletions

File tree

.fern/metadata.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
{
22
"cliVersion": "5.7.9",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "5.10.0",
4+
"generatorVersion": "5.3.6",
55
"generatorConfig": {
66
"client_class_name": "payabli",
77
"enable_wire_tests": false
88
},
9-
"originGitCommit": "919e0fd55b82258508b2a0e599e53efa19469522",
10-
"originGitCommitIsDirty": true,
11-
"invokedBy": "ci",
12-
"requestedVersion": "0.0.317",
13-
"ciProvider": "github",
14-
"sdkVersion": "0.0.317"
9+
"originGitCommit": "d69946f7f37797d4fdc34de0c29dc377dbc94fc5",
10+
"sdkVersion": "0.0.318"
1511
}

README.md

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -189,21 +189,11 @@ The SDK is instrumented with automatic retries with exponential backoff. A reque
189189
as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
190190
retry limit (default: 2).
191191

192-
Which status codes are retried depends on the `retryStatusCodes` generator configuration:
192+
A request is deemed retryable when any of the following HTTP status codes is returned:
193193

194-
**`legacy`** (current default): retries on
195194
- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
196-
- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict)
197195
- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
198-
- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500)
199-
200-
**`recommended`**: retries on
201-
- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
202-
- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict)
203-
- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
204-
- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway)
205-
- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable)
206-
- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout)
196+
- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
207197

208198
Use the `max_retries` request option to configure this behavior.
209199

poetry.lock

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

pyproject.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dynamic = ["version"]
44

55
[tool.poetry]
66
name = "payabli"
7-
version = "0.0.317"
7+
version = "0.0.318"
88
description = ""
99
readme = "README.md"
1010
authors = []
@@ -37,21 +37,20 @@ Repository = 'https://github.com/payabli/sdk-python'
3737

3838
[tool.poetry.dependencies]
3939
python = "^3.10"
40-
aiohttp = { version = ">=3.13.4,<4", optional = true, python = ">=3.9"}
40+
aiohttp = { version = ">=3.10.0,<4", optional = true}
4141
httpx = ">=0.21.2"
42-
httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.9"}
42+
httpx-aiohttp = { version = "0.1.8", optional = true}
4343
pydantic = ">= 1.9.2"
44-
pydantic-core = ">=2.18.2,<3.0.0"
44+
pydantic-core = ">=2.18.2,<2.44.0"
4545
typing_extensions = ">= 4.0.0"
4646

4747
[tool.poetry.group.dev.dependencies]
4848
mypy = "==1.13.0"
49-
pytest = "^9.0.3"
49+
pytest = "^8.2.0"
5050
pytest-asyncio = "^1.0.0"
5151
pytest-xdist = "^3.6.1"
5252
python-dateutil = "^2.9.0"
5353
types-python-dateutil = "^2.9.0.20240316"
54-
urllib3 = ">=2.6.3,<3.0.0"
5554
ruff = "==0.11.5"
5655

5756
[tool.pytest.ini_options]

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
httpx>=0.21.2
22
pydantic>= 1.9.2
3-
pydantic-core>=2.18.2,<3.0.0
3+
pydantic-core>=2.18.2,<2.44.0
44
typing_extensions>= 4.0.0

src/payabli/core/client_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ def get_headers(self) -> typing.Dict[str, str]:
2727
import platform
2828

2929
headers: typing.Dict[str, str] = {
30-
"User-Agent": "payabli/0.0.317",
30+
"User-Agent": "payabli/0.0.318",
3131
"X-Fern-Language": "Python",
3232
"X-Fern-Runtime": f"python/{platform.python_version()}",
3333
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
3434
"X-Fern-SDK-Name": "payabli",
35-
"X-Fern-SDK-Version": "0.0.317",
35+
"X-Fern-SDK-Version": "0.0.318",
3636
**(self.get_custom_headers() or {}),
3737
}
3838
headers["requestToken"] = self.api_key

src/payabli/core/http_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ def _retry_timeout_from_retries(retries: int) -> float:
125125

126126

127127
def _should_retry(response: httpx.Response) -> bool:
128-
return response.status_code >= 500 or response.status_code in [429, 408, 409]
128+
retryable_400s = [429, 408, 409]
129+
return response.status_code >= 500 or response.status_code in retryable_400s
129130

130131

131132
_SENSITIVE_HEADERS = frozenset(

src/payabli/core/http_sse/_api.py

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# This file was auto-generated by Fern from our API Definition.
22

3-
import codecs
43
import re
54
from contextlib import asynccontextmanager, contextmanager
6-
from typing import Any, AsyncGenerator, AsyncIterator, Iterator
5+
from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast
76

87
import httpx
98
from ._decoders import SSEDecoder
@@ -46,81 +45,46 @@ def _get_charset(self) -> str:
4645
def response(self) -> httpx.Response:
4746
return self._response
4847

49-
@staticmethod
50-
def _normalize_sse_line_endings(buf: str) -> str:
51-
"""Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n).
52-
53-
A trailing \\r is preserved because it may pair with a leading \\n in
54-
the next chunk to form a single \\r\\n terminator.
55-
"""
56-
buf = buf.replace("\r\n", "\n")
57-
if buf.endswith("\r"):
58-
return buf[:-1].replace("\r", "\n") + "\r"
59-
return buf.replace("\r", "\n")
60-
6148
def iter_sse(self) -> Iterator[ServerSentEvent]:
6249
self._check_content_type()
6350
decoder = SSEDecoder()
6451
charset = self._get_charset()
65-
text_decoder = codecs.getincrementaldecoder(charset)(errors="replace")
6652

67-
buf = ""
53+
buffer = ""
6854
for chunk in self._response.iter_bytes():
69-
buf += text_decoder.decode(chunk)
70-
buf = self._normalize_sse_line_endings(buf)
71-
72-
while "\n" in buf:
73-
line, buf = buf.split("\n", 1)
55+
# Decode chunk using detected charset
56+
text_chunk = chunk.decode(charset, errors="replace")
57+
buffer += text_chunk
58+
59+
# Process complete lines
60+
while "\n" in buffer:
61+
line, buffer = buffer.split("\n", 1)
62+
line = line.rstrip("\r")
7463
sse = decoder.decode(line)
64+
# when we reach a "\n\n" => line = ''
65+
# => decoder will attempt to return an SSE Event
7566
if sse is not None:
7667
yield sse
7768

78-
# Flush any remaining bytes from the incremental decoder
79-
buf += text_decoder.decode(b"", final=True)
80-
buf = buf.replace("\r\n", "\n").replace("\r", "\n")
81-
82-
while "\n" in buf:
83-
line, buf = buf.split("\n", 1)
69+
# Process any remaining data in buffer
70+
if buffer.strip():
71+
line = buffer.rstrip("\r")
8472
sse = decoder.decode(line)
8573
if sse is not None:
8674
yield sse
8775

88-
if buf.strip():
89-
sse = decoder.decode(buf)
90-
if sse is not None:
91-
yield sse
92-
9376
async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]:
9477
self._check_content_type()
9578
decoder = SSEDecoder()
96-
charset = self._get_charset()
97-
text_decoder = codecs.getincrementaldecoder(charset)(errors="replace")
98-
99-
buf = ""
100-
async for chunk in self._response.aiter_bytes():
101-
buf += text_decoder.decode(chunk)
102-
buf = self._normalize_sse_line_endings(buf)
103-
104-
while "\n" in buf:
105-
line, buf = buf.split("\n", 1)
79+
lines = cast(AsyncGenerator[str, None], self._response.aiter_lines())
80+
try:
81+
async for line in lines:
82+
line = line.rstrip("\n")
10683
sse = decoder.decode(line)
10784
if sse is not None:
10885
yield sse
109-
110-
# Flush any remaining bytes from the incremental decoder
111-
buf += text_decoder.decode(b"", final=True)
112-
buf = buf.replace("\r\n", "\n").replace("\r", "\n")
113-
114-
while "\n" in buf:
115-
line, buf = buf.split("\n", 1)
116-
sse = decoder.decode(line)
117-
if sse is not None:
118-
yield sse
119-
120-
if buf.strip():
121-
sse = decoder.decode(buf)
122-
if sse is not None:
123-
yield sse
86+
finally:
87+
await lines.aclose()
12488

12589

12690
@contextmanager

tests/utils/test_http_client.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
AsyncHttpClient,
1111
HttpClient,
1212
_build_url,
13-
_should_retry,
1413
get_request_body,
1514
remove_none_from_dict,
1615
)
@@ -661,34 +660,3 @@ async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) ->
661660
assert response.status_code == 200
662661
# 1 initial + 3 retries = 4 total attempts
663662
assert mock_client.request.call_count == 4
664-
665-
666-
# ---------------------------------------------------------------------------
667-
# _should_retry unit tests
668-
# ---------------------------------------------------------------------------
669-
670-
671-
def _make_response(status_code: int) -> httpx.Response:
672-
return httpx.Response(status_code=status_code, content=b"")
673-
674-
675-
@pytest.mark.parametrize(
676-
"status_code",
677-
[408, 409, 429, 500, 501, 502, 503, 504, 599],
678-
)
679-
def test_should_retry_retryable_status_codes(status_code: int) -> None:
680-
"""Legacy mode: retries on 408, 409, 429, and all >= 500."""
681-
assert _should_retry(_make_response(status_code)) is True
682-
683-
684-
@pytest.mark.parametrize(
685-
"status_code",
686-
[200, 201, 301, 400, 401, 403, 404],
687-
)
688-
def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None:
689-
assert _should_retry(_make_response(status_code)) is False
690-
691-
692-
def test_should_retry_599_upper_boundary() -> None:
693-
"""Legacy mode retries on >= 500, which includes 599."""
694-
assert _should_retry(_make_response(599)) is True

0 commit comments

Comments
 (0)