mirror of
https://github.com/anomalyco/opencode-sdk-python.git
synced 2026-05-17 12:42:25 +00:00
chore(internal): codegen related update
This commit is contained in:
parent
81dc9145e0
commit
33f3521d9b
1 changed files with 200 additions and 165 deletions
|
|
@ -59,47 +59,45 @@ def _get_open_connections(client: Opencode | AsyncOpencode) -> int:
|
|||
|
||||
|
||||
class TestOpencode:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_raw_response(self, respx_mock: MockRouter) -> None:
|
||||
def test_raw_response(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = self.client.post("/foo", cast_to=httpx.Response)
|
||||
response = client.post("/foo", cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, httpx.Response)
|
||||
assert response.json() == {"foo": "bar"}
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
|
||||
def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
respx_mock.post("/foo").mock(
|
||||
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
|
||||
)
|
||||
|
||||
response = self.client.post("/foo", cast_to=httpx.Response)
|
||||
response = client.post("/foo", cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, httpx.Response)
|
||||
assert response.json() == {"foo": "bar"}
|
||||
|
||||
def test_copy(self) -> None:
|
||||
copied = self.client.copy()
|
||||
assert id(copied) != id(self.client)
|
||||
def test_copy(self, client: Opencode) -> None:
|
||||
copied = client.copy()
|
||||
assert id(copied) != id(client)
|
||||
|
||||
def test_copy_default_options(self) -> None:
|
||||
def test_copy_default_options(self, client: Opencode) -> None:
|
||||
# options that have a default are overridden correctly
|
||||
copied = self.client.copy(max_retries=7)
|
||||
copied = client.copy(max_retries=7)
|
||||
assert copied.max_retries == 7
|
||||
assert self.client.max_retries == 2
|
||||
assert client.max_retries == 2
|
||||
|
||||
copied2 = copied.copy(max_retries=6)
|
||||
assert copied2.max_retries == 6
|
||||
assert copied.max_retries == 7
|
||||
|
||||
# timeout
|
||||
assert isinstance(self.client.timeout, httpx.Timeout)
|
||||
copied = self.client.copy(timeout=None)
|
||||
assert isinstance(client.timeout, httpx.Timeout)
|
||||
copied = client.copy(timeout=None)
|
||||
assert copied.timeout is None
|
||||
assert isinstance(self.client.timeout, httpx.Timeout)
|
||||
assert isinstance(client.timeout, httpx.Timeout)
|
||||
|
||||
def test_copy_default_headers(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
|
||||
|
|
@ -132,6 +130,7 @@ class TestOpencode:
|
|||
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
|
||||
):
|
||||
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
|
||||
client.close()
|
||||
|
||||
def test_copy_default_query(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"})
|
||||
|
|
@ -167,13 +166,15 @@ class TestOpencode:
|
|||
):
|
||||
client.copy(set_default_query={}, default_query={"foo": "Bar"})
|
||||
|
||||
def test_copy_signature(self) -> None:
|
||||
client.close()
|
||||
|
||||
def test_copy_signature(self, client: Opencode) -> None:
|
||||
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
|
||||
init_signature = inspect.signature(
|
||||
# mypy doesn't like that we access the `__init__` property.
|
||||
self.client.__init__, # type: ignore[misc]
|
||||
client.__init__, # type: ignore[misc]
|
||||
)
|
||||
copy_signature = inspect.signature(self.client.copy)
|
||||
copy_signature = inspect.signature(client.copy)
|
||||
exclude_params = {"transport", "proxies", "_strict_response_validation"}
|
||||
|
||||
for name in init_signature.parameters.keys():
|
||||
|
|
@ -184,12 +185,12 @@ class TestOpencode:
|
|||
assert copy_param is not None, f"copy() signature is missing the {name} param"
|
||||
|
||||
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
|
||||
def test_copy_build_request(self) -> None:
|
||||
def test_copy_build_request(self, client: Opencode) -> None:
|
||||
options = FinalRequestOptions(method="get", url="/foo")
|
||||
|
||||
def build_request(options: FinalRequestOptions) -> None:
|
||||
client = self.client.copy()
|
||||
client._build_request(options)
|
||||
client_copy = client.copy()
|
||||
client_copy._build_request(options)
|
||||
|
||||
# ensure that the machinery is warmed up before tracing starts.
|
||||
build_request(options)
|
||||
|
|
@ -246,14 +247,12 @@ class TestOpencode:
|
|||
print(frame)
|
||||
raise AssertionError()
|
||||
|
||||
def test_request_timeout(self) -> None:
|
||||
request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
def test_request_timeout(self, client: Opencode) -> None:
|
||||
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT
|
||||
|
||||
request = self.client._build_request(
|
||||
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
|
||||
)
|
||||
request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
|
||||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == httpx.Timeout(100.0)
|
||||
|
||||
|
|
@ -264,6 +263,8 @@ class TestOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == httpx.Timeout(0)
|
||||
|
||||
client.close()
|
||||
|
||||
def test_http_client_timeout_option(self) -> None:
|
||||
# custom timeout given to the httpx client should be used
|
||||
with httpx.Client(timeout=None) as http_client:
|
||||
|
|
@ -273,6 +274,8 @@ class TestOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == httpx.Timeout(None)
|
||||
|
||||
client.close()
|
||||
|
||||
# no timeout given to the httpx client should not use the httpx default
|
||||
with httpx.Client() as http_client:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
|
||||
|
|
@ -281,6 +284,8 @@ class TestOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT
|
||||
|
||||
client.close()
|
||||
|
||||
# explicitly passing the default timeout currently results in it being ignored
|
||||
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
|
||||
|
|
@ -289,18 +294,20 @@ class TestOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT # our default
|
||||
|
||||
client.close()
|
||||
|
||||
async def test_invalid_http_client(self) -> None:
|
||||
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
|
||||
async with httpx.AsyncClient() as http_client:
|
||||
Opencode(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client))
|
||||
|
||||
def test_default_headers_option(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
|
||||
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
test_client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
|
||||
request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
assert request.headers.get("x-foo") == "bar"
|
||||
assert request.headers.get("x-stainless-lang") == "python"
|
||||
|
||||
client2 = Opencode(
|
||||
test_client2 = Opencode(
|
||||
base_url=base_url,
|
||||
_strict_response_validation=True,
|
||||
default_headers={
|
||||
|
|
@ -308,10 +315,13 @@ class TestOpencode:
|
|||
"X-Stainless-Lang": "my-overriding-header",
|
||||
},
|
||||
)
|
||||
request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
assert request.headers.get("x-foo") == "stainless"
|
||||
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
|
||||
|
||||
test_client.close()
|
||||
test_client2.close()
|
||||
|
||||
def test_default_query_option(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"})
|
||||
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
|
|
@ -328,8 +338,10 @@ class TestOpencode:
|
|||
url = httpx.URL(request.url)
|
||||
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
|
||||
|
||||
def test_request_extra_json(self) -> None:
|
||||
request = self.client._build_request(
|
||||
client.close()
|
||||
|
||||
def test_request_extra_json(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -340,7 +352,7 @@ class TestOpencode:
|
|||
data = json.loads(request.content.decode("utf-8"))
|
||||
assert data == {"foo": "bar", "baz": False}
|
||||
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -351,7 +363,7 @@ class TestOpencode:
|
|||
assert data == {"baz": False}
|
||||
|
||||
# `extra_json` takes priority over `json_data` when keys clash
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -362,8 +374,8 @@ class TestOpencode:
|
|||
data = json.loads(request.content.decode("utf-8"))
|
||||
assert data == {"foo": "bar", "baz": None}
|
||||
|
||||
def test_request_extra_headers(self) -> None:
|
||||
request = self.client._build_request(
|
||||
def test_request_extra_headers(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -373,7 +385,7 @@ class TestOpencode:
|
|||
assert request.headers.get("X-Foo") == "Foo"
|
||||
|
||||
# `extra_headers` takes priority over `default_headers` when keys clash
|
||||
request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
|
||||
request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -384,8 +396,8 @@ class TestOpencode:
|
|||
)
|
||||
assert request.headers.get("X-Bar") == "false"
|
||||
|
||||
def test_request_extra_query(self) -> None:
|
||||
request = self.client._build_request(
|
||||
def test_request_extra_query(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -398,7 +410,7 @@ class TestOpencode:
|
|||
assert params == {"my_query_param": "Foo"}
|
||||
|
||||
# if both `query` and `extra_query` are given, they are merged
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -412,7 +424,7 @@ class TestOpencode:
|
|||
assert params == {"bar": "1", "foo": "2"}
|
||||
|
||||
# `extra_query` takes priority over `query` when keys clash
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -455,7 +467,7 @@ class TestOpencode:
|
|||
]
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_basic_union_response(self, respx_mock: MockRouter) -> None:
|
||||
def test_basic_union_response(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
class Model1(BaseModel):
|
||||
name: str
|
||||
|
||||
|
|
@ -464,12 +476,12 @@ class TestOpencode:
|
|||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model2)
|
||||
assert response.foo == "bar"
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
|
||||
def test_union_response_different_types(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
"""Union of objects with the same field name using a different type"""
|
||||
|
||||
class Model1(BaseModel):
|
||||
|
|
@ -480,18 +492,18 @@ class TestOpencode:
|
|||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model2)
|
||||
assert response.foo == "bar"
|
||||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
|
||||
|
||||
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model1)
|
||||
assert response.foo == 1
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
|
||||
def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
"""
|
||||
Response that sets Content-Type to something other than application/json but returns json data
|
||||
"""
|
||||
|
|
@ -507,7 +519,7 @@ class TestOpencode:
|
|||
)
|
||||
)
|
||||
|
||||
response = self.client.get("/foo", cast_to=Model)
|
||||
response = client.get("/foo", cast_to=Model)
|
||||
assert isinstance(response, Model)
|
||||
assert response.foo == 2
|
||||
|
||||
|
|
@ -519,6 +531,8 @@ class TestOpencode:
|
|||
|
||||
assert client.base_url == "https://example.com/from_setter/"
|
||||
|
||||
client.close()
|
||||
|
||||
def test_base_url_env(self) -> None:
|
||||
with update_env(OPENCODE_BASE_URL="http://localhost:5000/from/env"):
|
||||
client = Opencode(_strict_response_validation=True)
|
||||
|
|
@ -545,6 +559,7 @@ class TestOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "http://localhost:5000/custom/path/foo"
|
||||
client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client",
|
||||
|
|
@ -567,6 +582,7 @@ class TestOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "http://localhost:5000/custom/path/foo"
|
||||
client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client",
|
||||
|
|
@ -589,35 +605,36 @@ class TestOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "https://myapi.com/foo"
|
||||
client.close()
|
||||
|
||||
def test_copied_client_does_not_close_http(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
assert not client.is_closed()
|
||||
test_client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
assert not test_client.is_closed()
|
||||
|
||||
copied = client.copy()
|
||||
assert copied is not client
|
||||
copied = test_client.copy()
|
||||
assert copied is not test_client
|
||||
|
||||
del copied
|
||||
|
||||
assert not client.is_closed()
|
||||
assert not test_client.is_closed()
|
||||
|
||||
def test_client_context_manager(self) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
with client as c2:
|
||||
assert c2 is client
|
||||
test_client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
with test_client as c2:
|
||||
assert c2 is test_client
|
||||
assert not c2.is_closed()
|
||||
assert not client.is_closed()
|
||||
assert client.is_closed()
|
||||
assert not test_client.is_closed()
|
||||
assert test_client.is_closed()
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
|
||||
def test_client_response_validation_error(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
class Model(BaseModel):
|
||||
foo: str
|
||||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
|
||||
|
||||
with pytest.raises(APIResponseValidationError) as exc:
|
||||
self.client.get("/foo", cast_to=Model)
|
||||
client.get("/foo", cast_to=Model)
|
||||
|
||||
assert isinstance(exc.value.__cause__, ValidationError)
|
||||
|
||||
|
|
@ -626,13 +643,13 @@ class TestOpencode:
|
|||
Opencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None))
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
|
||||
def test_default_stream_cls(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
class Model(BaseModel):
|
||||
name: str
|
||||
|
||||
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
|
||||
stream = client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
|
||||
assert isinstance(stream, Stream)
|
||||
stream.response.close()
|
||||
|
||||
|
|
@ -648,11 +665,14 @@ class TestOpencode:
|
|||
with pytest.raises(APIResponseValidationError):
|
||||
strict_client.get("/foo", cast_to=Model)
|
||||
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=False)
|
||||
non_strict_client = Opencode(base_url=base_url, _strict_response_validation=False)
|
||||
|
||||
response = client.get("/foo", cast_to=Model)
|
||||
response = non_strict_client.get("/foo", cast_to=Model)
|
||||
assert isinstance(response, str) # type: ignore[unreachable]
|
||||
|
||||
strict_client.close()
|
||||
non_strict_client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"remaining_retries,retry_after,timeout",
|
||||
[
|
||||
|
|
@ -675,9 +695,9 @@ class TestOpencode:
|
|||
],
|
||||
)
|
||||
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
|
||||
def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
|
||||
client = Opencode(base_url=base_url, _strict_response_validation=True)
|
||||
|
||||
def test_parse_retry_after_header(
|
||||
self, remaining_retries: int, retry_after: str, timeout: float, client: Opencode
|
||||
) -> None:
|
||||
headers = httpx.Headers({"retry-after": retry_after})
|
||||
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
|
||||
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
|
||||
|
|
@ -691,7 +711,7 @@ class TestOpencode:
|
|||
with pytest.raises(APITimeoutError):
|
||||
client.session.with_streaming_response.list().__enter__()
|
||||
|
||||
assert _get_open_connections(self.client) == 0
|
||||
assert _get_open_connections(client) == 0
|
||||
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
|
|
@ -700,7 +720,7 @@ class TestOpencode:
|
|||
|
||||
with pytest.raises(APIStatusError):
|
||||
client.session.with_streaming_response.list().__enter__()
|
||||
assert _get_open_connections(self.client) == 0
|
||||
assert _get_open_connections(client) == 0
|
||||
|
||||
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
|
|
@ -802,79 +822,73 @@ class TestOpencode:
|
|||
)
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
|
||||
def test_follow_redirects(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
# Test that the default follow_redirects=True allows following redirects
|
||||
respx_mock.post("/redirect").mock(
|
||||
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
||||
)
|
||||
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
|
||||
|
||||
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
||||
response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
|
||||
def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Opencode) -> None:
|
||||
# Test that follow_redirects=False prevents following redirects
|
||||
respx_mock.post("/redirect").mock(
|
||||
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
||||
)
|
||||
|
||||
with pytest.raises(APIStatusError) as exc_info:
|
||||
self.client.post(
|
||||
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
|
||||
)
|
||||
client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
|
||||
|
||||
assert exc_info.value.response.status_code == 302
|
||||
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
|
||||
|
||||
|
||||
class TestAsyncOpencode:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_raw_response(self, respx_mock: MockRouter) -> None:
|
||||
async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = await self.client.post("/foo", cast_to=httpx.Response)
|
||||
response = await async_client.post("/foo", cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, httpx.Response)
|
||||
assert response.json() == {"foo": "bar"}
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
|
||||
async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
respx_mock.post("/foo").mock(
|
||||
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
|
||||
)
|
||||
|
||||
response = await self.client.post("/foo", cast_to=httpx.Response)
|
||||
response = await async_client.post("/foo", cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, httpx.Response)
|
||||
assert response.json() == {"foo": "bar"}
|
||||
|
||||
def test_copy(self) -> None:
|
||||
copied = self.client.copy()
|
||||
assert id(copied) != id(self.client)
|
||||
def test_copy(self, async_client: AsyncOpencode) -> None:
|
||||
copied = async_client.copy()
|
||||
assert id(copied) != id(async_client)
|
||||
|
||||
def test_copy_default_options(self) -> None:
|
||||
def test_copy_default_options(self, async_client: AsyncOpencode) -> None:
|
||||
# options that have a default are overridden correctly
|
||||
copied = self.client.copy(max_retries=7)
|
||||
copied = async_client.copy(max_retries=7)
|
||||
assert copied.max_retries == 7
|
||||
assert self.client.max_retries == 2
|
||||
assert async_client.max_retries == 2
|
||||
|
||||
copied2 = copied.copy(max_retries=6)
|
||||
assert copied2.max_retries == 6
|
||||
assert copied.max_retries == 7
|
||||
|
||||
# timeout
|
||||
assert isinstance(self.client.timeout, httpx.Timeout)
|
||||
copied = self.client.copy(timeout=None)
|
||||
assert isinstance(async_client.timeout, httpx.Timeout)
|
||||
copied = async_client.copy(timeout=None)
|
||||
assert copied.timeout is None
|
||||
assert isinstance(self.client.timeout, httpx.Timeout)
|
||||
assert isinstance(async_client.timeout, httpx.Timeout)
|
||||
|
||||
def test_copy_default_headers(self) -> None:
|
||||
async def test_copy_default_headers(self) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
|
||||
assert client.default_headers["X-Foo"] == "bar"
|
||||
|
||||
|
|
@ -905,8 +919,9 @@ class TestAsyncOpencode:
|
|||
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
|
||||
):
|
||||
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
|
||||
await client.close()
|
||||
|
||||
def test_copy_default_query(self) -> None:
|
||||
async def test_copy_default_query(self) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"})
|
||||
assert _get_params(client)["foo"] == "bar"
|
||||
|
||||
|
|
@ -940,13 +955,15 @@ class TestAsyncOpencode:
|
|||
):
|
||||
client.copy(set_default_query={}, default_query={"foo": "Bar"})
|
||||
|
||||
def test_copy_signature(self) -> None:
|
||||
await client.close()
|
||||
|
||||
def test_copy_signature(self, async_client: AsyncOpencode) -> None:
|
||||
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
|
||||
init_signature = inspect.signature(
|
||||
# mypy doesn't like that we access the `__init__` property.
|
||||
self.client.__init__, # type: ignore[misc]
|
||||
async_client.__init__, # type: ignore[misc]
|
||||
)
|
||||
copy_signature = inspect.signature(self.client.copy)
|
||||
copy_signature = inspect.signature(async_client.copy)
|
||||
exclude_params = {"transport", "proxies", "_strict_response_validation"}
|
||||
|
||||
for name in init_signature.parameters.keys():
|
||||
|
|
@ -957,12 +974,12 @@ class TestAsyncOpencode:
|
|||
assert copy_param is not None, f"copy() signature is missing the {name} param"
|
||||
|
||||
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
|
||||
def test_copy_build_request(self) -> None:
|
||||
def test_copy_build_request(self, async_client: AsyncOpencode) -> None:
|
||||
options = FinalRequestOptions(method="get", url="/foo")
|
||||
|
||||
def build_request(options: FinalRequestOptions) -> None:
|
||||
client = self.client.copy()
|
||||
client._build_request(options)
|
||||
client_copy = async_client.copy()
|
||||
client_copy._build_request(options)
|
||||
|
||||
# ensure that the machinery is warmed up before tracing starts.
|
||||
build_request(options)
|
||||
|
|
@ -1019,12 +1036,12 @@ class TestAsyncOpencode:
|
|||
print(frame)
|
||||
raise AssertionError()
|
||||
|
||||
async def test_request_timeout(self) -> None:
|
||||
request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
async def test_request_timeout(self, async_client: AsyncOpencode) -> None:
|
||||
request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT
|
||||
|
||||
request = self.client._build_request(
|
||||
request = async_client._build_request(
|
||||
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
|
||||
)
|
||||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
|
|
@ -1037,6 +1054,8 @@ class TestAsyncOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == httpx.Timeout(0)
|
||||
|
||||
await client.close()
|
||||
|
||||
async def test_http_client_timeout_option(self) -> None:
|
||||
# custom timeout given to the httpx client should be used
|
||||
async with httpx.AsyncClient(timeout=None) as http_client:
|
||||
|
|
@ -1046,6 +1065,8 @@ class TestAsyncOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == httpx.Timeout(None)
|
||||
|
||||
await client.close()
|
||||
|
||||
# no timeout given to the httpx client should not use the httpx default
|
||||
async with httpx.AsyncClient() as http_client:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
|
||||
|
|
@ -1054,6 +1075,8 @@ class TestAsyncOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT
|
||||
|
||||
await client.close()
|
||||
|
||||
# explicitly passing the default timeout currently results in it being ignored
|
||||
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
|
||||
|
|
@ -1062,18 +1085,22 @@ class TestAsyncOpencode:
|
|||
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
|
||||
assert timeout == DEFAULT_TIMEOUT # our default
|
||||
|
||||
await client.close()
|
||||
|
||||
def test_invalid_http_client(self) -> None:
|
||||
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
|
||||
with httpx.Client() as http_client:
|
||||
AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client))
|
||||
|
||||
def test_default_headers_option(self) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
|
||||
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
async def test_default_headers_option(self) -> None:
|
||||
test_client = AsyncOpencode(
|
||||
base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
|
||||
)
|
||||
request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
assert request.headers.get("x-foo") == "bar"
|
||||
assert request.headers.get("x-stainless-lang") == "python"
|
||||
|
||||
client2 = AsyncOpencode(
|
||||
test_client2 = AsyncOpencode(
|
||||
base_url=base_url,
|
||||
_strict_response_validation=True,
|
||||
default_headers={
|
||||
|
|
@ -1081,11 +1108,14 @@ class TestAsyncOpencode:
|
|||
"X-Stainless-Lang": "my-overriding-header",
|
||||
},
|
||||
)
|
||||
request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
|
||||
assert request.headers.get("x-foo") == "stainless"
|
||||
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
|
||||
|
||||
def test_default_query_option(self) -> None:
|
||||
await test_client.close()
|
||||
await test_client2.close()
|
||||
|
||||
async def test_default_query_option(self) -> None:
|
||||
client = AsyncOpencode(
|
||||
base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"}
|
||||
)
|
||||
|
|
@ -1103,8 +1133,10 @@ class TestAsyncOpencode:
|
|||
url = httpx.URL(request.url)
|
||||
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
|
||||
|
||||
def test_request_extra_json(self) -> None:
|
||||
request = self.client._build_request(
|
||||
await client.close()
|
||||
|
||||
def test_request_extra_json(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1115,7 +1147,7 @@ class TestAsyncOpencode:
|
|||
data = json.loads(request.content.decode("utf-8"))
|
||||
assert data == {"foo": "bar", "baz": False}
|
||||
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1126,7 +1158,7 @@ class TestAsyncOpencode:
|
|||
assert data == {"baz": False}
|
||||
|
||||
# `extra_json` takes priority over `json_data` when keys clash
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1137,8 +1169,8 @@ class TestAsyncOpencode:
|
|||
data = json.loads(request.content.decode("utf-8"))
|
||||
assert data == {"foo": "bar", "baz": None}
|
||||
|
||||
def test_request_extra_headers(self) -> None:
|
||||
request = self.client._build_request(
|
||||
def test_request_extra_headers(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1148,7 +1180,7 @@ class TestAsyncOpencode:
|
|||
assert request.headers.get("X-Foo") == "Foo"
|
||||
|
||||
# `extra_headers` takes priority over `default_headers` when keys clash
|
||||
request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
|
||||
request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1159,8 +1191,8 @@ class TestAsyncOpencode:
|
|||
)
|
||||
assert request.headers.get("X-Bar") == "false"
|
||||
|
||||
def test_request_extra_query(self) -> None:
|
||||
request = self.client._build_request(
|
||||
def test_request_extra_query(self, client: Opencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1173,7 +1205,7 @@ class TestAsyncOpencode:
|
|||
assert params == {"my_query_param": "Foo"}
|
||||
|
||||
# if both `query` and `extra_query` are given, they are merged
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1187,7 +1219,7 @@ class TestAsyncOpencode:
|
|||
assert params == {"bar": "1", "foo": "2"}
|
||||
|
||||
# `extra_query` takes priority over `query` when keys clash
|
||||
request = self.client._build_request(
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
url="/foo",
|
||||
|
|
@ -1230,7 +1262,7 @@ class TestAsyncOpencode:
|
|||
]
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
|
||||
async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
class Model1(BaseModel):
|
||||
name: str
|
||||
|
||||
|
|
@ -1239,12 +1271,12 @@ class TestAsyncOpencode:
|
|||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model2)
|
||||
assert response.foo == "bar"
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
|
||||
async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
"""Union of objects with the same field name using a different type"""
|
||||
|
||||
class Model1(BaseModel):
|
||||
|
|
@ -1255,18 +1287,20 @@ class TestAsyncOpencode:
|
|||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model2)
|
||||
assert response.foo == "bar"
|
||||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
|
||||
|
||||
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
|
||||
assert isinstance(response, Model1)
|
||||
assert response.foo == 1
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
|
||||
async def test_non_application_json_content_type_for_json_data(
|
||||
self, respx_mock: MockRouter, async_client: AsyncOpencode
|
||||
) -> None:
|
||||
"""
|
||||
Response that sets Content-Type to something other than application/json but returns json data
|
||||
"""
|
||||
|
|
@ -1282,11 +1316,11 @@ class TestAsyncOpencode:
|
|||
)
|
||||
)
|
||||
|
||||
response = await self.client.get("/foo", cast_to=Model)
|
||||
response = await async_client.get("/foo", cast_to=Model)
|
||||
assert isinstance(response, Model)
|
||||
assert response.foo == 2
|
||||
|
||||
def test_base_url_setter(self) -> None:
|
||||
async def test_base_url_setter(self) -> None:
|
||||
client = AsyncOpencode(base_url="https://example.com/from_init", _strict_response_validation=True)
|
||||
assert client.base_url == "https://example.com/from_init/"
|
||||
|
||||
|
|
@ -1294,7 +1328,9 @@ class TestAsyncOpencode:
|
|||
|
||||
assert client.base_url == "https://example.com/from_setter/"
|
||||
|
||||
def test_base_url_env(self) -> None:
|
||||
await client.close()
|
||||
|
||||
async def test_base_url_env(self) -> None:
|
||||
with update_env(OPENCODE_BASE_URL="http://localhost:5000/from/env"):
|
||||
client = AsyncOpencode(_strict_response_validation=True)
|
||||
assert client.base_url == "http://localhost:5000/from/env/"
|
||||
|
|
@ -1311,7 +1347,7 @@ class TestAsyncOpencode:
|
|||
],
|
||||
ids=["standard", "custom http client"],
|
||||
)
|
||||
def test_base_url_trailing_slash(self, client: AsyncOpencode) -> None:
|
||||
async def test_base_url_trailing_slash(self, client: AsyncOpencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
|
|
@ -1320,6 +1356,7 @@ class TestAsyncOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "http://localhost:5000/custom/path/foo"
|
||||
await client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client",
|
||||
|
|
@ -1333,7 +1370,7 @@ class TestAsyncOpencode:
|
|||
],
|
||||
ids=["standard", "custom http client"],
|
||||
)
|
||||
def test_base_url_no_trailing_slash(self, client: AsyncOpencode) -> None:
|
||||
async def test_base_url_no_trailing_slash(self, client: AsyncOpencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
|
|
@ -1342,6 +1379,7 @@ class TestAsyncOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "http://localhost:5000/custom/path/foo"
|
||||
await client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client",
|
||||
|
|
@ -1355,7 +1393,7 @@ class TestAsyncOpencode:
|
|||
],
|
||||
ids=["standard", "custom http client"],
|
||||
)
|
||||
def test_absolute_request_url(self, client: AsyncOpencode) -> None:
|
||||
async def test_absolute_request_url(self, client: AsyncOpencode) -> None:
|
||||
request = client._build_request(
|
||||
FinalRequestOptions(
|
||||
method="post",
|
||||
|
|
@ -1364,37 +1402,37 @@ class TestAsyncOpencode:
|
|||
),
|
||||
)
|
||||
assert request.url == "https://myapi.com/foo"
|
||||
await client.close()
|
||||
|
||||
async def test_copied_client_does_not_close_http(self) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
assert not client.is_closed()
|
||||
test_client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
assert not test_client.is_closed()
|
||||
|
||||
copied = client.copy()
|
||||
assert copied is not client
|
||||
copied = test_client.copy()
|
||||
assert copied is not test_client
|
||||
|
||||
del copied
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
assert not client.is_closed()
|
||||
assert not test_client.is_closed()
|
||||
|
||||
async def test_client_context_manager(self) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
async with client as c2:
|
||||
assert c2 is client
|
||||
test_client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
async with test_client as c2:
|
||||
assert c2 is test_client
|
||||
assert not c2.is_closed()
|
||||
assert not client.is_closed()
|
||||
assert client.is_closed()
|
||||
assert not test_client.is_closed()
|
||||
assert test_client.is_closed()
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
|
||||
async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
class Model(BaseModel):
|
||||
foo: str
|
||||
|
||||
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
|
||||
|
||||
with pytest.raises(APIResponseValidationError) as exc:
|
||||
await self.client.get("/foo", cast_to=Model)
|
||||
await async_client.get("/foo", cast_to=Model)
|
||||
|
||||
assert isinstance(exc.value.__cause__, ValidationError)
|
||||
|
||||
|
|
@ -1403,19 +1441,17 @@ class TestAsyncOpencode:
|
|||
AsyncOpencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None))
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
|
||||
async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
class Model(BaseModel):
|
||||
name: str
|
||||
|
||||
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
|
||||
|
||||
stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
|
||||
stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
|
||||
assert isinstance(stream, AsyncStream)
|
||||
await stream.response.aclose()
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
|
||||
class Model(BaseModel):
|
||||
name: str
|
||||
|
|
@ -1427,11 +1463,14 @@ class TestAsyncOpencode:
|
|||
with pytest.raises(APIResponseValidationError):
|
||||
await strict_client.get("/foo", cast_to=Model)
|
||||
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=False)
|
||||
non_strict_client = AsyncOpencode(base_url=base_url, _strict_response_validation=False)
|
||||
|
||||
response = await client.get("/foo", cast_to=Model)
|
||||
response = await non_strict_client.get("/foo", cast_to=Model)
|
||||
assert isinstance(response, str) # type: ignore[unreachable]
|
||||
|
||||
await strict_client.close()
|
||||
await non_strict_client.close()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"remaining_retries,retry_after,timeout",
|
||||
[
|
||||
|
|
@ -1454,13 +1493,12 @@ class TestAsyncOpencode:
|
|||
],
|
||||
)
|
||||
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
|
||||
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
|
||||
|
||||
async def test_parse_retry_after_header(
|
||||
self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncOpencode
|
||||
) -> None:
|
||||
headers = httpx.Headers({"retry-after": retry_after})
|
||||
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
|
||||
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
|
||||
calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
|
||||
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
|
|
@ -1473,7 +1511,7 @@ class TestAsyncOpencode:
|
|||
with pytest.raises(APITimeoutError):
|
||||
await async_client.session.with_streaming_response.list().__aenter__()
|
||||
|
||||
assert _get_open_connections(self.client) == 0
|
||||
assert _get_open_connections(async_client) == 0
|
||||
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
|
|
@ -1484,12 +1522,11 @@ class TestAsyncOpencode:
|
|||
|
||||
with pytest.raises(APIStatusError):
|
||||
await async_client.session.with_streaming_response.list().__aenter__()
|
||||
assert _get_open_connections(self.client) == 0
|
||||
assert _get_open_connections(async_client) == 0
|
||||
|
||||
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("failure_mode", ["status", "exception"])
|
||||
async def test_retries_taken(
|
||||
self,
|
||||
|
|
@ -1521,7 +1558,6 @@ class TestAsyncOpencode:
|
|||
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_omit_retry_count_header(
|
||||
self, async_client: AsyncOpencode, failures_before_success: int, respx_mock: MockRouter
|
||||
) -> None:
|
||||
|
|
@ -1545,7 +1581,6 @@ class TestAsyncOpencode:
|
|||
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
||||
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
@pytest.mark.asyncio
|
||||
async def test_overwrite_retry_count_header(
|
||||
self, async_client: AsyncOpencode, failures_before_success: int, respx_mock: MockRouter
|
||||
) -> None:
|
||||
|
|
@ -1593,26 +1628,26 @@ class TestAsyncOpencode:
|
|||
)
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
|
||||
async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
# Test that the default follow_redirects=True allows following redirects
|
||||
respx_mock.post("/redirect").mock(
|
||||
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
||||
)
|
||||
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
|
||||
|
||||
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
||||
response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
@pytest.mark.respx(base_url=base_url)
|
||||
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
|
||||
async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
|
||||
# Test that follow_redirects=False prevents following redirects
|
||||
respx_mock.post("/redirect").mock(
|
||||
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
||||
)
|
||||
|
||||
with pytest.raises(APIStatusError) as exc_info:
|
||||
await self.client.post(
|
||||
await async_client.post(
|
||||
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue