diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d9a5d555d..3b5fc24d7 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.20.2"
+ ".": "1.20.3"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index c5abf7216..20e30a2d9 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 115
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-563a11030291b5dd44e1b1b917e3e7bb865d7c873bf49c82056bfade22166843.yml
-openapi_spec_hash: 20770e5f6ed8370fc14ff0e1351ccffc
-config_hash: 12de9459ff629b6a3072a75b236b7b70
+configured_endpoints: 116
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6cf4d9a6afac92d72787088b3aefa941f5240ee522d9e98e1160eea2e29f87f4.yml
+openapi_spec_hash: e07fc8349cf507b083830b4e2b0caca0
+config_hash: 436c8d4e665915db22b5d98fe58382c1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c560e47a..19de81d83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
# Changelog
+## 1.20.3 (2026-05-08)
+
+Full Changelog: [v1.20.2...v1.20.3](https://github.com/runloopai/api-client-python/compare/v1.20.2...v1.20.3)
+
+### Features
+
+* make agent version optional in API ([#8858](https://github.com/runloopai/api-client-python/issues/8858)) ([7e11a9d](https://github.com/runloopai/api-client-python/commit/7e11a9db85aff6c28dcc04b8d391979027f38549))
+* share HTTP connection pool across SDK instances; refactor polling ([#797](https://github.com/runloopai/api-client-python/issues/797)) ([4f2fc4c](https://github.com/runloopai/api-client-python/commit/4f2fc4ca6d0b7a0b13b236dafb0e4e3148c2ed58))
+* support setting headers via env ([54ead49](https://github.com/runloopai/api-client-python/commit/54ead49fd28a61f60e18197d727fa57216c785fd))
+
+
+### Bug Fixes
+
+* use correct field name format for multipart file arrays ([c564da8](https://github.com/runloopai/api-client-python/commit/c564da85b7dfdbb77edf347f6b25ca4ca57e470e))
+
+
+### Chores
+
+* add get secret to stainless ([#7833](https://github.com/runloopai/api-client-python/issues/7833)) ([ce39778](https://github.com/runloopai/api-client-python/commit/ce39778de67907365c90f11ba3b3602cbc7daa2a))
+* **internal:** more robust bootstrap script ([115744e](https://github.com/runloopai/api-client-python/commit/115744e3c181822a1ec172e0526684839e278899))
+* **internal:** reformat pyproject.toml ([89e8401](https://github.com/runloopai/api-client-python/commit/89e8401b518f0ec15cb1e394dde66cb876bf0578))
+
## 1.20.2 (2026-05-01)
Full Changelog: [v1.20.1...v1.20.2](https://github.com/runloopai/api-client-python/compare/v1.20.1...v1.20.2)
diff --git a/api.md b/api.md
index 1d97dc90c..555f0c4f8 100644
--- a/api.md
+++ b/api.md
@@ -381,6 +381,7 @@ from runloop_api_client.types import (
Methods:
- client.secrets.create(\*\*params) -> SecretView
+- client.secrets.retrieve(name) -> SecretView
- client.secrets.update(name, \*\*params) -> SecretView
- client.secrets.list(\*\*params) -> SecretListView
- client.secrets.delete(name) -> SecretView
diff --git a/pyproject.toml b/pyproject.toml
index 705aaa40f..1178d263e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
-version = "1.20.2"
+version = "1.20.3"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
@@ -157,7 +157,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/runloop_api_client/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/runloop_api_client/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 76185f88c..ec7c87055 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
diff --git a/src/runloop_api_client/_client.py b/src/runloop_api_client/_client.py
index 1867f9997..3d032dfd6 100644
--- a/src/runloop_api_client/_client.py
+++ b/src/runloop_api_client/_client.py
@@ -19,7 +19,11 @@
RequestOptions,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ is_mapping_t,
+ get_async_library,
+)
from ._compat import cached_property
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
@@ -115,6 +119,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.runloop.ai"
+ custom_headers_env = os.environ.get("RUNLOOP_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
@@ -388,6 +401,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.runloop.ai"
+ custom_headers_env = os.environ.get("RUNLOOP_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
diff --git a/src/runloop_api_client/_qs.py b/src/runloop_api_client/_qs.py
index de8c99bc6..4127c19c6 100644
--- a/src/runloop_api_client/_qs.py
+++ b/src/runloop_api_client/_qs.py
@@ -2,17 +2,13 @@
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
-from typing_extensions import Literal, get_args
+from typing_extensions import get_args
-from ._types import NotGiven, not_given
+from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten
_T = TypeVar("_T")
-
-ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
-NestedFormat = Literal["dots", "brackets"]
-
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
diff --git a/src/runloop_api_client/_types.py b/src/runloop_api_client/_types.py
index 9f7d6eb21..db47875ab 100644
--- a/src/runloop_api_client/_types.py
+++ b/src/runloop_api_client/_types.py
@@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
+ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
+NestedFormat = Literal["dots", "brackets"]
+
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
diff --git a/src/runloop_api_client/_utils/_utils.py b/src/runloop_api_client/_utils/_utils.py
index 771859f5e..199cd231f 100644
--- a/src/runloop_api_client/_utils/_utils.py
+++ b/src/runloop_api_client/_utils/_utils.py
@@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
-from typing_extensions import TypeGuard
+from typing_extensions import TypeGuard, get_args
import sniffio
-from .._types import Omit, NotGiven, FileTypes, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '', 'data'].
+ ``array_format`` controls how ```` segments contribute to the emitted
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files
+def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
+ if array_format == "brackets":
+ return "[]"
+ if array_format == "indices":
+ return f"[{array_index}]"
+ if array_format == "repeat" or array_format == "comma":
+ # Both repeat the bare field name for each file part; there is no
+ # meaningful way to comma-join binary parts.
+ return ""
+ raise NotImplementedError(
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
+ )
+
+
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
+ array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
- for entry in obj:
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ for array_index, entry in enumerate(obj):
+ suffix = _array_suffix(array_format, array_index)
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
+ assert_is_file_content(entry, key=emitted_key)
+ files.append((emitted_key, cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
@@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
+ array_format=array_format,
)
elif is_list(obj):
if key != "":
@@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
+ flattened_key=(
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
+ ),
+ array_format=array_format,
)
- for item in obj
+ for array_index, item in enumerate(obj)
]
)
diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py
index 686bad05c..62ae17d31 100644
--- a/src/runloop_api_client/_version.py
+++ b/src/runloop_api_client/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "runloop_api_client"
-__version__ = "1.20.2" # x-release-please-version
+__version__ = "1.20.3" # x-release-please-version
diff --git a/src/runloop_api_client/resources/agents.py b/src/runloop_api_client/resources/agents.py
index 6febe22f0..6ea0f6b73 100644
--- a/src/runloop_api_client/resources/agents.py
+++ b/src/runloop_api_client/resources/agents.py
@@ -50,8 +50,8 @@ def create(
self,
*,
name: str,
- version: str,
source: Optional[AgentSource] | Omit = omit,
+ version: Optional[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,
@@ -68,10 +68,12 @@ def create(
Args:
name: The name of the Agent.
- version: The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA.
-
source: The source configuration for the Agent.
+ version: Optional version identifier for the Agent. For npm/pip sources this is typically
+ a semver string (e.g. '2.0.65'). For git sources it can be a branch or tag.
+ Semantics are user-defined for object sources.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -87,8 +89,8 @@ def create(
body=maybe_transform(
{
"name": name,
- "version": version,
"source": source,
+ "version": version,
},
agent_create_params.AgentCreateParams,
),
@@ -357,8 +359,8 @@ async def create(
self,
*,
name: str,
- version: str,
source: Optional[AgentSource] | Omit = omit,
+ version: Optional[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,
@@ -375,10 +377,12 @@ async def create(
Args:
name: The name of the Agent.
- version: The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA.
-
source: The source configuration for the Agent.
+ version: Optional version identifier for the Agent. For npm/pip sources this is typically
+ a semver string (e.g. '2.0.65'). For git sources it can be a branch or tag.
+ Semantics are user-defined for object sources.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -394,8 +398,8 @@ async def create(
body=await async_maybe_transform(
{
"name": name,
- "version": version,
"source": source,
+ "version": version,
},
agent_create_params.AgentCreateParams,
),
diff --git a/src/runloop_api_client/resources/secrets.py b/src/runloop_api_client/resources/secrets.py
index 38a9d8fc0..0dab937c4 100644
--- a/src/runloop_api_client/resources/secrets.py
+++ b/src/runloop_api_client/resources/secrets.py
@@ -100,6 +100,8 @@ def retrieve(
self,
name: str,
*,
+ # 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,
@@ -107,6 +109,8 @@ def retrieve(
) -> SecretView:
"""Retrieve a Secret by name.
+ The secret value is not included for security.
+
Args:
extra_headers: Send extra headers
@@ -119,12 +123,9 @@ def retrieve(
if not name:
raise ValueError(f"Expected a non-empty value for `name` but received {name!r}")
return self._get(
- f"/v1/secrets/{name}",
+ path_template("/v1/secrets/{name}", name=name),
options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=SecretView,
)
@@ -336,6 +337,8 @@ async def retrieve(
self,
name: str,
*,
+ # 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,
@@ -343,6 +346,8 @@ async def retrieve(
) -> SecretView:
"""Retrieve a Secret by name.
+ The secret value is not included for security.
+
Args:
extra_headers: Send extra headers
@@ -355,12 +360,9 @@ async def retrieve(
if not name:
raise ValueError(f"Expected a non-empty value for `name` but received {name!r}")
return await self._get(
- f"/v1/secrets/{name}",
+ path_template("/v1/secrets/{name}", name=name),
options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=SecretView,
)
diff --git a/src/runloop_api_client/types/agent_create_params.py b/src/runloop_api_client/types/agent_create_params.py
index 3c2deff2a..c1b70a046 100644
--- a/src/runloop_api_client/types/agent_create_params.py
+++ b/src/runloop_api_client/types/agent_create_params.py
@@ -14,8 +14,13 @@ class AgentCreateParams(TypedDict, total=False):
name: Required[str]
"""The name of the Agent."""
- version: Required[str]
- """The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA."""
-
source: Optional[AgentSource]
"""The source configuration for the Agent."""
+
+ version: Optional[str]
+ """Optional version identifier for the Agent.
+
+ For npm/pip sources this is typically a semver string (e.g. '2.0.65'). For git
+ sources it can be a branch or tag. Semantics are user-defined for object
+ sources.
+ """
diff --git a/src/runloop_api_client/types/agent_view.py b/src/runloop_api_client/types/agent_view.py
index 23b1f68ff..d77527731 100644
--- a/src/runloop_api_client/types/agent_view.py
+++ b/src/runloop_api_client/types/agent_view.py
@@ -23,8 +23,13 @@ class AgentView(BaseModel):
name: str
"""The name of the Agent."""
- version: str
- """The version of the Agent. A semver string (e.g., '2.0.65') or a SHA."""
-
source: Optional[AgentSource] = None
"""The source configuration for the Agent."""
+
+ version: Optional[str] = None
+ """Optional version identifier for the Agent.
+
+ For npm/pip sources this is typically a semver string (e.g. '2.0.65'). For git
+ sources it can be a branch or tag. Omitted for object sources or when not
+ provided.
+ """
diff --git a/tests/api_resources/test_agents.py b/tests/api_resources/test_agents.py
index fb602d148..d4a98d26e 100644
--- a/tests/api_resources/test_agents.py
+++ b/tests/api_resources/test_agents.py
@@ -25,7 +25,6 @@ class TestAgents:
def test_method_create(self, client: Runloop) -> None:
agent = client.agents.create(
name="name",
- version="version",
)
assert_matches_type(AgentView, agent, path=["response"])
@@ -33,7 +32,6 @@ def test_method_create(self, client: Runloop) -> None:
def test_method_create_with_all_params(self, client: Runloop) -> None:
agent = client.agents.create(
name="name",
- version="version",
source={
"type": "type",
"git": {
@@ -56,6 +54,7 @@ def test_method_create_with_all_params(self, client: Runloop) -> None:
"registry_url": "registry_url",
},
},
+ version="version",
)
assert_matches_type(AgentView, agent, path=["response"])
@@ -63,7 +62,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None:
def test_raw_response_create(self, client: Runloop) -> None:
response = client.agents.with_raw_response.create(
name="name",
- version="version",
)
assert response.is_closed is True
@@ -75,7 +73,6 @@ def test_raw_response_create(self, client: Runloop) -> None:
def test_streaming_response_create(self, client: Runloop) -> None:
with client.agents.with_streaming_response.create(
name="name",
- version="version",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -271,7 +268,6 @@ class TestAsyncAgents:
async def test_method_create(self, async_client: AsyncRunloop) -> None:
agent = await async_client.agents.create(
name="name",
- version="version",
)
assert_matches_type(AgentView, agent, path=["response"])
@@ -279,7 +275,6 @@ async def test_method_create(self, async_client: AsyncRunloop) -> None:
async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None:
agent = await async_client.agents.create(
name="name",
- version="version",
source={
"type": "type",
"git": {
@@ -302,6 +297,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -
"registry_url": "registry_url",
},
},
+ version="version",
)
assert_matches_type(AgentView, agent, path=["response"])
@@ -309,7 +305,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -
async def test_raw_response_create(self, async_client: AsyncRunloop) -> None:
response = await async_client.agents.with_raw_response.create(
name="name",
- version="version",
)
assert response.is_closed is True
@@ -321,7 +316,6 @@ async def test_raw_response_create(self, async_client: AsyncRunloop) -> None:
async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None:
async with async_client.agents.with_streaming_response.create(
name="name",
- version="version",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/test_secrets.py b/tests/api_resources/test_secrets.py
index 7f0ff8e21..8e0abff43 100644
--- a/tests/api_resources/test_secrets.py
+++ b/tests/api_resources/test_secrets.py
@@ -54,6 +54,44 @@ def test_streaming_response_create(self, client: Runloop) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_retrieve(self, client: Runloop) -> None:
+ secret = client.secrets.retrieve(
+ "name",
+ )
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Runloop) -> None:
+ response = client.secrets.with_raw_response.retrieve(
+ "name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ secret = response.parse()
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Runloop) -> None:
+ with client.secrets.with_streaming_response.retrieve(
+ "name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ secret = response.parse()
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"):
+ client.secrets.with_raw_response.retrieve(
+ "",
+ )
+
@parametrize
def test_method_update(self, client: Runloop) -> None:
secret = client.secrets.update(
@@ -206,6 +244,44 @@ async def test_streaming_response_create(self, async_client: AsyncRunloop) -> No
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncRunloop) -> None:
+ secret = await async_client.secrets.retrieve(
+ "name",
+ )
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.secrets.with_raw_response.retrieve(
+ "name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ secret = await response.parse()
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None:
+ async with async_client.secrets.with_streaming_response.retrieve(
+ "name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ secret = await response.parse()
+ assert_matches_type(SecretView, secret, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"):
+ await async_client.secrets.with_raw_response.retrieve(
+ "",
+ )
+
@parametrize
async def test_method_update(self, async_client: AsyncRunloop) -> None:
secret = await async_client.secrets.update(
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index a76b07d19..2822c7028 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -4,7 +4,7 @@
import pytest
-from runloop_api_client._types import FileTypes
+from runloop_api_client._types import FileTypes, ArrayFormat
from runloop_api_client._utils import extract_files
@@ -37,10 +37,7 @@ def test_multiple_files() -> None:
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [
- ("files[]", b"file one"),
- ("files[]", b"file two"),
- ]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
@@ -71,3 +68,24 @@ def test_ignores_incorrect_paths(
expected: list[tuple[str, FileTypes]],
) -> None:
assert extract_files(query, paths=paths) == expected
+
+
+@pytest.mark.parametrize(
+ "array_format,expected_top_level,expected_nested",
+ [
+ ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]),
+ ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]),
+ ],
+)
+def test_array_format_controls_file_field_names(
+ array_format: ArrayFormat,
+ expected_top_level: list[tuple[str, FileTypes]],
+ expected_nested: list[tuple[str, FileTypes]],
+) -> None:
+ top_level = {"files": [b"a", b"b"]}
+ assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level
+
+ nested = {"items": [{"file": b"a"}, {"file": b"b"}]}
+ assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested
diff --git a/tests/test_files.py b/tests/test_files.py
index f78bb843b..b5ee73ae2 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
copied = deepcopy_with_paths(original, [["items", "", "file"]])
extracted = extract_files(copied, paths=[["items", "", "file"]])
- assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert [entry for _, entry in extracted] == [file1, file2]
assert original == {
"items": [
{"file": file1, "extra": 1},