Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

10 changes: 4 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ $ ./scripts/test
### Snapshots

Some tests use [inline-snapshot](https://15r10nk.github.io/inline-snapshot/latest/). To update them after making changes, rerun the tests with the `--inline-snapshot=fix` and `-n0` options:

```bash
./scripts/test --inline-snapshot=fix -n0
```
Expand All @@ -108,15 +107,14 @@ Some tests use [inline-snapshot](https://15r10nk.github.io/inline-snapshot/lates
> `inline-snapshot` is incompatible with [pytest-xdist](https://github.com/pytest-dev/pytest-xdist), so you need to disable parallel execution `(-n0)` when using the `--inline-snapshot` option.

In addition, some tests capture snapshots of the HTTP requests they make.
To refresh these snapshots, run the tests with the `ANTHROPIC_LIVE=1` environment variable enabled.

To refresh these snapshots, run the tests with the `--http-record` flag:
```bash
ANTHROPIC_LIVE=1 ./scripts/test --inline-snapshot=fix
./scripts/test --inline-snapshot=fix --http-record -n0
```

> [!NOTE]
> Sometimes it makes sense to update only the inline snapshots `(--inline-snapshot=fix)` without refreshing the HTTP snapshots `(ANTHROPIC_LIVE=1)`.
> This is useful when the endpoint hasnt changed, but your code handles the response differently and the assertions need updating.
> Sometimes it makes sense to update only the inline snapshots `(--inline-snapshot=fix)` without refreshing the HTTP snapshots `(--http-record)`.
> This is useful when the endpoint hasn't changed, but your code handles the response differently and the assertions need updating.

## Linting and formatting

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ dev = [
"pytest-xdist>=3.6.1",
"inline-snapshot>=0.28.0",
"griffe>=1",
"http-snapshot[httpx]==0.1.6",
]
pydantic-v1 = [
"pydantic>=1.9.0,<2",
Expand Down
41 changes: 41 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import httpx
import pytest
from http_snapshot import SnapshotSerializerOptions
from pytest_asyncio import is_async_test

from anthropic import Anthropic, AsyncAnthropic, DefaultAioHttpClient
Expand All @@ -20,6 +21,18 @@

logging.getLogger("anthropic").setLevel(logging.DEBUG)

SNAPSHOT_RESPONSE_HEADERS_EXCLUDE = [
"date",
"request-id",
"anthropic-organization-id",
"x-envoy-upstream-service-time",
"cf-ray",
]

SNAPSHOT_REQUEST_HEADERS_EXCLUDE = [
"x-api-key",
]


# automatically add `pytest.mark.asyncio()` to all of our async tests
# so we don't have to add that boilerplate everywhere
Expand Down Expand Up @@ -58,6 +71,34 @@ def client(request: FixtureRequest) -> Iterator[Anthropic]:
yield client


@pytest.fixture
def http_snapshot_serializer_options() -> SnapshotSerializerOptions:
return SnapshotSerializerOptions(
exclude_response_headers=SNAPSHOT_RESPONSE_HEADERS_EXCLUDE,
exclude_request_headers=SNAPSHOT_REQUEST_HEADERS_EXCLUDE,
include_request=True,
)


@pytest.fixture(scope="function")
def snapshot_client(
snapshot_sync_httpx_client: httpx.Client,
is_recording: bool,
) -> Iterator[Anthropic]:
with Anthropic(http_client=snapshot_sync_httpx_client, api_key=None if is_recording else api_key) as client:
yield client


@pytest.fixture(scope="function")
async def async_snapshot_client(
snapshot_async_httpx_client: httpx.AsyncClient, is_recording: bool
) -> AsyncIterator[AsyncAnthropic]:
async with AsyncAnthropic(
http_client=snapshot_async_httpx_client, api_key=None if is_recording else api_key
) as client:
yield client


@pytest.fixture(scope="session")
async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncAnthropic]:
param = getattr(request, "param", True)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
[
{
"request": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages?beta=true",
"headers": {
"host": "api.anthropic.com",
"accept-encoding": "gzip, deflate",
"connection": "keep-alive",
"x-stainless-timeout": "NOT_GIVEN",
"accept": "application/json",
"content-type": "application/json",
"user-agent": "AsyncAnthropic/Python 0.75.0",
"x-stainless-lang": "python",
"x-stainless-package-version": "0.75.0",
"x-stainless-os": "MacOS",
"x-stainless-arch": "arm64",
"x-stainless-runtime": "CPython",
"x-stainless-runtime-version": "3.9.18",
"x-stainless-async": "async:asyncio",
"anthropic-version": "2023-06-01",
"x-stainless-helper-method": "stream",
"x-stainless-stream-helper": "beta.messages",
"anthropic-beta": "structured-outputs-2025-11-13",
"x-stainless-retry-count": "0",
"x-stainless-read-timeout": "600",
"content-length": "265"
},
"body": {
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "Extract order IDs from the following text:\n\nOrder 12345\nOrder 67890"
}
],
"model": "claude-sonnet-4-5",
"output_format": {
"type": "json_schema",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"stream": true
}
},
"response": {
"status_code": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8",
"connection": "keep-alive",
"server": "cloudflare",
"cf-cache-status": "DYNAMIC",
"cache-control": "no-cache",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01JbzU4eQy8GBAU9N2KuLT93\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":135,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"[\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"12\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"345,\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"67890]\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":135,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":10} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
}
}
]
23 changes: 11 additions & 12 deletions tests/lib/_parse/test_beta_messages.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
from typing import Any, cast

import pytest
from respx import MockRouter
from inline_snapshot import external, snapshot

from anthropic import AsyncAnthropic, _compat
from anthropic.types.beta.parsed_beta_message import ParsedBetaMessage

from ..snapshots import make_async_stream_snapshot_request


@pytest.mark.skipif(_compat.PYDANTIC_V1, reason="tool runner not supported with pydantic v1")
class TestAsyncMessages:
async def test_stream_with_raw_schema(self, async_client: AsyncAnthropic, respx_mock: MockRouter) -> None:
@pytest.mark.parametrize(
"http_snapshot",
[
cast(Any, external("uuid:606342ef-f614-4bc1-b5e7-2c3305a48bf3.json")),
],
)
async def test_stream_with_raw_schema(self, async_snapshot_client: AsyncAnthropic) -> None:
async def async_stream_parse(client: AsyncAnthropic) -> ParsedBetaMessage[None]:
async with client.beta.messages.stream(
model="claude-sonnet-4-5",
Expand All @@ -32,12 +37,6 @@ async def async_stream_parse(client: AsyncAnthropic) -> ParsedBetaMessage[None]:
) as stream:
return await stream.get_final_message()

response = await make_async_stream_snapshot_request(
async_stream_parse,
content_snapshot=external("uuid:48aac7c3-f271-47b3-854b-af4ed31e10bb.json"),
respx_mock=respx_mock,
mock_client=async_client,
path="/v1/messages?beta=true",
)
response = await async_stream_parse(async_snapshot_client)

assert response.content[0].text == snapshot("[12345,67890]")
assert response.content[0].text == snapshot("[12345,67890]") # type: ignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why do we need the type ignore now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's for accessing the text property, since it might be missing. Idk what's better an extra line to check for it, or just type: ignore. pytest would anyway fail with the good message if there would not be .text 🤷‍♂️

Loading