From 6b07602b4230082295c7ed5e90e461b0d63dc71e Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:22:53 +0000 Subject: [PATCH] SDK regeneration --- .fern/metadata.json | 5 +++-- pyproject.toml | 2 +- src/credal/core/client_wrapper.py | 4 ++-- src/credal/core/http_client.py | 33 +++++++++++++++++++++++----- tests/utils/test_http_client.py | 36 ++++++++++++++++++++++++++++++- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/.fern/metadata.json b/.fern/metadata.json index 9e72389..5a9a4b5 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,5 +1,6 @@ { - "cliVersion": "3.27.0", + "cliVersion": "3.32.0", "generatorName": "fernapi/fern-python-sdk", - "generatorVersion": "4.46.3" + "generatorVersion": "4.46.6", + "sdkVersion": "0.1.16" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a66e7f2..246f7a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "credal" -version = "0.1.15" +version = "0.1.16" description = "" readme = "README.md" authors = [] diff --git a/src/credal/core/client_wrapper.py b/src/credal/core/client_wrapper.py index bad2a39..87b2e14 100644 --- a/src/credal/core/client_wrapper.py +++ b/src/credal/core/client_wrapper.py @@ -22,10 +22,10 @@ def __init__( def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "credal/0.1.15", + "User-Agent": "credal/0.1.16", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "credal", - "X-Fern-SDK-Version": "0.1.15", + "X-Fern-SDK-Version": "0.1.16", **(self.get_custom_headers() or {}), } headers["Authorization"] = f"Bearer {self._get_api_key()}" diff --git a/src/credal/core/http_client.py b/src/credal/core/http_client.py index fb7cd4e..7c6c936 100644 --- a/src/credal/core/http_client.py +++ b/src/credal/core/http_client.py @@ -5,7 +5,6 @@ import re import time import typing -import urllib.parse from contextlib import asynccontextmanager, contextmanager from random import random @@ -123,6 +122,30 @@ def _should_retry(response: httpx.Response) -> bool: return response.status_code >= 500 or response.status_code in retryable_400s +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + def _maybe_filter_none_from_multipart_data( data: typing.Optional[typing.Any], request_files: typing.Optional[RequestFiles], @@ -294,7 +317,7 @@ def request( response = self.httpx_client.request( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { @@ -397,7 +420,7 @@ def stream( with self.httpx_client.stream( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { @@ -515,7 +538,7 @@ async def request( # Add the input to each of these and do None-safety checks response = await self.httpx_client.request( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { @@ -620,7 +643,7 @@ async def stream( async with self.httpx_client.stream( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), + url=_build_url(base_url, path), headers=jsonable_encoder( remove_none_from_dict( { diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index 788eea9..3112837 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -4,7 +4,13 @@ import pytest -from credal.core.http_client import AsyncHttpClient, HttpClient, get_request_body, remove_none_from_dict +from credal.core.http_client import ( + AsyncHttpClient, + HttpClient, + _build_url, + get_request_body, + remove_none_from_dict, +) from credal.core.request_options import RequestOptions @@ -264,3 +270,31 @@ async def test_async_http_client_passes_encoded_params_when_present() -> None: params = dummy_client.last_request_kwargs["params"] # For a simple dict, encode_query should give a single (key, value) tuple assert params == [("after", "456")] + + +def test_basic_url_joining() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com", "/users") + assert result == "https://api.example.com/users" + + +def test_basic_url_joining_trailing_slash() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com/", "/users") + assert result == "https://api.example.com/users" + + +def test_preserves_base_url_path_prefix() -> None: + """Test that path prefixes in base URL are preserved. + + This is the critical bug fix - urllib.parse.urljoin() would strip + the path prefix when the path starts with '/'. + """ + result = _build_url("https://cloud.example.com/org/tenant/api", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +def test_preserves_base_url_path_prefix_trailing_slash() -> None: + """Test that path prefixes in base URL are preserved.""" + result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users"