diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 582db2fa7..78e7f271d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.2" + ".": "0.11.0" } diff --git a/.stats.yml b/.stats.yml index 89e80cc66..988c8623d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 45 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-eeb5bf63b18d948611eec48d0225e9bba63b170f64eeeb35d91825724b7cf6c3.yml -openapi_spec_hash: 5bbd18a405a11e8497d38a5a88b98018 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-636aa63c588134e6f47fc45212049593d91f810a9c7bd8d7a57810cf1b5ffc92.yml +openapi_spec_hash: 5339c136760ece9fa1efb9a4f7749673 config_hash: fb079ef7936611b032568661b8165f19 diff --git a/CHANGELOG.md b/CHANGELOG.md index 536753e33..f6d63d227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.11.0 (2026-04-28) + +Full Changelog: [v0.10.2...v0.11.0](https://github.com/scaleapi/scale-agentex-python/compare/v0.10.2...v0.11.0) + +### Features + +* **api:** api update ([cd7c9b7](https://github.com/scaleapi/scale-agentex-python/commit/cd7c9b76a28f61d95c1c9f5477048c85f7e9f277)) +* support setting headers via env ([57a862c](https://github.com/scaleapi/scale-agentex-python/commit/57a862c8543f67a6cdc62dc9da623467676d2c53)) + + +### Bug Fixes + +* use correct field name format for multipart file arrays ([44078f4](https://github.com/scaleapi/scale-agentex-python/commit/44078f417c347b031933c6f6faae40b7681deb5f)) + + +### Chores + +* **internal:** more robust bootstrap script ([dd59283](https://github.com/scaleapi/scale-agentex-python/commit/dd592831d5d0304cf2c585b8a995ac4a8c407773)) + ## 0.10.2 (2026-04-21) Full Changelog: [v0.10.1...v0.10.2](https://github.com/scaleapi/scale-agentex-python/compare/v0.10.1...v0.10.2) diff --git a/pyproject.toml b/pyproject.toml index a1851c1ac..64a16d5b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agentex-sdk" -version = "0.10.2" +version = "0.11.0" description = "The official Python library for the agentex API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee36..fe8451e4f 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/agentex/_client.py b/src/agentex/_client.py index f3eeeb819..ecb11b155 100644 --- a/src/agentex/_client.py +++ b/src/agentex/_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 @@ -123,6 +127,15 @@ def __init__( except KeyError as exc: raise ValueError(f"Unknown environment: {environment}") from exc + custom_headers_env = os.environ.get("AGENTEX_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, @@ -363,6 +376,15 @@ def __init__( except KeyError as exc: raise ValueError(f"Unknown environment: {environment}") from exc + custom_headers_env = os.environ.get("AGENTEX_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/agentex/_qs.py b/src/agentex/_qs.py index de8c99bc6..4127c19c6 100644 --- a/src/agentex/_qs.py +++ b/src/agentex/_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/agentex/_types.py b/src/agentex/_types.py index f9c107dc3..3b662b594 100644 --- a/src/agentex/_types.py +++ b/src/agentex/_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/agentex/_utils/_utils.py b/src/agentex/_utils/_utils.py index 771859f5e..199cd231f 100644 --- a/src/agentex/_utils/_utils.py +++ b/src/agentex/_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/agentex/_version.py b/src/agentex/_version.py index 5d881e84d..59720802a 100644 --- a/src/agentex/_version.py +++ b/src/agentex/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "agentex" -__version__ = "0.10.2" # x-release-please-version +__version__ = "0.11.0" # x-release-please-version diff --git a/src/agentex/resources/spans.py b/src/agentex/resources/spans.py index ecd692644..f8d97e0de 100644 --- a/src/agentex/resources/spans.py +++ b/src/agentex/resources/spans.py @@ -57,7 +57,6 @@ def create( input: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, output: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, parent_id: Optional[str] | Omit = omit, - task_id: 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, @@ -87,8 +86,6 @@ def create( parent_id: ID of the parent span if this is a child span in a trace - task_id: ID of the task this span belongs to - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -110,7 +107,6 @@ def create( "input": input, "output": output, "parent_id": parent_id, - "task_id": task_id, }, span_create_params.SpanCreateParams, ), @@ -164,7 +160,6 @@ def update( output: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, parent_id: Optional[str] | Omit = omit, start_time: Union[str, datetime, None] | Omit = omit, - task_id: Optional[str] | Omit = omit, trace_id: 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. @@ -191,8 +186,6 @@ def update( start_time: The time the span started - task_id: ID of the task this span belongs to - trace_id: Unique identifier for the trace this span belongs to extra_headers: Send extra headers @@ -216,7 +209,6 @@ def update( "output": output, "parent_id": parent_id, "start_time": start_time, - "task_id": task_id, "trace_id": trace_id, }, span_update_params.SpanUpdateParams, @@ -234,7 +226,6 @@ def list( order_by: Optional[str] | Omit = omit, order_direction: str | Omit = omit, page_number: int | Omit = omit, - task_id: Optional[str] | Omit = omit, trace_id: 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. @@ -244,7 +235,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SpanListResponse: """ - List spans, optionally filtered by trace_id and/or task_id + List all spans for a given trace ID Args: extra_headers: Send extra headers @@ -268,7 +259,6 @@ def list( "order_by": order_by, "order_direction": order_direction, "page_number": page_number, - "task_id": task_id, "trace_id": trace_id, }, span_list_params.SpanListParams, @@ -310,7 +300,6 @@ async def create( input: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, output: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, parent_id: Optional[str] | Omit = omit, - task_id: 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, @@ -340,8 +329,6 @@ async def create( parent_id: ID of the parent span if this is a child span in a trace - task_id: ID of the task this span belongs to - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -363,7 +350,6 @@ async def create( "input": input, "output": output, "parent_id": parent_id, - "task_id": task_id, }, span_create_params.SpanCreateParams, ), @@ -417,7 +403,6 @@ async def update( output: Union[Dict[str, object], Iterable[Dict[str, object]], None] | Omit = omit, parent_id: Optional[str] | Omit = omit, start_time: Union[str, datetime, None] | Omit = omit, - task_id: Optional[str] | Omit = omit, trace_id: 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. @@ -444,8 +429,6 @@ async def update( start_time: The time the span started - task_id: ID of the task this span belongs to - trace_id: Unique identifier for the trace this span belongs to extra_headers: Send extra headers @@ -469,7 +452,6 @@ async def update( "output": output, "parent_id": parent_id, "start_time": start_time, - "task_id": task_id, "trace_id": trace_id, }, span_update_params.SpanUpdateParams, @@ -487,7 +469,6 @@ async def list( order_by: Optional[str] | Omit = omit, order_direction: str | Omit = omit, page_number: int | Omit = omit, - task_id: Optional[str] | Omit = omit, trace_id: 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. @@ -497,7 +478,7 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SpanListResponse: """ - List spans, optionally filtered by trace_id and/or task_id + List all spans for a given trace ID Args: extra_headers: Send extra headers @@ -521,7 +502,6 @@ async def list( "order_by": order_by, "order_direction": order_direction, "page_number": page_number, - "task_id": task_id, "trace_id": trace_id, }, span_list_params.SpanListParams, diff --git a/src/agentex/types/span.py b/src/agentex/types/span.py index 98793c03b..e5719bbe6 100644 --- a/src/agentex/types/span.py +++ b/src/agentex/types/span.py @@ -34,6 +34,3 @@ class Span(BaseModel): parent_id: Optional[str] = None """ID of the parent span if this is a child span in a trace""" - - task_id: Optional[str] = None - """ID of the task this span belongs to""" diff --git a/src/agentex/types/span_create_params.py b/src/agentex/types/span_create_params.py index 7debfc8d4..c7111d6fa 100644 --- a/src/agentex/types/span_create_params.py +++ b/src/agentex/types/span_create_params.py @@ -38,6 +38,3 @@ class SpanCreateParams(TypedDict, total=False): parent_id: Optional[str] """ID of the parent span if this is a child span in a trace""" - - task_id: Optional[str] - """ID of the task this span belongs to""" diff --git a/src/agentex/types/span_list_params.py b/src/agentex/types/span_list_params.py index 286c3d2bf..40d4d651b 100644 --- a/src/agentex/types/span_list_params.py +++ b/src/agentex/types/span_list_params.py @@ -17,6 +17,4 @@ class SpanListParams(TypedDict, total=False): page_number: int - task_id: Optional[str] - trace_id: Optional[str] diff --git a/src/agentex/types/span_update_params.py b/src/agentex/types/span_update_params.py index fda32dbad..3ebb54654 100644 --- a/src/agentex/types/span_update_params.py +++ b/src/agentex/types/span_update_params.py @@ -33,8 +33,5 @@ class SpanUpdateParams(TypedDict, total=False): start_time: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] """The time the span started""" - task_id: Optional[str] - """ID of the task this span belongs to""" - trace_id: Optional[str] """Unique identifier for the trace this span belongs to""" diff --git a/tests/api_resources/test_spans.py b/tests/api_resources/test_spans.py index 7cccec2ad..948760a67 100644 --- a/tests/api_resources/test_spans.py +++ b/tests/api_resources/test_spans.py @@ -42,7 +42,6 @@ def test_method_create_with_all_params(self, client: Agentex) -> None: input={"foo": "bar"}, output={"foo": "bar"}, parent_id="parent_id", - task_id="task_id", ) assert_matches_type(Span, span, path=["response"]) @@ -138,7 +137,6 @@ def test_method_update_with_all_params(self, client: Agentex) -> None: output={"foo": "bar"}, parent_id="parent_id", start_time=parse_datetime("2019-12-27T18:11:19.117Z"), - task_id="task_id", trace_id="trace_id", ) assert_matches_type(Span, span, path=["response"]) @@ -191,7 +189,6 @@ def test_method_list_with_all_params(self, client: Agentex) -> None: order_by="order_by", order_direction="order_direction", page_number=1, - task_id="task_id", trace_id="trace_id", ) assert_matches_type(SpanListResponse, span, path=["response"]) @@ -247,7 +244,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgentex) - input={"foo": "bar"}, output={"foo": "bar"}, parent_id="parent_id", - task_id="task_id", ) assert_matches_type(Span, span, path=["response"]) @@ -343,7 +339,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgentex) - output={"foo": "bar"}, parent_id="parent_id", start_time=parse_datetime("2019-12-27T18:11:19.117Z"), - task_id="task_id", trace_id="trace_id", ) assert_matches_type(Span, span, path=["response"]) @@ -396,7 +391,6 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgentex) -> order_by="order_by", order_direction="order_direction", page_number=1, - task_id="task_id", trace_id="trace_id", ) assert_matches_type(SpanListResponse, span, path=["response"]) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 9c32b2a52..fabb0f3c2 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from agentex._types import FileTypes +from agentex._types import FileTypes, ArrayFormat from agentex._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 fe5c22590..947bc6a51 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},