Skip to content

Commit 58d59e3

Browse files
feat: Address Stainless diagnostics
1 parent 99808a5 commit 58d59e3

File tree

19 files changed

+286
-103
lines changed

19 files changed

+286
-103
lines changed

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 14
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-a931490557d2c27bcec6b9d7cb0502913f6a3969a5907c810a3c2073ca2ca9e0.yml
3-
openapi_spec_hash: 632bc51224846da7edc3996983d35ff3
4-
config_hash: e894152aaebba5a2e65e27efaf2712e2
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-d9e0c6bea8d4c2414ec2bcd18c2e748b43110079c81906aa6c79b350b408f96b.yml
3+
openapi_spec_hash: 598927b816525740620b414c6d147184
4+
config_hash: 1888db8b2f33dc16874aea51a90e78f7

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,69 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
170170

171171
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
172172

173+
## Pagination
174+
175+
List methods in the Oz API API are paginated.
176+
177+
This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
178+
179+
```python
180+
from oz_agent_sdk import OzAPI
181+
182+
client = OzAPI()
183+
184+
all_runs = []
185+
# Automatically fetches more pages as needed.
186+
for run in client.agent.runs.list():
187+
# Do something with run here
188+
all_runs.append(run)
189+
print(all_runs)
190+
```
191+
192+
Or, asynchronously:
193+
194+
```python
195+
import asyncio
196+
from oz_agent_sdk import AsyncOzAPI
197+
198+
client = AsyncOzAPI()
199+
200+
201+
async def main() -> None:
202+
all_runs = []
203+
# Iterate through items across all pages, issuing requests as needed.
204+
async for run in client.agent.runs.list():
205+
all_runs.append(run)
206+
print(all_runs)
207+
208+
209+
asyncio.run(main())
210+
```
211+
212+
Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
213+
214+
```python
215+
first_page = await client.agent.runs.list()
216+
if first_page.has_next_page():
217+
print(f"will fetch next page using these details: {first_page.next_page_info()}")
218+
next_page = await first_page.get_next_page()
219+
print(f"number of items we just fetched: {len(next_page.runs)}")
220+
221+
# Remove `await` for non-async usage.
222+
```
223+
224+
Or just work directly with the returned data:
225+
226+
```python
227+
first_page = await client.agent.runs.list()
228+
229+
print(f"next page cursor: {first_page.page_info.next_cursor}") # => "next page cursor: ..."
230+
for run in first_page.runs:
231+
print(run.run_id)
232+
233+
# Remove `await` for non-async usage.
234+
```
235+
173236
## Nested params
174237

175238
Nested parameters are dictionaries, typed using `TypedDict`, for example:

api.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ Types:
66
from oz_agent_sdk.types import (
77
AgentSkill,
88
AmbientAgentConfig,
9+
AwsProviderConfig,
910
CloudEnvironmentConfig,
1011
Error,
1112
ErrorCode,
13+
GcpProviderConfig,
1214
McpServerConfig,
1315
Scope,
1416
UserProfile,
@@ -22,7 +24,7 @@ Methods:
2224

2325
- <code title="get /agent">client.agent.<a href="./src/oz_agent_sdk/resources/agent/agent.py">list</a>(\*\*<a href="src/oz_agent_sdk/types/agent_list_params.py">params</a>) -> <a href="./src/oz_agent_sdk/types/agent_list_response.py">AgentListResponse</a></code>
2426
- <code title="get /agent/artifacts/{artifactUid}">client.agent.<a href="./src/oz_agent_sdk/resources/agent/agent.py">get_artifact</a>(artifact_uid) -> <a href="./src/oz_agent_sdk/types/agent_get_artifact_response.py">AgentGetArtifactResponse</a></code>
25-
- <code title="post /agent/run">client.agent.<a href="./src/oz_agent_sdk/resources/agent/agent.py">run</a>(\*\*<a href="src/oz_agent_sdk/types/agent_run_params.py">params</a>) -> <a href="./src/oz_agent_sdk/types/agent_run_response.py">AgentRunResponse</a></code>
27+
- <code title="post /agent/runs">client.agent.<a href="./src/oz_agent_sdk/resources/agent/agent.py">run</a>(\*\*<a href="src/oz_agent_sdk/types/agent_run_params.py">params</a>) -> <a href="./src/oz_agent_sdk/types/agent_run_response.py">AgentRunResponse</a></code>
2628

2729
## Runs
2830

@@ -34,15 +36,14 @@ from oz_agent_sdk.types.agent import (
3436
RunItem,
3537
RunSourceType,
3638
RunState,
37-
RunListResponse,
3839
RunCancelResponse,
3940
)
4041
```
4142

4243
Methods:
4344

4445
- <code title="get /agent/runs/{runId}">client.agent.runs.<a href="./src/oz_agent_sdk/resources/agent/runs.py">retrieve</a>(run_id) -> <a href="./src/oz_agent_sdk/types/agent/run_item.py">RunItem</a></code>
45-
- <code title="get /agent/runs">client.agent.runs.<a href="./src/oz_agent_sdk/resources/agent/runs.py">list</a>(\*\*<a href="src/oz_agent_sdk/types/agent/run_list_params.py">params</a>) -> <a href="./src/oz_agent_sdk/types/agent/run_list_response.py">RunListResponse</a></code>
46+
- <code title="get /agent/runs">client.agent.runs.<a href="./src/oz_agent_sdk/resources/agent/runs.py">list</a>(\*\*<a href="src/oz_agent_sdk/types/agent/run_list_params.py">params</a>) -> <a href="./src/oz_agent_sdk/types/agent/run_item.py">SyncRunsCursorPage[RunItem]</a></code>
4647
- <code title="post /agent/runs/{runId}/cancel">client.agent.runs.<a href="./src/oz_agent_sdk/resources/agent/runs.py">cancel</a>(run_id) -> str</code>
4748

4849
## Schedules

src/oz_agent_sdk/_base_client.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
)
6464
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
6565
from ._compat import PYDANTIC_V1, model_copy, model_dump
66-
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
66+
from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type
6767
from ._response import (
6868
APIResponse,
6969
BaseAPIResponse,
@@ -432,9 +432,27 @@ def _make_status_error(
432432
) -> _exceptions.APIStatusError:
433433
raise NotImplementedError()
434434

435+
def _auth_headers(
436+
self,
437+
security: SecurityOptions, # noqa: ARG002
438+
) -> dict[str, str]:
439+
return {}
440+
441+
def _auth_query(
442+
self,
443+
security: SecurityOptions, # noqa: ARG002
444+
) -> dict[str, str]:
445+
return {}
446+
447+
def _custom_auth(
448+
self,
449+
security: SecurityOptions, # noqa: ARG002
450+
) -> httpx.Auth | None:
451+
return None
452+
435453
def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers:
436454
custom_headers = options.headers or {}
437-
headers_dict = _merge_mappings(self.default_headers, custom_headers)
455+
headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers)
438456
self._validate_headers(headers_dict, custom_headers)
439457

440458
# headers are case-insensitive while dictionaries are not.
@@ -506,7 +524,7 @@ def _build_request(
506524
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
507525

508526
headers = self._build_headers(options, retries_taken=retries_taken)
509-
params = _merge_mappings(self.default_query, options.params)
527+
params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params)
510528
content_type = headers.get("Content-Type")
511529
files = options.files
512530

@@ -671,7 +689,6 @@ def default_headers(self) -> dict[str, str | Omit]:
671689
"Content-Type": "application/json",
672690
"User-Agent": self.user_agent,
673691
**self.platform_headers(),
674-
**self.auth_headers,
675692
**self._custom_headers,
676693
}
677694

@@ -990,8 +1007,9 @@ def request(
9901007
self._prepare_request(request)
9911008

9921009
kwargs: HttpxSendArgs = {}
993-
if self.custom_auth is not None:
994-
kwargs["auth"] = self.custom_auth
1010+
custom_auth = self._custom_auth(options.security)
1011+
if custom_auth is not None:
1012+
kwargs["auth"] = custom_auth
9951013

9961014
if options.follow_redirects is not None:
9971015
kwargs["follow_redirects"] = options.follow_redirects
@@ -1952,6 +1970,7 @@ def make_request_options(
19521970
idempotency_key: str | None = None,
19531971
timeout: float | httpx.Timeout | None | NotGiven = not_given,
19541972
post_parser: PostParser | NotGiven = not_given,
1973+
security: SecurityOptions | None = None,
19551974
) -> RequestOptions:
19561975
"""Create a dict of type RequestOptions without keys of NotGiven values."""
19571976
options: RequestOptions = {}
@@ -1977,6 +1996,9 @@ def make_request_options(
19771996
# internal
19781997
options["post_parser"] = post_parser # type: ignore
19791998

1999+
if security is not None:
2000+
options["security"] = security
2001+
19802002
return options
19812003

19822004

src/oz_agent_sdk/_client.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from ._utils import is_given, get_async_library
2323
from ._compat import cached_property
24+
from ._models import SecurityOptions
2425
from ._version import __version__
2526
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
2627
from ._exceptions import OzAPIError, APIStatusError
@@ -112,9 +113,14 @@ def with_streaming_response(self) -> OzAPIWithStreamedResponse:
112113
def qs(self) -> Querystring:
113114
return Querystring(array_format="repeat")
114115

115-
@property
116116
@override
117-
def auth_headers(self) -> dict[str, str]:
117+
def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
118+
return {
119+
**(self._bearer_auth if security.get("bearer_auth", False) else {}),
120+
}
121+
122+
@property
123+
def _bearer_auth(self) -> dict[str, str]:
118124
api_key = self.api_key
119125
return {"Authorization": f"Bearer {api_key}"}
120126

@@ -287,9 +293,14 @@ def with_streaming_response(self) -> AsyncOzAPIWithStreamedResponse:
287293
def qs(self) -> Querystring:
288294
return Querystring(array_format="repeat")
289295

290-
@property
291296
@override
292-
def auth_headers(self) -> dict[str, str]:
297+
def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
298+
return {
299+
**(self._bearer_auth if security.get("bearer_auth", False) else {}),
300+
}
301+
302+
@property
303+
def _bearer_auth(self) -> dict[str, str]:
293304
api_key = self.api_key
294305
return {"Authorization": f"Bearer {api_key}"}
295306

src/oz_agent_sdk/_models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,10 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
791791
return RootModel[type_] # type: ignore
792792

793793

794+
class SecurityOptions(TypedDict, total=False):
795+
bearer_auth: bool
796+
797+
794798
class FinalRequestOptionsInput(TypedDict, total=False):
795799
method: Required[str]
796800
url: Required[str]
@@ -804,6 +808,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
804808
json_data: Body
805809
extra_json: AnyMapping
806810
follow_redirects: bool
811+
security: SecurityOptions
807812

808813

809814
@final
@@ -818,6 +823,7 @@ class FinalRequestOptions(pydantic.BaseModel):
818823
idempotency_key: Union[str, None] = None
819824
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
820825
follow_redirects: Union[bool, None] = None
826+
security: SecurityOptions = {"bearer_auth": True}
821827

822828
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
823829
# It should be noted that we cannot use `json` here as that would override

src/oz_agent_sdk/_types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport
3737

3838
if TYPE_CHECKING:
39-
from ._models import BaseModel
39+
from ._models import BaseModel, SecurityOptions
4040
from ._response import APIResponse, AsyncAPIResponse
4141

4242
Transport = BaseTransport
@@ -121,6 +121,7 @@ class RequestOptions(TypedDict, total=False):
121121
extra_json: AnyMapping
122122
idempotency_key: str
123123
follow_redirects: bool
124+
security: SecurityOptions
124125

125126

126127
# Sentinel class used until PEP 0661 is accepted

src/oz_agent_sdk/pagination.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
from typing import List, Generic, TypeVar, Optional
4+
from typing_extensions import override
5+
6+
from ._models import BaseModel
7+
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
8+
9+
__all__ = ["RunsCursorPagePageInfo", "SyncRunsCursorPage", "AsyncRunsCursorPage"]
10+
11+
_T = TypeVar("_T")
12+
13+
14+
class RunsCursorPagePageInfo(BaseModel):
15+
has_next_page: Optional[bool] = None
16+
17+
next_cursor: Optional[str] = None
18+
19+
20+
class SyncRunsCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
21+
runs: List[_T]
22+
page_info: Optional[RunsCursorPagePageInfo] = None
23+
24+
@override
25+
def _get_page_items(self) -> List[_T]:
26+
runs = self.runs
27+
if not runs:
28+
return []
29+
return runs
30+
31+
@override
32+
def has_next_page(self) -> bool:
33+
has_next_page = None
34+
if self.page_info is not None:
35+
if self.page_info.has_next_page is not None:
36+
has_next_page = self.page_info.has_next_page
37+
if has_next_page is not None and has_next_page is False:
38+
return False
39+
40+
return super().has_next_page()
41+
42+
@override
43+
def next_page_info(self) -> Optional[PageInfo]:
44+
next_cursor = None
45+
if self.page_info is not None:
46+
if self.page_info.next_cursor is not None:
47+
next_cursor = self.page_info.next_cursor
48+
if not next_cursor:
49+
return None
50+
51+
return PageInfo(params={"cursor": next_cursor})
52+
53+
54+
class AsyncRunsCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
55+
runs: List[_T]
56+
page_info: Optional[RunsCursorPagePageInfo] = None
57+
58+
@override
59+
def _get_page_items(self) -> List[_T]:
60+
runs = self.runs
61+
if not runs:
62+
return []
63+
return runs
64+
65+
@override
66+
def has_next_page(self) -> bool:
67+
has_next_page = None
68+
if self.page_info is not None:
69+
if self.page_info.has_next_page is not None:
70+
has_next_page = self.page_info.has_next_page
71+
if has_next_page is not None and has_next_page is False:
72+
return False
73+
74+
return super().has_next_page()
75+
76+
@override
77+
def next_page_info(self) -> Optional[PageInfo]:
78+
next_cursor = None
79+
if self.page_info is not None:
80+
if self.page_info.next_cursor is not None:
81+
next_cursor = self.page_info.next_cursor
82+
if not next_cursor:
83+
return None
84+
85+
return PageInfo(params={"cursor": next_cursor})

0 commit comments

Comments
 (0)