Skip to content

Commit e638d2e

Browse files
feat(client): add support for binary request streaming
1 parent 939a9aa commit e638d2e

File tree

4 files changed

+344
-14
lines changed

4 files changed

+344
-14
lines changed

src/anthropic/_base_client.py

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import inspect
1111
import logging
1212
import platform
13+
import warnings
1314
import email.utils
1415
from types import TracebackType
1516
from random import random
@@ -54,9 +55,11 @@
5455
ResponseT,
5556
AnyMapping,
5657
PostParser,
58+
BinaryTypes,
5759
RequestFiles,
5860
HttpxSendArgs,
5961
RequestOptions,
62+
AsyncBinaryTypes,
6063
HttpxRequestFiles,
6164
ModelBuilderProtocol,
6265
not_given,
@@ -490,8 +493,19 @@ def _build_request(
490493
retries_taken: int = 0,
491494
) -> httpx.Request:
492495
if log.isEnabledFor(logging.DEBUG):
493-
log.debug("Request options: %s", model_dump(options, exclude_unset=True))
494-
496+
log.debug(
497+
"Request options: %s",
498+
model_dump(
499+
options,
500+
exclude_unset=True,
501+
# Pydantic v1 can't dump every type we support in content, so we exclude it for now.
502+
exclude={
503+
"content",
504+
}
505+
if PYDANTIC_V1
506+
else {},
507+
),
508+
)
495509
kwargs: dict[str, Any] = {}
496510

497511
json_data = options.json_data
@@ -545,7 +559,13 @@ def _build_request(
545559
is_body_allowed = options.method.lower() != "get"
546560

547561
if is_body_allowed:
548-
if isinstance(json_data, bytes):
562+
if options.content is not None and json_data is not None:
563+
raise TypeError("Passing both `content` and `json_data` is not supported")
564+
if options.content is not None and files is not None:
565+
raise TypeError("Passing both `content` and `files` is not supported")
566+
if options.content is not None:
567+
kwargs["content"] = options.content
568+
elif isinstance(json_data, bytes):
549569
kwargs["content"] = json_data
550570
else:
551571
kwargs["json"] = json_data if is_given(json_data) else None
@@ -1278,6 +1298,7 @@ def post(
12781298
*,
12791299
cast_to: Type[ResponseT],
12801300
body: Body | None = None,
1301+
content: BinaryTypes | None = None,
12811302
options: RequestOptions = {},
12821303
files: RequestFiles | None = None,
12831304
stream: Literal[False] = False,
@@ -1290,6 +1311,7 @@ def post(
12901311
*,
12911312
cast_to: Type[ResponseT],
12921313
body: Body | None = None,
1314+
content: BinaryTypes | None = None,
12931315
options: RequestOptions = {},
12941316
files: RequestFiles | None = None,
12951317
stream: Literal[True],
@@ -1303,6 +1325,7 @@ def post(
13031325
*,
13041326
cast_to: Type[ResponseT],
13051327
body: Body | None = None,
1328+
content: BinaryTypes | None = None,
13061329
options: RequestOptions = {},
13071330
files: RequestFiles | None = None,
13081331
stream: bool,
@@ -1315,13 +1338,25 @@ def post(
13151338
*,
13161339
cast_to: Type[ResponseT],
13171340
body: Body | None = None,
1341+
content: BinaryTypes | None = None,
13181342
options: RequestOptions = {},
13191343
files: RequestFiles | None = None,
13201344
stream: bool = False,
13211345
stream_cls: type[_StreamT] | None = None,
13221346
) -> ResponseT | _StreamT:
1347+
if body is not None and content is not None:
1348+
raise TypeError("Passing both `body` and `content` is not supported")
1349+
if files is not None and content is not None:
1350+
raise TypeError("Passing both `files` and `content` is not supported")
1351+
if isinstance(body, bytes):
1352+
warnings.warn(
1353+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
1354+
"Please pass raw bytes via the `content` parameter instead.",
1355+
DeprecationWarning,
1356+
stacklevel=2,
1357+
)
13231358
opts = FinalRequestOptions.construct(
1324-
method="post", url=path, json_data=body, files=to_httpx_files(files), **options
1359+
method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
13251360
)
13261361
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
13271362

@@ -1331,11 +1366,23 @@ def patch(
13311366
*,
13321367
cast_to: Type[ResponseT],
13331368
body: Body | None = None,
1369+
content: BinaryTypes | None = None,
13341370
files: RequestFiles | None = None,
13351371
options: RequestOptions = {},
13361372
) -> ResponseT:
1373+
if body is not None and content is not None:
1374+
raise TypeError("Passing both `body` and `content` is not supported")
1375+
if files is not None and content is not None:
1376+
raise TypeError("Passing both `files` and `content` is not supported")
1377+
if isinstance(body, bytes):
1378+
warnings.warn(
1379+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
1380+
"Please pass raw bytes via the `content` parameter instead.",
1381+
DeprecationWarning,
1382+
stacklevel=2,
1383+
)
13371384
opts = FinalRequestOptions.construct(
1338-
method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
1385+
method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
13391386
)
13401387
return self.request(cast_to, opts)
13411388

@@ -1345,11 +1392,23 @@ def put(
13451392
*,
13461393
cast_to: Type[ResponseT],
13471394
body: Body | None = None,
1395+
content: BinaryTypes | None = None,
13481396
files: RequestFiles | None = None,
13491397
options: RequestOptions = {},
13501398
) -> ResponseT:
1399+
if body is not None and content is not None:
1400+
raise TypeError("Passing both `body` and `content` is not supported")
1401+
if files is not None and content is not None:
1402+
raise TypeError("Passing both `files` and `content` is not supported")
1403+
if isinstance(body, bytes):
1404+
warnings.warn(
1405+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
1406+
"Please pass raw bytes via the `content` parameter instead.",
1407+
DeprecationWarning,
1408+
stacklevel=2,
1409+
)
13511410
opts = FinalRequestOptions.construct(
1352-
method="put", url=path, json_data=body, files=to_httpx_files(files), **options
1411+
method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
13531412
)
13541413
return self.request(cast_to, opts)
13551414

@@ -1359,9 +1418,19 @@ def delete(
13591418
*,
13601419
cast_to: Type[ResponseT],
13611420
body: Body | None = None,
1421+
content: BinaryTypes | None = None,
13621422
options: RequestOptions = {},
13631423
) -> ResponseT:
1364-
opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
1424+
if body is not None and content is not None:
1425+
raise TypeError("Passing both `body` and `content` is not supported")
1426+
if isinstance(body, bytes):
1427+
warnings.warn(
1428+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
1429+
"Please pass raw bytes via the `content` parameter instead.",
1430+
DeprecationWarning,
1431+
stacklevel=2,
1432+
)
1433+
opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
13651434
return self.request(cast_to, opts)
13661435

13671436
def get_api_list(
@@ -1857,6 +1926,7 @@ async def post(
18571926
*,
18581927
cast_to: Type[ResponseT],
18591928
body: Body | None = None,
1929+
content: AsyncBinaryTypes | None = None,
18601930
files: RequestFiles | None = None,
18611931
options: RequestOptions = {},
18621932
stream: Literal[False] = False,
@@ -1869,6 +1939,7 @@ async def post(
18691939
*,
18701940
cast_to: Type[ResponseT],
18711941
body: Body | None = None,
1942+
content: AsyncBinaryTypes | None = None,
18721943
files: RequestFiles | None = None,
18731944
options: RequestOptions = {},
18741945
stream: Literal[True],
@@ -1882,6 +1953,7 @@ async def post(
18821953
*,
18831954
cast_to: Type[ResponseT],
18841955
body: Body | None = None,
1956+
content: AsyncBinaryTypes | None = None,
18851957
files: RequestFiles | None = None,
18861958
options: RequestOptions = {},
18871959
stream: bool,
@@ -1894,13 +1966,25 @@ async def post(
18941966
*,
18951967
cast_to: Type[ResponseT],
18961968
body: Body | None = None,
1969+
content: AsyncBinaryTypes | None = None,
18971970
files: RequestFiles | None = None,
18981971
options: RequestOptions = {},
18991972
stream: bool = False,
19001973
stream_cls: type[_AsyncStreamT] | None = None,
19011974
) -> ResponseT | _AsyncStreamT:
1975+
if body is not None and content is not None:
1976+
raise TypeError("Passing both `body` and `content` is not supported")
1977+
if files is not None and content is not None:
1978+
raise TypeError("Passing both `files` and `content` is not supported")
1979+
if isinstance(body, bytes):
1980+
warnings.warn(
1981+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
1982+
"Please pass raw bytes via the `content` parameter instead.",
1983+
DeprecationWarning,
1984+
stacklevel=2,
1985+
)
19021986
opts = FinalRequestOptions.construct(
1903-
method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1987+
method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
19041988
)
19051989
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
19061990

@@ -1910,11 +1994,28 @@ async def patch(
19101994
*,
19111995
cast_to: Type[ResponseT],
19121996
body: Body | None = None,
1997+
content: AsyncBinaryTypes | None = None,
19131998
files: RequestFiles | None = None,
19141999
options: RequestOptions = {},
19152000
) -> ResponseT:
2001+
if body is not None and content is not None:
2002+
raise TypeError("Passing both `body` and `content` is not supported")
2003+
if files is not None and content is not None:
2004+
raise TypeError("Passing both `files` and `content` is not supported")
2005+
if isinstance(body, bytes):
2006+
warnings.warn(
2007+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
2008+
"Please pass raw bytes via the `content` parameter instead.",
2009+
DeprecationWarning,
2010+
stacklevel=2,
2011+
)
19162012
opts = FinalRequestOptions.construct(
1917-
method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
2013+
method="patch",
2014+
url=path,
2015+
json_data=body,
2016+
content=content,
2017+
files=await async_to_httpx_files(files),
2018+
**options,
19182019
)
19192020
return await self.request(cast_to, opts)
19202021

@@ -1924,11 +2025,23 @@ async def put(
19242025
*,
19252026
cast_to: Type[ResponseT],
19262027
body: Body | None = None,
2028+
content: AsyncBinaryTypes | None = None,
19272029
files: RequestFiles | None = None,
19282030
options: RequestOptions = {},
19292031
) -> ResponseT:
2032+
if body is not None and content is not None:
2033+
raise TypeError("Passing both `body` and `content` is not supported")
2034+
if files is not None and content is not None:
2035+
raise TypeError("Passing both `files` and `content` is not supported")
2036+
if isinstance(body, bytes):
2037+
warnings.warn(
2038+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
2039+
"Please pass raw bytes via the `content` parameter instead.",
2040+
DeprecationWarning,
2041+
stacklevel=2,
2042+
)
19302043
opts = FinalRequestOptions.construct(
1931-
method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
2044+
method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
19322045
)
19332046
return await self.request(cast_to, opts)
19342047

@@ -1938,9 +2051,19 @@ async def delete(
19382051
*,
19392052
cast_to: Type[ResponseT],
19402053
body: Body | None = None,
2054+
content: AsyncBinaryTypes | None = None,
19412055
options: RequestOptions = {},
19422056
) -> ResponseT:
1943-
opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
2057+
if body is not None and content is not None:
2058+
raise TypeError("Passing both `body` and `content` is not supported")
2059+
if isinstance(body, bytes):
2060+
warnings.warn(
2061+
"Passing raw bytes as `body` is deprecated and will be removed in a future version. "
2062+
"Please pass raw bytes via the `content` parameter instead.",
2063+
DeprecationWarning,
2064+
stacklevel=2,
2065+
)
2066+
opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
19442067
return await self.request(cast_to, opts)
19452068

19462069
def get_api_list(

src/anthropic/_models.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@
33
import os
44
import inspect
55
import weakref
6-
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
6+
from typing import (
7+
IO,
8+
TYPE_CHECKING,
9+
Any,
10+
Type,
11+
Union,
12+
Generic,
13+
TypeVar,
14+
Callable,
15+
Iterable,
16+
Optional,
17+
AsyncIterable,
18+
cast,
19+
)
720
from datetime import date, datetime
821
from typing_extensions import (
922
List,
@@ -833,6 +846,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
833846
timeout: float | Timeout | None
834847
files: HttpxRequestFiles | None
835848
idempotency_key: str
849+
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
836850
json_data: Body
837851
extra_json: AnyMapping
838852
follow_redirects: bool
@@ -851,6 +865,7 @@ class FinalRequestOptions(pydantic.BaseModel):
851865
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
852866
follow_redirects: Union[bool, None] = None
853867

868+
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
854869
# It should be noted that we cannot use `json` here as that would override
855870
# a BaseModel method in an incompatible fashion.
856871
json_data: Union[Body, None] = None

src/anthropic/_types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
Mapping,
1414
TypeVar,
1515
Callable,
16+
Iterable,
1617
Iterator,
1718
Optional,
1819
Sequence,
20+
AsyncIterable,
1921
)
2022
from typing_extensions import (
2123
Set,
@@ -57,6 +59,13 @@
5759
else:
5860
Base64FileInput = Union[IO[bytes], PathLike]
5961
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
62+
63+
64+
# Used for sending raw binary data / streaming data in request bodies
65+
# e.g. for file uploads without multipart encoding
66+
BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
67+
AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
68+
6069
FileTypes = Union[
6170
# file (or bytes)
6271
FileContent,

0 commit comments

Comments
 (0)