From 59baf9cbd48f967fe0602dd21e273e1dceb7b740 Mon Sep 17 00:00:00 2001 From: Robert Milkowski Date: Tue, 7 Oct 2025 17:13:58 +0100 Subject: [PATCH] Fix videos timeout propagation --- src/openai/resources/videos.py | 12 +++ tests/api_resources/test_videos.py | 148 +++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/src/openai/resources/videos.py b/src/openai/resources/videos.py index 4df5f02004..9920fd02aa 100644 --- a/src/openai/resources/videos.py +++ b/src/openai/resources/videos.py @@ -155,6 +155,7 @@ def create_and_poll( return self.poll( video.id, poll_interval_ms=poll_interval_ms, + timeout=timeout, ) def poll( @@ -162,11 +163,15 @@ def poll( video_id: str, *, poll_interval_ms: int | Omit = omit, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Video: """Wait for the vector store file to finish processing. Note: this will return even if the file failed to process, you need to check file.last_error and file.status to handle these cases + + Args: + timeout: Override the client-level default timeout applied to each poll request, in seconds. """ headers: dict[str, str] = {"X-Stainless-Poll-Helper": "true"} if is_given(poll_interval_ms): @@ -176,6 +181,7 @@ def poll( response = self.with_raw_response.retrieve( video_id, extra_headers=headers, + timeout=timeout, ) video = response.parse() @@ -508,6 +514,7 @@ async def create_and_poll( return await self.poll( video.id, poll_interval_ms=poll_interval_ms, + timeout=timeout, ) async def poll( @@ -515,11 +522,15 @@ async def poll( video_id: str, *, poll_interval_ms: int | Omit = omit, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Video: """Wait for the vector store file to finish processing. Note: this will return even if the file failed to process, you need to check file.last_error and file.status to handle these cases + + Args: + timeout: Override the client-level default timeout applied to each poll request, in seconds. """ headers: dict[str, str] = {"X-Stainless-Poll-Helper": "true"} if is_given(poll_interval_ms): @@ -529,6 +540,7 @@ async def poll( response = await self.with_raw_response.retrieve( video_id, extra_headers=headers, + timeout=timeout, ) video = response.parse() diff --git a/tests/api_resources/test_videos.py b/tests/api_resources/test_videos.py index 623cfc2153..a9a20ffa84 100644 --- a/tests/api_resources/test_videos.py +++ b/tests/api_resources/test_videos.py @@ -24,6 +24,21 @@ base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +def _build_video(status: str, progress: int) -> Video: + return Video.model_validate( + { + "id": "video_123", + "created_at": 0, + "model": "sora-2", + "object": "video", + "progress": progress, + "seconds": "4", + "size": "720x1280", + "status": status, + } + ) + + class TestVideos: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -279,6 +294,70 @@ def test_path_params_remix(self, client: OpenAI) -> None: prompt="x", ) + @parametrize + def test_create_and_poll_respects_timeout(self, client: OpenAI, monkeypatch: pytest.MonkeyPatch) -> None: + captured_timeouts: list[object] = [] + create_kwargs: dict[str, object] = {} + create_video = _build_video("queued", 0) + poll_responses = [ + (_build_video("in_progress", 50), {"openai-poll-after-ms": "10"}), + (_build_video("completed", 100), {}), + ] + + def fake_create(**kwargs: object) -> Video: + create_kwargs.update(kwargs) + return create_video + + class FakeResponse: + def __init__(self, video: Video, headers: dict[str, str]) -> None: + self._video = video + self.headers = headers + + def parse(self) -> Video: + return self._video + + def fake_retrieve(video_id: str, *, extra_headers: object, timeout: object) -> FakeResponse: + video, headers = poll_responses.pop(0) + captured_timeouts.append(timeout) + return FakeResponse(video, headers) + + monkeypatch.setattr(client.videos, "create", fake_create) + monkeypatch.setattr(client.videos.with_raw_response, "retrieve", fake_retrieve) + monkeypatch.setattr(client.videos, "_sleep", lambda _: None) + + timeout_value = 123.0 + final_video = client.videos.create_and_poll(prompt="x", timeout=timeout_value) + + assert final_video.status == "completed" + assert captured_timeouts == [timeout_value, timeout_value] + assert poll_responses == [] + assert create_kwargs.get("timeout") == timeout_value + + @parametrize + def test_remix_passes_timeout(self, client: OpenAI, monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, object] = {} + + def fake_post( + path: str, + *, + body: object, + options: dict[str, object], + cast_to: object, + **kwargs: object, + ) -> Video: + captured["path"] = path + captured["timeout"] = options.get("timeout") + return _build_video("completed", 100) + + monkeypatch.setattr(client.videos, "_post", fake_post) + + timeout_value = 0.5 + video = client.videos.remix(video_id="video_123", prompt="x", timeout=timeout_value) + + assert video.status == "completed" + assert captured["path"] == "/videos/video_123/remix" + assert captured["timeout"] == timeout_value + class TestAsyncVideos: parametrize = pytest.mark.parametrize( @@ -539,6 +618,75 @@ async def test_path_params_remix(self, async_client: AsyncOpenAI) -> None: prompt="x", ) + @parametrize + async def test_create_and_poll_respects_timeout( + self, async_client: AsyncOpenAI, monkeypatch: pytest.MonkeyPatch + ) -> None: + captured_timeouts: list[object] = [] + create_kwargs: dict[str, object] = {} + create_video = _build_video("queued", 0) + poll_responses = [ + (_build_video("in_progress", 50), {"openai-poll-after-ms": "10"}), + (_build_video("completed", 100), {}), + ] + + async def fake_create(**kwargs: object) -> Video: + create_kwargs.update(kwargs) + return create_video + + class FakeResponse: + def __init__(self, video: Video, headers: dict[str, str]) -> None: + self._video = video + self.headers = headers + + def parse(self) -> Video: + return self._video + + async def fake_retrieve(video_id: str, *, extra_headers: object, timeout: object) -> FakeResponse: + video, headers = poll_responses.pop(0) + captured_timeouts.append(timeout) + return FakeResponse(video, headers) + + async def fake_sleep(_: float) -> None: + return None + + monkeypatch.setattr(async_client.videos, "create", fake_create) + monkeypatch.setattr(async_client.videos.with_raw_response, "retrieve", fake_retrieve) + monkeypatch.setattr(async_client.videos, "_sleep", fake_sleep) + + timeout_value = 123.0 + final_video = await async_client.videos.create_and_poll(prompt="x", timeout=timeout_value) + + assert final_video.status == "completed" + assert captured_timeouts == [timeout_value, timeout_value] + assert poll_responses == [] + assert create_kwargs.get("timeout") == timeout_value + + @parametrize + async def test_remix_passes_timeout(self, async_client: AsyncOpenAI, monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, object] = {} + + async def fake_post( + path: str, + *, + body: object, + options: dict[str, object], + cast_to: object, + **kwargs: object, + ) -> Video: + captured["path"] = path + captured["timeout"] = options.get("timeout") + return _build_video("completed", 100) + + monkeypatch.setattr(async_client.videos, "_post", fake_post) + + timeout_value = 0.5 + video = await async_client.videos.remix(video_id="video_123", prompt="x", timeout=timeout_value) + + assert video.status == "completed" + assert captured["path"] == "/videos/video_123/remix" + assert captured["timeout"] == timeout_value + @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) def test_create_and_poll_method_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None: