Skip to content

Commit 6b07602

Browse files
SDK regeneration
1 parent de4a8a0 commit 6b07602

5 files changed

Lines changed: 69 additions & 11 deletions

File tree

.fern/metadata.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"cliVersion": "3.27.0",
2+
"cliVersion": "3.32.0",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "4.46.3"
4+
"generatorVersion": "4.46.6",
5+
"sdkVersion": "0.1.16"
56
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dynamic = ["version"]
44

55
[tool.poetry]
66
name = "credal"
7-
version = "0.1.15"
7+
version = "0.1.16"
88
description = ""
99
readme = "README.md"
1010
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.15",
25+
"User-Agent": "credal/0.1.16",
2626
"X-Fern-Language": "Python",
2727
"X-Fern-SDK-Name": "credal",
28-
"X-Fern-SDK-Version": "0.1.15",
28+
"X-Fern-SDK-Version": "0.1.16",
2929
**(self.get_custom_headers() or {}),
3030
}
3131
headers["Authorization"] = f"Bearer {self._get_api_key()}"

src/credal/core/http_client.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import re
66
import time
77
import typing
8-
import urllib.parse
98
from contextlib import asynccontextmanager, contextmanager
109
from random import random
1110

@@ -123,6 +122,30 @@ def _should_retry(response: httpx.Response) -> bool:
123122
return response.status_code >= 500 or response.status_code in retryable_400s
124123

125124

125+
def _build_url(base_url: str, path: typing.Optional[str]) -> str:
126+
"""
127+
Build a full URL by joining a base URL with a path.
128+
129+
This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs)
130+
by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly
131+
strip path components when the path starts with '/'.
132+
133+
Example:
134+
>>> _build_url("https://cloud.example.com/org/tenant/api", "/users")
135+
'https://cloud.example.com/org/tenant/api/users'
136+
137+
Args:
138+
base_url: The base URL, which may contain path prefixes.
139+
path: The path to append. Can be None or empty string.
140+
141+
Returns:
142+
The full URL with base_url and path properly joined.
143+
"""
144+
if not path:
145+
return base_url
146+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
147+
148+
126149
def _maybe_filter_none_from_multipart_data(
127150
data: typing.Optional[typing.Any],
128151
request_files: typing.Optional[RequestFiles],
@@ -294,7 +317,7 @@ def request(
294317

295318
response = self.httpx_client.request(
296319
method=method,
297-
url=urllib.parse.urljoin(f"{base_url}/", path),
320+
url=_build_url(base_url, path),
298321
headers=jsonable_encoder(
299322
remove_none_from_dict(
300323
{
@@ -397,7 +420,7 @@ def stream(
397420

398421
with self.httpx_client.stream(
399422
method=method,
400-
url=urllib.parse.urljoin(f"{base_url}/", path),
423+
url=_build_url(base_url, path),
401424
headers=jsonable_encoder(
402425
remove_none_from_dict(
403426
{
@@ -515,7 +538,7 @@ async def request(
515538
# Add the input to each of these and do None-safety checks
516539
response = await self.httpx_client.request(
517540
method=method,
518-
url=urllib.parse.urljoin(f"{base_url}/", path),
541+
url=_build_url(base_url, path),
519542
headers=jsonable_encoder(
520543
remove_none_from_dict(
521544
{
@@ -620,7 +643,7 @@ async def stream(
620643

621644
async with self.httpx_client.stream(
622645
method=method,
623-
url=urllib.parse.urljoin(f"{base_url}/", path),
646+
url=_build_url(base_url, path),
624647
headers=jsonable_encoder(
625648
remove_none_from_dict(
626649
{

tests/utils/test_http_client.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
import pytest
66

7-
from credal.core.http_client import AsyncHttpClient, HttpClient, get_request_body, remove_none_from_dict
7+
from credal.core.http_client import (
8+
AsyncHttpClient,
9+
HttpClient,
10+
_build_url,
11+
get_request_body,
12+
remove_none_from_dict,
13+
)
814
from credal.core.request_options import RequestOptions
915

1016

@@ -264,3 +270,31 @@ async def test_async_http_client_passes_encoded_params_when_present() -> None:
264270
params = dummy_client.last_request_kwargs["params"]
265271
# For a simple dict, encode_query should give a single (key, value) tuple
266272
assert params == [("after", "456")]
273+
274+
275+
def test_basic_url_joining() -> None:
276+
"""Test basic URL joining with a simple base URL and path."""
277+
result = _build_url("https://api.example.com", "/users")
278+
assert result == "https://api.example.com/users"
279+
280+
281+
def test_basic_url_joining_trailing_slash() -> None:
282+
"""Test basic URL joining with a simple base URL and path."""
283+
result = _build_url("https://api.example.com/", "/users")
284+
assert result == "https://api.example.com/users"
285+
286+
287+
def test_preserves_base_url_path_prefix() -> None:
288+
"""Test that path prefixes in base URL are preserved.
289+
290+
This is the critical bug fix - urllib.parse.urljoin() would strip
291+
the path prefix when the path starts with '/'.
292+
"""
293+
result = _build_url("https://cloud.example.com/org/tenant/api", "/users")
294+
assert result == "https://cloud.example.com/org/tenant/api/users"
295+
296+
297+
def test_preserves_base_url_path_prefix_trailing_slash() -> None:
298+
"""Test that path prefixes in base URL are preserved."""
299+
result = _build_url("https://cloud.example.com/org/tenant/api/", "/users")
300+
assert result == "https://cloud.example.com/org/tenant/api/users"

0 commit comments

Comments
 (0)