From 8c8717069bf064ca9a467b1c478ed642a10d5342 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Oct 2025 02:04:32 +0000
Subject: [PATCH 1/7] fix(client): close streams without requiring full
consumption
---
src/brand/dev/_streaming.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/brand/dev/_streaming.py b/src/brand/dev/_streaming.py
index 0ecd094..a61de0d 100644
--- a/src/brand/dev/_streaming.py
+++ b/src/brand/dev/_streaming.py
@@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ response.close()
def __enter__(self) -> Self:
return self
@@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- async for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From ccaa28b34cfbd24a0f46371dca4e43ab7a15fa61 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 02:09:45 +0000
Subject: [PATCH 2/7] chore(internal/tests): avoid race condition with implicit
client cleanup
---
tests/test_client.py | 362 +++++++++++++++++++++++--------------------
1 file changed, 198 insertions(+), 164 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 37d8e22..0e001a2 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -59,51 +59,49 @@ def _get_open_connections(client: BrandDev | AsyncBrandDev) -> int:
class TestBrandDev:
- client = BrandDev(base_url=base_url, api_key=api_key, _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: BrandDev) -> 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: BrandDev) -> 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: BrandDev) -> None:
+ copied = client.copy()
+ assert id(copied) != id(client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, client: BrandDev) -> 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 = BrandDev(
@@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None:
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 = BrandDev(
@@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ client.close()
+
+ def test_copy_signature(self, client: BrandDev) -> 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():
@@ -192,12 +193,12 @@ def test_copy_signature(self) -> None:
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: BrandDev) -> 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)
@@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
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: BrandDev) -> 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)
@@ -274,6 +273,8 @@ def test_client_timeout_option(self) -> None:
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:
@@ -285,6 +286,8 @@ def test_http_client_timeout_option(self) -> None:
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 = BrandDev(
@@ -295,6 +298,8 @@ def test_http_client_timeout_option(self) -> None:
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 = BrandDev(
@@ -305,6 +310,8 @@ def test_http_client_timeout_option(self) -> None:
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:
@@ -316,14 +323,14 @@ async def test_invalid_http_client(self) -> None:
)
def test_default_headers_option(self) -> None:
- client = BrandDev(
+ test_client = BrandDev(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ 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 = BrandDev(
+ test_client2 = BrandDev(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -332,10 +339,13 @@ def test_default_headers_option(self) -> None:
"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_validate_headers(self) -> None:
client = BrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -364,8 +374,10 @@ def test_default_query_option(self) -> None:
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -376,7 +388,7 @@ def test_request_extra_json(self) -> None:
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",
@@ -387,7 +399,7 @@ def test_request_extra_json(self) -> None:
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",
@@ -398,8 +410,8 @@ def test_request_extra_json(self) -> None:
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -409,7 +421,7 @@ def test_request_extra_headers(self) -> None:
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",
@@ -420,8 +432,8 @@ def test_request_extra_headers(self) -> None:
)
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -434,7 +446,7 @@ def test_request_extra_query(self) -> None:
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",
@@ -448,7 +460,7 @@ def test_request_extra_query(self) -> None:
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",
@@ -491,7 +503,7 @@ def test_multipart_repeating_array(self, client: BrandDev) -> None:
]
@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: BrandDev) -> None:
class Model1(BaseModel):
name: str
@@ -500,12 +512,12 @@ class Model2(BaseModel):
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: BrandDev) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -516,18 +528,18 @@ class Model2(BaseModel):
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: BrandDev) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -543,7 +555,7 @@ class Model(BaseModel):
)
)
- response = self.client.get("/foo", cast_to=Model)
+ response = client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
@@ -555,6 +567,8 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
+ client.close()
+
def test_base_url_env(self) -> None:
with update_env(BRAND_DEV_BASE_URL="http://localhost:5000/from/env"):
client = BrandDev(api_key=api_key, _strict_response_validation=True)
@@ -582,6 +596,7 @@ def test_base_url_trailing_slash(self, client: BrandDev) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -605,6 +620,7 @@ def test_base_url_no_trailing_slash(self, client: BrandDev) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -628,35 +644,36 @@ def test_absolute_request_url(self, client: BrandDev) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ client.close()
def test_copied_client_does_not_close_http(self) -> None:
- client = BrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = BrandDev(base_url=base_url, api_key=api_key, _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 = BrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- with client as c2:
- assert c2 is client
+ test_client = BrandDev(base_url=base_url, api_key=api_key, _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: BrandDev) -> 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)
@@ -676,11 +693,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
- client = BrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = BrandDev(base_url=base_url, api_key=api_key, _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",
[
@@ -703,9 +723,9 @@ class Model(BaseModel):
],
)
@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 = BrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, client: BrandDev
+ ) -> 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)
@@ -719,7 +739,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
with pytest.raises(APITimeoutError):
client.brand.with_streaming_response.retrieve().__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@mock.patch("brand.dev._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -728,7 +748,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client
with pytest.raises(APIStatusError):
client.brand.with_streaming_response.retrieve().__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("brand.dev._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -830,83 +850,77 @@ def test_default_client_creation(self) -> None:
)
@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: BrandDev) -> 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: BrandDev) -> 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 TestAsyncBrandDev:
- client = AsyncBrandDev(base_url=base_url, api_key=api_key, _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: AsyncBrandDev) -> 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: AsyncBrandDev) -> 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: AsyncBrandDev) -> None:
+ copied = async_client.copy()
+ assert id(copied) != id(async_client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = async_client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert async_client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, async_client: AsyncBrandDev) -> 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 = AsyncBrandDev(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
@@ -939,8 +953,9 @@ def test_copy_default_headers(self) -> None:
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 = AsyncBrandDev(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
)
@@ -976,13 +991,15 @@ def test_copy_default_query(self) -> None:
):
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: AsyncBrandDev) -> 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():
@@ -993,12 +1010,12 @@ def test_copy_signature(self) -> None:
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: AsyncBrandDev) -> 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)
@@ -1055,12 +1072,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
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: AsyncBrandDev) -> 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
@@ -1075,6 +1092,8 @@ async def test_client_timeout_option(self) -> None:
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:
@@ -1086,6 +1105,8 @@ async def test_http_client_timeout_option(self) -> None:
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 = AsyncBrandDev(
@@ -1096,6 +1117,8 @@ async def test_http_client_timeout_option(self) -> None:
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 = AsyncBrandDev(
@@ -1106,6 +1129,8 @@ async def test_http_client_timeout_option(self) -> None:
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:
@@ -1116,15 +1141,15 @@ def test_invalid_http_client(self) -> None:
http_client=cast(Any, http_client),
)
- def test_default_headers_option(self) -> None:
- client = AsyncBrandDev(
+ async def test_default_headers_option(self) -> None:
+ test_client = AsyncBrandDev(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ 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 = AsyncBrandDev(
+ test_client2 = AsyncBrandDev(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -1133,10 +1158,13 @@ def test_default_headers_option(self) -> None:
"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"
+ await test_client.close()
+ await test_client2.close()
+
def test_validate_headers(self) -> None:
client = AsyncBrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -1147,7 +1175,7 @@ def test_validate_headers(self) -> None:
client2 = AsyncBrandDev(base_url=base_url, api_key=None, _strict_response_validation=True)
_ = client2
- def test_default_query_option(self) -> None:
+ async def test_default_query_option(self) -> None:
client = AsyncBrandDev(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
)
@@ -1165,8 +1193,10 @@ def test_default_query_option(self) -> None:
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1177,7 +1207,7 @@ def test_request_extra_json(self) -> None:
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",
@@ -1188,7 +1218,7 @@ def test_request_extra_json(self) -> None:
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",
@@ -1199,8 +1229,8 @@ def test_request_extra_json(self) -> None:
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1210,7 +1240,7 @@ def test_request_extra_headers(self) -> None:
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",
@@ -1221,8 +1251,8 @@ def test_request_extra_headers(self) -> None:
)
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: BrandDev) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1235,7 +1265,7 @@ def test_request_extra_query(self) -> None:
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",
@@ -1249,7 +1279,7 @@ def test_request_extra_query(self) -> None:
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",
@@ -1292,7 +1322,7 @@ def test_multipart_repeating_array(self, async_client: AsyncBrandDev) -> None:
]
@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: AsyncBrandDev) -> None:
class Model1(BaseModel):
name: str
@@ -1301,12 +1331,12 @@ class Model2(BaseModel):
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: AsyncBrandDev) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -1317,18 +1347,20 @@ class Model2(BaseModel):
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: AsyncBrandDev
+ ) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -1344,11 +1376,11 @@ class Model(BaseModel):
)
)
- 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 = AsyncBrandDev(
base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
)
@@ -1358,7 +1390,9 @@ def test_base_url_setter(self) -> None:
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(BRAND_DEV_BASE_URL="http://localhost:5000/from/env"):
client = AsyncBrandDev(api_key=api_key, _strict_response_validation=True)
assert client.base_url == "http://localhost:5000/from/env/"
@@ -1378,7 +1412,7 @@ def test_base_url_env(self) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_trailing_slash(self, client: AsyncBrandDev) -> None:
+ async def test_base_url_trailing_slash(self, client: AsyncBrandDev) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1387,6 +1421,7 @@ def test_base_url_trailing_slash(self, client: AsyncBrandDev) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1403,7 +1438,7 @@ def test_base_url_trailing_slash(self, client: AsyncBrandDev) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_no_trailing_slash(self, client: AsyncBrandDev) -> None:
+ async def test_base_url_no_trailing_slash(self, client: AsyncBrandDev) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1412,6 +1447,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncBrandDev) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1428,7 +1464,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncBrandDev) -> None:
],
ids=["standard", "custom http client"],
)
- def test_absolute_request_url(self, client: AsyncBrandDev) -> None:
+ async def test_absolute_request_url(self, client: AsyncBrandDev) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1437,37 +1473,37 @@ def test_absolute_request_url(self, client: AsyncBrandDev) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ await client.close()
async def test_copied_client_does_not_close_http(self) -> None:
- client = AsyncBrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = AsyncBrandDev(base_url=base_url, api_key=api_key, _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 = AsyncBrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- async with client as c2:
- assert c2 is client
+ test_client = AsyncBrandDev(base_url=base_url, api_key=api_key, _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: AsyncBrandDev) -> 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)
@@ -1478,7 +1514,6 @@ async def test_client_max_retries_validation(self) -> None:
)
@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
@@ -1490,11 +1525,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
- client = AsyncBrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = AsyncBrandDev(base_url=base_url, api_key=api_key, _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",
[
@@ -1517,13 +1555,12 @@ class Model(BaseModel):
],
)
@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 = AsyncBrandDev(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ async def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncBrandDev
+ ) -> 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("brand.dev._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -1536,7 +1573,7 @@ async def test_retrying_timeout_errors_doesnt_leak(
with pytest.raises(APITimeoutError):
await async_client.brand.with_streaming_response.retrieve().__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@mock.patch("brand.dev._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -1547,12 +1584,11 @@ async def test_retrying_status_errors_doesnt_leak(
with pytest.raises(APIStatusError):
await async_client.brand.with_streaming_response.retrieve().__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("brand.dev._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,
@@ -1584,7 +1620,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("brand.dev._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: AsyncBrandDev, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1608,7 +1643,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("brand.dev._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: AsyncBrandDev, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1656,26 +1690,26 @@ async def test_default_client_creation(self) -> None:
)
@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: AsyncBrandDev) -> 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: AsyncBrandDev) -> 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
)
From d43798b7aa0dce98d898468cec44a1438648e5b9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:22:06 +0000
Subject: [PATCH 3/7] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b6baa96..1123c29 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 10
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-a634e2867f22f7485bf8ef51d18a25c010274dcbd60a420c8b35e68d017c8c95.yml
-openapi_spec_hash: 8990e4b274d4563c77525b15a2723f63
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-03d0d129690a7e6178f207b283001e66c7ef4a74d340647f18ce21ca3ccd8a1a.yml
+openapi_spec_hash: f64086044d57f32cba3ead2151fb4d61
config_hash: a1303564edd6276a63d584a02b2238b2
From 29bfd61545b43b405999e1f624466c17bf26062f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:22:48 +0000
Subject: [PATCH 4/7] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 1123c29..5d8534e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 10
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-03d0d129690a7e6178f207b283001e66c7ef4a74d340647f18ce21ca3ccd8a1a.yml
-openapi_spec_hash: f64086044d57f32cba3ead2151fb4d61
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-6620330945de41f1c453692af40842f08fe1fd281ff6ba4e79d447c941ebd783.yml
+openapi_spec_hash: 861a43669d27d942d4bd3e36a398e95b
config_hash: a1303564edd6276a63d584a02b2238b2
From 887cd82aaf78b8752cbf2fe8956ce81a9905d882 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:23:10 +0000
Subject: [PATCH 5/7] feat(api): manual updates
---
.stats.yml | 8 +-
api.md | 2 +
src/brand/dev/resources/brand.py | 248 +++++++++
src/brand/dev/types/__init__.py | 2 +
.../types/brand_retrieve_by_email_params.py | 88 ++++
.../types/brand_retrieve_by_email_response.py | 481 ++++++++++++++++++
tests/api_resources/test_brand.py | 91 ++++
7 files changed, 916 insertions(+), 4 deletions(-)
create mode 100644 src/brand/dev/types/brand_retrieve_by_email_params.py
create mode 100644 src/brand/dev/types/brand_retrieve_by_email_response.py
diff --git a/.stats.yml b/.stats.yml
index 5d8534e..aeecb0c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 10
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-6620330945de41f1c453692af40842f08fe1fd281ff6ba4e79d447c941ebd783.yml
-openapi_spec_hash: 861a43669d27d942d4bd3e36a398e95b
-config_hash: a1303564edd6276a63d584a02b2238b2
+configured_endpoints: 11
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-03d0d129690a7e6178f207b283001e66c7ef4a74d340647f18ce21ca3ccd8a1a.yml
+openapi_spec_hash: f64086044d57f32cba3ead2151fb4d61
+config_hash: 083e432ea397a9018371145493400188
diff --git a/api.md b/api.md
index 99766f8..cec128a 100644
--- a/api.md
+++ b/api.md
@@ -8,6 +8,7 @@ from brand.dev.types import (
BrandAIQueryResponse,
BrandIdentifyFromTransactionResponse,
BrandPrefetchResponse,
+ BrandRetrieveByEmailResponse,
BrandRetrieveByNameResponse,
BrandRetrieveByTickerResponse,
BrandRetrieveNaicsResponse,
@@ -23,6 +24,7 @@ Methods:
- client.brand.ai_query(\*\*params) -> BrandAIQueryResponse
- client.brand.identify_from_transaction(\*\*params) -> BrandIdentifyFromTransactionResponse
- client.brand.prefetch(\*\*params) -> BrandPrefetchResponse
+- client.brand.retrieve_by_email(\*\*params) -> BrandRetrieveByEmailResponse
- client.brand.retrieve_by_name(\*\*params) -> BrandRetrieveByNameResponse
- client.brand.retrieve_by_ticker(\*\*params) -> BrandRetrieveByTickerResponse
- client.brand.retrieve_naics(\*\*params) -> BrandRetrieveNaicsResponse
diff --git a/src/brand/dev/resources/brand.py b/src/brand/dev/resources/brand.py
index 76f275e..8749438 100644
--- a/src/brand/dev/resources/brand.py
+++ b/src/brand/dev/resources/brand.py
@@ -15,6 +15,7 @@
brand_styleguide_params,
brand_retrieve_naics_params,
brand_retrieve_by_name_params,
+ brand_retrieve_by_email_params,
brand_retrieve_by_ticker_params,
brand_retrieve_simplified_params,
brand_identify_from_transaction_params,
@@ -37,6 +38,7 @@
from ..types.brand_styleguide_response import BrandStyleguideResponse
from ..types.brand_retrieve_naics_response import BrandRetrieveNaicsResponse
from ..types.brand_retrieve_by_name_response import BrandRetrieveByNameResponse
+from ..types.brand_retrieve_by_email_response import BrandRetrieveByEmailResponse
from ..types.brand_retrieve_by_ticker_response import BrandRetrieveByTickerResponse
from ..types.brand_retrieve_simplified_response import BrandRetrieveSimplifiedResponse
from ..types.brand_identify_from_transaction_response import BrandIdentifyFromTransactionResponse
@@ -396,6 +398,123 @@ def prefetch(
cast_to=BrandPrefetchResponse,
)
+ def retrieve_by_email(
+ self,
+ *,
+ email: str,
+ force_language: Literal[
+ "albanian",
+ "arabic",
+ "azeri",
+ "bengali",
+ "bulgarian",
+ "cebuano",
+ "croatian",
+ "czech",
+ "danish",
+ "dutch",
+ "english",
+ "estonian",
+ "farsi",
+ "finnish",
+ "french",
+ "german",
+ "hausa",
+ "hawaiian",
+ "hindi",
+ "hungarian",
+ "icelandic",
+ "indonesian",
+ "italian",
+ "kazakh",
+ "kyrgyz",
+ "latin",
+ "latvian",
+ "lithuanian",
+ "macedonian",
+ "mongolian",
+ "nepali",
+ "norwegian",
+ "pashto",
+ "pidgin",
+ "polish",
+ "portuguese",
+ "romanian",
+ "russian",
+ "serbian",
+ "slovak",
+ "slovene",
+ "somali",
+ "spanish",
+ "swahili",
+ "swedish",
+ "tagalog",
+ "turkish",
+ "ukrainian",
+ "urdu",
+ "uzbek",
+ "vietnamese",
+ "welsh",
+ ]
+ | Omit = omit,
+ max_speed: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BrandRetrieveByEmailResponse:
+ """
+ Retrieve brand information using an email address while detecting disposable and
+ free email addresses. This endpoint extracts the domain from the email address
+ and returns brand data for that domain. Disposable and free email addresses
+ (like gmail.com, yahoo.com) will throw a 422 error.
+
+ Args:
+ email: Email address to retrieve brand data for (e.g., 'contact@example.com'). The
+ domain will be extracted from the email. Free email providers (gmail.com,
+ yahoo.com, etc.) and disposable email addresses are not allowed.
+
+ force_language: Optional parameter to force the language of the retrieved brand data.
+
+ max_speed: Optional parameter to optimize the API call for maximum speed. When set to true,
+ the API will skip time-consuming operations for faster response at the cost of
+ less comprehensive data.
+
+ timeout_ms: Optional timeout in milliseconds for the request. If the request takes longer
+ than this value, it will be aborted with a 408 status code. Maximum allowed
+ value is 300000ms (5 minutes).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get(
+ "/brand/retrieve-by-email",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "email": email,
+ "force_language": force_language,
+ "max_speed": max_speed,
+ "timeout_ms": timeout_ms,
+ },
+ brand_retrieve_by_email_params.BrandRetrieveByEmailParams,
+ ),
+ ),
+ cast_to=BrandRetrieveByEmailResponse,
+ )
+
def retrieve_by_name(
self,
*,
@@ -1281,6 +1400,123 @@ async def prefetch(
cast_to=BrandPrefetchResponse,
)
+ async def retrieve_by_email(
+ self,
+ *,
+ email: str,
+ force_language: Literal[
+ "albanian",
+ "arabic",
+ "azeri",
+ "bengali",
+ "bulgarian",
+ "cebuano",
+ "croatian",
+ "czech",
+ "danish",
+ "dutch",
+ "english",
+ "estonian",
+ "farsi",
+ "finnish",
+ "french",
+ "german",
+ "hausa",
+ "hawaiian",
+ "hindi",
+ "hungarian",
+ "icelandic",
+ "indonesian",
+ "italian",
+ "kazakh",
+ "kyrgyz",
+ "latin",
+ "latvian",
+ "lithuanian",
+ "macedonian",
+ "mongolian",
+ "nepali",
+ "norwegian",
+ "pashto",
+ "pidgin",
+ "polish",
+ "portuguese",
+ "romanian",
+ "russian",
+ "serbian",
+ "slovak",
+ "slovene",
+ "somali",
+ "spanish",
+ "swahili",
+ "swedish",
+ "tagalog",
+ "turkish",
+ "ukrainian",
+ "urdu",
+ "uzbek",
+ "vietnamese",
+ "welsh",
+ ]
+ | Omit = omit,
+ max_speed: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BrandRetrieveByEmailResponse:
+ """
+ Retrieve brand information using an email address while detecting disposable and
+ free email addresses. This endpoint extracts the domain from the email address
+ and returns brand data for that domain. Disposable and free email addresses
+ (like gmail.com, yahoo.com) will throw a 422 error.
+
+ Args:
+ email: Email address to retrieve brand data for (e.g., 'contact@example.com'). The
+ domain will be extracted from the email. Free email providers (gmail.com,
+ yahoo.com, etc.) and disposable email addresses are not allowed.
+
+ force_language: Optional parameter to force the language of the retrieved brand data.
+
+ max_speed: Optional parameter to optimize the API call for maximum speed. When set to true,
+ the API will skip time-consuming operations for faster response at the cost of
+ less comprehensive data.
+
+ timeout_ms: Optional timeout in milliseconds for the request. If the request takes longer
+ than this value, it will be aborted with a 408 status code. Maximum allowed
+ value is 300000ms (5 minutes).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._get(
+ "/brand/retrieve-by-email",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "email": email,
+ "force_language": force_language,
+ "max_speed": max_speed,
+ "timeout_ms": timeout_ms,
+ },
+ brand_retrieve_by_email_params.BrandRetrieveByEmailParams,
+ ),
+ ),
+ cast_to=BrandRetrieveByEmailResponse,
+ )
+
async def retrieve_by_name(
self,
*,
@@ -1830,6 +2066,9 @@ def __init__(self, brand: BrandResource) -> None:
self.prefetch = to_raw_response_wrapper(
brand.prefetch,
)
+ self.retrieve_by_email = to_raw_response_wrapper(
+ brand.retrieve_by_email,
+ )
self.retrieve_by_name = to_raw_response_wrapper(
brand.retrieve_by_name,
)
@@ -1866,6 +2105,9 @@ def __init__(self, brand: AsyncBrandResource) -> None:
self.prefetch = async_to_raw_response_wrapper(
brand.prefetch,
)
+ self.retrieve_by_email = async_to_raw_response_wrapper(
+ brand.retrieve_by_email,
+ )
self.retrieve_by_name = async_to_raw_response_wrapper(
brand.retrieve_by_name,
)
@@ -1902,6 +2144,9 @@ def __init__(self, brand: BrandResource) -> None:
self.prefetch = to_streamed_response_wrapper(
brand.prefetch,
)
+ self.retrieve_by_email = to_streamed_response_wrapper(
+ brand.retrieve_by_email,
+ )
self.retrieve_by_name = to_streamed_response_wrapper(
brand.retrieve_by_name,
)
@@ -1938,6 +2183,9 @@ def __init__(self, brand: AsyncBrandResource) -> None:
self.prefetch = async_to_streamed_response_wrapper(
brand.prefetch,
)
+ self.retrieve_by_email = async_to_streamed_response_wrapper(
+ brand.retrieve_by_email,
+ )
self.retrieve_by_name = async_to_streamed_response_wrapper(
brand.retrieve_by_name,
)
diff --git a/src/brand/dev/types/__init__.py b/src/brand/dev/types/__init__.py
index e038cbb..bd36eb4 100644
--- a/src/brand/dev/types/__init__.py
+++ b/src/brand/dev/types/__init__.py
@@ -15,8 +15,10 @@
from .brand_retrieve_naics_params import BrandRetrieveNaicsParams as BrandRetrieveNaicsParams
from .brand_retrieve_by_name_params import BrandRetrieveByNameParams as BrandRetrieveByNameParams
from .brand_retrieve_naics_response import BrandRetrieveNaicsResponse as BrandRetrieveNaicsResponse
+from .brand_retrieve_by_email_params import BrandRetrieveByEmailParams as BrandRetrieveByEmailParams
from .brand_retrieve_by_name_response import BrandRetrieveByNameResponse as BrandRetrieveByNameResponse
from .brand_retrieve_by_ticker_params import BrandRetrieveByTickerParams as BrandRetrieveByTickerParams
+from .brand_retrieve_by_email_response import BrandRetrieveByEmailResponse as BrandRetrieveByEmailResponse
from .brand_retrieve_simplified_params import BrandRetrieveSimplifiedParams as BrandRetrieveSimplifiedParams
from .brand_retrieve_by_ticker_response import BrandRetrieveByTickerResponse as BrandRetrieveByTickerResponse
from .brand_retrieve_simplified_response import BrandRetrieveSimplifiedResponse as BrandRetrieveSimplifiedResponse
diff --git a/src/brand/dev/types/brand_retrieve_by_email_params.py b/src/brand/dev/types/brand_retrieve_by_email_params.py
new file mode 100644
index 0000000..da361c6
--- /dev/null
+++ b/src/brand/dev/types/brand_retrieve_by_email_params.py
@@ -0,0 +1,88 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["BrandRetrieveByEmailParams"]
+
+
+class BrandRetrieveByEmailParams(TypedDict, total=False):
+ email: Required[str]
+ """Email address to retrieve brand data for (e.g., 'contact@example.com').
+
+ The domain will be extracted from the email. Free email providers (gmail.com,
+ yahoo.com, etc.) and disposable email addresses are not allowed.
+ """
+
+ force_language: Literal[
+ "albanian",
+ "arabic",
+ "azeri",
+ "bengali",
+ "bulgarian",
+ "cebuano",
+ "croatian",
+ "czech",
+ "danish",
+ "dutch",
+ "english",
+ "estonian",
+ "farsi",
+ "finnish",
+ "french",
+ "german",
+ "hausa",
+ "hawaiian",
+ "hindi",
+ "hungarian",
+ "icelandic",
+ "indonesian",
+ "italian",
+ "kazakh",
+ "kyrgyz",
+ "latin",
+ "latvian",
+ "lithuanian",
+ "macedonian",
+ "mongolian",
+ "nepali",
+ "norwegian",
+ "pashto",
+ "pidgin",
+ "polish",
+ "portuguese",
+ "romanian",
+ "russian",
+ "serbian",
+ "slovak",
+ "slovene",
+ "somali",
+ "spanish",
+ "swahili",
+ "swedish",
+ "tagalog",
+ "turkish",
+ "ukrainian",
+ "urdu",
+ "uzbek",
+ "vietnamese",
+ "welsh",
+ ]
+ """Optional parameter to force the language of the retrieved brand data."""
+
+ max_speed: Annotated[bool, PropertyInfo(alias="maxSpeed")]
+ """Optional parameter to optimize the API call for maximum speed.
+
+ When set to true, the API will skip time-consuming operations for faster
+ response at the cost of less comprehensive data.
+ """
+
+ timeout_ms: Annotated[int, PropertyInfo(alias="timeoutMS")]
+ """Optional timeout in milliseconds for the request.
+
+ If the request takes longer than this value, it will be aborted with a 408
+ status code. Maximum allowed value is 300000ms (5 minutes).
+ """
diff --git a/src/brand/dev/types/brand_retrieve_by_email_response.py b/src/brand/dev/types/brand_retrieve_by_email_response.py
new file mode 100644
index 0000000..47411cd
--- /dev/null
+++ b/src/brand/dev/types/brand_retrieve_by_email_response.py
@@ -0,0 +1,481 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = [
+ "BrandRetrieveByEmailResponse",
+ "Brand",
+ "BrandAddress",
+ "BrandBackdrop",
+ "BrandBackdropColor",
+ "BrandBackdropResolution",
+ "BrandColor",
+ "BrandIndustries",
+ "BrandIndustriesEic",
+ "BrandLinks",
+ "BrandLogo",
+ "BrandLogoColor",
+ "BrandLogoResolution",
+ "BrandSocial",
+ "BrandStock",
+]
+
+
+class BrandAddress(BaseModel):
+ city: Optional[str] = None
+ """City name"""
+
+ country: Optional[str] = None
+ """Country name"""
+
+ country_code: Optional[str] = None
+ """Country code"""
+
+ postal_code: Optional[str] = None
+ """Postal or ZIP code"""
+
+ state_code: Optional[str] = None
+ """State or province code"""
+
+ state_province: Optional[str] = None
+ """State or province name"""
+
+ street: Optional[str] = None
+ """Street address"""
+
+
+class BrandBackdropColor(BaseModel):
+ hex: Optional[str] = None
+ """Color in hexadecimal format"""
+
+ name: Optional[str] = None
+ """Name of the color"""
+
+
+class BrandBackdropResolution(BaseModel):
+ aspect_ratio: Optional[float] = None
+ """Aspect ratio of the image (width/height)"""
+
+ height: Optional[int] = None
+ """Height of the image in pixels"""
+
+ width: Optional[int] = None
+ """Width of the image in pixels"""
+
+
+class BrandBackdrop(BaseModel):
+ colors: Optional[List[BrandBackdropColor]] = None
+ """Array of colors in the backdrop image"""
+
+ resolution: Optional[BrandBackdropResolution] = None
+ """Resolution of the backdrop image"""
+
+ url: Optional[str] = None
+ """URL of the backdrop image"""
+
+
+class BrandColor(BaseModel):
+ hex: Optional[str] = None
+ """Color in hexadecimal format"""
+
+ name: Optional[str] = None
+ """Name of the color"""
+
+
+class BrandIndustriesEic(BaseModel):
+ industry: Literal[
+ "Aerospace & Defense",
+ "Technology",
+ "Finance",
+ "Healthcare",
+ "Retail & E-commerce",
+ "Entertainment",
+ "Education",
+ "Government & Nonprofit",
+ "Industrial & Energy",
+ "Automotive & Transportation",
+ "Lifestyle & Leisure",
+ "Luxury & Fashion",
+ "News & Media",
+ "Sports",
+ "Real Estate & PropTech",
+ "Legal & Compliance",
+ "Telecommunications",
+ "Agriculture & Food",
+ "Professional Services & Agencies",
+ "Chemicals & Materials",
+ "Logistics & Supply Chain",
+ "Hospitality & Tourism",
+ "Construction & Built Environment",
+ "Consumer Packaged Goods (CPG)",
+ ]
+ """Industry classification enum"""
+
+ subindustry: Literal[
+ "Defense Systems & Military Hardware",
+ "Aerospace Manufacturing",
+ "Avionics & Navigation Technology",
+ "Subsea & Naval Defense Systems",
+ "Space & Satellite Technology",
+ "Defense IT & Systems Integration",
+ "Software (B2B)",
+ "Software (B2C)",
+ "Cloud Infrastructure & DevOps",
+ "Cybersecurity",
+ "Artificial Intelligence & Machine Learning",
+ "Data Infrastructure & Analytics",
+ "Hardware & Semiconductors",
+ "Fintech Infrastructure",
+ "eCommerce & Marketplace Platforms",
+ "Developer Tools & APIs",
+ "Web3 & Blockchain",
+ "XR & Spatial Computing",
+ "Banking & Lending",
+ "Investment Management & WealthTech",
+ "Insurance & InsurTech",
+ "Payments & Money Movement",
+ "Accounting, Tax & Financial Planning Tools",
+ "Capital Markets & Trading Platforms",
+ "Financial Infrastructure & APIs",
+ "Credit Scoring & Risk Management",
+ "Cryptocurrency & Digital Assets",
+ "BNPL & Alternative Financing",
+ "Healthcare Providers & Services",
+ "Pharmaceuticals & Drug Development",
+ "Medical Devices & Diagnostics",
+ "Biotechnology & Genomics",
+ "Digital Health & Telemedicine",
+ "Health Insurance & Benefits Tech",
+ "Clinical Trials & Research Platforms",
+ "Mental Health & Wellness",
+ "Healthcare IT & EHR Systems",
+ "Consumer Health & Wellness Products",
+ "Online Marketplaces",
+ "Direct-to-Consumer (DTC) Brands",
+ "Retail Tech & Point-of-Sale Systems",
+ "Omnichannel & In-Store Retail",
+ "E-commerce Enablement & Infrastructure",
+ "Subscription & Membership Commerce",
+ "Social Commerce & Influencer Platforms",
+ "Fashion & Apparel Retail",
+ "Food, Beverage & Grocery E-commerce",
+ "Streaming Platforms (Video, Music, Audio)",
+ "Gaming & Interactive Entertainment",
+ "Creator Economy & Influencer Platforms",
+ "Advertising, Adtech & Media Buying",
+ "Film, TV & Production Studios",
+ "Events, Venues & Live Entertainment",
+ "Virtual Worlds & Metaverse Experiences",
+ "K-12 Education Platforms & Tools",
+ "Higher Education & University Tech",
+ "Online Learning & MOOCs",
+ "Test Prep & Certification",
+ "Corporate Training & Upskilling",
+ "Tutoring & Supplemental Learning",
+ "Education Management Systems (LMS/SIS)",
+ "Language Learning",
+ "Creator-Led & Cohort-Based Courses",
+ "Special Education & Accessibility Tools",
+ "Government Technology & Digital Services",
+ "Civic Engagement & Policy Platforms",
+ "International Development & Humanitarian Aid",
+ "Philanthropy & Grantmaking",
+ "Nonprofit Operations & Fundraising Tools",
+ "Public Health & Social Services",
+ "Education & Youth Development Programs",
+ "Environmental & Climate Action Organizations",
+ "Legal Aid & Social Justice Advocacy",
+ "Municipal & Infrastructure Services",
+ "Manufacturing & Industrial Automation",
+ "Energy Production (Oil, Gas, Nuclear)",
+ "Renewable Energy & Cleantech",
+ "Utilities & Grid Infrastructure",
+ "Industrial IoT & Monitoring Systems",
+ "Construction & Heavy Equipment",
+ "Mining & Natural Resources",
+ "Environmental Engineering & Sustainability",
+ "Energy Storage & Battery Technology",
+ "Automotive OEMs & Vehicle Manufacturing",
+ "Electric Vehicles (EVs) & Charging Infrastructure",
+ "Mobility-as-a-Service (MaaS)",
+ "Fleet Management",
+ "Public Transit & Urban Mobility",
+ "Autonomous Vehicles & ADAS",
+ "Aftermarket Parts & Services",
+ "Telematics & Vehicle Connectivity",
+ "Aviation & Aerospace Transport",
+ "Maritime Shipping",
+ "Fitness & Wellness",
+ "Beauty & Personal Care",
+ "Home & Living",
+ "Dating & Relationships",
+ "Hobbies, Crafts & DIY",
+ "Outdoor & Recreational Gear",
+ "Events, Experiences & Ticketing Platforms",
+ "Designer & Luxury Apparel",
+ "Accessories, Jewelry & Watches",
+ "Footwear & Leather Goods",
+ "Beauty, Fragrance & Skincare",
+ "Fashion Marketplaces & Retail Platforms",
+ "Sustainable & Ethical Fashion",
+ "Resale, Vintage & Circular Fashion",
+ "Fashion Tech & Virtual Try-Ons",
+ "Streetwear & Emerging Luxury",
+ "Couture & Made-to-Measure",
+ "News Publishing & Journalism",
+ "Digital Media & Content Platforms",
+ "Broadcasting (TV & Radio)",
+ "Podcasting & Audio Media",
+ "News Aggregators & Curation Tools",
+ "Independent & Creator-Led Media",
+ "Newsletters & Substack-Style Platforms",
+ "Political & Investigative Media",
+ "Trade & Niche Publications",
+ "Media Monitoring & Analytics",
+ "Professional Teams & Leagues",
+ "Sports Media & Broadcasting",
+ "Sports Betting & Fantasy Sports",
+ "Fitness & Athletic Training Platforms",
+ "Sportswear & Equipment",
+ "Esports & Competitive Gaming",
+ "Sports Venues & Event Management",
+ "Athlete Management & Talent Agencies",
+ "Sports Tech & Performance Analytics",
+ "Youth, Amateur & Collegiate Sports",
+ "Real Estate Marketplaces",
+ "Property Management Software",
+ "Rental Platforms",
+ "Mortgage & Lending Tech",
+ "Real Estate Investment Platforms",
+ "Law Firms & Legal Services",
+ "Legal Tech & Automation",
+ "Regulatory Compliance",
+ "E-Discovery & Litigation Tools",
+ "Contract Management",
+ "Governance, Risk & Compliance (GRC)",
+ "IP & Trademark Management",
+ "Legal Research & Intelligence",
+ "Compliance Training & Certification",
+ "Whistleblower & Ethics Reporting",
+ "Mobile & Wireless Networks (3G/4G/5G)",
+ "Broadband & Fiber Internet",
+ "Satellite & Space-Based Communications",
+ "Network Equipment & Infrastructure",
+ "Telecom Billing & OSS/BSS Systems",
+ "VoIP & Unified Communications",
+ "Internet Service Providers (ISPs)",
+ "Edge Computing & Network Virtualization",
+ "IoT Connectivity Platforms",
+ "Precision Agriculture & AgTech",
+ "Crop & Livestock Production",
+ "Food & Beverage Manufacturing & Processing",
+ "Food Distribution",
+ "Restaurants & Food Service",
+ "Agricultural Inputs & Equipment",
+ "Sustainable & Regenerative Agriculture",
+ "Seafood & Aquaculture",
+ "Management Consulting",
+ "Marketing & Advertising Agencies",
+ "Design, Branding & Creative Studios",
+ "IT Services & Managed Services",
+ "Staffing, Recruiting & Talent",
+ "Accounting & Tax Firms",
+ "Public Relations & Communications",
+ "Business Process Outsourcing (BPO)",
+ "Professional Training & Coaching",
+ "Specialty Chemicals",
+ "Commodity & Petrochemicals",
+ "Polymers, Plastics & Rubber",
+ "Coatings, Adhesives & Sealants",
+ "Industrial Gases",
+ "Advanced Materials & Composites",
+ "Battery Materials & Energy Storage",
+ "Electronic Materials & Semiconductor Chemicals",
+ "Agrochemicals & Fertilizers",
+ "Freight & Transportation Tech",
+ "Last-Mile Delivery",
+ "Warehouse Automation",
+ "Supply Chain Visibility Platforms",
+ "Logistics Marketplaces",
+ "Shipping & Freight Forwarding",
+ "Cold Chain Logistics",
+ "Reverse Logistics & Returns",
+ "Cross-Border Trade Tech",
+ "Transportation Management Systems (TMS)",
+ "Hotels & Accommodation",
+ "Vacation Rentals & Short-Term Stays",
+ "Restaurant Tech & Management",
+ "Travel Booking Platforms",
+ "Tourism Experiences & Activities",
+ "Cruise Lines & Marine Tourism",
+ "Hospitality Management Systems",
+ "Event & Venue Management",
+ "Corporate Travel Management",
+ "Travel Insurance & Protection",
+ "Construction Management Software",
+ "BIM/CAD & Design Tools",
+ "Construction Marketplaces",
+ "Equipment Rental & Management",
+ "Building Materials & Procurement",
+ "Construction Workforce Management",
+ "Project Estimation & Bidding",
+ "Modular & Prefab Construction",
+ "Construction Safety & Compliance",
+ "Smart Building Technology",
+ "Food & Beverage CPG",
+ "Home & Personal Care CPG",
+ "CPG Analytics & Insights",
+ "Direct-to-Consumer CPG Brands",
+ "CPG Supply Chain & Distribution",
+ "Private Label Manufacturing",
+ "CPG Retail Intelligence",
+ "Sustainable CPG & Packaging",
+ "Beauty & Cosmetics CPG",
+ "Health & Wellness CPG",
+ ]
+ """Subindustry classification enum"""
+
+
+class BrandIndustries(BaseModel):
+ eic: Optional[List[BrandIndustriesEic]] = None
+ """Easy Industry Classification - array of industry and subindustry pairs"""
+
+
+class BrandLinks(BaseModel):
+ blog: Optional[str] = None
+ """URL to the brand's blog or news page"""
+
+ careers: Optional[str] = None
+ """URL to the brand's careers or job opportunities page"""
+
+ contact: Optional[str] = None
+ """URL to the brand's contact or contact us page"""
+
+ pricing: Optional[str] = None
+ """URL to the brand's pricing or plans page"""
+
+ privacy: Optional[str] = None
+ """URL to the brand's privacy policy page"""
+
+ terms: Optional[str] = None
+ """URL to the brand's terms of service or terms and conditions page"""
+
+
+class BrandLogoColor(BaseModel):
+ hex: Optional[str] = None
+ """Color in hexadecimal format"""
+
+ name: Optional[str] = None
+ """Name of the color"""
+
+
+class BrandLogoResolution(BaseModel):
+ aspect_ratio: Optional[float] = None
+ """Aspect ratio of the image (width/height)"""
+
+ height: Optional[int] = None
+ """Height of the image in pixels"""
+
+ width: Optional[int] = None
+ """Width of the image in pixels"""
+
+
+class BrandLogo(BaseModel):
+ colors: Optional[List[BrandLogoColor]] = None
+ """Array of colors in the logo"""
+
+ mode: Optional[Literal["light", "dark", "has_opaque_background"]] = None
+ """
+ Indicates when this logo is best used: 'light' = best for light mode, 'dark' =
+ best for dark mode, 'has_opaque_background' = can be used for either as image
+ has its own background
+ """
+
+ resolution: Optional[BrandLogoResolution] = None
+ """Resolution of the logo image"""
+
+ type: Optional[Literal["icon", "logo"]] = None
+ """Type of the logo based on resolution (e.g., 'icon', 'logo')"""
+
+ url: Optional[str] = None
+ """CDN hosted url of the logo (ready for display)"""
+
+
+class BrandSocial(BaseModel):
+ type: Optional[str] = None
+ """Type of social media, e.g., 'facebook', 'twitter'"""
+
+ url: Optional[str] = None
+ """URL of the social media page"""
+
+
+class BrandStock(BaseModel):
+ exchange: Optional[str] = None
+ """Stock exchange name"""
+
+ ticker: Optional[str] = None
+ """Stock ticker symbol"""
+
+
+class Brand(BaseModel):
+ address: Optional[BrandAddress] = None
+ """Physical address of the brand"""
+
+ backdrops: Optional[List[BrandBackdrop]] = None
+ """An array of backdrop images for the brand"""
+
+ colors: Optional[List[BrandColor]] = None
+ """An array of brand colors"""
+
+ description: Optional[str] = None
+ """A brief description of the brand"""
+
+ domain: Optional[str] = None
+ """The domain name of the brand"""
+
+ email: Optional[str] = None
+ """Company email address"""
+
+ industries: Optional[BrandIndustries] = None
+ """Industry classification information for the brand"""
+
+ is_nsfw: Optional[bool] = None
+ """Indicates whether the brand content is not safe for work (NSFW)"""
+
+ links: Optional[BrandLinks] = None
+ """Important website links for the brand"""
+
+ logos: Optional[List[BrandLogo]] = None
+ """An array of logos associated with the brand"""
+
+ phone: Optional[str] = None
+ """Company phone number"""
+
+ slogan: Optional[str] = None
+ """The brand's slogan"""
+
+ socials: Optional[List[BrandSocial]] = None
+ """An array of social media links for the brand"""
+
+ stock: Optional[BrandStock] = None
+ """
+ Stock market information for this brand (will be null if not a publicly traded
+ company)
+ """
+
+ title: Optional[str] = None
+ """The title or name of the brand"""
+
+
+class BrandRetrieveByEmailResponse(BaseModel):
+ brand: Optional[Brand] = None
+ """Detailed brand information"""
+
+ code: Optional[int] = None
+ """HTTP status code"""
+
+ status: Optional[str] = None
+ """Status of the response, e.g., 'ok'"""
diff --git a/tests/api_resources/test_brand.py b/tests/api_resources/test_brand.py
index 7467a0a..7dfc647 100644
--- a/tests/api_resources/test_brand.py
+++ b/tests/api_resources/test_brand.py
@@ -17,6 +17,7 @@
BrandStyleguideResponse,
BrandRetrieveNaicsResponse,
BrandRetrieveByNameResponse,
+ BrandRetrieveByEmailResponse,
BrandRetrieveByTickerResponse,
BrandRetrieveSimplifiedResponse,
BrandIdentifyFromTransactionResponse,
@@ -240,6 +241,51 @@ def test_streaming_response_prefetch(self, client: BrandDev) -> None:
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_retrieve_by_email(self, client: BrandDev) -> None:
+ brand = client.brand.retrieve_by_email(
+ email="dev@stainless.com",
+ )
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_retrieve_by_email_with_all_params(self, client: BrandDev) -> None:
+ brand = client.brand.retrieve_by_email(
+ email="dev@stainless.com",
+ force_language="albanian",
+ max_speed=True,
+ timeout_ms=1,
+ )
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve_by_email(self, client: BrandDev) -> None:
+ response = client.brand.with_raw_response.retrieve_by_email(
+ email="dev@stainless.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ brand = response.parse()
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve_by_email(self, client: BrandDev) -> None:
+ with client.brand.with_streaming_response.retrieve_by_email(
+ email="dev@stainless.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ brand = response.parse()
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
def test_method_retrieve_by_name(self, client: BrandDev) -> None:
@@ -724,6 +770,51 @@ async def test_streaming_response_prefetch(self, async_client: AsyncBrandDev) ->
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_retrieve_by_email(self, async_client: AsyncBrandDev) -> None:
+ brand = await async_client.brand.retrieve_by_email(
+ email="dev@stainless.com",
+ )
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_retrieve_by_email_with_all_params(self, async_client: AsyncBrandDev) -> None:
+ brand = await async_client.brand.retrieve_by_email(
+ email="dev@stainless.com",
+ force_language="albanian",
+ max_speed=True,
+ timeout_ms=1,
+ )
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve_by_email(self, async_client: AsyncBrandDev) -> None:
+ response = await async_client.brand.with_raw_response.retrieve_by_email(
+ email="dev@stainless.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ brand = await response.parse()
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve_by_email(self, async_client: AsyncBrandDev) -> None:
+ async with async_client.brand.with_streaming_response.retrieve_by_email(
+ email="dev@stainless.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ brand = await response.parse()
+ assert_matches_type(BrandRetrieveByEmailResponse, brand, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@pytest.mark.skip(reason="Prism tests are disabled")
@parametrize
async def test_method_retrieve_by_name(self, async_client: AsyncBrandDev) -> None:
From 4d822f76f35db0a2d18fdabe2d541ee489f06ebb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:29:14 +0000
Subject: [PATCH 6/7] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index aeecb0c..26179c7 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 11
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-03d0d129690a7e6178f207b283001e66c7ef4a74d340647f18ce21ca3ccd8a1a.yml
-openapi_spec_hash: f64086044d57f32cba3ead2151fb4d61
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-6620330945de41f1c453692af40842f08fe1fd281ff6ba4e79d447c941ebd783.yml
+openapi_spec_hash: 861a43669d27d942d4bd3e36a398e95b
config_hash: 083e432ea397a9018371145493400188
From 562a05035d992c599ebba4a5defa475d0703de08 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:29:33 +0000
Subject: [PATCH 7/7] release: 1.19.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 18 ++++++++++++++++++
pyproject.toml | 2 +-
src/brand/dev/_version.py | 2 +-
4 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 4ce109a..de44c40 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.18.0"
+ ".": "1.19.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 277aeb9..8f8735a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## 1.19.0 (2025-10-31)
+
+Full Changelog: [v1.18.0...v1.19.0](https://github.com/brand-dot-dev/python-sdk/compare/v1.18.0...v1.19.0)
+
+### Features
+
+* **api:** manual updates ([887cd82](https://github.com/brand-dot-dev/python-sdk/commit/887cd82aaf78b8752cbf2fe8956ce81a9905d882))
+
+
+### Bug Fixes
+
+* **client:** close streams without requiring full consumption ([8c87170](https://github.com/brand-dot-dev/python-sdk/commit/8c8717069bf064ca9a467b1c478ed642a10d5342))
+
+
+### Chores
+
+* **internal/tests:** avoid race condition with implicit client cleanup ([ccaa28b](https://github.com/brand-dot-dev/python-sdk/commit/ccaa28b34cfbd24a0f46371dca4e43ab7a15fa61))
+
## 1.18.0 (2025-10-30)
Full Changelog: [v1.17.1...v1.18.0](https://github.com/brand-dot-dev/python-sdk/compare/v1.17.1...v1.18.0)
diff --git a/pyproject.toml b/pyproject.toml
index 9d6cdd2..019eb48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "brand.dev"
-version = "1.18.0"
+version = "1.19.0"
description = "The official Python library for the brand.dev API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/brand/dev/_version.py b/src/brand/dev/_version.py
index 77f162f..0fa6eb6 100644
--- a/src/brand/dev/_version.py
+++ b/src/brand/dev/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "brand.dev"
-__version__ = "1.18.0" # x-release-please-version
+__version__ = "1.19.0" # x-release-please-version