Skip to content

Commit 2d8d042

Browse files
author
BitsAdmin
committed
Merge branch 'feat/ark/respapi' into 'integration_2025-08-21_1037026207234'
feat: [development task] ark runtime (1576880) See merge request iaasng/volcengine-python-sdk!764
2 parents eb446c4 + d0c7216 commit 2d8d042

File tree

224 files changed

+6391
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

224 files changed

+6391
-37
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"httpx>=0.23.0, <1",
3030
"anyio>=3.5.0, <5",
3131
"cached-property; python_version < '3.8'",
32-
"cryptography>=42.0.0"
32+
"cryptography>=44.0.1"
3333
]
3434
},
3535
)

volcenginesdkarkruntime/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
2+
# Copyright (c) [2025] [OpenAI]
3+
# Copyright (c) [2025] [ByteDance Ltd. and/or its affiliates.]
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# This file has been modified by [ByteDance Ltd. and/or its affiliates.] on 2025.7
7+
#
8+
# Original file was released under Apache License Version 2.0, with the full license text
9+
# available at https://github.com/openai/openai-python/blob/main/LICENSE.
10+
#
11+
# This modified file is released under the same license.
12+
113
from ._client import Ark, AsyncArk
214
from ._utils import setup_logging as _setup_logging
315
from .common import pydantic_function_tool

volcenginesdkarkruntime/_base_client.py

Lines changed: 314 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
2+
# Copyright (c) [2025] [OpenAI]
3+
# Copyright (c) [2025] [ByteDance Ltd. and/or its affiliates.]
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# This file has been modified by [ByteDance Ltd. and/or its affiliates.] on 2025.7
7+
#
8+
# Original file was released under Apache License Version 2.0, with the full license text
9+
# available at https://github.com/openai/openai-python/blob/main/LICENSE.
10+
#
11+
# This modified file is released under the same license.
12+
113
from __future__ import annotations
214

315
import asyncio
@@ -16,11 +28,18 @@
1628
TYPE_CHECKING,
1729
Union,
1830
Generic,
31+
Iterable,
32+
AsyncIterator,
33+
Iterator,
34+
Generator
1935
)
36+
from typing_extensions import override
2037

2138
import anyio
2239
import httpx
2340
import pydantic
41+
42+
from pydantic import PrivateAttr
2443
from httpx import URL, Timeout, Limits
2544
from httpx._types import RequestFiles
2645

@@ -41,12 +60,23 @@
4160
ArkAPIStatusError,
4261
ArkAPIResponseValidationError,
4362
)
44-
from ._models import construct_type
63+
from ._models import construct_type, GenericModel
4564
from ._request_options import RequestOptions, ExtraRequestOptions
4665
from ._response import ArkAPIResponse, ArkAsyncAPIResponse
4766
from ._streaming import SSEDecoder, SSEBytesDecoder, Stream, AsyncStream
48-
from ._types import ResponseT, NotGiven, NOT_GIVEN, PostParser
49-
from ._utils._utils import _gen_request_id, is_given
67+
from ._types import (
68+
ResponseT,
69+
NotGiven,
70+
NOT_GIVEN,
71+
PostParser,
72+
Body,
73+
Query,
74+
)
75+
from ._utils._utils import _gen_request_id, is_given, is_mapping
76+
from ._compat import model_copy, PYDANTIC_V2
77+
78+
SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]")
79+
AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]")
5080

5181
_T = TypeVar("_T")
5282
_StreamT = TypeVar("_StreamT", bound=Stream[Any])
@@ -638,6 +668,36 @@ def post_without_retry(
638668
),
639669
)
640670

671+
def get_api_list(
672+
self,
673+
path: str,
674+
*,
675+
model: Type[object],
676+
page: Type[SyncPageT],
677+
body: Body | None = None,
678+
options: ExtraRequestOptions = {},
679+
method: str = "get",
680+
) -> AsyncPageT:
681+
opts = RequestOptions.construct(method=method, url=path, json_data=body, **options)
682+
return self._request_api_list(model, page, opts)
683+
684+
def _request_api_list(
685+
self,
686+
model: Type[object],
687+
page: Type[SyncPageT],
688+
options: RequestOptions,
689+
) -> AsyncPageT:
690+
def _parser(resp: AsyncPageT) -> SyncPageT:
691+
resp._set_private_attributes(
692+
client=self,
693+
model=model,
694+
options=options,
695+
)
696+
return resp
697+
698+
options.post_parser = _parser
699+
return self.request(page, options, stream=False)
700+
641701
def request(
642702
self,
643703
cast_to: Type[ResponseT],
@@ -820,6 +880,37 @@ async def post_without_retry(
820880
cast_to, opts, remaining_retries=0, stream=stream, stream_cls=stream_cls
821881
)
822882

883+
async def get_api_list(
884+
self,
885+
path: str,
886+
*,
887+
model: Type[object],
888+
page: Type[AsyncPageT],
889+
body: Body | None = None,
890+
options: ExtraRequestOptions = {},
891+
method: str = "get",
892+
) -> AsyncPageT:
893+
opts = RequestOptions.construct(method=method, url=path, json_data=body, **options)
894+
return await self._request_api_list(model, page, opts)
895+
896+
async def _request_api_list(
897+
self,
898+
model: Type[object],
899+
page: Type[AsyncPageT],
900+
options: RequestOptions,
901+
) -> AsyncPageT:
902+
def _parser(resp: AsyncPageT) -> SyncPageT:
903+
resp._set_private_attributes(
904+
client=self,
905+
model=model,
906+
options=options,
907+
)
908+
return resp
909+
910+
options.post_parser = _parser
911+
912+
return await self.request(page, options, stream=False)
913+
823914
async def request(
824915
self,
825916
cast_to: Type[ResponseT],
@@ -999,3 +1090,223 @@ async def __aexit__(
9991090
exc_tb: TracebackType | None,
10001091
) -> None:
10011092
await self.close()
1093+
1094+
1095+
class PageInfo:
1096+
"""Stores the necessary information to build the request to retrieve the next page.
1097+
1098+
Either `url` or `params` must be set.
1099+
"""
1100+
1101+
url: URL | NotGiven
1102+
params: Query | NotGiven
1103+
json: Body | NotGiven
1104+
1105+
def __init__(
1106+
self,
1107+
*,
1108+
url: URL | NotGiven = NOT_GIVEN,
1109+
json: Body | NotGiven = NOT_GIVEN,
1110+
params: Query | NotGiven = NOT_GIVEN,
1111+
) -> None:
1112+
self.url = url
1113+
self.json = json
1114+
self.params = params
1115+
1116+
@override
1117+
def __repr__(self) -> str:
1118+
if self.url:
1119+
return f"{self.__class__.__name__}(url={self.url})"
1120+
if self.json:
1121+
return f"{self.__class__.__name__}(json={self.json})"
1122+
return f"{self.__class__.__name__}(params={self.params})"
1123+
1124+
1125+
class BasePage(GenericModel, Generic[_T]):
1126+
"""
1127+
Defines the core interface for pagination.
1128+
1129+
Type Args:
1130+
ModelT: The pydantic model that represents an item in the response.
1131+
1132+
Methods:
1133+
has_next_page(): Check if there is another page available
1134+
next_page_info(): Get the necessary information to make a request for the next page
1135+
"""
1136+
1137+
_options: RequestOptions = PrivateAttr()
1138+
_model: Type[_T] = PrivateAttr()
1139+
1140+
def has_next_page(self) -> bool:
1141+
items = self._get_page_items()
1142+
if not items:
1143+
return False
1144+
return self.next_page_info() is not None
1145+
1146+
def next_page_info(self) -> Optional[PageInfo]: ...
1147+
1148+
def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body]
1149+
...
1150+
1151+
def _params_from_url(self, url: URL) -> httpx.QueryParams:
1152+
# TODO: do we have to preprocess params here?
1153+
return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params)
1154+
1155+
def _info_to_options(self, info: PageInfo) -> RequestOptions:
1156+
options = model_copy(self._options)
1157+
options._strip_raw_response_header()
1158+
1159+
if not isinstance(info.params, NotGiven):
1160+
options.params = {**options.params, **info.params}
1161+
return options
1162+
1163+
if not isinstance(info.url, NotGiven):
1164+
params = self._params_from_url(info.url)
1165+
url = info.url.copy_with(params=params)
1166+
options.params = dict(url.params)
1167+
options.url = str(url)
1168+
return options
1169+
1170+
if not isinstance(info.json, NotGiven):
1171+
if not is_mapping(info.json):
1172+
raise TypeError("Pagination is only supported with mappings")
1173+
1174+
if not options.json_data:
1175+
options.json_data = {**info.json}
1176+
else:
1177+
if not is_mapping(options.json_data):
1178+
raise TypeError("Pagination is only supported with mappings")
1179+
1180+
options.json_data = {**options.json_data, **info.json}
1181+
return options
1182+
1183+
raise ValueError("Unexpected PageInfo state")
1184+
1185+
1186+
class BaseSyncPage(BasePage[_T], Generic[_T]):
1187+
_client: SyncAPIClient = pydantic.PrivateAttr()
1188+
1189+
def _set_private_attributes(
1190+
self,
1191+
client: SyncAPIClient,
1192+
model: Type[_T],
1193+
options: RequestOptions,
1194+
) -> None:
1195+
if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
1196+
self.__pydantic_private__ = {}
1197+
1198+
self._model = model
1199+
self._client = client
1200+
self._options = options
1201+
1202+
# Pydantic uses a custom `__iter__` method to support casting BaseModels
1203+
# to dictionaries. e.g. dict(model).
1204+
# As we want to support `for item in page`, this is inherently incompatible
1205+
# with the default pydantic behaviour. It is not possible to support both
1206+
# use cases at once. Fortunately, this is not a big deal as all other pydantic
1207+
# methods should continue to work as expected as there is an alternative method
1208+
# to cast a model to a dictionary, model.dict(), which is used internally
1209+
# by pydantic.
1210+
def __iter__(self) -> Iterator[_T]: # type: ignore
1211+
for page in self.iter_pages():
1212+
for item in page._get_page_items():
1213+
yield item
1214+
1215+
def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]:
1216+
page = self
1217+
while True:
1218+
yield page
1219+
if page.has_next_page():
1220+
page = page.get_next_page()
1221+
else:
1222+
return
1223+
1224+
def get_next_page(self: SyncPageT) -> SyncPageT:
1225+
info = self.next_page_info()
1226+
if not info:
1227+
raise RuntimeError(
1228+
"No next page expected; please check `.has_next_page()` before calling `.get_next_page()`."
1229+
)
1230+
1231+
options = self._info_to_options(info)
1232+
return self._client._request_api_list(self._model, page=self.__class__, options=options)
1233+
1234+
1235+
class AsyncPaginator(Generic[_T, AsyncPageT]):
1236+
def __init__(
1237+
self,
1238+
client: AsyncAPIClient,
1239+
options: RequestOptions,
1240+
page_cls: Type[AsyncPageT],
1241+
model: Type[_T],
1242+
) -> None:
1243+
self._model = model
1244+
self._client = client
1245+
self._options = options
1246+
self._page_cls = page_cls
1247+
1248+
def __await__(self) -> Generator[Any, None, AsyncPageT]:
1249+
return self._get_page().__await__()
1250+
1251+
async def _get_page(self) -> AsyncPageT:
1252+
def _parser(resp: AsyncPageT) -> AsyncPageT:
1253+
resp._set_private_attributes(
1254+
model=self._model,
1255+
options=self._options,
1256+
client=self._client,
1257+
)
1258+
return resp
1259+
1260+
self._options.post_parser = _parser
1261+
1262+
return await self._client.request(self._page_cls, self._options)
1263+
1264+
async def __aiter__(self) -> AsyncIterator[_T]:
1265+
# https://github.com/microsoft/pyright/issues/3464
1266+
page = cast(
1267+
AsyncPageT,
1268+
await self, # type: ignore
1269+
)
1270+
async for item in page:
1271+
yield item
1272+
1273+
1274+
class BaseAsyncPage(BasePage[_T], Generic[_T]):
1275+
_client: AsyncAPIClient = pydantic.PrivateAttr()
1276+
1277+
def _set_private_attributes(
1278+
self,
1279+
model: Type[_T],
1280+
client: AsyncAPIClient,
1281+
options: RequestOptions,
1282+
) -> None:
1283+
if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
1284+
self.__pydantic_private__ = {}
1285+
1286+
self._model = model
1287+
self._client = client
1288+
self._options = options
1289+
1290+
async def __aiter__(self) -> AsyncIterator[_T]:
1291+
async for page in self.iter_pages():
1292+
for item in page._get_page_items():
1293+
yield item
1294+
1295+
async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]:
1296+
page = self
1297+
while True:
1298+
yield page
1299+
if page.has_next_page():
1300+
page = await page.get_next_page()
1301+
else:
1302+
return
1303+
1304+
async def get_next_page(self: AsyncPageT) -> AsyncPageT:
1305+
info = self.next_page_info()
1306+
if not info:
1307+
raise RuntimeError(
1308+
"No next page expected; please check `.has_next_page()` before calling `.get_next_page()`."
1309+
)
1310+
1311+
options = self._info_to_options(info)
1312+
return await self._client._request_api_list(self._model, page=self.__class__, options=options)

0 commit comments

Comments
 (0)