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