diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a266ab6..82d2167 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/warp-api-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 uv uses: astral-sh/setup-uv@v5 @@ -41,7 +41,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/warp-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v5 @@ -75,7 +75,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/warp-api-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 uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 7cc7fc0..0913bfa 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 uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 39d6102..dbc24df 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'warpdotdev/warp-sdk-python' && (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 6b7b74c..da59f99 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "0.4.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 4fd2e98..b964fdf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-c4c5f89f67a73e4d17377d2b96fc201a63cd5458cbebaa23e78f92b59b90cc5b.yml -openapi_spec_hash: 931c6189a4fc4ee320963646b1b7edbe -config_hash: 9c6b93a5f4b658b946f0ab7fcfedbaa3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-24029c9a3bb61d8ed8807686035c52060f97505d57ad827ae4debdec426ea0a0.yml +openapi_spec_hash: ffe58e3dd2d1c5c1552af6c0330e6fb1 +config_hash: 386210f0e52fc8dd00b78e25011b9980 diff --git a/CHANGELOG.md b/CHANGELOG.md index b3df9a5..52e45b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.4.0 (2026-01-21) + +Full Changelog: [v0.3.0...v0.4.0](https://github.com/warpdotdev/warp-sdk-python/compare/v0.3.0...v0.4.0) + +### ⚠ BREAKING CHANGES + +* **api:** catch up openapi, rename tasks -> runs + +### Features + +* **api:** catch up openapi, rename tasks -> runs ([fe8c5b3](https://github.com/warpdotdev/warp-sdk-python/commit/fe8c5b3f2da07a415ecd21d0f1ae5985e45f1a4d)) +* **api:** created at filter in list view ([71adb45](https://github.com/warpdotdev/warp-sdk-python/commit/71adb45481ca3248d892e62ff767b9ea011e7f21)) +* **client:** add support for binary request streaming ([c64e6c4](https://github.com/warpdotdev/warp-sdk-python/commit/c64e6c44b6d02a63609d02f4976a7977dc3b0045)) + + +### Chores + +* **internal:** update `actions/checkout` version ([fd0a90f](https://github.com/warpdotdev/warp-sdk-python/commit/fd0a90fae32b8a7e352217e3672e63b9b4dbc594)) + + +### Documentation + +* **dev:** Add WARP.md file ([7f1b835](https://github.com/warpdotdev/warp-sdk-python/commit/7f1b835240574dc517d424dca84251f86c4b1276)) + ## 0.3.0 (2026-01-05) Full Changelog: [v0.2.1...v0.3.0](https://github.com/warpdotdev/warp-sdk-python/compare/v0.2.1...v0.3.0) diff --git a/README.md b/README.md index dafd2a7..22c4697 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = WarpAPI( response = client.agent.run( prompt="Fix the bug in auth.go", ) -print(response.task_id) +print(response.run_id) ``` ### Using environments and configuration @@ -119,7 +119,7 @@ async def main() -> None: response = await client.agent.run( prompt="Fix the bug in auth.go", ) - print(response.task_id) + print(response.run_id) asyncio.run(main()) @@ -155,7 +155,7 @@ async def main() -> None: response = await client.agent.run( prompt="Fix the bug in auth.go", ) - print(response.task_id) + print(response.run_id) asyncio.run(main()) @@ -321,7 +321,7 @@ response = client.agent.with_raw_response.run( print(response.headers.get('X-My-Header')) agent = response.parse() # get the object that `agent.run()` would have returned -print(agent.task_id) +print(agent.run_id) ``` These methods return an [`APIResponse`](https://github.com/warpdotdev/warp-sdk-python/tree/main/src/warp_agent_sdk/_response.py) object. diff --git a/WARP.md b/WARP.md new file mode 100644 index 0000000..1cf8a11 --- /dev/null +++ b/WARP.md @@ -0,0 +1,119 @@ +# WARP.md + +This file provides guidance to WARP (warp.dev) when working with code in this repository. + +## Repository Overview + +This is the official Python SDK for the Warp API, providing convenient access to the Warp API REST API. The SDK is **generated code** created using [Stainless](https://www.stainless.com/) from an OpenAPI specification. Most files are auto-generated, with exceptions for `src/warp_agent_sdk/lib/` and `examples/` directories which are manually maintained. + +## Development Commands + +### Setup +```bash +# Bootstrap the development environment (installs uv, Python, and dependencies) +./scripts/bootstrap +``` + +### Testing +```bash +# Run full test suite (tests with both Pydantic v1 and v2, multiple Python versions) +./scripts/test + +# Run specific tests +uv run pytest tests/test_client.py + +# Tests require a mock Prism server running on port 4010 +# The test script will automatically start one if not running +# To manually start: ./scripts/mock --daemon +``` + +### Linting and Type Checking +```bash +# Run all linters (ruff, pyright, mypy) +./scripts/lint + +# Run with auto-fix +./scripts/lint --fix + +# Format code +./scripts/format +``` + +### Building +```bash +# Build distribution packages (.tar.gz and .whl) +uv build +``` + +## Code Architecture + +### Generated vs Manual Code + +**Generated code** (DO NOT manually edit - changes will be overwritten): +- `src/warp_agent_sdk/_client.py` - Main client classes (WarpAPI, AsyncWarpAPI) +- `src/warp_agent_sdk/resources/` - API resource classes +- `src/warp_agent_sdk/types/` - Type definitions and models +- Most utility files in `src/warp_agent_sdk/_utils/` + +**Manual code** (safe to edit): +- `src/warp_agent_sdk/lib/` - Custom library code +- `examples/` - Example scripts +- `tests/` - Test files + +### Core Components + +**Client Architecture**: +- `WarpAPI` (sync) and `AsyncWarpAPI` (async) are the main entry points +- Both inherit from `SyncAPIClient` and `AsyncAPIClient` base classes +- Support for both `httpx` (default) and `aiohttp` (optional) HTTP backends +- API key authentication via `Authorization: Bearer` header +- Default base URL: `https://app.warp.dev/api/v1` + +**Resource Structure**: +- Resources are organized hierarchically (e.g., `client.agent.tasks.retrieve()`) +- Each resource has sync/async variants and raw/streaming response wrappers +- Main resource: `AgentResource` with `run()` method and nested `TasksResource` + +**Type System**: +- Uses Pydantic models for request/response validation +- TypedDict for nested parameters +- Custom types: `NotGiven`, `Omit` for optional parameters +- Supports both Pydantic v1 and v2 + +## Environment Variables + +- `WARP_API_KEY` - API key for authentication (required) +- `WARP_API_BASE_URL` - Override default base URL +- `WARP_API_LOG` - Enable logging (`info` or `debug`) +- `TEST_API_BASE_URL` - Use custom API endpoint for tests + +## Testing Conventions + +- Tests use `pytest` with `pytest-asyncio` for async tests +- Both sync and async variants must be tested +- Tests run against a mock Prism server based on OpenAPI spec +- Tests run with both Pydantic v1 and v2 on Python 3.9 and 3.14+ +- Use `respx` for mocking HTTP requests in tests + +## Development Workflow + +1. **Making changes**: + - Only edit files in `lib/` and `examples/` directories + - Other changes should be made to the OpenAPI spec and regenerated + +2. **Adding examples**: + - Create executable Python scripts in `examples/` + - Use shebang: `#!/usr/bin/env -S uv run python` + - Make executable: `chmod +x examples/.py` + +3. **Code quality**: + - Run `./scripts/format` before committing + - Ensure `./scripts/lint` passes + - Run `./scripts/test` to verify changes + +## Package Management + +- Uses [uv](https://docs.astral.sh/uv/) for fast, reliable dependency management +- `pyproject.toml` defines project metadata and dependencies +- `uv.lock` pins exact versions for reproducibility +- `requirements-dev.lock` exported for pip compatibility diff --git a/api.md b/api.md index f438daa..b840946 100644 --- a/api.md +++ b/api.md @@ -10,15 +10,15 @@ Methods: - client.agent.run(\*\*params) -> AgentRunResponse -## Tasks +## Runs Types: ```python -from warp_agent_sdk.types.agent import TaskItem, TaskSourceType, TaskState, TaskListResponse +from warp_agent_sdk.types.agent import RunItem, RunSourceType, RunState, RunListResponse ``` Methods: -- client.agent.tasks.retrieve(task_id) -> TaskItem -- client.agent.tasks.list(\*\*params) -> TaskListResponse +- client.agent.runs.retrieve(run_id) -> RunItem +- client.agent.runs.list(\*\*params) -> RunListResponse diff --git a/pyproject.toml b/pyproject.toml index f514cde..1f0ef39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "warp-agent-sdk" -version = "0.3.0" +version = "0.4.0" description = "The official Python library for the warp-api API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/warp_agent_sdk/_base_client.py b/src/warp_agent_sdk/_base_client.py index 41099fd..e0f9736 100644 --- a/src/warp_agent_sdk/_base_client.py +++ b/src/warp_agent_sdk/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/warp_agent_sdk/_models.py b/src/warp_agent_sdk/_models.py index ca9500b..29070e0 100644 --- a/src/warp_agent_sdk/_models.py +++ b/src/warp_agent_sdk/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/warp_agent_sdk/_types.py b/src/warp_agent_sdk/_types.py index 6c063c0..39b1522 100644 --- a/src/warp_agent_sdk/_types.py +++ b/src/warp_agent_sdk/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/src/warp_agent_sdk/_version.py b/src/warp_agent_sdk/_version.py index 3f8939a..62e4ed0 100644 --- a/src/warp_agent_sdk/_version.py +++ b/src/warp_agent_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "warp_agent_sdk" -__version__ = "0.3.0" # x-release-please-version +__version__ = "0.4.0" # x-release-please-version diff --git a/src/warp_agent_sdk/resources/agent/__init__.py b/src/warp_agent_sdk/resources/agent/__init__.py index 481bf57..71feab7 100644 --- a/src/warp_agent_sdk/resources/agent/__init__.py +++ b/src/warp_agent_sdk/resources/agent/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) from .agent import ( AgentResource, AsyncAgentResource, @@ -8,22 +16,14 @@ AgentResourceWithStreamingResponse, AsyncAgentResourceWithStreamingResponse, ) -from .tasks import ( - TasksResource, - AsyncTasksResource, - TasksResourceWithRawResponse, - AsyncTasksResourceWithRawResponse, - TasksResourceWithStreamingResponse, - AsyncTasksResourceWithStreamingResponse, -) __all__ = [ - "TasksResource", - "AsyncTasksResource", - "TasksResourceWithRawResponse", - "AsyncTasksResourceWithRawResponse", - "TasksResourceWithStreamingResponse", - "AsyncTasksResourceWithStreamingResponse", + "RunsResource", + "AsyncRunsResource", + "RunsResourceWithRawResponse", + "AsyncRunsResourceWithRawResponse", + "RunsResourceWithStreamingResponse", + "AsyncRunsResourceWithStreamingResponse", "AgentResource", "AsyncAgentResource", "AgentResourceWithRawResponse", diff --git a/src/warp_agent_sdk/resources/agent/agent.py b/src/warp_agent_sdk/resources/agent/agent.py index f3635b9..7914f7a 100644 --- a/src/warp_agent_sdk/resources/agent/agent.py +++ b/src/warp_agent_sdk/resources/agent/agent.py @@ -4,13 +4,13 @@ import httpx -from .tasks import ( - TasksResource, - AsyncTasksResource, - TasksResourceWithRawResponse, - AsyncTasksResourceWithRawResponse, - TasksResourceWithStreamingResponse, - AsyncTasksResourceWithStreamingResponse, +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, ) from ...types import agent_run_params from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given @@ -32,8 +32,8 @@ class AgentResource(SyncAPIResource): @cached_property - def tasks(self) -> TasksResource: - return TasksResource(self._client) + def runs(self) -> RunsResource: + return RunsResource(self._client) @cached_property def with_raw_response(self) -> AgentResourceWithRawResponse: @@ -59,6 +59,7 @@ def run( *, prompt: str, config: AmbientAgentConfigParam | Omit = omit, + team: bool | Omit = omit, title: 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. @@ -70,14 +71,16 @@ def run( """Spawn an ambient agent with a prompt and optional configuration. The agent will - be queued for execution and assigned a unique task ID. + be queued for execution and assigned a unique run ID. Args: prompt: The prompt/instruction for the agent to execute - config: Configuration for an ambient agent task + config: Configuration for an ambient agent run - title: Custom title for the task (auto-generated if not provided) + team: Make the run visible to all team members, not only the calling user + + title: Custom title for the run (auto-generated if not provided) extra_headers: Send extra headers @@ -93,6 +96,7 @@ def run( { "prompt": prompt, "config": config, + "team": team, "title": title, }, agent_run_params.AgentRunParams, @@ -106,8 +110,8 @@ def run( class AsyncAgentResource(AsyncAPIResource): @cached_property - def tasks(self) -> AsyncTasksResource: - return AsyncTasksResource(self._client) + def runs(self) -> AsyncRunsResource: + return AsyncRunsResource(self._client) @cached_property def with_raw_response(self) -> AsyncAgentResourceWithRawResponse: @@ -133,6 +137,7 @@ async def run( *, prompt: str, config: AmbientAgentConfigParam | Omit = omit, + team: bool | Omit = omit, title: 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. @@ -144,14 +149,16 @@ async def run( """Spawn an ambient agent with a prompt and optional configuration. The agent will - be queued for execution and assigned a unique task ID. + be queued for execution and assigned a unique run ID. Args: prompt: The prompt/instruction for the agent to execute - config: Configuration for an ambient agent task + config: Configuration for an ambient agent run + + team: Make the run visible to all team members, not only the calling user - title: Custom title for the task (auto-generated if not provided) + title: Custom title for the run (auto-generated if not provided) extra_headers: Send extra headers @@ -167,6 +174,7 @@ async def run( { "prompt": prompt, "config": config, + "team": team, "title": title, }, agent_run_params.AgentRunParams, @@ -187,8 +195,8 @@ def __init__(self, agent: AgentResource) -> None: ) @cached_property - def tasks(self) -> TasksResourceWithRawResponse: - return TasksResourceWithRawResponse(self._agent.tasks) + def runs(self) -> RunsResourceWithRawResponse: + return RunsResourceWithRawResponse(self._agent.runs) class AsyncAgentResourceWithRawResponse: @@ -200,8 +208,8 @@ def __init__(self, agent: AsyncAgentResource) -> None: ) @cached_property - def tasks(self) -> AsyncTasksResourceWithRawResponse: - return AsyncTasksResourceWithRawResponse(self._agent.tasks) + def runs(self) -> AsyncRunsResourceWithRawResponse: + return AsyncRunsResourceWithRawResponse(self._agent.runs) class AgentResourceWithStreamingResponse: @@ -213,8 +221,8 @@ def __init__(self, agent: AgentResource) -> None: ) @cached_property - def tasks(self) -> TasksResourceWithStreamingResponse: - return TasksResourceWithStreamingResponse(self._agent.tasks) + def runs(self) -> RunsResourceWithStreamingResponse: + return RunsResourceWithStreamingResponse(self._agent.runs) class AsyncAgentResourceWithStreamingResponse: @@ -226,5 +234,5 @@ def __init__(self, agent: AsyncAgentResource) -> None: ) @cached_property - def tasks(self) -> AsyncTasksResourceWithStreamingResponse: - return AsyncTasksResourceWithStreamingResponse(self._agent.tasks) + def runs(self) -> AsyncRunsResourceWithStreamingResponse: + return AsyncRunsResourceWithStreamingResponse(self._agent.runs) diff --git a/src/warp_agent_sdk/resources/agent/tasks.py b/src/warp_agent_sdk/resources/agent/runs.py similarity index 70% rename from src/warp_agent_sdk/resources/agent/tasks.py rename to src/warp_agent_sdk/resources/agent/runs.py index ba77527..c1c0162 100644 --- a/src/warp_agent_sdk/resources/agent/tasks.py +++ b/src/warp_agent_sdk/resources/agent/runs.py @@ -17,39 +17,39 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...types.agent import TaskSourceType, task_list_params +from ...types.agent import RunSourceType, run_list_params from ..._base_client import make_request_options -from ...types.agent.task_item import TaskItem -from ...types.agent.task_state import TaskState -from ...types.agent.task_source_type import TaskSourceType -from ...types.agent.task_list_response import TaskListResponse +from ...types.agent.run_item import RunItem +from ...types.agent.run_state import RunState +from ...types.agent.run_source_type import RunSourceType +from ...types.agent.run_list_response import RunListResponse -__all__ = ["TasksResource", "AsyncTasksResource"] +__all__ = ["RunsResource", "AsyncRunsResource"] -class TasksResource(SyncAPIResource): +class RunsResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> TasksResourceWithRawResponse: + def with_raw_response(self) -> RunsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/warpdotdev/warp-sdk-python#accessing-raw-response-data-eg-headers """ - return TasksResourceWithRawResponse(self) + return RunsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> TasksResourceWithStreamingResponse: + def with_streaming_response(self) -> RunsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/warpdotdev/warp-sdk-python#with_streaming_response """ - return TasksResourceWithStreamingResponse(self) + return RunsResourceWithStreamingResponse(self) def retrieve( self, - task_id: str, + run_id: 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. @@ -57,9 +57,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> TaskItem: + ) -> RunItem: """ - Retrieve detailed information about a specific agent task, including the full + Retrieve detailed information about a specific agent run, including the full prompt, session link, and resolved configuration. Args: @@ -71,14 +71,14 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") return self._get( - f"/agent/tasks/{task_id}", + f"/agent/runs/{run_id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=TaskItem, + cast_to=RunItem, ) def list( @@ -91,16 +91,16 @@ def list( cursor: str | Omit = omit, limit: int | Omit = omit, model_id: str | Omit = omit, - source: TaskSourceType | Omit = omit, - state: List[TaskState] | Omit = omit, + source: RunSourceType | Omit = omit, + state: List[RunState] | 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, - ) -> TaskListResponse: - """Retrieve a paginated list of agent tasks with optional filtering. + ) -> RunListResponse: + """Retrieve a paginated list of agent runs with optional filtering. Results are ordered by creation time (newest first). @@ -108,21 +108,21 @@ def list( Args: config_name: Filter by agent config name - created_after: Filter tasks created after this timestamp (RFC3339 format) + created_after: Filter runs created after this timestamp (RFC3339 format) - created_before: Filter tasks created before this timestamp (RFC3339 format) + created_before: Filter runs created before this timestamp (RFC3339 format) creator: Filter by creator UID (user or service account) cursor: Pagination cursor from previous response - limit: Maximum number of tasks to return + limit: Maximum number of runs to return model_id: Filter by model ID - source: Filter by task source type + source: Filter by run source type - state: Filter by task state. Can be specified multiple times to match any of the given + state: Filter by run state. Can be specified multiple times to match any of the given states. extra_headers: Send extra headers @@ -134,7 +134,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get( - "/agent/tasks", + "/agent/runs", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -152,36 +152,36 @@ def list( "source": source, "state": state, }, - task_list_params.TaskListParams, + run_list_params.RunListParams, ), ), - cast_to=TaskListResponse, + cast_to=RunListResponse, ) -class AsyncTasksResource(AsyncAPIResource): +class AsyncRunsResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncTasksResourceWithRawResponse: + def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/warpdotdev/warp-sdk-python#accessing-raw-response-data-eg-headers """ - return AsyncTasksResourceWithRawResponse(self) + return AsyncRunsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncTasksResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncRunsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/warpdotdev/warp-sdk-python#with_streaming_response """ - return AsyncTasksResourceWithStreamingResponse(self) + return AsyncRunsResourceWithStreamingResponse(self) async def retrieve( self, - task_id: str, + run_id: 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. @@ -189,9 +189,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> TaskItem: + ) -> RunItem: """ - Retrieve detailed information about a specific agent task, including the full + Retrieve detailed information about a specific agent run, including the full prompt, session link, and resolved configuration. Args: @@ -203,14 +203,14 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") return await self._get( - f"/agent/tasks/{task_id}", + f"/agent/runs/{run_id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=TaskItem, + cast_to=RunItem, ) async def list( @@ -223,16 +223,16 @@ async def list( cursor: str | Omit = omit, limit: int | Omit = omit, model_id: str | Omit = omit, - source: TaskSourceType | Omit = omit, - state: List[TaskState] | Omit = omit, + source: RunSourceType | Omit = omit, + state: List[RunState] | 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, - ) -> TaskListResponse: - """Retrieve a paginated list of agent tasks with optional filtering. + ) -> RunListResponse: + """Retrieve a paginated list of agent runs with optional filtering. Results are ordered by creation time (newest first). @@ -240,21 +240,21 @@ async def list( Args: config_name: Filter by agent config name - created_after: Filter tasks created after this timestamp (RFC3339 format) + created_after: Filter runs created after this timestamp (RFC3339 format) - created_before: Filter tasks created before this timestamp (RFC3339 format) + created_before: Filter runs created before this timestamp (RFC3339 format) creator: Filter by creator UID (user or service account) cursor: Pagination cursor from previous response - limit: Maximum number of tasks to return + limit: Maximum number of runs to return model_id: Filter by model ID - source: Filter by task source type + source: Filter by run source type - state: Filter by task state. Can be specified multiple times to match any of the given + state: Filter by run state. Can be specified multiple times to match any of the given states. extra_headers: Send extra headers @@ -266,7 +266,7 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ return await self._get( - "/agent/tasks", + "/agent/runs", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -284,56 +284,56 @@ async def list( "source": source, "state": state, }, - task_list_params.TaskListParams, + run_list_params.RunListParams, ), ), - cast_to=TaskListResponse, + cast_to=RunListResponse, ) -class TasksResourceWithRawResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks +class RunsResourceWithRawResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs self.retrieve = to_raw_response_wrapper( - tasks.retrieve, + runs.retrieve, ) self.list = to_raw_response_wrapper( - tasks.list, + runs.list, ) -class AsyncTasksResourceWithRawResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks +class AsyncRunsResourceWithRawResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs self.retrieve = async_to_raw_response_wrapper( - tasks.retrieve, + runs.retrieve, ) self.list = async_to_raw_response_wrapper( - tasks.list, + runs.list, ) -class TasksResourceWithStreamingResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks +class RunsResourceWithStreamingResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs self.retrieve = to_streamed_response_wrapper( - tasks.retrieve, + runs.retrieve, ) self.list = to_streamed_response_wrapper( - tasks.list, + runs.list, ) -class AsyncTasksResourceWithStreamingResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks +class AsyncRunsResourceWithStreamingResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs self.retrieve = async_to_streamed_response_wrapper( - tasks.retrieve, + runs.retrieve, ) self.list = async_to_streamed_response_wrapper( - tasks.list, + runs.list, ) diff --git a/src/warp_agent_sdk/types/agent/__init__.py b/src/warp_agent_sdk/types/agent/__init__.py index bbde35a..ee07ed6 100644 --- a/src/warp_agent_sdk/types/agent/__init__.py +++ b/src/warp_agent_sdk/types/agent/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations -from .task_item import TaskItem as TaskItem -from .task_state import TaskState as TaskState -from .task_list_params import TaskListParams as TaskListParams -from .task_source_type import TaskSourceType as TaskSourceType -from .task_list_response import TaskListResponse as TaskListResponse +from .run_item import RunItem as RunItem +from .run_state import RunState as RunState +from .run_list_params import RunListParams as RunListParams +from .run_source_type import RunSourceType as RunSourceType +from .run_list_response import RunListResponse as RunListResponse diff --git a/src/warp_agent_sdk/types/agent/run_item.py b/src/warp_agent_sdk/types/agent/run_item.py new file mode 100644 index 0000000..103e3a6 --- /dev/null +++ b/src/warp_agent_sdk/types/agent/run_item.py @@ -0,0 +1,110 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel +from .run_state import RunState +from .run_source_type import RunSourceType +from ..ambient_agent_config import AmbientAgentConfig + +__all__ = ["RunItem", "Creator", "RequestUsage", "StatusMessage"] + + +class Creator(BaseModel): + display_name: Optional[str] = None + """Display name of the creator""" + + photo_url: Optional[str] = None + """URL to the creator's photo""" + + type: Optional[Literal["user", "service_account"]] = None + """Type of the creator principal""" + + uid: Optional[str] = None + """Unique identifier of the creator""" + + +class RequestUsage(BaseModel): + """Resource usage information for the run""" + + compute_cost: Optional[float] = None + """Cost of compute resources for the run""" + + inference_cost: Optional[float] = None + """Cost of LLM inference for the run""" + + +class StatusMessage(BaseModel): + message: Optional[str] = None + """Human-readable status message""" + + +class RunItem(BaseModel): + created_at: datetime + """Timestamp when the run was created (RFC3339)""" + + prompt: str + """The prompt/instruction for the agent""" + + run_id: str + """Unique identifier for the run""" + + state: RunState + """Current state of the run: + + - QUEUED: Run is waiting to be picked up + - PENDING: Run is being prepared + - CLAIMED: Run has been claimed by a worker + - INPROGRESS: Run is actively being executed + - SUCCEEDED: Run completed successfully + - FAILED: Run failed + """ + + task_id: str + """Unique identifier for the task (typically matches run_id). + + Deprecated - use run_id instead. + """ + + title: str + """Human-readable title for the run""" + + updated_at: datetime + """Timestamp when the run was last updated (RFC3339)""" + + agent_config: Optional[AmbientAgentConfig] = None + """Configuration for an ambient agent run""" + + conversation_id: Optional[str] = None + """UUID of the conversation associated with the run""" + + creator: Optional[Creator] = None + + is_sandbox_running: Optional[bool] = None + """Whether the sandbox environment is currently running""" + + request_usage: Optional[RequestUsage] = None + """Resource usage information for the run""" + + session_id: Optional[str] = None + """UUID of the shared session (if available)""" + + session_link: Optional[str] = None + """URL to view the agent session""" + + source: Optional[RunSourceType] = None + """Source that created the run: + + - LINEAR: Created from Linear integration + - API: Created via the Warp API + - SLACK: Created from Slack integration + - LOCAL: Created from local CLI/app + - SCHEDULED_AGENT: Created by a scheduled agent + """ + + started_at: Optional[datetime] = None + """Timestamp when the agent started working on the run (RFC3339)""" + + status_message: Optional[StatusMessage] = None diff --git a/src/warp_agent_sdk/types/agent/task_list_params.py b/src/warp_agent_sdk/types/agent/run_list_params.py similarity index 63% rename from src/warp_agent_sdk/types/agent/task_list_params.py rename to src/warp_agent_sdk/types/agent/run_list_params.py index 5a4e28e..d7ef57c 100644 --- a/src/warp_agent_sdk/types/agent/task_list_params.py +++ b/src/warp_agent_sdk/types/agent/run_list_params.py @@ -7,21 +7,21 @@ from typing_extensions import Annotated, TypedDict from ..._utils import PropertyInfo -from .task_state import TaskState -from .task_source_type import TaskSourceType +from .run_state import RunState +from .run_source_type import RunSourceType -__all__ = ["TaskListParams"] +__all__ = ["RunListParams"] -class TaskListParams(TypedDict, total=False): +class RunListParams(TypedDict, total=False): config_name: str """Filter by agent config name""" created_after: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """Filter tasks created after this timestamp (RFC3339 format)""" + """Filter runs created after this timestamp (RFC3339 format)""" created_before: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """Filter tasks created before this timestamp (RFC3339 format)""" + """Filter runs created before this timestamp (RFC3339 format)""" creator: str """Filter by creator UID (user or service account)""" @@ -30,16 +30,16 @@ class TaskListParams(TypedDict, total=False): """Pagination cursor from previous response""" limit: int - """Maximum number of tasks to return""" + """Maximum number of runs to return""" model_id: str """Filter by model ID""" - source: TaskSourceType - """Filter by task source type""" + source: RunSourceType + """Filter by run source type""" - state: List[TaskState] - """Filter by task state. + state: List[RunState] + """Filter by run state. Can be specified multiple times to match any of the given states. """ diff --git a/src/warp_agent_sdk/types/agent/task_list_response.py b/src/warp_agent_sdk/types/agent/run_list_response.py similarity index 73% rename from src/warp_agent_sdk/types/agent/task_list_response.py rename to src/warp_agent_sdk/types/agent/run_list_response.py index 64bc2e9..db1beb9 100644 --- a/src/warp_agent_sdk/types/agent/task_list_response.py +++ b/src/warp_agent_sdk/types/agent/run_list_response.py @@ -2,10 +2,10 @@ from typing import List, Optional +from .run_item import RunItem from ..._models import BaseModel -from .task_item import TaskItem -__all__ = ["TaskListResponse", "PageInfo"] +__all__ = ["RunListResponse", "PageInfo"] class PageInfo(BaseModel): @@ -16,7 +16,7 @@ class PageInfo(BaseModel): """Opaque cursor for fetching the next page""" -class TaskListResponse(BaseModel): +class RunListResponse(BaseModel): page_info: PageInfo - tasks: List[TaskItem] + runs: List[RunItem] diff --git a/src/warp_agent_sdk/types/agent/task_source_type.py b/src/warp_agent_sdk/types/agent/run_source_type.py similarity index 53% rename from src/warp_agent_sdk/types/agent/task_source_type.py rename to src/warp_agent_sdk/types/agent/run_source_type.py index 702ab70..c1a4b52 100644 --- a/src/warp_agent_sdk/types/agent/task_source_type.py +++ b/src/warp_agent_sdk/types/agent/run_source_type.py @@ -2,6 +2,6 @@ from typing_extensions import Literal, TypeAlias -__all__ = ["TaskSourceType"] +__all__ = ["RunSourceType"] -TaskSourceType: TypeAlias = Literal["LINEAR", "API", "SLACK", "LOCAL", "SCHEDULED_AGENT"] +RunSourceType: TypeAlias = Literal["LINEAR", "API", "SLACK", "LOCAL", "SCHEDULED_AGENT"] diff --git a/src/warp_agent_sdk/types/agent/task_state.py b/src/warp_agent_sdk/types/agent/run_state.py similarity index 52% rename from src/warp_agent_sdk/types/agent/task_state.py rename to src/warp_agent_sdk/types/agent/run_state.py index 8584e25..af072aa 100644 --- a/src/warp_agent_sdk/types/agent/task_state.py +++ b/src/warp_agent_sdk/types/agent/run_state.py @@ -2,6 +2,6 @@ from typing_extensions import Literal, TypeAlias -__all__ = ["TaskState"] +__all__ = ["RunState"] -TaskState: TypeAlias = Literal["QUEUED", "PENDING", "CLAIMED", "INPROGRESS", "SUCCEEDED", "FAILED"] +RunState: TypeAlias = Literal["QUEUED", "PENDING", "CLAIMED", "INPROGRESS", "SUCCEEDED", "FAILED"] diff --git a/src/warp_agent_sdk/types/agent/task_item.py b/src/warp_agent_sdk/types/agent/task_item.py deleted file mode 100644 index d33c538..0000000 --- a/src/warp_agent_sdk/types/agent/task_item.py +++ /dev/null @@ -1,76 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ..._models import BaseModel -from .task_state import TaskState -from .task_source_type import TaskSourceType -from ..ambient_agent_config import AmbientAgentConfig - -__all__ = ["TaskItem", "Creator", "StatusMessage"] - - -class Creator(BaseModel): - type: Optional[Literal["user", "service_account"]] = None - """Type of the creator principal""" - - uid: Optional[str] = None - """Unique identifier of the creator""" - - -class StatusMessage(BaseModel): - message: Optional[str] = None - """Human-readable status message""" - - -class TaskItem(BaseModel): - created_at: datetime - """Timestamp when the task was created (RFC3339)""" - - prompt: str - """The prompt/instruction for the agent""" - - state: TaskState - """Current state of the task: - - - QUEUED: Task is waiting to be picked up - - PENDING: Task is being prepared - - CLAIMED: Task has been claimed by a worker - - INPROGRESS: Task is actively being executed - - SUCCEEDED: Task completed successfully - - FAILED: Task failed - """ - - task_id: str - """Unique identifier for the task""" - - title: str - """Human-readable title for the task""" - - updated_at: datetime - """Timestamp when the task was last updated (RFC3339)""" - - agent_config: Optional[AmbientAgentConfig] = None - """Configuration for an ambient agent task""" - - creator: Optional[Creator] = None - - session_id: Optional[str] = None - """UUID of the shared session (if available)""" - - session_link: Optional[str] = None - """URL to view the agent session""" - - source: Optional[TaskSourceType] = None - """Source that created the task: - - - LINEAR: Created from Linear integration - - API: Created via the public API - - SLACK: Created from Slack integration - - LOCAL: Created from local CLI/app - - SCHEDULED_AGENT: Created by a scheduled agent - """ - - status_message: Optional[StatusMessage] = None diff --git a/src/warp_agent_sdk/types/agent_run_params.py b/src/warp_agent_sdk/types/agent_run_params.py index 4220f53..3761f66 100644 --- a/src/warp_agent_sdk/types/agent_run_params.py +++ b/src/warp_agent_sdk/types/agent_run_params.py @@ -14,7 +14,10 @@ class AgentRunParams(TypedDict, total=False): """The prompt/instruction for the agent to execute""" config: AmbientAgentConfigParam - """Configuration for an ambient agent task""" + """Configuration for an ambient agent run""" + + team: bool + """Make the run visible to all team members, not only the calling user""" title: str - """Custom title for the task (auto-generated if not provided)""" + """Custom title for the run (auto-generated if not provided)""" diff --git a/src/warp_agent_sdk/types/agent_run_response.py b/src/warp_agent_sdk/types/agent_run_response.py index 878eb89..5134eac 100644 --- a/src/warp_agent_sdk/types/agent_run_response.py +++ b/src/warp_agent_sdk/types/agent_run_response.py @@ -1,22 +1,22 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from .._models import BaseModel -from .agent.task_state import TaskState +from .agent.run_state import RunState __all__ = ["AgentRunResponse"] class AgentRunResponse(BaseModel): - state: TaskState - """Current state of the task: + run_id: str + """Unique identifier for the created run""" - - QUEUED: Task is waiting to be picked up - - PENDING: Task is being prepared - - CLAIMED: Task has been claimed by a worker - - INPROGRESS: Task is actively being executed - - SUCCEEDED: Task completed successfully - - FAILED: Task failed - """ + state: RunState + """Current state of the run: - task_id: str - """Unique identifier for the created task""" + - QUEUED: Run is waiting to be picked up + - PENDING: Run is being prepared + - CLAIMED: Run has been claimed by a worker + - INPROGRESS: Run is actively being executed + - SUCCEEDED: Run completed successfully + - FAILED: Run failed + """ diff --git a/src/warp_agent_sdk/types/ambient_agent_config.py b/src/warp_agent_sdk/types/ambient_agent_config.py index ae3c545..32d67ff 100644 --- a/src/warp_agent_sdk/types/ambient_agent_config.py +++ b/src/warp_agent_sdk/types/ambient_agent_config.py @@ -35,19 +35,19 @@ class McpServers(BaseModel): class AmbientAgentConfig(BaseModel): - """Configuration for an ambient agent task""" + """Configuration for an ambient agent run""" base_prompt: Optional[str] = None """Custom base prompt for the agent""" environment_id: Optional[str] = None - """UID of a CloudEnvironment GSO to use""" + """UID of the environment to run the agent in""" mcp_servers: Optional[Dict[str, McpServers]] = None """Map of MCP server configurations by name""" api_model_id: Optional[str] = FieldInfo(alias="model_id", default=None) - """LLM model to use (uses workspace default if not specified)""" + """LLM model to use (uses team default if not specified)""" name: Optional[str] = None """Config name for searchability and traceability""" diff --git a/src/warp_agent_sdk/types/ambient_agent_config_param.py b/src/warp_agent_sdk/types/ambient_agent_config_param.py index 28bcb82..7a9e91c 100644 --- a/src/warp_agent_sdk/types/ambient_agent_config_param.py +++ b/src/warp_agent_sdk/types/ambient_agent_config_param.py @@ -36,19 +36,19 @@ class McpServers(TypedDict, total=False): class AmbientAgentConfigParam(TypedDict, total=False): - """Configuration for an ambient agent task""" + """Configuration for an ambient agent run""" base_prompt: str """Custom base prompt for the agent""" environment_id: str - """UID of a CloudEnvironment GSO to use""" + """UID of the environment to run the agent in""" mcp_servers: Dict[str, McpServers] """Map of MCP server configurations by name""" model_id: str - """LLM model to use (uses workspace default if not specified)""" + """LLM model to use (uses team default if not specified)""" name: str """Config name for searchability and traceability""" diff --git a/tests/api_resources/agent/test_tasks.py b/tests/api_resources/agent/test_runs.py similarity index 67% rename from tests/api_resources/agent/test_tasks.py rename to tests/api_resources/agent/test_runs.py index f5e4d52..d17cd50 100644 --- a/tests/api_resources/agent/test_tasks.py +++ b/tests/api_resources/agent/test_runs.py @@ -10,66 +10,66 @@ from tests.utils import assert_matches_type from warp_agent_sdk import WarpAPI, AsyncWarpAPI from warp_agent_sdk._utils import parse_datetime -from warp_agent_sdk.types.agent import TaskItem, TaskListResponse +from warp_agent_sdk.types.agent import RunItem, RunListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -class TestTasks: +class TestRuns: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_retrieve(self, client: WarpAPI) -> None: - task = client.agent.tasks.retrieve( - "taskId", + run = client.agent.runs.retrieve( + "runId", ) - assert_matches_type(TaskItem, task, path=["response"]) + assert_matches_type(RunItem, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: WarpAPI) -> None: - response = client.agent.tasks.with_raw_response.retrieve( - "taskId", + response = client.agent.runs.with_raw_response.retrieve( + "runId", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskItem, task, path=["response"]) + run = response.parse() + assert_matches_type(RunItem, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: WarpAPI) -> None: - with client.agent.tasks.with_streaming_response.retrieve( - "taskId", + with client.agent.runs.with_streaming_response.retrieve( + "runId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskItem, task, path=["response"]) + run = response.parse() + assert_matches_type(RunItem, run, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_retrieve(self, client: WarpAPI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.agent.tasks.with_raw_response.retrieve( + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.agent.runs.with_raw_response.retrieve( "", ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: WarpAPI) -> None: - task = client.agent.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = client.agent.runs.list() + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: WarpAPI) -> None: - task = client.agent.tasks.list( + run = client.agent.runs.list( config_name="config_name", created_after=parse_datetime("2019-12-27T18:11:19.117Z"), created_before=parse_datetime("2019-12-27T18:11:19.117Z"), @@ -80,32 +80,32 @@ def test_method_list_with_all_params(self, client: WarpAPI) -> None: source="LINEAR", state=["QUEUED"], ) - assert_matches_type(TaskListResponse, task, path=["response"]) + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: WarpAPI) -> None: - response = client.agent.tasks.with_raw_response.list() + response = client.agent.runs.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = response.parse() + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: WarpAPI) -> None: - with client.agent.tasks.with_streaming_response.list() as response: + with client.agent.runs.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = response.parse() + assert_matches_type(RunListResponse, run, path=["response"]) assert cast(Any, response.is_closed) is True -class TestAsyncTasks: +class TestAsyncRuns: parametrize = pytest.mark.parametrize( "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) @@ -113,55 +113,55 @@ class TestAsyncTasks: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncWarpAPI) -> None: - task = await async_client.agent.tasks.retrieve( - "taskId", + run = await async_client.agent.runs.retrieve( + "runId", ) - assert_matches_type(TaskItem, task, path=["response"]) + assert_matches_type(RunItem, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncWarpAPI) -> None: - response = await async_client.agent.tasks.with_raw_response.retrieve( - "taskId", + response = await async_client.agent.runs.with_raw_response.retrieve( + "runId", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskItem, task, path=["response"]) + run = await response.parse() + assert_matches_type(RunItem, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncWarpAPI) -> None: - async with async_client.agent.tasks.with_streaming_response.retrieve( - "taskId", + async with async_client.agent.runs.with_streaming_response.retrieve( + "runId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskItem, task, path=["response"]) + run = await response.parse() + assert_matches_type(RunItem, run, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncWarpAPI) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.agent.tasks.with_raw_response.retrieve( + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.agent.runs.with_raw_response.retrieve( "", ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncWarpAPI) -> None: - task = await async_client.agent.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = await async_client.agent.runs.list() + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWarpAPI) -> None: - task = await async_client.agent.tasks.list( + run = await async_client.agent.runs.list( config_name="config_name", created_after=parse_datetime("2019-12-27T18:11:19.117Z"), created_before=parse_datetime("2019-12-27T18:11:19.117Z"), @@ -172,26 +172,26 @@ async def test_method_list_with_all_params(self, async_client: AsyncWarpAPI) -> source="LINEAR", state=["QUEUED"], ) - assert_matches_type(TaskListResponse, task, path=["response"]) + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncWarpAPI) -> None: - response = await async_client.agent.tasks.with_raw_response.list() + response = await async_client.agent.runs.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = await response.parse() + assert_matches_type(RunListResponse, run, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncWarpAPI) -> None: - async with async_client.agent.tasks.with_streaming_response.list() as response: + async with async_client.agent.runs.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) + run = await response.parse() + assert_matches_type(RunListResponse, run, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index fcb0851..f873025 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -46,6 +46,7 @@ def test_method_run_with_all_params(self, client: WarpAPI) -> None: "model_id": "model_id", "name": "name", }, + team=True, title="title", ) assert_matches_type(AgentRunResponse, agent, path=["response"]) @@ -111,6 +112,7 @@ async def test_method_run_with_all_params(self, async_client: AsyncWarpAPI) -> N "model_id": "model_id", "name": "name", }, + team=True, title="title", ) assert_matches_type(AgentRunResponse, agent, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index 7e28add..92ea2da 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: WarpAPI | AsyncWarpAPI) -> 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: WarpAPI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: WarpAPI) -> 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 WarpAPI( + 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: WarpAPI) -> 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: WarpAPI) -> None: class Model1(BaseModel): @@ -1323,6 +1440,72 @@ def test_multipart_repeating_array(self, async_client: AsyncWarpAPI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncWarpAPI) -> 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 AsyncWarpAPI( + 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: AsyncWarpAPI + ) -> 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: AsyncWarpAPI) -> None: class Model1(BaseModel):