diff --git a/VERSION b/VERSION index 1d0ba9e..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.5.0 diff --git a/pyproject.toml b/pyproject.toml index 6ee7c2f..6d81038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "arkruntime" -version = "0.4.0" +version = "0.5.0" description = "Ark runtime SDK for Python" readme = "README.md" requires-python = ">=3.8" diff --git a/src/arkruntime/resources/files/files.py b/src/arkruntime/resources/files/files.py index 8ebf565..1cdd4f7 100644 --- a/src/arkruntime/resources/files/files.py +++ b/src/arkruntime/resources/files/files.py @@ -58,7 +58,7 @@ def with_streaming_response(self) -> "FilesWithStreamingResponse": def create( self, *, - file: FileTypes, + file: Optional[FileTypes] = None, # AUTOGEN-START create-kwargs purpose: Purpose, preprocess_configs: Optional[PreprocessConfigsParam] = None, @@ -84,9 +84,11 @@ def create( Mutually exclusive with `file`. tos: User-owned TOS bucket destination. """ + if (file is None) == (url is None): + raise ValueError("Exactly one of `file` or `url` must be provided") + body = deepcopy_minimal( { - "file": file, # AUTOGEN-START create-body "purpose": purpose, "preprocess_configs": preprocess_configs, @@ -96,6 +98,8 @@ def create( # AUTOGEN-END create-body } ) + if file is not None: + body["file"] = file files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( @@ -225,7 +229,7 @@ def with_streaming_response(self) -> "AsyncFilesWithStreamingResponse": async def create( self, *, - file: FileTypes, + file: Optional[FileTypes] = None, # AUTOGEN-START create-kwargs purpose: Purpose, preprocess_configs: Optional[PreprocessConfigsParam] = None, @@ -238,9 +242,11 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None = None, ) -> FileObject: + if (file is None) == (url is None): + raise ValueError("Exactly one of `file` or `url` must be provided") + body = deepcopy_minimal( { - "file": file, # AUTOGEN-START create-body "purpose": purpose, "preprocess_configs": preprocess_configs, @@ -250,6 +256,8 @@ async def create( # AUTOGEN-END create-body } ) + if file is not None: + body["file"] = file files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( diff --git a/src/arkruntime/types/agent/__init__.py b/src/arkruntime/types/agent/__init__.py index 92ea5cd..8a62c58 100644 --- a/src/arkruntime/types/agent/__init__.py +++ b/src/arkruntime/types/agent/__init__.py @@ -57,4 +57,4 @@ ] # Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py). -from ._init_extras_shim import * # noqa: F401,F403,E402 +from ._init_extras_shim import * # noqa: F401,F403 diff --git a/src/arkruntime/types/environment/__init__.py b/src/arkruntime/types/environment/__init__.py index 3988ba4..c0c9def 100644 --- a/src/arkruntime/types/environment/__init__.py +++ b/src/arkruntime/types/environment/__init__.py @@ -51,4 +51,4 @@ ] # Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py). -from ._init_extras_shim import * # noqa: F401,F403,E402 +from ._init_extras_shim import * # noqa: F401,F403 diff --git a/src/arkruntime/types/session/__init__.py b/src/arkruntime/types/session/__init__.py index a504f85..87f6f38 100644 --- a/src/arkruntime/types/session/__init__.py +++ b/src/arkruntime/types/session/__init__.py @@ -163,4 +163,4 @@ ] # Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py). -from ._init_extras_shim import * # noqa: F401,F403,E402 +from ._init_extras_shim import * # noqa: F401,F403 diff --git a/src/arkruntime/types/skill/__init__.py b/src/arkruntime/types/skill/__init__.py index 4088a7d..4312b39 100644 --- a/src/arkruntime/types/skill/__init__.py +++ b/src/arkruntime/types/skill/__init__.py @@ -15,4 +15,4 @@ ] # Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py). -from ._init_extras_shim import * # noqa: F401,F403,E402 +from ._init_extras_shim import * # noqa: F401,F403 diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 0000000..21edbec --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest + +from arkruntime.resources.files.files import AsyncFiles, Files + + +class _SyncClient: + def __init__(self) -> None: + self.request: dict[str, object] | None = None + + def post(self, *args: object, **kwargs: object) -> dict[str, object]: + self.request = {"args": args, **kwargs} + return {} + + def post_without_retry(self, *args: object, **kwargs: object) -> dict[str, object]: + return self.post(*args, **kwargs) + + def get(self, *args: object, **kwargs: object) -> dict[str, object]: + return {} + + def delete(self, *args: object, **kwargs: object) -> dict[str, object]: + return {} + + def get_api_list(self, *args: object, **kwargs: object) -> list[object]: + return [] + + +class _AsyncClient: + def __init__(self) -> None: + self.request: dict[str, object] | None = None + + async def post(self, *args: object, **kwargs: object) -> dict[str, object]: + self.request = {"args": args, **kwargs} + return {} + + async def post_without_retry(self, *args: object, **kwargs: object) -> dict[str, object]: + return await self.post(*args, **kwargs) + + async def get(self, *args: object, **kwargs: object) -> dict[str, object]: + return {} + + async def delete(self, *args: object, **kwargs: object) -> dict[str, object]: + return {} + + async def get_api_list(self, *args: object, **kwargs: object) -> list[object]: + return [] + + +def test_create_from_url_does_not_treat_url_as_local_file() -> None: + client = _SyncClient() + Files(client).create(purpose="user_data", url="https://example.com/file.pdf") + + assert client.request is not None + assert client.request["files"] == [] + assert client.request["body"] == { + "purpose": "user_data", + "url": "https://example.com/file.pdf", + } + + +@pytest.mark.asyncio +async def test_async_create_from_url_does_not_treat_url_as_local_file() -> None: + client = _AsyncClient() + await AsyncFiles(client).create(purpose="user_data", url="https://example.com/file.pdf") + + assert client.request is not None + assert client.request["files"] == [] + assert client.request["body"] == { + "purpose": "user_data", + "url": "https://example.com/file.pdf", + } + + +@pytest.mark.parametrize("kwargs", [{}, {"file": b"data", "url": "https://example.com/file.pdf"}]) +def test_create_requires_exactly_one_file_source(kwargs: dict[str, object]) -> None: + with pytest.raises(ValueError, match="Exactly one"): + Files(_SyncClient()).create(purpose="user_data", **kwargs) diff --git a/uv.lock b/uv.lock index b06e48b..7377dd6 100644 --- a/uv.lock +++ b/uv.lock @@ -73,7 +73,7 @@ wheels = [ [[package]] name = "arkruntime" -version = "0.4.0" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },