diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e0765113..98aed718 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: |
@@ -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());
@@ -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: |
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/.stats.yml b/.stats.yml
index d521f651..0c8dd4d0 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
-config_hash: 70cdb57c982c578d1961657c07b8b397
+configured_endpoints: 30
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-85fa40a220e2009303b6dcda87d3526d6b85d4fd7d847f3087fa1c25298c9b73.yml
+openapi_spec_hash: 89b272ca7a371506a472167cbd606f13
+config_hash: bf64816643634a621cd0ffd93d9c4347
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/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:
diff --git a/README.md b/README.md
index 0a1de1e6..11dfe94d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](https://pypi.org/project/arcadepy/)
-The Arcade Python library provides convenient access to the Arcade REST API from any Python 3.8+
+The Arcade Python library provides convenient access to the Arcade REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -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(
@@ -117,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:
@@ -416,7 +482,7 @@ print(arcadepy.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
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/pyproject.toml b/pyproject.toml
index c5887191..2eed60ce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,30 +1,32 @@
[project]
name = "arcadepy"
-version = "1.10.0"
+version = "1.11.0"
description = "The official Python library for the Arcade API"
dynamic = ["readme"]
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.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",
"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",
@@ -46,7 +48,7 @@ managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
@@ -141,7 +143,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/requirements-dev.lock b/requirements-dev.lock
index 3c2bd65d..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,80 +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.14.1
-mypy-extensions==1.0.0
+mypy==1.17.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
-platformdirs==3.11.0
+pathspec==0.12.1
+ # via mypy
+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 b5a97193..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.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
+sniffio==1.3.1
# via arcadepy
-typing-extensions==4.12.2
+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.1
+typing-inspection==0.4.2
# via pydantic
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
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'
diff --git a/src/arcadepy/_base_client.py b/src/arcadepy/_base_client.py
index 57cf33c7..5d342b7b 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,
@@ -83,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -477,8 +481,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,10 +547,18 @@ 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
+ 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)
@@ -1194,6 +1217,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 +1230,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 +1244,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 +1257,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,9 +1285,24 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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, content=content, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1258,11 +1311,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)
@@ -1272,9 +1337,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(
@@ -1714,6 +1789,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,
@@ -1726,6 +1802,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],
@@ -1739,6 +1816,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1751,13 +1829,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)
@@ -1767,9 +1857,29 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1778,11 +1888,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)
@@ -1792,9 +1914,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/_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
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/_models.py b/src/arcadepy/_models.py
index 6a3cd1d2..29070e05 100644
--- a/src/arcadepy/_models.py
+++ b/src/arcadepy/_models.py
@@ -2,7 +2,21 @@
import os
import inspect
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
+import weakref
+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,
@@ -256,15 +270,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
@@ -272,16 +287,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.
@@ -298,6 +321,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,
@@ -314,15 +339,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:
@@ -354,6 +381,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,
@@ -573,6 +604,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 +649,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 +704,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
@@ -765,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
@@ -783,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/_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
diff --git a/src/arcadepy/_types.py b/src/arcadepy/_types.py
index 39e9386d..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,
@@ -243,6 +252,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 +263,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
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/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:
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
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/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/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/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.
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",
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/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()
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",
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):
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")
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}}'