From 8ca7ff886773539906d2f214892409af7c77bc47 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 7 Nov 2025 17:47:38 +0000
Subject: [PATCH 01/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index d521f651..57791d3a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 29
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-0a15ddd7e03addf08468ff36ac294458f86a3a990277a71870e4bc293635bef9.yml
-openapi_spec_hash: 8640228f8a86e5dc464dfa2c8205a2a7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-46ea61822976f3993310e2c139f133f450b489682d8df4c61b65c731edba8639.yml
+openapi_spec_hash: 8cd802f4d9cdfa000d35792175b3b203
config_hash: 70cdb57c982c578d1961657c07b8b397
From d68084f663f4cf682457a195918d95cce6584772 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 04:49:51 +0000
Subject: [PATCH 02/37] chore(package): drop Python 3.8 support
---
README.md | 4 ++--
pyproject.toml | 5 ++---
src/arcadepy/_utils/_sync.py | 34 +++-------------------------------
3 files changed, 7 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 0a1de1e6..5589a188 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](https://pypi.org/project/arcadepy/)
-The Arcade Python library provides convenient access to the Arcade REST API from any Python 3.8+
+The Arcade Python library provides convenient access to the Arcade REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -416,7 +416,7 @@ print(arcadepy.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
diff --git a/pyproject.toml b/pyproject.toml
index c5887191..fa0ea6e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,11 +15,10 @@ dependencies = [
"distro>=1.7.0, <2",
"sniffio",
]
-requires-python = ">= 3.8"
+requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -141,7 +140,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
-pythonVersion = "3.8"
+pythonVersion = "3.9"
exclude = [
"_dev",
diff --git a/src/arcadepy/_utils/_sync.py b/src/arcadepy/_utils/_sync.py
index ad7ec71b..f6027c18 100644
--- a/src/arcadepy/_utils/_sync.py
+++ b/src/arcadepy/_utils/_sync.py
@@ -1,10 +1,8 @@
from __future__ import annotations
-import sys
import asyncio
import functools
-import contextvars
-from typing import Any, TypeVar, Callable, Awaitable
+from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
import anyio
@@ -15,34 +13,11 @@
T_ParamSpec = ParamSpec("T_ParamSpec")
-if sys.version_info >= (3, 9):
- _asyncio_to_thread = asyncio.to_thread
-else:
- # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
- # for Python 3.8 support
- async def _asyncio_to_thread(
- func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
- ) -> Any:
- """Asynchronously run function *func* in a separate thread.
-
- Any *args and **kwargs supplied for this function are directly passed
- to *func*. Also, the current :class:`contextvars.Context` is propagated,
- allowing context variables from the main thread to be accessed in the
- separate thread.
-
- Returns a coroutine that can be awaited to get the eventual result of *func*.
- """
- loop = asyncio.events.get_running_loop()
- ctx = contextvars.copy_context()
- func_call = functools.partial(ctx.run, func, *args, **kwargs)
- return await loop.run_in_executor(None, func_call)
-
-
async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
- return await _asyncio_to_thread(func, *args, **kwargs)
+ return await asyncio.to_thread(func, *args, **kwargs)
return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
@@ -53,10 +28,7 @@ async def to_thread(
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
- positional and keyword arguments. For python version 3.9 and above, it uses
- asyncio.to_thread to run the function in a separate thread. For python version
- 3.8, it uses locally defined copy of the asyncio.to_thread function which was
- introduced in python 3.9.
+ positional and keyword arguments.
Usage:
From 4453f62ffe78f5bf6ec84c89566a2581ef381cca Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 04:50:28 +0000
Subject: [PATCH 03/37] fix: compat with Python 3.14
---
src/arcadepy/_models.py | 11 ++++++++---
tests/test_models.py | 8 ++++----
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/arcadepy/_models.py b/src/arcadepy/_models.py
index 6a3cd1d2..fcec2cf9 100644
--- a/src/arcadepy/_models.py
+++ b/src/arcadepy/_models.py
@@ -2,6 +2,7 @@
import os
import inspect
+import weakref
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
@@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails
+DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
+
+
class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
@@ -615,8 +619,9 @@ def __init__(
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
- if isinstance(union, CachedDiscriminatorType):
- return union.__discriminator__
+ cached = DISCRIMINATOR_CACHE.get(union)
+ if cached is not None:
+ return cached
discriminator_field_name: str | None = None
@@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
- cast(CachedDiscriminatorType, union).__discriminator__ = details
+ DISCRIMINATOR_CACHE.setdefault(union, details)
return details
diff --git a/tests/test_models.py b/tests/test_models.py
index 202c5175..62c67d83 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -9,7 +9,7 @@
from arcadepy._utils import PropertyInfo
from arcadepy._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from arcadepy._models import BaseModel, construct_type
+from arcadepy._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
class BasicModel(BaseModel):
@@ -809,7 +809,7 @@ class B(BaseModel):
UnionType = cast(Any, Union[A, B])
- assert not hasattr(UnionType, "__discriminator__")
+ assert not DISCRIMINATOR_CACHE.get(UnionType)
m = construct_type(
value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
@@ -818,7 +818,7 @@ class B(BaseModel):
assert m.type == "b"
assert m.data == "foo" # type: ignore[comparison-overlap]
- discriminator = UnionType.__discriminator__
+ discriminator = DISCRIMINATOR_CACHE.get(UnionType)
assert discriminator is not None
m = construct_type(
@@ -830,7 +830,7 @@ class B(BaseModel):
# if the discriminator details object stays the same between invocations then
# we hit the cache
- assert UnionType.__discriminator__ is discriminator
+ assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator
@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
From d4527cf2a0ab18f31c0b75d6b452a09f47bd15f5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 12 Nov 2025 04:33:03 +0000
Subject: [PATCH 04/37] fix(compat): update signatures of `model_dump` and
`model_dump_json` for Pydantic v1
---
src/arcadepy/_models.py | 41 +++++++++++++++++++++++++++++------------
1 file changed, 29 insertions(+), 12 deletions(-)
diff --git a/src/arcadepy/_models.py b/src/arcadepy/_models.py
index fcec2cf9..ca9500b2 100644
--- a/src/arcadepy/_models.py
+++ b/src/arcadepy/_models.py
@@ -257,15 +257,16 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
- serialize_as_any: bool = False,
fallback: Callable[[Any], Any] | None = None,
+ serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -273,16 +274,24 @@ def model_dump(
Args:
mode: The mode in which `to_python` should run.
- If mode is 'json', the dictionary will only contain JSON serializable types.
- If mode is 'python', the dictionary may contain any Python objects.
- include: A list of fields to include in the output.
- exclude: A list of fields to exclude from the output.
+ If mode is 'json', the output will only contain JSON serializable types.
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
+ include: A set of fields to include in the output.
+ exclude: A set of fields to exclude from the output.
+ context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
- exclude_unset: Whether to exclude fields that are unset or None from the output.
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
- round_trip: Whether to enable serialization and deserialization round-trip support.
- warnings: Whether to log warnings when invalid fields are encountered.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ exclude_computed_fields: Whether to exclude computed fields.
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
+ `round_trip` parameter instead.
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
+ fallback: A function to call when an unknown value is encountered. If not provided,
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
@@ -299,6 +308,8 @@ def model_dump(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
@@ -315,15 +326,17 @@ def model_dump_json(
self,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
@@ -355,6 +368,10 @@ def model_dump_json(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if ensure_ascii != False:
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
From 8c6d5c5ace8884839590c11a0b72f9ea70731e0e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 22 Nov 2025 04:15:50 +0000
Subject: [PATCH 05/37] chore: add Python 3.14 classifier and testing
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index fa0ea6e0..864da502 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
From e4be19ee70952d16361d55acfc082875bd926c01 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:27:15 +0000
Subject: [PATCH 06/37] fix: ensure streams are always closed
---
src/arcadepy/_streaming.py | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/arcadepy/_streaming.py b/src/arcadepy/_streaming.py
index 6bf52e75..d539a622 100644
--- a/src/arcadepy/_streaming.py
+++ b/src/arcadepy/_streaming.py
@@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- response.close()
+ try:
+ for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ response.close()
def __enter__(self) -> Self:
return self
@@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- async for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- await response.aclose()
+ try:
+ async for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From 0c9e3402e6714d2c1e4273ff5bd0e2dc00cce7b2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:28:21 +0000
Subject: [PATCH 07/37] chore(deps): mypy 1.18.1 has a regression, pin to 1.17
---
pyproject.toml | 2 +-
requirements-dev.lock | 4 +++-
requirements.lock | 8 ++++----
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 864da502..4ca4f409 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,7 +46,7 @@ managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 3c2bd65d..2b8f8f01 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -72,7 +72,7 @@ mdurl==0.1.2
multidict==6.4.4
# via aiohttp
# via yarl
-mypy==1.14.1
+mypy==1.17.0
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
@@ -81,6 +81,8 @@ nox==2023.4.22
packaging==23.2
# via nox
# via pytest
+pathspec==0.12.1
+ # via mypy
platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
diff --git a/requirements.lock b/requirements.lock
index b5a97193..e946d7ea 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -55,21 +55,21 @@ multidict==6.4.4
propcache==0.3.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via arcadepy
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
sniffio==1.3.0
# via anyio
# via arcadepy
-typing-extensions==4.12.2
+typing-extensions==4.15.0
# via anyio
# via arcadepy
# via multidict
# via pydantic
# via pydantic-core
# via typing-inspection
-typing-inspection==0.4.1
+typing-inspection==0.4.2
# via pydantic
yarl==1.20.0
# via aiohttp
From d0dae695284c4ee2873529f0e24034dc409e99e7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 05:36:26 +0000
Subject: [PATCH 08/37] chore: update lockfile
---
pyproject.toml | 14 +++---
requirements-dev.lock | 108 +++++++++++++++++++++++-------------------
requirements.lock | 31 ++++++------
3 files changed, 83 insertions(+), 70 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 4ca4f409..ce262328 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,14 +7,16 @@ license = "MIT"
authors = [
{ name = "Arcade", email = "dev@arcade.dev" },
]
+
dependencies = [
- "httpx>=0.23.0, <1",
- "pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
- "anyio>=3.5.0, <5",
- "distro>=1.7.0, <2",
- "sniffio",
+ "httpx>=0.23.0, <1",
+ "pydantic>=1.9.0, <3",
+ "typing-extensions>=4.10, <5",
+ "anyio>=3.5.0, <5",
+ "distro>=1.7.0, <2",
+ "sniffio",
]
+
requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 2b8f8f01..cb9974b3 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -12,40 +12,45 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via arcadepy
# via httpx-aiohttp
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via arcadepy
# via httpx
-argcomplete==3.1.2
+argcomplete==3.6.3
# via nox
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+ # via nox
+backports-asyncio-runner==1.2.0
+ # via pytest-asyncio
+certifi==2025.11.12
# via httpcore
# via httpx
-colorlog==6.7.0
+colorlog==6.10.1
+ # via nox
+dependency-groups==1.3.1
# via nox
-dirty-equals==0.6.0
-distlib==0.3.7
+dirty-equals==0.11
+distlib==0.4.0
# via virtualenv
-distro==1.8.0
+distro==1.9.0
# via arcadepy
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
# via pytest
-execnet==2.1.1
+execnet==2.1.2
# via pytest-xdist
-filelock==3.12.4
+filelock==3.19.1
# via virtualenv
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -58,82 +63,87 @@ httpx==0.28.1
# via respx
httpx-aiohttp==0.1.9
# via arcadepy
-idna==3.4
+humanize==4.13.0
+ # via nox
+idna==3.11
# via anyio
# via httpx
# via yarl
-importlib-metadata==7.0.0
-iniconfig==2.0.0
+importlib-metadata==8.7.0
+iniconfig==2.1.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
mypy==1.17.0
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
# via mypy
-nodeenv==1.8.0
+nodeenv==1.9.1
# via pyright
-nox==2023.4.22
-packaging==23.2
+nox==2025.11.12
+packaging==25.0
+ # via dependency-groups
# via nox
# via pytest
pathspec==0.12.1
# via mypy
-platformdirs==3.11.0
+platformdirs==4.4.0
# via virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via arcadepy
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
-pygments==2.18.0
+pygments==2.19.2
+ # via pytest
# via rich
pyright==1.1.399
-pytest==8.3.3
+pytest==8.4.2
# via pytest-asyncio
# via pytest-xdist
-pytest-asyncio==0.24.0
-pytest-xdist==3.7.0
-python-dateutil==2.8.2
+pytest-asyncio==1.2.0
+pytest-xdist==3.8.0
+python-dateutil==2.9.0.post0
# via time-machine
-pytz==2023.3.post1
- # via dirty-equals
respx==0.22.0
-rich==13.7.1
-ruff==0.9.4
-setuptools==68.2.2
- # via nodeenv
-six==1.16.0
+rich==14.2.0
+ruff==0.14.7
+six==1.17.0
# via python-dateutil
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via arcadepy
-time-machine==2.9.0
-tomli==2.0.2
+time-machine==2.19.0
+tomli==2.3.0
+ # via dependency-groups
# via mypy
+ # via nox
# via pytest
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
# via arcadepy
+ # via exceptiongroup
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
+ # via pytest-asyncio
# via typing-inspection
-typing-inspection==0.4.1
+ # via virtualenv
+typing-inspection==0.4.2
# via pydantic
-virtualenv==20.24.5
+virtualenv==20.35.4
# via nox
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
-zipp==3.17.0
+zipp==3.23.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index e946d7ea..9c47af42 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -12,28 +12,28 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via arcadepy
# via httpx-aiohttp
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via arcadepy
# via httpx
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+certifi==2025.11.12
# via httpcore
# via httpx
-distro==1.8.0
+distro==1.9.0
# via arcadepy
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -45,31 +45,32 @@ httpx==0.28.1
# via httpx-aiohttp
httpx-aiohttp==0.1.9
# via arcadepy
-idna==3.4
+idna==3.11
# via anyio
# via httpx
# via yarl
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
pydantic==2.12.5
# via arcadepy
pydantic-core==2.41.5
# via pydantic
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via arcadepy
typing-extensions==4.15.0
+ # via aiosignal
# via anyio
# via arcadepy
+ # via exceptiongroup
# via multidict
# via pydantic
# via pydantic-core
# via typing-inspection
typing-inspection==0.4.2
# via pydantic
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
From de0cde1a836d5b1992d8cdc284c3decbfa8eb20b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 05:44:11 +0000
Subject: [PATCH 09/37] chore(docs): use environment variables for
authentication in code snippets
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5589a188..80f0d663 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,7 @@ pip install arcadepy[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from arcadepy import DefaultAioHttpClient
from arcadepy import AsyncArcade
@@ -94,7 +95,7 @@ from arcadepy import AsyncArcade
async def main() -> None:
async with AsyncArcade(
- api_key="My API Key",
+ api_key=os.environ.get("ARCADE_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
execute_tool_response = await client.tools.execute(
From 60e16d2e59496bcf64c28328e3a351925b82dccd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 23:27:57 +0000
Subject: [PATCH 10/37] feat(api): api update
---
.stats.yml | 4 ++--
src/arcadepy/resources/tools/formatted.py | 8 ++++++++
src/arcadepy/resources/tools/tools.py | 8 ++++++++
src/arcadepy/types/tool_list_params.py | 3 +++
src/arcadepy/types/tools/formatted_list_params.py | 3 +++
tests/api_resources/test_tools.py | 2 ++
tests/api_resources/tools/test_formatted.py | 2 ++
7 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 57791d3a..104433a7 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 29
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-46ea61822976f3993310e2c139f133f450b489682d8df4c61b65c731edba8639.yml
-openapi_spec_hash: 8cd802f4d9cdfa000d35792175b3b203
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-4dc4e58ef402ce5362e0a8988b3928a8bfa0d5ba847f7ad8b14226a0cf282f28.yml
+openapi_spec_hash: fb72b9121306240c419669a3d42e45f6
config_hash: 70cdb57c982c578d1961657c07b8b397
diff --git a/src/arcadepy/resources/tools/formatted.py b/src/arcadepy/resources/tools/formatted.py
index 4b850161..3705e207 100644
--- a/src/arcadepy/resources/tools/formatted.py
+++ b/src/arcadepy/resources/tools/formatted.py
@@ -45,6 +45,7 @@ def list(
self,
*,
format: str | Omit = omit,
+ include_all_versions: bool | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
toolkit: str | Omit = omit,
@@ -63,6 +64,8 @@ def list(
Args:
format: Provider format
+ include_all_versions: Include all versions of each tool
+
limit: Number of items to return (default: 25, max: 100)
offset: Offset from the start of the list (default: 0)
@@ -90,6 +93,7 @@ def list(
query=maybe_transform(
{
"format": format,
+ "include_all_versions": include_all_versions,
"limit": limit,
"offset": offset,
"toolkit": toolkit,
@@ -175,6 +179,7 @@ def list(
self,
*,
format: str | Omit = omit,
+ include_all_versions: bool | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
toolkit: str | Omit = omit,
@@ -193,6 +198,8 @@ def list(
Args:
format: Provider format
+ include_all_versions: Include all versions of each tool
+
limit: Number of items to return (default: 25, max: 100)
offset: Offset from the start of the list (default: 0)
@@ -220,6 +227,7 @@ def list(
query=maybe_transform(
{
"format": format,
+ "include_all_versions": include_all_versions,
"limit": limit,
"offset": offset,
"toolkit": toolkit,
diff --git a/src/arcadepy/resources/tools/tools.py b/src/arcadepy/resources/tools/tools.py
index 30aec452..e45a77c2 100644
--- a/src/arcadepy/resources/tools/tools.py
+++ b/src/arcadepy/resources/tools/tools.py
@@ -74,6 +74,7 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse:
def list(
self,
*,
+ include_all_versions: bool | Omit = omit,
include_format: List[Literal["arcade", "openai", "anthropic"]] | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
@@ -91,6 +92,8 @@ def list(
toolkit
Args:
+ include_all_versions: Include all versions of each tool
+
include_format: Comma separated tool formats that will be included in the response.
limit: Number of items to return (default: 25, max: 100)
@@ -119,6 +122,7 @@ def list(
timeout=timeout,
query=maybe_transform(
{
+ "include_all_versions": include_all_versions,
"include_format": include_format,
"limit": limit,
"offset": offset,
@@ -319,6 +323,7 @@ def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse:
def list(
self,
*,
+ include_all_versions: bool | Omit = omit,
include_format: List[Literal["arcade", "openai", "anthropic"]] | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
@@ -336,6 +341,8 @@ def list(
toolkit
Args:
+ include_all_versions: Include all versions of each tool
+
include_format: Comma separated tool formats that will be included in the response.
limit: Number of items to return (default: 25, max: 100)
@@ -364,6 +371,7 @@ def list(
timeout=timeout,
query=maybe_transform(
{
+ "include_all_versions": include_all_versions,
"include_format": include_format,
"limit": limit,
"offset": offset,
diff --git a/src/arcadepy/types/tool_list_params.py b/src/arcadepy/types/tool_list_params.py
index 6a68b48c..7984944e 100644
--- a/src/arcadepy/types/tool_list_params.py
+++ b/src/arcadepy/types/tool_list_params.py
@@ -9,6 +9,9 @@
class ToolListParams(TypedDict, total=False):
+ include_all_versions: bool
+ """Include all versions of each tool"""
+
include_format: List[Literal["arcade", "openai", "anthropic"]]
"""Comma separated tool formats that will be included in the response."""
diff --git a/src/arcadepy/types/tools/formatted_list_params.py b/src/arcadepy/types/tools/formatted_list_params.py
index e9e53fde..97790dec 100644
--- a/src/arcadepy/types/tools/formatted_list_params.py
+++ b/src/arcadepy/types/tools/formatted_list_params.py
@@ -11,6 +11,9 @@ class FormattedListParams(TypedDict, total=False):
format: str
"""Provider format"""
+ include_all_versions: bool
+ """Include all versions of each tool"""
+
limit: int
"""Number of items to return (default: 25, max: 100)"""
diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py
index d3d1cb67..364d216a 100644
--- a/tests/api_resources/test_tools.py
+++ b/tests/api_resources/test_tools.py
@@ -30,6 +30,7 @@ def test_method_list(self, client: Arcade) -> None:
@parametrize
def test_method_list_with_all_params(self, client: Arcade) -> None:
tool = client.tools.list(
+ include_all_versions=True,
include_format=["arcade"],
limit=0,
offset=0,
@@ -203,6 +204,7 @@ async def test_method_list(self, async_client: AsyncArcade) -> None:
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncArcade) -> None:
tool = await async_client.tools.list(
+ include_all_versions=True,
include_format=["arcade"],
limit=0,
offset=0,
diff --git a/tests/api_resources/tools/test_formatted.py b/tests/api_resources/tools/test_formatted.py
index 0ab89e66..271a2334 100644
--- a/tests/api_resources/tools/test_formatted.py
+++ b/tests/api_resources/tools/test_formatted.py
@@ -26,6 +26,7 @@ def test_method_list(self, client: Arcade) -> None:
def test_method_list_with_all_params(self, client: Arcade) -> None:
formatted = client.tools.formatted.list(
format="format",
+ include_all_versions=True,
limit=0,
offset=0,
toolkit="toolkit",
@@ -115,6 +116,7 @@ async def test_method_list(self, async_client: AsyncArcade) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncArcade) -> None:
formatted = await async_client.tools.formatted.list(
format="format",
+ include_all_versions=True,
limit=0,
offset=0,
toolkit="toolkit",
From 6c633443c6c61f409e1092693a30b5e4e2e31edd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 8 Dec 2025 15:38:48 +0000
Subject: [PATCH 11/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 104433a7..4510020d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 29
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-4dc4e58ef402ce5362e0a8988b3928a8bfa0d5ba847f7ad8b14226a0cf282f28.yml
openapi_spec_hash: fb72b9121306240c419669a3d42e45f6
-config_hash: 70cdb57c982c578d1961657c07b8b397
+config_hash: f0d78fdab30e3346ae9b6804632ae8b6
From f98c6d3ccf44a38525f056bc52011fbdcc3183c1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 8 Dec 2025 17:44:52 +0000
Subject: [PATCH 12/37] feat(api): api update
---
.stats.yml | 4 +-
api.md | 1 +
src/arcadepy/resources/admin/secrets.py | 101 ++++++++++++++++-
src/arcadepy/types/admin/__init__.py | 1 +
.../types/admin/secret_create_params.py | 13 +++
tests/api_resources/admin/test_secrets.py | 104 +++++++++++++++++-
6 files changed, 220 insertions(+), 4 deletions(-)
create mode 100644 src/arcadepy/types/admin/secret_create_params.py
diff --git a/.stats.yml b/.stats.yml
index 4510020d..09786b48 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 29
+configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-4dc4e58ef402ce5362e0a8988b3928a8bfa0d5ba847f7ad8b14226a0cf282f28.yml
openapi_spec_hash: fb72b9121306240c419669a3d42e45f6
-config_hash: f0d78fdab30e3346ae9b6804632ae8b6
+config_hash: 7d1d98a4a1938a6884fa81ccfa3184c5
diff --git a/api.md b/api.md
index 55a932da..61b6e133 100644
--- a/api.md
+++ b/api.md
@@ -50,6 +50,7 @@ from arcadepy.types.admin import SecretResponse, SecretListResponse
Methods:
+- client.admin.secrets.create(secret_key, \*\*params) -> SecretResponse
- client.admin.secrets.list() -> SecretListResponse
- client.admin.secrets.delete(secret_id) -> None
diff --git a/src/arcadepy/resources/admin/secrets.py b/src/arcadepy/resources/admin/secrets.py
index ac839d97..b3d55f52 100644
--- a/src/arcadepy/resources/admin/secrets.py
+++ b/src/arcadepy/resources/admin/secrets.py
@@ -4,7 +4,8 @@
import httpx
-from ..._types import Body, Query, Headers, NoneType, NotGiven, not_given
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -13,7 +14,9 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
+from ...types.admin import secret_create_params
from ..._base_client import make_request_options
+from ...types.admin.secret_response import SecretResponse
from ...types.admin.secret_list_response import SecretListResponse
__all__ = ["SecretsResource", "AsyncSecretsResource"]
@@ -39,6 +42,48 @@ def with_streaming_response(self) -> SecretsResourceWithStreamingResponse:
"""
return SecretsResourceWithStreamingResponse(self)
+ def create(
+ self,
+ secret_key: str,
+ *,
+ value: str,
+ description: str | 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,
+ ) -> SecretResponse:
+ """
+ Create or update a secret
+
+ Args:
+ 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
+ """
+ if not secret_key:
+ raise ValueError(f"Expected a non-empty value for `secret_key` but received {secret_key!r}")
+ return self._post(
+ f"/v1/admin/secrets/{secret_key}",
+ body=maybe_transform(
+ {
+ "value": value,
+ "description": description,
+ },
+ secret_create_params.SecretCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SecretResponse,
+ )
+
def list(
self,
*,
@@ -113,6 +158,48 @@ def with_streaming_response(self) -> AsyncSecretsResourceWithStreamingResponse:
"""
return AsyncSecretsResourceWithStreamingResponse(self)
+ async def create(
+ self,
+ secret_key: str,
+ *,
+ value: str,
+ description: str | 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,
+ ) -> SecretResponse:
+ """
+ Create or update a secret
+
+ Args:
+ 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
+ """
+ if not secret_key:
+ raise ValueError(f"Expected a non-empty value for `secret_key` but received {secret_key!r}")
+ return await self._post(
+ f"/v1/admin/secrets/{secret_key}",
+ body=await async_maybe_transform(
+ {
+ "value": value,
+ "description": description,
+ },
+ secret_create_params.SecretCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SecretResponse,
+ )
+
async def list(
self,
*,
@@ -171,6 +258,9 @@ class SecretsResourceWithRawResponse:
def __init__(self, secrets: SecretsResource) -> None:
self._secrets = secrets
+ self.create = to_raw_response_wrapper(
+ secrets.create,
+ )
self.list = to_raw_response_wrapper(
secrets.list,
)
@@ -183,6 +273,9 @@ class AsyncSecretsResourceWithRawResponse:
def __init__(self, secrets: AsyncSecretsResource) -> None:
self._secrets = secrets
+ self.create = async_to_raw_response_wrapper(
+ secrets.create,
+ )
self.list = async_to_raw_response_wrapper(
secrets.list,
)
@@ -195,6 +288,9 @@ class SecretsResourceWithStreamingResponse:
def __init__(self, secrets: SecretsResource) -> None:
self._secrets = secrets
+ self.create = to_streamed_response_wrapper(
+ secrets.create,
+ )
self.list = to_streamed_response_wrapper(
secrets.list,
)
@@ -207,6 +303,9 @@ class AsyncSecretsResourceWithStreamingResponse:
def __init__(self, secrets: AsyncSecretsResource) -> None:
self._secrets = secrets
+ self.create = async_to_streamed_response_wrapper(
+ secrets.create,
+ )
self.list = async_to_streamed_response_wrapper(
secrets.list,
)
diff --git a/src/arcadepy/types/admin/__init__.py b/src/arcadepy/types/admin/__init__.py
index 04e75d9c..637035f2 100644
--- a/src/arcadepy/types/admin/__init__.py
+++ b/src/arcadepy/types/admin/__init__.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from .secret_response import SecretResponse as SecretResponse
+from .secret_create_params import SecretCreateParams as SecretCreateParams
from .secret_list_response import SecretListResponse as SecretListResponse
from .auth_provider_response import AuthProviderResponse as AuthProviderResponse
from .user_connection_response import UserConnectionResponse as UserConnectionResponse
diff --git a/src/arcadepy/types/admin/secret_create_params.py b/src/arcadepy/types/admin/secret_create_params.py
new file mode 100644
index 00000000..2986cbf2
--- /dev/null
+++ b/src/arcadepy/types/admin/secret_create_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["SecretCreateParams"]
+
+
+class SecretCreateParams(TypedDict, total=False):
+ value: Required[str]
+
+ description: str
diff --git a/tests/api_resources/admin/test_secrets.py b/tests/api_resources/admin/test_secrets.py
index 3afbce3b..f5e71b61 100644
--- a/tests/api_resources/admin/test_secrets.py
+++ b/tests/api_resources/admin/test_secrets.py
@@ -9,7 +9,7 @@
from arcadepy import Arcade, AsyncArcade
from tests.utils import assert_matches_type
-from arcadepy.types.admin import SecretListResponse
+from arcadepy.types.admin import SecretResponse, SecretListResponse
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -17,6 +17,57 @@
class TestSecrets:
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+ @parametrize
+ def test_method_create(self, client: Arcade) -> None:
+ secret = client.admin.secrets.create(
+ secret_key="secret_key",
+ value="value",
+ )
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Arcade) -> None:
+ secret = client.admin.secrets.create(
+ secret_key="secret_key",
+ value="value",
+ description="description",
+ )
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Arcade) -> None:
+ response = client.admin.secrets.with_raw_response.create(
+ secret_key="secret_key",
+ value="value",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ secret = response.parse()
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Arcade) -> None:
+ with client.admin.secrets.with_streaming_response.create(
+ secret_key="secret_key",
+ value="value",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ secret = response.parse()
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_create(self, client: Arcade) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `secret_key` but received ''"):
+ client.admin.secrets.with_raw_response.create(
+ secret_key="",
+ value="value",
+ )
+
@parametrize
def test_method_list(self, client: Arcade) -> None:
secret = client.admin.secrets.list()
@@ -86,6 +137,57 @@ class TestAsyncSecrets:
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
)
+ @parametrize
+ async def test_method_create(self, async_client: AsyncArcade) -> None:
+ secret = await async_client.admin.secrets.create(
+ secret_key="secret_key",
+ value="value",
+ )
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncArcade) -> None:
+ secret = await async_client.admin.secrets.create(
+ secret_key="secret_key",
+ value="value",
+ description="description",
+ )
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncArcade) -> None:
+ response = await async_client.admin.secrets.with_raw_response.create(
+ secret_key="secret_key",
+ value="value",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ secret = await response.parse()
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncArcade) -> None:
+ async with async_client.admin.secrets.with_streaming_response.create(
+ secret_key="secret_key",
+ value="value",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ secret = await response.parse()
+ assert_matches_type(SecretResponse, secret, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArcade) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `secret_key` but received ''"):
+ await async_client.admin.secrets.with_raw_response.create(
+ secret_key="",
+ value="value",
+ )
+
@parametrize
async def test_method_list(self, async_client: AsyncArcade) -> None:
secret = await async_client.admin.secrets.list()
From efb9e39b8d4675e9c869b70f4b1366792ea519d7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 01:38:48 +0000
Subject: [PATCH 13/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 09786b48..7f6cedc4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-4dc4e58ef402ce5362e0a8988b3928a8bfa0d5ba847f7ad8b14226a0cf282f28.yml
openapi_spec_hash: fb72b9121306240c419669a3d42e45f6
-config_hash: 7d1d98a4a1938a6884fa81ccfa3184c5
+config_hash: b31a3f1bbe9abcc7bb144942d88ad1b6
From 6834686ece469c756682bb559f8bca5b5b316296 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 04:58:20 +0000
Subject: [PATCH 14/37] fix(types): allow pyright to infer TypedDict types
within SequenceNotStr
---
src/arcadepy/_types.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/arcadepy/_types.py b/src/arcadepy/_types.py
index 39e9386d..4e54c6da 100644
--- a/src/arcadepy/_types.py
+++ b/src/arcadepy/_types.py
@@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False):
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ #
+ # Note: index() and count() methods are intentionally omitted to allow pyright to properly
+ # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
- def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
- def count(self, value: Any, /) -> int: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
From d8347917cfbe05f70e64d6c01ede460e15e20896 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 05:01:03 +0000
Subject: [PATCH 15/37] chore: add missing docstrings
---
src/arcadepy/types/chat/completion_create_params.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/arcadepy/types/chat/completion_create_params.py b/src/arcadepy/types/chat/completion_create_params.py
index 31608dbe..b9079d59 100644
--- a/src/arcadepy/types/chat/completion_create_params.py
+++ b/src/arcadepy/types/chat/completion_create_params.py
@@ -78,6 +78,8 @@ class ResponseFormat(TypedDict, total=False):
class StreamOptions(TypedDict, total=False):
+ """Options for streaming response. Only set this when you set stream: true."""
+
include_usage: bool
"""
If set, an additional chunk will be streamed before the data: [DONE] message.
From bc3886aeb649f605137cbec239ab768152f97d15 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 13 Dec 2025 00:06:30 +0000
Subject: [PATCH 16/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 7f6cedc4..59d6b2e1 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-4dc4e58ef402ce5362e0a8988b3928a8bfa0d5ba847f7ad8b14226a0cf282f28.yml
-openapi_spec_hash: fb72b9121306240c419669a3d42e45f6
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-7ecb40b6650ff002eed02efd1b59c630abe1fb9eb70c9c916c15b115260e5003.yml
+openapi_spec_hash: 2e5c04d1a50afcd0bdbd9737cec227c9
config_hash: b31a3f1bbe9abcc7bb144942d88ad1b6
From eb32a7551debd52fdfc15f3e7948d35e48f20875 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 16 Dec 2025 04:31:44 +0000
Subject: [PATCH 17/37] chore(internal): add missing files argument to base
client
---
src/arcadepy/_base_client.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/arcadepy/_base_client.py b/src/arcadepy/_base_client.py
index 57cf33c7..0fc752fd 100644
--- a/src/arcadepy/_base_client.py
+++ b/src/arcadepy/_base_client.py
@@ -1247,9 +1247,12 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1767,9 +1770,12 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ )
return await self.request(cast_to, opts)
async def put(
From 12abf0c7c4493c0ff4e27db53679e65dde59eab9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 17 Dec 2025 06:04:40 +0000
Subject: [PATCH 18/37] chore: speedup initial import
---
src/arcadepy/_client.py | 316 ++++++++++++++++++++++++++++++++--------
1 file changed, 253 insertions(+), 63 deletions(-)
diff --git a/src/arcadepy/_client.py b/src/arcadepy/_client.py
index 7ad9056b..a261011d 100644
--- a/src/arcadepy/_client.py
+++ b/src/arcadepy/_client.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import os
-from typing import Any, Mapping
+from typing import TYPE_CHECKING, Any, Mapping
from typing_extensions import Self, override
import httpx
@@ -20,8 +20,8 @@
not_given,
)
from ._utils import is_given, get_async_library
+from ._compat import cached_property
from ._version import __version__
-from .resources import auth, health, workers
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import ArcadeError, APIStatusError
from ._base_client import (
@@ -29,23 +29,20 @@
SyncAPIClient,
AsyncAPIClient,
)
-from .resources.chat import chat
-from .resources.admin import admin
-from .resources.tools import tools
+
+if TYPE_CHECKING:
+ from .resources import auth, chat, admin, tools, health, workers
+ from .resources.auth import AuthResource, AsyncAuthResource
+ from .resources.health import HealthResource, AsyncHealthResource
+ from .resources.workers import WorkersResource, AsyncWorkersResource
+ from .resources.chat.chat import ChatResource, AsyncChatResource
+ from .resources.admin.admin import AdminResource, AsyncAdminResource
+ from .resources.tools.tools import ToolsResource, AsyncToolsResource
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Arcade", "AsyncArcade", "Client", "AsyncClient"]
class Arcade(SyncAPIClient):
- admin: admin.AdminResource
- auth: auth.AuthResource
- health: health.HealthResource
- chat: chat.ChatResource
- tools: tools.ToolsResource
- workers: workers.WorkersResource
- with_raw_response: ArcadeWithRawResponse
- with_streaming_response: ArcadeWithStreamedResponse
-
# client options
api_key: str
@@ -100,14 +97,49 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.admin = admin.AdminResource(self)
- self.auth = auth.AuthResource(self)
- self.health = health.HealthResource(self)
- self.chat = chat.ChatResource(self)
- self.tools = tools.ToolsResource(self)
- self.workers = workers.WorkersResource(self)
- self.with_raw_response = ArcadeWithRawResponse(self)
- self.with_streaming_response = ArcadeWithStreamedResponse(self)
+ @cached_property
+ def admin(self) -> AdminResource:
+ from .resources.admin import AdminResource
+
+ return AdminResource(self)
+
+ @cached_property
+ def auth(self) -> AuthResource:
+ from .resources.auth import AuthResource
+
+ return AuthResource(self)
+
+ @cached_property
+ def health(self) -> HealthResource:
+ from .resources.health import HealthResource
+
+ return HealthResource(self)
+
+ @cached_property
+ def chat(self) -> ChatResource:
+ from .resources.chat import ChatResource
+
+ return ChatResource(self)
+
+ @cached_property
+ def tools(self) -> ToolsResource:
+ from .resources.tools import ToolsResource
+
+ return ToolsResource(self)
+
+ @cached_property
+ def workers(self) -> WorkersResource:
+ from .resources.workers import WorkersResource
+
+ return WorkersResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> ArcadeWithRawResponse:
+ return ArcadeWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ArcadeWithStreamedResponse:
+ return ArcadeWithStreamedResponse(self)
@property
@override
@@ -215,15 +247,6 @@ def _make_status_error(
class AsyncArcade(AsyncAPIClient):
- admin: admin.AsyncAdminResource
- auth: auth.AsyncAuthResource
- health: health.AsyncHealthResource
- chat: chat.AsyncChatResource
- tools: tools.AsyncToolsResource
- workers: workers.AsyncWorkersResource
- with_raw_response: AsyncArcadeWithRawResponse
- with_streaming_response: AsyncArcadeWithStreamedResponse
-
# client options
api_key: str
@@ -278,14 +301,49 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.admin = admin.AsyncAdminResource(self)
- self.auth = auth.AsyncAuthResource(self)
- self.health = health.AsyncHealthResource(self)
- self.chat = chat.AsyncChatResource(self)
- self.tools = tools.AsyncToolsResource(self)
- self.workers = workers.AsyncWorkersResource(self)
- self.with_raw_response = AsyncArcadeWithRawResponse(self)
- self.with_streaming_response = AsyncArcadeWithStreamedResponse(self)
+ @cached_property
+ def admin(self) -> AsyncAdminResource:
+ from .resources.admin import AsyncAdminResource
+
+ return AsyncAdminResource(self)
+
+ @cached_property
+ def auth(self) -> AsyncAuthResource:
+ from .resources.auth import AsyncAuthResource
+
+ return AsyncAuthResource(self)
+
+ @cached_property
+ def health(self) -> AsyncHealthResource:
+ from .resources.health import AsyncHealthResource
+
+ return AsyncHealthResource(self)
+
+ @cached_property
+ def chat(self) -> AsyncChatResource:
+ from .resources.chat import AsyncChatResource
+
+ return AsyncChatResource(self)
+
+ @cached_property
+ def tools(self) -> AsyncToolsResource:
+ from .resources.tools import AsyncToolsResource
+
+ return AsyncToolsResource(self)
+
+ @cached_property
+ def workers(self) -> AsyncWorkersResource:
+ from .resources.workers import AsyncWorkersResource
+
+ return AsyncWorkersResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncArcadeWithRawResponse:
+ return AsyncArcadeWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncArcadeWithStreamedResponse:
+ return AsyncArcadeWithStreamedResponse(self)
@property
@override
@@ -393,43 +451,175 @@ def _make_status_error(
class ArcadeWithRawResponse:
+ _client: Arcade
+
def __init__(self, client: Arcade) -> None:
- self.admin = admin.AdminResourceWithRawResponse(client.admin)
- self.auth = auth.AuthResourceWithRawResponse(client.auth)
- self.health = health.HealthResourceWithRawResponse(client.health)
- self.chat = chat.ChatResourceWithRawResponse(client.chat)
- self.tools = tools.ToolsResourceWithRawResponse(client.tools)
- self.workers = workers.WorkersResourceWithRawResponse(client.workers)
+ self._client = client
+
+ @cached_property
+ def admin(self) -> admin.AdminResourceWithRawResponse:
+ from .resources.admin import AdminResourceWithRawResponse
+
+ return AdminResourceWithRawResponse(self._client.admin)
+
+ @cached_property
+ def auth(self) -> auth.AuthResourceWithRawResponse:
+ from .resources.auth import AuthResourceWithRawResponse
+
+ return AuthResourceWithRawResponse(self._client.auth)
+
+ @cached_property
+ def health(self) -> health.HealthResourceWithRawResponse:
+ from .resources.health import HealthResourceWithRawResponse
+
+ return HealthResourceWithRawResponse(self._client.health)
+
+ @cached_property
+ def chat(self) -> chat.ChatResourceWithRawResponse:
+ from .resources.chat import ChatResourceWithRawResponse
+
+ return ChatResourceWithRawResponse(self._client.chat)
+
+ @cached_property
+ def tools(self) -> tools.ToolsResourceWithRawResponse:
+ from .resources.tools import ToolsResourceWithRawResponse
+
+ return ToolsResourceWithRawResponse(self._client.tools)
+
+ @cached_property
+ def workers(self) -> workers.WorkersResourceWithRawResponse:
+ from .resources.workers import WorkersResourceWithRawResponse
+
+ return WorkersResourceWithRawResponse(self._client.workers)
class AsyncArcadeWithRawResponse:
+ _client: AsyncArcade
+
def __init__(self, client: AsyncArcade) -> None:
- self.admin = admin.AsyncAdminResourceWithRawResponse(client.admin)
- self.auth = auth.AsyncAuthResourceWithRawResponse(client.auth)
- self.health = health.AsyncHealthResourceWithRawResponse(client.health)
- self.chat = chat.AsyncChatResourceWithRawResponse(client.chat)
- self.tools = tools.AsyncToolsResourceWithRawResponse(client.tools)
- self.workers = workers.AsyncWorkersResourceWithRawResponse(client.workers)
+ self._client = client
+
+ @cached_property
+ def admin(self) -> admin.AsyncAdminResourceWithRawResponse:
+ from .resources.admin import AsyncAdminResourceWithRawResponse
+
+ return AsyncAdminResourceWithRawResponse(self._client.admin)
+
+ @cached_property
+ def auth(self) -> auth.AsyncAuthResourceWithRawResponse:
+ from .resources.auth import AsyncAuthResourceWithRawResponse
+
+ return AsyncAuthResourceWithRawResponse(self._client.auth)
+
+ @cached_property
+ def health(self) -> health.AsyncHealthResourceWithRawResponse:
+ from .resources.health import AsyncHealthResourceWithRawResponse
+
+ return AsyncHealthResourceWithRawResponse(self._client.health)
+
+ @cached_property
+ def chat(self) -> chat.AsyncChatResourceWithRawResponse:
+ from .resources.chat import AsyncChatResourceWithRawResponse
+
+ return AsyncChatResourceWithRawResponse(self._client.chat)
+
+ @cached_property
+ def tools(self) -> tools.AsyncToolsResourceWithRawResponse:
+ from .resources.tools import AsyncToolsResourceWithRawResponse
+
+ return AsyncToolsResourceWithRawResponse(self._client.tools)
+
+ @cached_property
+ def workers(self) -> workers.AsyncWorkersResourceWithRawResponse:
+ from .resources.workers import AsyncWorkersResourceWithRawResponse
+
+ return AsyncWorkersResourceWithRawResponse(self._client.workers)
class ArcadeWithStreamedResponse:
+ _client: Arcade
+
def __init__(self, client: Arcade) -> None:
- self.admin = admin.AdminResourceWithStreamingResponse(client.admin)
- self.auth = auth.AuthResourceWithStreamingResponse(client.auth)
- self.health = health.HealthResourceWithStreamingResponse(client.health)
- self.chat = chat.ChatResourceWithStreamingResponse(client.chat)
- self.tools = tools.ToolsResourceWithStreamingResponse(client.tools)
- self.workers = workers.WorkersResourceWithStreamingResponse(client.workers)
+ self._client = client
+
+ @cached_property
+ def admin(self) -> admin.AdminResourceWithStreamingResponse:
+ from .resources.admin import AdminResourceWithStreamingResponse
+
+ return AdminResourceWithStreamingResponse(self._client.admin)
+
+ @cached_property
+ def auth(self) -> auth.AuthResourceWithStreamingResponse:
+ from .resources.auth import AuthResourceWithStreamingResponse
+
+ return AuthResourceWithStreamingResponse(self._client.auth)
+
+ @cached_property
+ def health(self) -> health.HealthResourceWithStreamingResponse:
+ from .resources.health import HealthResourceWithStreamingResponse
+
+ return HealthResourceWithStreamingResponse(self._client.health)
+
+ @cached_property
+ def chat(self) -> chat.ChatResourceWithStreamingResponse:
+ from .resources.chat import ChatResourceWithStreamingResponse
+
+ return ChatResourceWithStreamingResponse(self._client.chat)
+
+ @cached_property
+ def tools(self) -> tools.ToolsResourceWithStreamingResponse:
+ from .resources.tools import ToolsResourceWithStreamingResponse
+
+ return ToolsResourceWithStreamingResponse(self._client.tools)
+
+ @cached_property
+ def workers(self) -> workers.WorkersResourceWithStreamingResponse:
+ from .resources.workers import WorkersResourceWithStreamingResponse
+
+ return WorkersResourceWithStreamingResponse(self._client.workers)
class AsyncArcadeWithStreamedResponse:
+ _client: AsyncArcade
+
def __init__(self, client: AsyncArcade) -> None:
- self.admin = admin.AsyncAdminResourceWithStreamingResponse(client.admin)
- self.auth = auth.AsyncAuthResourceWithStreamingResponse(client.auth)
- self.health = health.AsyncHealthResourceWithStreamingResponse(client.health)
- self.chat = chat.AsyncChatResourceWithStreamingResponse(client.chat)
- self.tools = tools.AsyncToolsResourceWithStreamingResponse(client.tools)
- self.workers = workers.AsyncWorkersResourceWithStreamingResponse(client.workers)
+ self._client = client
+
+ @cached_property
+ def admin(self) -> admin.AsyncAdminResourceWithStreamingResponse:
+ from .resources.admin import AsyncAdminResourceWithStreamingResponse
+
+ return AsyncAdminResourceWithStreamingResponse(self._client.admin)
+
+ @cached_property
+ def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse:
+ from .resources.auth import AsyncAuthResourceWithStreamingResponse
+
+ return AsyncAuthResourceWithStreamingResponse(self._client.auth)
+
+ @cached_property
+ def health(self) -> health.AsyncHealthResourceWithStreamingResponse:
+ from .resources.health import AsyncHealthResourceWithStreamingResponse
+
+ return AsyncHealthResourceWithStreamingResponse(self._client.health)
+
+ @cached_property
+ def chat(self) -> chat.AsyncChatResourceWithStreamingResponse:
+ from .resources.chat import AsyncChatResourceWithStreamingResponse
+
+ return AsyncChatResourceWithStreamingResponse(self._client.chat)
+
+ @cached_property
+ def tools(self) -> tools.AsyncToolsResourceWithStreamingResponse:
+ from .resources.tools import AsyncToolsResourceWithStreamingResponse
+
+ return AsyncToolsResourceWithStreamingResponse(self._client.tools)
+
+ @cached_property
+ def workers(self) -> workers.AsyncWorkersResourceWithStreamingResponse:
+ from .resources.workers import AsyncWorkersResourceWithStreamingResponse
+
+ return AsyncWorkersResourceWithStreamingResponse(self._client.workers)
Client = Arcade
From 75704b3bd2478de2815cbd90816c171a4017f6c7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 17 Dec 2025 22:40:40 +0000
Subject: [PATCH 19/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 59d6b2e1..6e0768f0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-7ecb40b6650ff002eed02efd1b59c630abe1fb9eb70c9c916c15b115260e5003.yml
openapi_spec_hash: 2e5c04d1a50afcd0bdbd9737cec227c9
-config_hash: b31a3f1bbe9abcc7bb144942d88ad1b6
+config_hash: f26ae67630e2fac8d08a018eefd1d2d9
From 74a7b771f42ef16b58544eb09df84f60e73db843 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 18 Dec 2025 01:01:24 +0000
Subject: [PATCH 20/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 6e0768f0..9883e5e8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-7ecb40b6650ff002eed02efd1b59c630abe1fb9eb70c9c916c15b115260e5003.yml
openapi_spec_hash: 2e5c04d1a50afcd0bdbd9737cec227c9
-config_hash: f26ae67630e2fac8d08a018eefd1d2d9
+config_hash: 01e6bd1df0d14c729087edec4e6b6650
From 855d52c4817ecb2421b8467a0190f1222b1306de Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 18 Dec 2025 06:59:12 +0000
Subject: [PATCH 21/37] fix: use async_to_httpx_files in patch method
---
src/arcadepy/_base_client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/arcadepy/_base_client.py b/src/arcadepy/_base_client.py
index 0fc752fd..02030fdc 100644
--- a/src/arcadepy/_base_client.py
+++ b/src/arcadepy/_base_client.py
@@ -1774,7 +1774,7 @@ async def patch(
options: RequestOptions = {},
) -> ResponseT:
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
From 461ab0bfb6285d29b773511c0350efcd1a011b7e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 19 Dec 2025 06:04:48 +0000
Subject: [PATCH 22/37] chore(internal): add `--fix` argument to lint script
---
scripts/lint | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/scripts/lint b/scripts/lint
index 17c73a33..dc8016ba 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,8 +4,13 @@ set -e
cd "$(dirname "$0")/.."
-echo "==> Running lints"
-rye run lint
+if [ "$1" = "--fix" ]; then
+ echo "==> Running lints with --fix"
+ rye run fix:ruff
+else
+ echo "==> Running lints"
+ rye run lint
+fi
echo "==> Making sure it imports"
rye run python -c 'import arcadepy'
From 9b478ad96ad23fa3db3e93263ae540158e5e098a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 20 Dec 2025 05:26:48 +0000
Subject: [PATCH 23/37] docs: add more examples
---
README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/README.md b/README.md
index 80f0d663..11dfe94d 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,71 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
+## Pagination
+
+List methods in the Arcade API are paginated.
+
+This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
+
+```python
+from arcadepy import Arcade
+
+client = Arcade()
+
+all_user_connections = []
+# Automatically fetches more pages as needed.
+for user_connection in client.admin.user_connections.list():
+ # Do something with user_connection here
+ all_user_connections.append(user_connection)
+print(all_user_connections)
+```
+
+Or, asynchronously:
+
+```python
+import asyncio
+from arcadepy import AsyncArcade
+
+client = AsyncArcade()
+
+
+async def main() -> None:
+ all_user_connections = []
+ # Iterate through items across all pages, issuing requests as needed.
+ async for user_connection in client.admin.user_connections.list():
+ all_user_connections.append(user_connection)
+ print(all_user_connections)
+
+
+asyncio.run(main())
+```
+
+Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
+
+```python
+first_page = await client.admin.user_connections.list()
+if first_page.has_next_page():
+ print(f"will fetch next page using these details: {first_page.next_page_info()}")
+ next_page = await first_page.get_next_page()
+ print(f"number of items we just fetched: {len(next_page.items)}")
+
+# Remove `await` for non-async usage.
+```
+
+Or just work directly with the returned data:
+
+```python
+first_page = await client.admin.user_connections.list()
+
+print(
+ f"the current start offset for this page: {first_page.offset}"
+) # => "the current start offset for this page: 1"
+for user_connection in first_page.items:
+ print(user_connection.id)
+
+# Remove `await` for non-async usage.
+```
+
## Nested params
Nested parameters are dictionaries, typed using `TypedDict`, for example:
From 0494c619815c501dd566368295c60889db84a54a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 01:40:36 +0000
Subject: [PATCH 24/37] feat(api): api update
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
index 586b1767..fed1c6ef 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2025 Arcade
+Copyright 2026 Arcade
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
From 65fc32075e4310b4d7ec78fbf886402caab49abc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 01:44:44 +0000
Subject: [PATCH 25/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 9883e5e8..7978e2b4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-7ecb40b6650ff002eed02efd1b59c630abe1fb9eb70c9c916c15b115260e5003.yml
openapi_spec_hash: 2e5c04d1a50afcd0bdbd9737cec227c9
-config_hash: 01e6bd1df0d14c729087edec4e6b6650
+config_hash: d40bcd601176b9a9b96d93b35da975b3
From 3bcf4ec558eb62666a9b3ee912dd078915bc9dfc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 9 Jan 2026 21:51:02 +0000
Subject: [PATCH 26/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 7978e2b4..b69f980a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-7ecb40b6650ff002eed02efd1b59c630abe1fb9eb70c9c916c15b115260e5003.yml
-openapi_spec_hash: 2e5c04d1a50afcd0bdbd9737cec227c9
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-58eb0d1a9443652ac0b9d17e2642e18922b1ec7253ba73dc9cf1e229454e35c6.yml
+openapi_spec_hash: b8ab8a9182d75f4ba0470e060a479973
config_hash: d40bcd601176b9a9b96d93b35da975b3
From 6fa872ff85ff3b33528ad6644938a2d1f65b0987 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 14 Jan 2026 07:43:59 +0000
Subject: [PATCH 27/37] feat(client): add support for binary request streaming
---
src/arcadepy/_base_client.py | 145 ++++++++++++++++++++++++---
src/arcadepy/_models.py | 17 +++-
src/arcadepy/_types.py | 9 ++
tests/test_client.py | 187 ++++++++++++++++++++++++++++++++++-
4 files changed, 344 insertions(+), 14 deletions(-)
diff --git a/src/arcadepy/_base_client.py b/src/arcadepy/_base_client.py
index 02030fdc..3844106e 100644
--- a/src/arcadepy/_base_client.py
+++ b/src/arcadepy/_base_client.py
@@ -9,6 +9,7 @@
import inspect
import logging
import platform
+import warnings
import email.utils
from types import TracebackType
from random import random
@@ -51,9 +52,11 @@
ResponseT,
AnyMapping,
PostParser,
+ BinaryTypes,
RequestFiles,
HttpxSendArgs,
RequestOptions,
+ AsyncBinaryTypes,
HttpxRequestFiles,
ModelBuilderProtocol,
not_given,
@@ -477,8 +480,19 @@ def _build_request(
retries_taken: int = 0,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
- log.debug("Request options: %s", model_dump(options, exclude_unset=True))
-
+ log.debug(
+ "Request options: %s",
+ model_dump(
+ options,
+ exclude_unset=True,
+ # Pydantic v1 can't dump every type we support in content, so we exclude it for now.
+ exclude={
+ "content",
+ }
+ if PYDANTIC_V1
+ else {},
+ ),
+ )
kwargs: dict[str, Any] = {}
json_data = options.json_data
@@ -532,7 +546,13 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- if isinstance(json_data, bytes):
+ if options.content is not None and json_data is not None:
+ raise TypeError("Passing both `content` and `json_data` is not supported")
+ if options.content is not None and files is not None:
+ raise TypeError("Passing both `content` and `files` is not supported")
+ if options.content is not None:
+ kwargs["content"] = options.content
+ elif isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
@@ -1194,6 +1214,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[False] = False,
@@ -1206,6 +1227,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[True],
@@ -1219,6 +1241,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool,
@@ -1231,13 +1254,25 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
@@ -1247,11 +1282,23 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1261,11 +1308,23 @@ def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1275,9 +1334,19 @@ def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return self.request(cast_to, opts)
def get_api_list(
@@ -1717,6 +1786,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[False] = False,
@@ -1729,6 +1799,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[True],
@@ -1742,6 +1813,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1754,13 +1826,25 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool = False,
stream_cls: type[_AsyncStreamT] | None = None,
) -> ResponseT | _AsyncStreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
@@ -1770,11 +1854,28 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="patch",
+ url=path,
+ json_data=body,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
)
return await self.request(cast_to, opts)
@@ -1784,11 +1885,23 @@ async def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
@@ -1798,9 +1911,19 @@ async def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return await self.request(cast_to, opts)
def get_api_list(
diff --git a/src/arcadepy/_models.py b/src/arcadepy/_models.py
index ca9500b2..29070e05 100644
--- a/src/arcadepy/_models.py
+++ b/src/arcadepy/_models.py
@@ -3,7 +3,20 @@
import os
import inspect
import weakref
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Type,
+ Union,
+ Generic,
+ TypeVar,
+ Callable,
+ Iterable,
+ Optional,
+ AsyncIterable,
+ cast,
+)
from datetime import date, datetime
from typing_extensions import (
List,
@@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
timeout: float | Timeout | None
files: HttpxRequestFiles | None
idempotency_key: str
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
@@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel):
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
diff --git a/src/arcadepy/_types.py b/src/arcadepy/_types.py
index 4e54c6da..5f712e80 100644
--- a/src/arcadepy/_types.py
+++ b/src/arcadepy/_types.py
@@ -13,9 +13,11 @@
Mapping,
TypeVar,
Callable,
+ Iterable,
Iterator,
Optional,
Sequence,
+ AsyncIterable,
)
from typing_extensions import (
Set,
@@ -56,6 +58,13 @@
else:
Base64FileInput = Union[IO[bytes], PathLike]
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
+
+
+# Used for sending raw binary data / streaming data in request bodies
+# e.g. for file uploads without multipart encoding
+BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
+AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
+
FileTypes = Union[
# file (or bytes)
FileContent,
diff --git a/tests/test_client.py b/tests/test_client.py
index 49b90dab..f18d984d 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -8,10 +8,11 @@
import json
import asyncio
import inspect
+import dataclasses
import tracemalloc
-from typing import Any, Union, cast
+from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast
from unittest import mock
-from typing_extensions import Literal
+from typing_extensions import Literal, AsyncIterator, override
import httpx
import pytest
@@ -36,6 +37,7 @@
from .utils import update_env
+T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
api_key = "My API Key"
@@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
return 0.1
+def mirror_request_content(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, content=request.content)
+
+
+# note: we can't use the httpx.MockTransport class as it consumes the request
+# body itself, which means we can't test that the body is read lazily
+class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
+ def __init__(
+ self,
+ handler: Callable[[httpx.Request], httpx.Response]
+ | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]],
+ ) -> None:
+ self.handler = handler
+
+ @override
+ def handle_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function"
+ assert inspect.isfunction(self.handler), "handler must be a function"
+ return self.handler(request)
+
+ @override
+ async def handle_async_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function"
+ return await self.handler(request)
+
+
+@dataclasses.dataclass
+class Counter:
+ value: int = 0
+
+
+def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
+async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
def _get_open_connections(client: Arcade | AsyncArcade) -> int:
transport = client._client._transport
assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
@@ -500,6 +553,70 @@ def test_multipart_repeating_array(self, client: Arcade) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload(self, respx_mock: MockRouter, client: Arcade) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ def test_binary_content_upload_with_iterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_sync_iterator([file_content], counter=counter)
+
+ def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=request.read())
+
+ with Arcade(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.Client(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Arcade) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
def test_basic_union_response(self, respx_mock: MockRouter, client: Arcade) -> None:
class Model1(BaseModel):
@@ -1319,6 +1436,72 @@ def test_multipart_repeating_array(self, async_client: AsyncArcade) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncArcade) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = await async_client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ async def test_binary_content_upload_with_asynciterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_async_iterator([file_content], counter=counter)
+
+ async def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=await request.aread())
+
+ async with AsyncArcade(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = await client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload_with_body_is_deprecated(
+ self, respx_mock: MockRouter, async_client: AsyncArcade
+ ) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = await async_client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncArcade) -> None:
class Model1(BaseModel):
From f640398c6e1668c4b78d5c359ac75575756e71ed Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 16 Jan 2026 23:08:17 +0000
Subject: [PATCH 28/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b69f980a..8f23d79e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-58eb0d1a9443652ac0b9d17e2642e18922b1ec7253ba73dc9cf1e229454e35c6.yml
-openapi_spec_hash: b8ab8a9182d75f4ba0470e060a479973
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-581f0f279edc04e88590341c69dd8281a2e61f9dbd06ba4d3d3ff8e09c7a8c53.yml
+openapi_spec_hash: 55f930731c318cdc8018470f32b065d4
config_hash: d40bcd601176b9a9b96d93b35da975b3
From 3f5e2e8e28f60a2b3ff31cf3e514e22720716007 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 17 Jan 2026 01:38:38 +0000
Subject: [PATCH 29/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8f23d79e..da5146f0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-581f0f279edc04e88590341c69dd8281a2e61f9dbd06ba4d3d3ff8e09c7a8c53.yml
-openapi_spec_hash: 55f930731c318cdc8018470f32b065d4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-26fe911710c2420129c251b262020897f69fed2639f5b2c723a0201eeb704d87.yml
+openapi_spec_hash: fa522af8f97365f58c91b6f46747521d
config_hash: d40bcd601176b9a9b96d93b35da975b3
From e003383f9c5cc4eb3818eef781a899badbe2c8f2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 17 Jan 2026 05:59:57 +0000
Subject: [PATCH 30/37] chore(internal): update `actions/checkout` version
---
.github/workflows/ci.yml | 6 +++---
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e0765113..d98be014 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/arcade-engine-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -44,7 +44,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/arcade-engine-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -81,7 +81,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/arcade-engine-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 0080ea4c..1c677819 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index d4ed5cf1..959887d4 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'ArcadeAI/arcade-py' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check release environment
run: |
From 98c6790718ee4846d1add4eb3c7a8f48ddef479f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 22 Jan 2026 21:00:05 +0000
Subject: [PATCH 31/37] feat(api): api update
---
.stats.yml | 4 ++--
src/arcadepy/types/execute_tool_response.py | 2 ++
src/arcadepy/types/tool_execution_attempt.py | 2 ++
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index da5146f0..05f7a686 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-26fe911710c2420129c251b262020897f69fed2639f5b2c723a0201eeb704d87.yml
-openapi_spec_hash: fa522af8f97365f58c91b6f46747521d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-0df52128804dae127821b5a4dd712b4ba1d18056cc91bdf0b98dfa8107baa6f1.yml
+openapi_spec_hash: bb90b322628af89b8e8ee3ce9a64cf19
config_hash: d40bcd601176b9a9b96d93b35da975b3
diff --git a/src/arcadepy/types/execute_tool_response.py b/src/arcadepy/types/execute_tool_response.py
index 7c83d5b4..63953e1e 100644
--- a/src/arcadepy/types/execute_tool_response.py
+++ b/src/arcadepy/types/execute_tool_response.py
@@ -23,6 +23,8 @@ class OutputError(BaseModel):
"TOOL_RUNTIME_RETRY",
"TOOL_RUNTIME_CONTEXT_REQUIRED",
"TOOL_RUNTIME_FATAL",
+ "CONTEXT_CHECK_FAILED",
+ "CONTEXT_DENIED",
"UPSTREAM_RUNTIME_BAD_REQUEST",
"UPSTREAM_RUNTIME_AUTH_ERROR",
"UPSTREAM_RUNTIME_NOT_FOUND",
diff --git a/src/arcadepy/types/tool_execution_attempt.py b/src/arcadepy/types/tool_execution_attempt.py
index cdaeb02f..108ac484 100644
--- a/src/arcadepy/types/tool_execution_attempt.py
+++ b/src/arcadepy/types/tool_execution_attempt.py
@@ -23,6 +23,8 @@ class OutputError(BaseModel):
"TOOL_RUNTIME_RETRY",
"TOOL_RUNTIME_CONTEXT_REQUIRED",
"TOOL_RUNTIME_FATAL",
+ "CONTEXT_CHECK_FAILED",
+ "CONTEXT_DENIED",
"UPSTREAM_RUNTIME_BAD_REQUEST",
"UPSTREAM_RUNTIME_AUTH_ERROR",
"UPSTREAM_RUNTIME_NOT_FOUND",
From 48d3629b09bb29815fc62424eb09850516d436f9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 24 Jan 2026 05:30:18 +0000
Subject: [PATCH 32/37] chore(ci): upgrade `actions/github-script`
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d98be014..98aed718 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,7 +63,7 @@ jobs:
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/arcade-engine-python'
id: github-oidc
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());
From 72dbe3c96f64822c130a2588fb749d0c8c8a3ecb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 24 Jan 2026 23:09:30 +0000
Subject: [PATCH 33/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 05f7a686..06be0239 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-0df52128804dae127821b5a4dd712b4ba1d18056cc91bdf0b98dfa8107baa6f1.yml
-openapi_spec_hash: bb90b322628af89b8e8ee3ce9a64cf19
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-ba1aa76b54891af8c72220bd811d652cb2e743c4f86b8a589dd6cb2938b09f1c.yml
+openapi_spec_hash: 10de3e6e4b87644087c976b62b571405
config_hash: d40bcd601176b9a9b96d93b35da975b3
From e81e27d04e90d7984f0ffd43e35123f372c8ba83 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 27 Jan 2026 18:47:35 +0000
Subject: [PATCH 34/37] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 06be0239..0b9b0427 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-ba1aa76b54891af8c72220bd811d652cb2e743c4f86b8a589dd6cb2938b09f1c.yml
openapi_spec_hash: 10de3e6e4b87644087c976b62b571405
-config_hash: d40bcd601176b9a9b96d93b35da975b3
+config_hash: bf64816643634a621cd0ffd93d9c4347
From 9c2e7bf6cf56bdaad5d98ad5a68b9ae0b049be7e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 05:16:44 +0000
Subject: [PATCH 35/37] feat(client): add custom JSON encoder for extended type
support
---
src/arcadepy/_base_client.py | 7 +-
src/arcadepy/_compat.py | 6 +-
src/arcadepy/_utils/_json.py | 35 ++++++++++
tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++
4 files changed, 169 insertions(+), 5 deletions(-)
create mode 100644 src/arcadepy/_utils/_json.py
create mode 100644 tests/test_utils/test_json.py
diff --git a/src/arcadepy/_base_client.py b/src/arcadepy/_base_client.py
index 3844106e..5d342b7b 100644
--- a/src/arcadepy/_base_client.py
+++ b/src/arcadepy/_base_client.py
@@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
- else:
- kwargs["json"] = json_data if is_given(json_data) else None
+ elif not files:
+ # Don't set content when JSON is sent as multipart/form-data,
+ # since httpx's content param overrides other body arguments
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/arcadepy/_compat.py b/src/arcadepy/_compat.py
index bdef67f0..786ff42a 100644
--- a/src/arcadepy/_compat.py
+++ b/src/arcadepy/_compat.py
@@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
+ by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
+ by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- exclude=exclude,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
diff --git a/src/arcadepy/_utils/_json.py b/src/arcadepy/_utils/_json.py
new file mode 100644
index 00000000..60584214
--- /dev/null
+++ b/src/arcadepy/_utils/_json.py
@@ -0,0 +1,35 @@
+import json
+from typing import Any
+from datetime import datetime
+from typing_extensions import override
+
+import pydantic
+
+from .._compat import model_dump
+
+
+def openapi_dumps(obj: Any) -> bytes:
+ """
+ Serialize an object to UTF-8 encoded JSON bytes.
+
+ Extends the standard json.dumps with support for additional types
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
+ """
+ return json.dumps(
+ obj,
+ cls=_CustomEncoder,
+ # Uses the same defaults as httpx's JSON serialization
+ ensure_ascii=False,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode()
+
+
+class _CustomEncoder(json.JSONEncoder):
+ @override
+ def default(self, o: Any) -> Any:
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if isinstance(o, pydantic.BaseModel):
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
+ return super().default(o)
diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py
new file mode 100644
index 00000000..30e48cca
--- /dev/null
+++ b/tests/test_utils/test_json.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import datetime
+from typing import Union
+
+import pydantic
+
+from arcadepy import _compat
+from arcadepy._utils._json import openapi_dumps
+
+
+class TestOpenapiDumps:
+ def test_basic(self) -> None:
+ data = {"key": "value", "number": 42}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"key":"value","number":42}'
+
+ def test_datetime_serialization(self) -> None:
+ dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
+ data = {"datetime": dt}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
+
+ def test_pydantic_model_serialization(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ age: int
+
+ model_instance = User(first_name="John", last_name="Kramer", age=83)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
+
+ def test_pydantic_model_with_default_values(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+ score: int = 0
+
+ model_instance = User(name="Alice")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Alice"}}'
+
+ def test_pydantic_model_with_default_values_overridden(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+
+ model_instance = User(name="Bob", role="admin", active=False)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
+
+ def test_pydantic_model_with_alias(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str = pydantic.Field(alias="firstName")
+ last_name: str = pydantic.Field(alias="lastName")
+
+ model_instance = User(firstName="John", lastName="Doe")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
+
+ def test_pydantic_model_with_alias_and_default(self) -> None:
+ class User(pydantic.BaseModel):
+ user_name: str = pydantic.Field(alias="userName")
+ user_role: str = pydantic.Field(default="member", alias="userRole")
+ is_active: bool = pydantic.Field(default=True, alias="isActive")
+
+ model_instance = User(userName="charlie")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"charlie"}}'
+
+ model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
+ data = {"model": model_with_overrides}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
+
+ def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
+ class Address(pydantic.BaseModel):
+ street: str
+ city: str = "Unknown"
+
+ class User(pydantic.BaseModel):
+ name: str
+ address: Address
+ verified: bool = False
+
+ if _compat.PYDANTIC_V1:
+ # to handle forward references in Pydantic v1
+ User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
+
+ address = Address(street="123 Main St")
+ user = User(name="Diana", address=address)
+ data = {"user": user}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
+
+ address_with_city = Address(street="456 Oak Ave", city="Boston")
+ user_verified = User(name="Eve", address=address_with_city, verified=True)
+ data = {"user": user_verified}
+ json_bytes = openapi_dumps(data)
+ assert (
+ json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
+ )
+
+ def test_pydantic_model_with_optional_fields(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ email: Union[str, None]
+ phone: Union[str, None]
+
+ model_with_none = User(name="Eve", email=None, phone=None)
+ data = {"model": model_with_none}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
+
+ model_with_values = User(name="Frank", email="frank@example.com", phone=None)
+ data = {"model": model_with_values}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'
From fa2198fb612c3704321bedf76744faa6847e3f52 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:04:14 +0000
Subject: [PATCH 36/37] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 0b9b0427..0c8dd4d0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-ba1aa76b54891af8c72220bd811d652cb2e743c4f86b8a589dd6cb2938b09f1c.yml
-openapi_spec_hash: 10de3e6e4b87644087c976b62b571405
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-85fa40a220e2009303b6dcda87d3526d6b85d4fd7d847f3087fa1c25298c9b73.yml
+openapi_spec_hash: 89b272ca7a371506a472167cbd606f13
config_hash: bf64816643634a621cd0ffd93d9c4347
From 2455a1a80b1805017a266f8d1a97c8e30bd92774 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:04:43 +0000
Subject: [PATCH 37/37] release: 1.11.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 42 +++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/arcadepy/_version.py | 2 +-
4 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index eb4e0dba..caf14871 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.10.0"
+ ".": "1.11.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59386719..d4e14b14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,47 @@
# Changelog
+## 1.11.0 (2026-02-03)
+
+Full Changelog: [v1.10.0...v1.11.0](https://github.com/ArcadeAI/arcade-py/compare/v1.10.0...v1.11.0)
+
+### Features
+
+* **api:** api update ([98c6790](https://github.com/ArcadeAI/arcade-py/commit/98c6790718ee4846d1add4eb3c7a8f48ddef479f))
+* **api:** api update ([0494c61](https://github.com/ArcadeAI/arcade-py/commit/0494c619815c501dd566368295c60889db84a54a))
+* **api:** api update ([f98c6d3](https://github.com/ArcadeAI/arcade-py/commit/f98c6d3ccf44a38525f056bc52011fbdcc3183c1))
+* **api:** api update ([60e16d2](https://github.com/ArcadeAI/arcade-py/commit/60e16d2e59496bcf64c28328e3a351925b82dccd))
+* **client:** add custom JSON encoder for extended type support ([9c2e7bf](https://github.com/ArcadeAI/arcade-py/commit/9c2e7bf6cf56bdaad5d98ad5a68b9ae0b049be7e))
+* **client:** add support for binary request streaming ([6fa872f](https://github.com/ArcadeAI/arcade-py/commit/6fa872ff85ff3b33528ad6644938a2d1f65b0987))
+
+
+### Bug Fixes
+
+* compat with Python 3.14 ([4453f62](https://github.com/ArcadeAI/arcade-py/commit/4453f62ffe78f5bf6ec84c89566a2581ef381cca))
+* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([d4527cf](https://github.com/ArcadeAI/arcade-py/commit/d4527cf2a0ab18f31c0b75d6b452a09f47bd15f5))
+* ensure streams are always closed ([e4be19e](https://github.com/ArcadeAI/arcade-py/commit/e4be19ee70952d16361d55acfc082875bd926c01))
+* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([6834686](https://github.com/ArcadeAI/arcade-py/commit/6834686ece469c756682bb559f8bca5b5b316296))
+* use async_to_httpx_files in patch method ([855d52c](https://github.com/ArcadeAI/arcade-py/commit/855d52c4817ecb2421b8467a0190f1222b1306de))
+
+
+### Chores
+
+* add missing docstrings ([d834791](https://github.com/ArcadeAI/arcade-py/commit/d8347917cfbe05f70e64d6c01ede460e15e20896))
+* add Python 3.14 classifier and testing ([8c6d5c5](https://github.com/ArcadeAI/arcade-py/commit/8c6d5c5ace8884839590c11a0b72f9ea70731e0e))
+* **ci:** upgrade `actions/github-script` ([48d3629](https://github.com/ArcadeAI/arcade-py/commit/48d3629b09bb29815fc62424eb09850516d436f9))
+* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([0c9e340](https://github.com/ArcadeAI/arcade-py/commit/0c9e3402e6714d2c1e4273ff5bd0e2dc00cce7b2))
+* **docs:** use environment variables for authentication in code snippets ([de0cde1](https://github.com/ArcadeAI/arcade-py/commit/de0cde1a836d5b1992d8cdc284c3decbfa8eb20b))
+* **internal:** add `--fix` argument to lint script ([461ab0b](https://github.com/ArcadeAI/arcade-py/commit/461ab0bfb6285d29b773511c0350efcd1a011b7e))
+* **internal:** add missing files argument to base client ([eb32a75](https://github.com/ArcadeAI/arcade-py/commit/eb32a7551debd52fdfc15f3e7948d35e48f20875))
+* **internal:** update `actions/checkout` version ([e003383](https://github.com/ArcadeAI/arcade-py/commit/e003383f9c5cc4eb3818eef781a899badbe2c8f2))
+* **package:** drop Python 3.8 support ([d68084f](https://github.com/ArcadeAI/arcade-py/commit/d68084f663f4cf682457a195918d95cce6584772))
+* speedup initial import ([12abf0c](https://github.com/ArcadeAI/arcade-py/commit/12abf0c7c4493c0ff4e27db53679e65dde59eab9))
+* update lockfile ([d0dae69](https://github.com/ArcadeAI/arcade-py/commit/d0dae695284c4ee2873529f0e24034dc409e99e7))
+
+
+### Documentation
+
+* add more examples ([9b478ad](https://github.com/ArcadeAI/arcade-py/commit/9b478ad96ad23fa3db3e93263ae540158e5e098a))
+
## 1.10.0 (2025-11-06)
Full Changelog: [v1.9.0...v1.10.0](https://github.com/ArcadeAI/arcade-py/compare/v1.9.0...v1.10.0)
diff --git a/pyproject.toml b/pyproject.toml
index ce262328..2eed60ce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "arcadepy"
-version = "1.10.0"
+version = "1.11.0"
description = "The official Python library for the Arcade API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/arcadepy/_version.py b/src/arcadepy/_version.py
index bb28e4be..3bd71df4 100644
--- a/src/arcadepy/_version.py
+++ b/src/arcadepy/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "arcadepy"
-__version__ = "1.10.0" # x-release-please-version
+__version__ = "1.11.0" # x-release-please-version