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 @@ [![PyPI version](https://img.shields.io/pypi/v/arcadepy.svg?label=pypi%20(stable))](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