Skip to content

Commit 4b377a0

Browse files
SDK regeneration (#67)
Co-authored-by: fern-api[bot] <115122769+fern-api[bot]@users.noreply.github.com>
1 parent c99fcd8 commit 4b377a0

File tree

5 files changed

+78
-7
lines changed

5 files changed

+78
-7
lines changed

.fern/metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"cliVersion": "1.9.1",
2+
"cliVersion": "3.27.0",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "4.41.1"
4+
"generatorVersion": "4.41.3"
55
}

pyproject.toml

Lines changed: 1 addition & 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.14"
6+
version = "0.1.15"
77
description = ""
88
readme = "README.md"
99
authors = []

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.14",
25+
"User-Agent": "credal/0.1.15",
2626
"X-Fern-Language": "Python",
2727
"X-Fern-SDK-Name": "credal",
28-
"X-Fern-SDK-Version": "0.1.14",
28+
"X-Fern-SDK-Version": "0.1.15",
2929
**(self.get_custom_headers() or {}),
3030
}
3131
headers["Authorization"] = f"Bearer {self._get_api_key()}"

src/credal/core/http_client.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .force_multipart import FORCE_MULTIPART
1515
from .jsonable_encoder import jsonable_encoder
1616
from .query_encoder import encode_query
17-
from .remove_none_from_dict import remove_none_from_dict
17+
from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict
1818
from .request_options import RequestOptions
1919
from httpx._types import RequestFiles
2020

@@ -123,6 +123,21 @@ def _should_retry(response: httpx.Response) -> bool:
123123
return response.status_code >= 500 or response.status_code in retryable_400s
124124

125125

126+
def _maybe_filter_none_from_multipart_data(
127+
data: typing.Optional[typing.Any],
128+
request_files: typing.Optional[RequestFiles],
129+
force_multipart: typing.Optional[bool],
130+
) -> typing.Optional[typing.Any]:
131+
"""
132+
Filter None values from data body for multipart/form requests.
133+
This prevents httpx from converting None to empty strings in multipart encoding.
134+
Only applies when files are present or force_multipart is True.
135+
"""
136+
if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart):
137+
return remove_none_from_dict(data)
138+
return data
139+
140+
126141
def remove_omit_from_dict(
127142
original: typing.Dict[str, typing.Optional[typing.Any]],
128143
omit: typing.Optional[typing.Any],
@@ -244,6 +259,8 @@ def request(
244259
if (request_files is None or len(request_files) == 0) and force_multipart:
245260
request_files = FORCE_MULTIPART
246261

262+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
263+
247264
response = self.httpx_client.request(
248265
method=method,
249266
url=urllib.parse.urljoin(f"{base_url}/", path),
@@ -341,6 +358,8 @@ def stream(
341358

342359
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
343360

361+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
362+
344363
with self.httpx_client.stream(
345364
method=method,
346365
url=urllib.parse.urljoin(f"{base_url}/", path),
@@ -442,6 +461,8 @@ async def request(
442461

443462
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
444463

464+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
465+
445466
# Add the input to each of these and do None-safety checks
446467
response = await self.httpx_client.request(
447468
method=method,
@@ -539,6 +560,8 @@ async def stream(
539560

540561
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
541562

563+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
564+
542565
async with self.httpx_client.stream(
543566
method=method,
544567
url=urllib.parse.urljoin(f"{base_url}/", path),

tests/utils/test_http_client.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
# This file was auto-generated by Fern from our API Definition.
22

3-
from credal.core.http_client import get_request_body
3+
from credal.core.http_client import get_request_body, remove_none_from_dict
44
from credal.core.request_options import RequestOptions
55

66

77
def get_request_options() -> RequestOptions:
88
return {"additional_body_parameters": {"see you": "later"}}
99

1010

11+
def get_request_options_with_none() -> RequestOptions:
12+
return {"additional_body_parameters": {"see you": "later", "optional": None}}
13+
14+
1115
def test_get_json_request_body() -> None:
1216
json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None)
1317
assert json_body == {"hello": "world"}
@@ -59,3 +63,47 @@ def test_get_empty_json_request_body() -> None:
5963

6064
assert json_body_extras is None
6165
assert data_body_extras is None
66+
67+
68+
def test_json_body_preserves_none_values() -> None:
69+
"""Test that JSON bodies preserve None values (they become JSON null)."""
70+
json_body, data_body = get_request_body(
71+
json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None
72+
)
73+
# JSON bodies should preserve None values
74+
assert json_body == {"hello": "world", "optional": None}
75+
assert data_body is None
76+
77+
78+
def test_data_body_preserves_none_values_without_multipart() -> None:
79+
"""Test that data bodies preserve None values when not using multipart.
80+
81+
The filtering of None values happens in HttpClient.request/stream methods,
82+
not in get_request_body. This test verifies get_request_body doesn't filter None.
83+
"""
84+
json_body, data_body = get_request_body(
85+
json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None
86+
)
87+
# get_request_body should preserve None values in data body
88+
# The filtering happens later in HttpClient.request when multipart is detected
89+
assert data_body == {"hello": "world", "optional": None}
90+
assert json_body is None
91+
92+
93+
def test_remove_none_from_dict_filters_none_values() -> None:
94+
"""Test that remove_none_from_dict correctly filters out None values."""
95+
original = {"hello": "world", "optional": None, "another": "value", "also_none": None}
96+
filtered = remove_none_from_dict(original)
97+
assert filtered == {"hello": "world", "another": "value"}
98+
# Original should not be modified
99+
assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None}
100+
101+
102+
def test_remove_none_from_dict_empty_dict() -> None:
103+
"""Test that remove_none_from_dict handles empty dict."""
104+
assert remove_none_from_dict({}) == {}
105+
106+
107+
def test_remove_none_from_dict_all_none() -> None:
108+
"""Test that remove_none_from_dict handles dict with all None values."""
109+
assert remove_none_from_dict({"a": None, "b": None}) == {}

0 commit comments

Comments
 (0)