From c18e5ed77700bc98ba1c85638a503e9e0a35afb7 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:55:44 +0000 Subject: [PATCH 01/10] docs: reword package description (#228) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a46e7648..21dd46cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "anthropic" version = "0.6.0" -description = "Client library for the anthropic API" +description = "The official Python library for the anthropic API" readme = "README.md" license = "MIT" authors = [ From a60d54331f8f6d28bf57dc979d4393759f5e1534 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Fri, 10 Nov 2023 10:00:19 +0000 Subject: [PATCH 02/10] fix(client): correctly flush the stream response body (#230) --- src/anthropic/_streaming.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/anthropic/_streaming.py b/src/anthropic/_streaming.py index e399257e..f2d2246a 100644 --- a/src/anthropic/_streaming.py +++ b/src/anthropic/_streaming.py @@ -45,8 +45,9 @@ def __stream__(self) -> Iterator[ResponseT]: cast_to = self._cast_to response = self.response process_data = self._client._process_response_data + iterator = self._iter_events() - for sse in self._iter_events(): + for sse in iterator: if sse.event == "completion": yield process_data(data=sse.json(), cast_to=cast_to, response=response) @@ -68,6 +69,10 @@ def __stream__(self) -> Iterator[ResponseT]: response=self.response, ) + # Ensure the entire stream is consumed + for sse in iterator: + ... + class AsyncStream(Generic[ResponseT]): """Provides the core interface to iterate over an asynchronous stream response.""" @@ -102,8 +107,9 @@ async def __stream__(self) -> AsyncIterator[ResponseT]: cast_to = self._cast_to response = self.response process_data = self._client._process_response_data + iterator = self._iter_events() - async for sse in self._iter_events(): + async for sse in iterator: if sse.event == "completion": yield process_data(data=sse.json(), cast_to=cast_to, response=response) @@ -125,6 +131,10 @@ async def __stream__(self) -> AsyncIterator[ResponseT]: response=self.response, ) + # Ensure the entire stream is consumed + async for sse in iterator: + ... + class ServerSentEvent: def __init__( From 4ce7a1e676023984be80fe0eacb1a0223780886c Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:49:56 +0000 Subject: [PATCH 03/10] fix(models): mark unknown fields as set in pydantic v1 (#231) --- src/anthropic/_models.py | 1 + tests/test_client.py | 12 ++++++------ tests/test_transform.py | 11 +++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/anthropic/_models.py b/src/anthropic/_models.py index 00d787ca..ebaef994 100644 --- a/src/anthropic/_models.py +++ b/src/anthropic/_models.py @@ -121,6 +121,7 @@ def construct( if PYDANTIC_V2: _extra[key] = value else: + _fields_set.add(key) fields_values[key] = value object.__setattr__(m, "__dict__", fields_values) diff --git a/tests/test_client.py b/tests/test_client.py index 56438da5..8cfa59ab 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -42,12 +42,12 @@ class TestAnthropic: @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock(return_value=httpx.Response(200, json='{"foo": "bar"}')) + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = self.client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) - assert response.json() == '{"foo": "bar"}' + assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: @@ -58,7 +58,7 @@ def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: response = self.client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) - assert response.json() == '{"foo": "bar"}' + assert response.json() == {"foo": "bar"} def test_copy(self) -> None: copied = self.client.copy() @@ -669,12 +669,12 @@ class TestAsyncAnthropic: @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio async def test_raw_response(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock(return_value=httpx.Response(200, json='{"foo": "bar"}')) + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) response = await self.client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) - assert response.json() == '{"foo": "bar"}' + assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -686,7 +686,7 @@ async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: response = await self.client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) - assert response.json() == '{"foo": "bar"}' + assert response.json() == {"foo": "bar"} def test_copy(self) -> None: copied = self.client.copy() diff --git a/tests/test_transform.py b/tests/test_transform.py index 8e1d4724..5f9abd21 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -7,6 +7,7 @@ import pytest from anthropic._utils import PropertyInfo, transform, parse_datetime +from anthropic._compat import PYDANTIC_V2 from anthropic._models import BaseModel @@ -210,14 +211,20 @@ def test_pydantic_unknown_field() -> None: def test_pydantic_mismatched_types() -> None: model = MyModel.construct(foo=True) - with pytest.warns(UserWarning): + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = transform(model, Any) + else: params = transform(model, Any) assert params == {"foo": True} def test_pydantic_mismatched_object_type() -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - with pytest.warns(UserWarning): + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = transform(model, Any) + else: params = transform(model, Any) assert params == {"foo": {"hello": "world"}} From d5e70e8b803c96c8640508b31773b8b9d827d903 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:06:17 +0000 Subject: [PATCH 04/10] fix(client): serialise pydantic v1 default fields correctly in params (#232) --- src/anthropic/_utils/_transform.py | 2 +- tests/test_transform.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/anthropic/_utils/_transform.py b/src/anthropic/_utils/_transform.py index dc497ea3..d953505f 100644 --- a/src/anthropic/_utils/_transform.py +++ b/src/anthropic/_utils/_transform.py @@ -168,7 +168,7 @@ def _transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, exclude_defaults=True) + return model_dump(data, exclude_unset=True) return _transform_value(data, annotation) diff --git a/tests/test_transform.py b/tests/test_transform.py index 5f9abd21..cfcf870e 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -237,3 +237,29 @@ def test_pydantic_nested_objects() -> None: model = ModelNestedObjects.construct(nested={"foo": "stainless"}) assert isinstance(model.nested, MyModel) assert transform(model, Any) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +def test_pydantic_default_field() -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert transform(model, Any) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert transform(model, Any) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert transform(model, Any) == {"with_none_default": "bar", "with_str_default": "baz"} From 33b553a8de5d45273ca9f335c59a263136385f14 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:11:19 +0000 Subject: [PATCH 05/10] fix(client): retry if SSLWantReadError occurs in the async client (#233) --- src/anthropic/_base_client.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/anthropic/_base_client.py b/src/anthropic/_base_client.py index b2fe2426..3db8b6fa 100644 --- a/src/anthropic/_base_client.py +++ b/src/anthropic/_base_client.py @@ -1320,12 +1320,6 @@ async def _request( if retries > 0: return await self._retry_request(options, cast_to, retries, stream=stream, stream_cls=stream_cls) raise APITimeoutError(request=request) from err - except httpx.ReadTimeout as err: - # We explicitly do not retry on ReadTimeout errors as this means - # that the server processing the request has taken 60 seconds - # (our default timeout). This likely indicates that something - # is not working as expected on the server side. - raise except httpx.TimeoutException as err: if retries > 0: return await self._retry_request(options, cast_to, retries, stream=stream, stream_cls=stream_cls) From ce5cccc9bc8482e4e3f6af034892a347eb2b52fc Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:59:53 +0000 Subject: [PATCH 06/10] chore(internal): fix typo in NotGiven docstring (#234) --- src/anthropic/_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/anthropic/_types.py b/src/anthropic/_types.py index fbd6e3af..7e95adbe 100644 --- a/src/anthropic/_types.py +++ b/src/anthropic/_types.py @@ -279,8 +279,8 @@ class NotGiven: ```py def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... - get(timout=1) # 1s timeout - get(timout=None) # No timeout + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout get() # Default timeout behavior, which may not be statically known at the method definition. ``` """ From 7f92e25d6fa15bed799994d173ad62bcf60e5b3b Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:50:43 +0000 Subject: [PATCH 07/10] chore(internal): fix devcontainer interpeter path (#235) --- .devcontainer/devcontainer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b9da964d..bbeb30b1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,6 +17,7 @@ "settings": { "terminal.integrated.shell.linux": "/bin/bash", "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", "python.typeChecking": "basic", "terminal.integrated.env.linux": { "PATH": "/home/vscode/.rye/shims:${env:PATH}" From 7ef0464724346d930ff1580526fd70b592759641 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:11:13 +0000 Subject: [PATCH 08/10] docs: fix code comment typo (#236) --- src/anthropic/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anthropic/_models.py b/src/anthropic/_models.py index ebaef994..6d5aad59 100644 --- a/src/anthropic/_models.py +++ b/src/anthropic/_models.py @@ -149,7 +149,7 @@ def construct( if not PYDANTIC_V2: # we define aliases for some of the new pydantic v2 methods so # that we can just document these methods without having to specify - # a specifc pydantic version as some users may not know which + # a specific pydantic version as some users may not know which # pydantic version they are currently using @override From dd91bfd278f4e2e76b2f194098f34070fd5a3ff9 Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:51:43 +0000 Subject: [PATCH 09/10] feat(client): support reading the base url from an env variable (#237) --- README.md | 1 + src/anthropic/_client.py | 4 ++++ tests/test_client.py | 12 ++++++++++++ tests/utils.py | 17 ++++++++++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d607542..836dc982 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,7 @@ import httpx from anthropic import Anthropic client = Anthropic( + # Or use the `ANTHROPIC_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=httpx.Client( proxies="http://my.test.proxy.example.com", diff --git a/src/anthropic/_client.py b/src/anthropic/_client.py index 62f80393..ab7a5cc6 100644 --- a/src/anthropic/_client.py +++ b/src/anthropic/_client.py @@ -103,6 +103,8 @@ def __init__( auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") self.auth_token = auth_token + if base_url is None: + base_url = os.environ.get("ANTHROPIC_BASE_URL") if base_url is None: base_url = f"https://api.anthropic.com" @@ -362,6 +364,8 @@ def __init__( auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") self.auth_token = auth_token + if base_url is None: + base_url = os.environ.get("ANTHROPIC_BASE_URL") if base_url is None: base_url = f"https://api.anthropic.com" diff --git a/tests/test_client.py b/tests/test_client.py index 8cfa59ab..7d03435d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,6 +27,8 @@ make_request_options, ) +from .utils import update_env + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "my-anthropic-api-key" @@ -408,6 +410,11 @@ class Model2(BaseModel): assert isinstance(response, Model1) assert response.foo == 1 + def test_base_url_env(self) -> None: + with update_env(ANTHROPIC_BASE_URL="http://localhost:5000/from/env"): + client = Anthropic(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + @pytest.mark.parametrize( "client", [ @@ -1036,6 +1043,11 @@ class Model2(BaseModel): assert isinstance(response, Model1) assert response.foo == 1 + def test_base_url_env(self) -> None: + with update_env(ANTHROPIC_BASE_URL="http://localhost:5000/from/env"): + client = AsyncAnthropic(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + @pytest.mark.parametrize( "client", [ diff --git a/tests/utils.py b/tests/utils.py index 0a1733f6..348363a5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,9 @@ from __future__ import annotations +import os import traceback -from typing import Any, TypeVar, cast +import contextlib +from typing import Any, TypeVar, Iterator, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -103,3 +105,16 @@ def _assert_list_type(type_: type[object], value: object) -> None: inner_type = get_args(type_)[0] for entry in value: assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str) -> Iterator[None]: + old = os.environ.copy() + + try: + os.environ.update(new_env) + + yield None + finally: + os.environ.clear() + os.environ.update(old) From 31f3e25c87977dc144eb763b20ccc0ba0133414d Mon Sep 17 00:00:00 2001 From: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:52:09 +0000 Subject: [PATCH 10/10] release: 0.7.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/anthropic/_version.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5cb..1b77f506 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c37fd707..148920d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.7.0 (2023-11-15) + +Full Changelog: [v0.6.0...v0.7.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.6.0...v0.7.0) + +### Features + +* **client:** support reading the base url from an env variable ([#237](https://github.com/anthropics/anthropic-sdk-python/issues/237)) ([dd91bfd](https://github.com/anthropics/anthropic-sdk-python/commit/dd91bfd278f4e2e76b2f194098f34070fd5a3ff9)) + + +### Bug Fixes + +* **client:** correctly flush the stream response body ([#230](https://github.com/anthropics/anthropic-sdk-python/issues/230)) ([a60d543](https://github.com/anthropics/anthropic-sdk-python/commit/a60d54331f8f6d28bf57dc979d4393759f5e1534)) +* **client:** retry if SSLWantReadError occurs in the async client ([#233](https://github.com/anthropics/anthropic-sdk-python/issues/233)) ([33b553a](https://github.com/anthropics/anthropic-sdk-python/commit/33b553a8de5d45273ca9f335c59a263136385f14)) +* **client:** serialise pydantic v1 default fields correctly in params ([#232](https://github.com/anthropics/anthropic-sdk-python/issues/232)) ([d5e70e8](https://github.com/anthropics/anthropic-sdk-python/commit/d5e70e8b803c96c8640508b31773b8b9d827d903)) +* **models:** mark unknown fields as set in pydantic v1 ([#231](https://github.com/anthropics/anthropic-sdk-python/issues/231)) ([4ce7a1e](https://github.com/anthropics/anthropic-sdk-python/commit/4ce7a1e676023984be80fe0eacb1a0223780886c)) + + +### Chores + +* **internal:** fix devcontainer interpeter path ([#235](https://github.com/anthropics/anthropic-sdk-python/issues/235)) ([7f92e25](https://github.com/anthropics/anthropic-sdk-python/commit/7f92e25d6fa15bed799994d173ad62bcf60e5b3b)) +* **internal:** fix typo in NotGiven docstring ([#234](https://github.com/anthropics/anthropic-sdk-python/issues/234)) ([ce5cccc](https://github.com/anthropics/anthropic-sdk-python/commit/ce5cccc9bc8482e4e3f6af034892a347eb2b52fc)) + + +### Documentation + +* fix code comment typo ([#236](https://github.com/anthropics/anthropic-sdk-python/issues/236)) ([7ef0464](https://github.com/anthropics/anthropic-sdk-python/commit/7ef0464724346d930ff1580526fd70b592759641)) +* reword package description ([#228](https://github.com/anthropics/anthropic-sdk-python/issues/228)) ([c18e5ed](https://github.com/anthropics/anthropic-sdk-python/commit/c18e5ed77700bc98ba1c85638a503e9e0a35afb7)) + ## 0.6.0 (2023-11-08) Full Changelog: [v0.5.1...v0.6.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.5.1...v0.6.0) diff --git a/pyproject.toml b/pyproject.toml index 21dd46cd..8bf5e1c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anthropic" -version = "0.6.0" +version = "0.7.0" description = "The official Python library for the anthropic API" readme = "README.md" license = "MIT" diff --git a/src/anthropic/_version.py b/src/anthropic/_version.py index 8987a497..c3e5a32f 100644 --- a/src/anthropic/_version.py +++ b/src/anthropic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. __title__ = "anthropic" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version