diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb542396..978fcecd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - main + - next jobs: lint: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f81bf992..8305d4ab 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.31.0" + ".": "0.31.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ff322255..3ad8fd53 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 2 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic-ce067ae8303fa9b7aae2e8ebf0b6e9e41509f169ba93c1807e6ed9c9e541be1a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic-e2a51f04a202c13736b6fa2061a89a0c443f99ab166d965d702baf371eb1ca8f.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 61edf009..b099f258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.31.1 (2024-07-15) + +Full Changelog: [v0.31.0...v0.31.1](https://github.com/anthropics/anthropic-sdk-python/compare/v0.31.0...v0.31.1) + +### Bug Fixes + +* **bedrock:** correct request options for retries ([#593](https://github.com/anthropics/anthropic-sdk-python/issues/593)) ([f68c81d](https://github.com/anthropics/anthropic-sdk-python/commit/f68c81d072fceb46d4c0d8ee62cf274eeea99415)) + + +### Chores + +* **ci:** also run workflows for PRs targeting `next` ([#587](https://github.com/anthropics/anthropic-sdk-python/issues/587)) ([f7e49f2](https://github.com/anthropics/anthropic-sdk-python/commit/f7e49f2f2ceb62cccd6961fc1bd799655ccd83ab)) +* **internal:** minor changes to tests ([#591](https://github.com/anthropics/anthropic-sdk-python/issues/591)) ([fabd591](https://github.com/anthropics/anthropic-sdk-python/commit/fabd5910f2e769b8bfbeaaa8b65ca8383b4954e3)) +* **internal:** minor formatting changes ([a71927b](https://github.com/anthropics/anthropic-sdk-python/commit/a71927b7c7cff4e83eb485d3b0eef928a18acef6)) +* **internal:** minor import restructuring ([#588](https://github.com/anthropics/anthropic-sdk-python/issues/588)) ([1d9db4f](https://github.com/anthropics/anthropic-sdk-python/commit/1d9db4f6c1393c3879e83e1a3e1d1b4fedc33b5a)) +* **internal:** minor options / compat functions updates ([#592](https://github.com/anthropics/anthropic-sdk-python/issues/592)) ([d41a880](https://github.com/anthropics/anthropic-sdk-python/commit/d41a8807057958d4505e16325e4a06359a760260)) +* **internal:** update mypy ([#584](https://github.com/anthropics/anthropic-sdk-python/issues/584)) ([0a0edce](https://github.com/anthropics/anthropic-sdk-python/commit/0a0edce53e9eebd47770e71493302527e7f43751)) + ## 0.31.0 (2024-07-10) Full Changelog: [v0.30.1...v0.31.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.30.1...v0.31.0) diff --git a/pyproject.toml b/pyproject.toml index 491cbfc5..9ce10710 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anthropic" -version = "0.31.0" +version = "0.31.1" description = "The official Python library for the anthropic API" dynamic = ["readme"] license = "MIT" diff --git a/requirements-dev.lock b/requirements-dev.lock index f226bea6..f7680375 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -76,7 +76,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.7.1 +mypy==1.10.1 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 diff --git a/src/anthropic/_base_client.py b/src/anthropic/_base_client.py index 39c311fd..c8e190d1 100644 --- a/src/anthropic/_base_client.py +++ b/src/anthropic/_base_client.py @@ -880,9 +880,9 @@ def __exit__( def _prepare_options( self, options: FinalRequestOptions, # noqa: ARG002 - ) -> None: + ) -> FinalRequestOptions: """Hook for mutating the given options""" - return None + return options def _prepare_request( self, @@ -962,7 +962,7 @@ def _request( input_options = model_copy(options) cast_to = self._maybe_override_cast_to(cast_to, options) - self._prepare_options(options) + options = self._prepare_options(options) retries = self._remaining_retries(remaining_retries, options) request = self._build_request(options) @@ -1457,9 +1457,9 @@ async def __aexit__( async def _prepare_options( self, options: FinalRequestOptions, # noqa: ARG002 - ) -> None: + ) -> FinalRequestOptions: """Hook for mutating the given options""" - return None + return options async def _prepare_request( self, @@ -1544,7 +1544,7 @@ async def _request( input_options = model_copy(options) cast_to = self._maybe_override_cast_to(cast_to, options) - await self._prepare_options(options) + options = await self._prepare_options(options) retries = self._remaining_retries(remaining_retries, options) request = self._build_request(options) diff --git a/src/anthropic/_compat.py b/src/anthropic/_compat.py index 74c7639b..c919b5ad 100644 --- a/src/anthropic/_compat.py +++ b/src/anthropic/_compat.py @@ -118,10 +118,10 @@ def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: return model.__fields__ # type: ignore -def model_copy(model: _ModelT) -> _ModelT: +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: if PYDANTIC_V2: - return model.model_copy() - return model.copy() # type: ignore + return model.model_copy(deep=deep) + return model.copy(deep=deep) # type: ignore def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: diff --git a/src/anthropic/_version.py b/src/anthropic/_version.py index bb4a1435..bfeda194 100644 --- a/src/anthropic/_version.py +++ b/src/anthropic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "anthropic" -__version__ = "0.31.0" # x-release-please-version +__version__ = "0.31.1" # x-release-please-version diff --git a/src/anthropic/lib/bedrock/_client.py b/src/anthropic/lib/bedrock/_client.py index b3f388e5..f7298adc 100644 --- a/src/anthropic/lib/bedrock/_client.py +++ b/src/anthropic/lib/bedrock/_client.py @@ -9,6 +9,7 @@ from ... import _exceptions from ..._types import NOT_GIVEN, Timeout, NotGiven from ..._utils import is_dict, is_given +from ..._compat import model_copy from ..._version import __version__ from ..._streaming import Stream, AsyncStream from ..._exceptions import APIStatusError @@ -29,28 +30,27 @@ _DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) -class BaseBedrockClient(BaseClient[_HttpxClientT, _DefaultStreamT]): - @override - def _build_request( - self, - options: FinalRequestOptions, - ) -> httpx.Request: - if is_dict(options.json_data): - options.json_data.setdefault("anthropic_version", DEFAULT_VERSION) +def _prepare_options(input_options: FinalRequestOptions) -> FinalRequestOptions: + options = model_copy(input_options, deep=True) + + if is_dict(options.json_data): + options.json_data.setdefault("anthropic_version", DEFAULT_VERSION) + + if options.url in {"/v1/complete", "/v1/messages"} and options.method == "post": + if not is_dict(options.json_data): + raise RuntimeError("Expected dictionary json_data for post /completions endpoint") - if options.url in {"/v1/complete", "/v1/messages"} and options.method == "post": - if not is_dict(options.json_data): - raise RuntimeError("Expected dictionary json_data for post /completions endpoint") + model = options.json_data.pop("model", None) + stream = options.json_data.pop("stream", False) + if stream: + options.url = f"/model/{model}/invoke-with-response-stream" + else: + options.url = f"/model/{model}/invoke" - model = options.json_data.pop("model", None) - stream = options.json_data.pop("stream", False) - if stream: - options.url = f"/model/{model}/invoke-with-response-stream" - else: - options.url = f"/model/{model}/invoke" + return options - return super()._build_request(options) +class BaseBedrockClient(BaseClient[_HttpxClientT, _DefaultStreamT]): @override def _make_status_error( self, @@ -145,6 +145,10 @@ def __init__( def _make_sse_decoder(self) -> AWSEventStreamDecoder: return AWSEventStreamDecoder() + @override + def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: + return _prepare_options(options) + @override def _prepare_request(self, request: httpx.Request) -> None: from ._auth import get_auth_headers @@ -280,6 +284,10 @@ def __init__( def _make_sse_decoder(self) -> AWSEventStreamDecoder: return AWSEventStreamDecoder() + @override + async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: + return _prepare_options(options) + @override async def _prepare_request(self, request: httpx.Request) -> None: from ._auth import get_auth_headers diff --git a/src/anthropic/resources/completions.py b/src/anthropic/resources/completions.py index 97ba680e..f0c9afaf 100644 --- a/src/anthropic/resources/completions.py +++ b/src/anthropic/resources/completions.py @@ -19,9 +19,7 @@ from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper from .._streaming import Stream, AsyncStream -from .._base_client import ( - make_request_options, -) +from .._base_client import make_request_options from ..types.completion import Completion __all__ = ["Completions", "AsyncCompletions"] diff --git a/src/anthropic/resources/messages.py b/src/anthropic/resources/messages.py index 18badfa8..c7d3639f 100644 --- a/src/anthropic/resources/messages.py +++ b/src/anthropic/resources/messages.py @@ -20,9 +20,7 @@ from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper from .._streaming import Stream, AsyncStream -from .._base_client import ( - make_request_options, -) +from .._base_client import make_request_options from ..lib.streaming import MessageStreamManager, AsyncMessageStreamManager from ..types.message import Message from ..types.tool_param import ToolParam diff --git a/src/anthropic/types/content_block_delta_event.py b/src/anthropic/types/content_block_delta_event.py index 4bdbb2ee..a32602b4 100644 --- a/src/anthropic/types/content_block_delta_event.py +++ b/src/anthropic/types/content_block_delta_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_content_block_delta_event import RawContentBlockDeltaEvent __all__ = ["ContentBlockDeltaEvent"] diff --git a/src/anthropic/types/content_block_start_event.py b/src/anthropic/types/content_block_start_event.py index cedfe802..873cba3b 100644 --- a/src/anthropic/types/content_block_start_event.py +++ b/src/anthropic/types/content_block_start_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_content_block_start_event import RawContentBlockStartEvent __all__ = ["ContentBlockStartEvent"] diff --git a/src/anthropic/types/content_block_stop_event.py b/src/anthropic/types/content_block_stop_event.py index 82d02ee8..36c62c89 100644 --- a/src/anthropic/types/content_block_stop_event.py +++ b/src/anthropic/types/content_block_stop_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_content_block_stop_event import RawContentBlockStopEvent __all__ = ["ContentBlockStopEvent"] diff --git a/src/anthropic/types/message_delta_event.py b/src/anthropic/types/message_delta_event.py index 3f272240..3803629a 100644 --- a/src/anthropic/types/message_delta_event.py +++ b/src/anthropic/types/message_delta_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_message_delta_event import RawMessageDeltaEvent __all__ = ["MessageDeltaEvent"] diff --git a/src/anthropic/types/message_start_event.py b/src/anthropic/types/message_start_event.py index 83944a53..c210d3ad 100644 --- a/src/anthropic/types/message_start_event.py +++ b/src/anthropic/types/message_start_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_message_start_event import RawMessageStartEvent __all__ = ["MessageStartEvent"] diff --git a/src/anthropic/types/message_stop_event.py b/src/anthropic/types/message_stop_event.py index c25f2dcc..1076a62c 100644 --- a/src/anthropic/types/message_stop_event.py +++ b/src/anthropic/types/message_stop_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_message_stop_event import RawMessageStopEvent __all__ = ["MessageStopEvent"] diff --git a/src/anthropic/types/message_stream_event.py b/src/anthropic/types/message_stream_event.py index 507eb47b..ec5a0125 100644 --- a/src/anthropic/types/message_stream_event.py +++ b/src/anthropic/types/message_stream_event.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .raw_message_stream_event import RawMessageStreamEvent __all__ = ["MessageStreamEvent"] diff --git a/tests/lib/test_bedrock.py b/tests/lib/test_bedrock.py new file mode 100644 index 00000000..5c4cf9f5 --- /dev/null +++ b/tests/lib/test_bedrock.py @@ -0,0 +1,93 @@ +import re +from typing import cast +from typing_extensions import Protocol + +import httpx +import pytest +from respx import MockRouter + +from anthropic import AnthropicBedrock, AsyncAnthropicBedrock + +sync_client = AnthropicBedrock( + aws_region="us-east-1", + aws_access_key="example-access-key", + aws_secret_key="example-secret-key", +) +async_client = AsyncAnthropicBedrock( + aws_region="us-east-1", + aws_access_key="example-access-key", + aws_secret_key="example-secret-key", +) + + +class MockRequestCall(Protocol): + request: httpx.Request + + +@pytest.mark.respx() +def test_messages_retries(respx_mock: MockRouter) -> None: + respx_mock.post(re.compile(r"https://bedrock-runtime\.us-east-1\.amazonaws\.com/model/.*/invoke")).mock( + side_effect=[ + httpx.Response(500, json={"error": "server error"}), + httpx.Response(200, json={"foo": "bar"}), + ] + ) + + sync_client.messages.create( + max_tokens=1024, + messages=[ + { + "role": "user", + "content": "Say hello there!", + } + ], + model="anthropic.claude-3-sonnet-20240229-v1:0", + ) + + calls = cast("list[MockRequestCall]", respx_mock.calls) + + assert len(calls) == 2 + + assert ( + calls[0].request.url + == "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke" + ) + assert ( + calls[1].request.url + == "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke" + ) + + +@pytest.mark.respx() +@pytest.mark.asyncio() +async def test_messages_retries_async(respx_mock: MockRouter) -> None: + respx_mock.post(re.compile(r"https://bedrock-runtime\.us-east-1\.amazonaws\.com/model/.*/invoke")).mock( + side_effect=[ + httpx.Response(500, json={"error": "server error"}), + httpx.Response(200, json={"foo": "bar"}), + ] + ) + + await async_client.messages.create( + max_tokens=1024, + messages=[ + { + "role": "user", + "content": "Say hello there!", + } + ], + model="anthropic.claude-3-sonnet-20240229-v1:0", + ) + + calls = cast("list[MockRequestCall]", respx_mock.calls) + + assert len(calls) == 2 + + assert ( + calls[0].request.url + == "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke" + ) + assert ( + calls[1].request.url + == "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-sonnet-20240229-v1:0/invoke" + )