From e9a4b20ff443478236a223c3f49e4f9993d4f515 Mon Sep 17 00:00:00 2001 From: Apeksha_2022 <70309060+Apeksha9389@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:37:34 +0530 Subject: [PATCH] fix: bound transcript polling sleeps by the remaining timeout --- assemblyai/__version__.py | 2 +- assemblyai/prerecorded/v2/async_transcript.py | 20 +++--- assemblyai/prerecorded/v2/transcript.py | 20 +++--- tests/unit/test_poll_sleep_budget.py | 72 +++++++++++++++++++ 4 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_poll_sleep_budget.py diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index 320141d8..a44132de 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.8.2" +__version__ = "1.8.3" diff --git a/assemblyai/prerecorded/v2/async_transcript.py b/assemblyai/prerecorded/v2/async_transcript.py index f9a1401f..933df1de 100644 --- a/assemblyai/prerecorded/v2/async_transcript.py +++ b/assemblyai/prerecorded/v2/async_transcript.py @@ -115,16 +115,20 @@ async def wait_for_completion( if self._transcript.status in TERMINAL_STATUSES: return self - if poll_timeout is not None and time.monotonic() - start >= poll_timeout: - raise types.TranscriptError( - _poll_timeout_message( - transcript_id=transcript_id, - status=self._transcript.status, - poll_timeout=poll_timeout, + interval = self._client.settings.polling_interval + if poll_timeout is not None: + remaining = poll_timeout - (time.monotonic() - start) + if remaining <= 0: + raise types.TranscriptError( + _poll_timeout_message( + transcript_id=transcript_id, + status=self._transcript.status, + poll_timeout=poll_timeout, + ) ) - ) + interval = min(interval, remaining) - await asyncio.sleep(self._client.settings.polling_interval) + await asyncio.sleep(interval) async def export_subtitles_srt( self, diff --git a/assemblyai/prerecorded/v2/transcript.py b/assemblyai/prerecorded/v2/transcript.py index 406942c3..018b03bf 100644 --- a/assemblyai/prerecorded/v2/transcript.py +++ b/assemblyai/prerecorded/v2/transcript.py @@ -83,16 +83,20 @@ def wait_for_completion(self, *, poll_timeout: Optional[float] = None) -> Self: if self.transcript.status in TERMINAL_STATUSES: break - if poll_timeout is not None and time.monotonic() - start >= poll_timeout: - raise types.TranscriptError( - _poll_timeout_message( - transcript_id=self.transcript_id, - status=self.transcript.status, - poll_timeout=poll_timeout, + interval = self._client.settings.polling_interval + if poll_timeout is not None: + remaining = poll_timeout - (time.monotonic() - start) + if remaining <= 0: + raise types.TranscriptError( + _poll_timeout_message( + transcript_id=self.transcript_id, + status=self.transcript.status, + poll_timeout=poll_timeout, + ) ) - ) + interval = min(interval, remaining) - time.sleep(self._client.settings.polling_interval) + time.sleep(interval) return self diff --git a/tests/unit/test_poll_sleep_budget.py b/tests/unit/test_poll_sleep_budget.py new file mode 100644 index 00000000..da31a527 --- /dev/null +++ b/tests/unit/test_poll_sleep_budget.py @@ -0,0 +1,72 @@ +"""Polling delays must not exceed the remaining timeout budget.""" + +from types import SimpleNamespace + +import pytest +from pytest_httpx import HTTPXMock + +import assemblyai as aai +from assemblyai.prerecorded.v2 import async_transcript, transcript +from tests.unit import factories + + +class Clock: + def __init__(self): + self.now = 0.0 + + def monotonic(self): + return self.now + + def sleep(self, seconds): + self.now += seconds + + async def async_sleep(self, seconds): + self.sleep(seconds) + + +def response_and_settings(): + response = factories.generate_dict_factory( + factories.TranscriptProcessingResponseFactory + )() + settings = aai.Settings(api_key="test", polling_interval=2.0) + return aai.types.TranscriptResponse(**response), settings + + +@pytest.mark.httpx_mock(can_send_already_matched_responses=True) +def test_poll_sleep_respects_remaining_timeout(httpx_mock: HTTPXMock, monkeypatch): + response, settings = response_and_settings() + httpx_mock.add_response( + url=f"{settings.base_url}/v2/transcript/{response.id}", json=response.dict() + ) + clock = Clock() + monkeypatch.setattr(transcript, "time", clock) + result = aai.Transcript.from_response( + client=aai.Client(settings=settings), response=response + ) + + with pytest.raises(aai.TranscriptError, match="did not finish"): + result.wait_for_completion(poll_timeout=0.1) + + assert clock.now == pytest.approx(0.1) + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(can_send_already_matched_responses=True) +async def test_async_poll_sleep_respects_remaining_timeout( + httpx_mock: HTTPXMock, monkeypatch +): + response, settings = response_and_settings() + httpx_mock.add_response( + url=f"{settings.base_url}/v2/transcript/{response.id}", json=response.dict() + ) + clock = Clock() + monkeypatch.setattr(async_transcript, "time", clock) + monkeypatch.setattr( + async_transcript, "asyncio", SimpleNamespace(sleep=clock.async_sleep) + ) + async with aai.AsyncClient(settings=settings) as client: + result = aai.AsyncTranscript.from_response(client=client, response=response) + with pytest.raises(aai.TranscriptError, match="did not finish"): + await result.wait_for_completion(poll_timeout=0.1) + + assert clock.now == pytest.approx(0.1)