Skip to content

Commit f9bdda7

Browse files
feat(client): add follow_redirects request option
1 parent 2b6929f commit f9bdda7

File tree

4 files changed

+65
-1
lines changed

4 files changed

+65
-1
lines changed

src/finch/_base_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,9 @@ def request(
961961
if self.custom_auth is not None:
962962
kwargs["auth"] = self.custom_auth
963963

964+
if options.follow_redirects is not None:
965+
kwargs["follow_redirects"] = options.follow_redirects
966+
964967
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
965968

966969
response = None
@@ -1475,6 +1478,9 @@ async def request(
14751478
if self.custom_auth is not None:
14761479
kwargs["auth"] = self.custom_auth
14771480

1481+
if options.follow_redirects is not None:
1482+
kwargs["follow_redirects"] = options.follow_redirects
1483+
14781484
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
14791485

14801486
response = None

src/finch/_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
737737
idempotency_key: str
738738
json_data: Body
739739
extra_json: AnyMapping
740+
follow_redirects: bool
740741

741742

742743
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
750751
files: Union[HttpxRequestFiles, None] = None
751752
idempotency_key: Union[str, None] = None
752753
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
754+
follow_redirects: Union[bool, None] = None
753755

754756
# It should be noted that we cannot use `json` here as that would override
755757
# a BaseModel method in an incompatible fashion.

src/finch/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class RequestOptions(TypedDict, total=False):
101101
params: Query
102102
extra_json: AnyMapping
103103
idempotency_key: str
104+
follow_redirects: bool
104105

105106

106107
# Sentinel class used until PEP 0661 is accepted
@@ -217,3 +218,4 @@ class _GenericAlias(Protocol):
217218

218219
class HttpxSendArgs(TypedDict, total=False):
219220
auth: httpx.Auth
221+
follow_redirects: bool

tests/test_client.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from finch import Finch, AsyncFinch, APIResponseValidationError
2525
from finch._types import Omit
2626
from finch._models import BaseModel, FinalRequestOptions
27-
from finch._exceptions import APIResponseValidationError
27+
from finch._exceptions import APIStatusError, APIResponseValidationError
2828
from finch._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
2929

3030
from .utils import update_env
@@ -829,6 +829,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
829829
assert response.retries_taken == failures_before_success
830830
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
831831

832+
@pytest.mark.respx(base_url=base_url)
833+
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
834+
# Test that the default follow_redirects=True allows following redirects
835+
respx_mock.post("/redirect").mock(
836+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
837+
)
838+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
839+
840+
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
841+
assert response.status_code == 200
842+
assert response.json() == {"status": "ok"}
843+
844+
@pytest.mark.respx(base_url=base_url)
845+
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
846+
# Test that follow_redirects=False prevents following redirects
847+
respx_mock.post("/redirect").mock(
848+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
849+
)
850+
851+
with pytest.raises(APIStatusError) as exc_info:
852+
self.client.post(
853+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
854+
)
855+
856+
assert exc_info.value.response.status_code == 302
857+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
858+
832859

833860
class TestAsyncFinch:
834861
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
@@ -1672,3 +1699,30 @@ async def test_main() -> None:
16721699
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
16731700

16741701
time.sleep(0.1)
1702+
1703+
@pytest.mark.respx(base_url=base_url)
1704+
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1705+
# Test that the default follow_redirects=True allows following redirects
1706+
respx_mock.post("/redirect").mock(
1707+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1708+
)
1709+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1710+
1711+
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1712+
assert response.status_code == 200
1713+
assert response.json() == {"status": "ok"}
1714+
1715+
@pytest.mark.respx(base_url=base_url)
1716+
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1717+
# Test that follow_redirects=False prevents following redirects
1718+
respx_mock.post("/redirect").mock(
1719+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1720+
)
1721+
1722+
with pytest.raises(APIStatusError) as exc_info:
1723+
await self.client.post(
1724+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1725+
)
1726+
1727+
assert exc_info.value.response.status_code == 302
1728+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"

0 commit comments

Comments
 (0)