Skip to content

Commit 37e4cc5

Browse files
committed
Handle plain-text async assembly responses
1 parent 85697ba commit 37e4cc5

3 files changed

Lines changed: 96 additions & 16 deletions

File tree

tests/test_async_client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,21 @@ async def test_async_client_normalizes_service_and_rejects_missing_ids(self):
357357
with self.assertRaises(RuntimeError):
358358
await closed_client.get_assembly(assembly_id="abc123")
359359

360+
async def test_async_client_close_reopens_owned_session(self):
361+
client = AsyncTransloadit("key", "secret", service=self.server.base_url)
362+
363+
first_session = await client.request._ensure_session()
364+
self.assertFalse(first_session.closed)
365+
366+
await client.close()
367+
self.assertTrue(first_session.closed)
368+
369+
second_session = await client.request._ensure_session()
370+
self.assertIsNot(first_session, second_session)
371+
self.assertFalse(second_session.closed)
372+
373+
await client.close()
374+
360375
async def test_async_client_delete_template_get_bill_and_plain_text_fallback(self):
361376
async with AsyncTransloadit("key", "secret", service=self.server.base_url) as client:
362377
response = await client.delete_template("tpl-1")
@@ -377,6 +392,59 @@ async def test_async_client_delete_template_get_bill_and_plain_text_fallback(sel
377392
self.assertEqual(response.status_code, 200)
378393
self.assertEqual(response.headers["X-Async-Route"], "get_assembly_plain")
379394

395+
async def test_async_assembly_create_returns_plain_text_response_without_crashing(self):
396+
plain_response = Response(
397+
data="plain assembly response",
398+
status_code=502,
399+
headers={"X-Async-Route": "plain"},
400+
)
401+
402+
async with AsyncTransloadit("key", "secret", service=self.server.base_url) as client:
403+
assembly = client.new_assembly()
404+
405+
with mock.patch.object(client.request, "post", new=mock.AsyncMock(return_value=plain_response)) as post_mock:
406+
with mock.patch.object(client, "get_assembly", new=mock.AsyncMock()) as get_mock:
407+
with mock.patch("asyncio.sleep", new_callable=mock.AsyncMock) as sleep_mock:
408+
response = await assembly.create(wait=True, resumable=False)
409+
410+
self.assertIs(response, plain_response)
411+
self.assertEqual(response.data, "plain assembly response")
412+
post_mock.assert_awaited_once()
413+
get_mock.assert_not_awaited()
414+
sleep_mock.assert_not_awaited()
415+
416+
async def test_async_assembly_wait_returns_plain_text_poll_response_without_crashing(self):
417+
initial_response = Response(
418+
data={
419+
"ok": "ASSEMBLY_PROCESSING",
420+
"info": {"retryIn": 0},
421+
"assembly_ssl_url": f"{self.server.base_url}/assemblies/assembly-123",
422+
},
423+
status_code=200,
424+
headers={"X-Async-Route": "initial"},
425+
)
426+
plain_response = Response(
427+
data="plain assembly response",
428+
status_code=502,
429+
headers={"X-Async-Route": "plain"},
430+
)
431+
432+
async with AsyncTransloadit("key", "secret", service=self.server.base_url) as client:
433+
assembly = client.new_assembly()
434+
435+
with mock.patch.object(client.request, "post", new=mock.AsyncMock(return_value=initial_response)) as post_mock:
436+
with mock.patch.object(client, "get_assembly", new=mock.AsyncMock(return_value=plain_response)) as get_mock:
437+
with mock.patch("asyncio.sleep", new_callable=mock.AsyncMock) as sleep_mock:
438+
response = await assembly.create(wait=True, resumable=False)
439+
440+
self.assertIs(response, plain_response)
441+
self.assertEqual(response.data, "plain assembly response")
442+
post_mock.assert_awaited_once()
443+
get_mock.assert_awaited_once_with(
444+
assembly_url=f"{self.server.base_url}/assemblies/assembly-123"
445+
)
446+
sleep_mock.assert_awaited_once_with(0)
447+
380448
def test_async_signed_smart_cdn_url_matches_sync_and_rejects_bad_types(self):
381449
async_client = AsyncTransloadit("test-key", "test-secret")
382450
sync_client = Transloadit("test-key", "test-secret")

transloadit/async_assembly.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,42 +95,53 @@ async def create(self, wait=False, resumable=True, retries=3):
9595
"/assemblies", data=data, files=self.files
9696
)
9797

98-
if self._rate_limit_reached(response):
98+
response_data = self._response_data(response)
99+
if response_data is None:
100+
return response
101+
102+
if self._rate_limit_reached(response_data):
99103
if retries:
100-
await asyncio.sleep(response.data.get("info", {}).get("retryIn", 1))
104+
await asyncio.sleep(response_data.get("info", {}).get("retryIn", 1))
101105
self._rewind_files(file_positions)
102106
return await self.create(wait, resumable, retries - 1)
103107
return response
104108

105109
if resumable:
106110
await self._do_tus_upload_async(
107-
response.data.get("assembly_ssl_url"),
108-
response.data.get("tus_url"),
111+
response_data.get("assembly_ssl_url"),
112+
response_data.get("tus_url"),
109113
retries,
110114
)
111115

112116
if wait:
113-
assembly_url = response.data.get("assembly_ssl_url")
114-
while not self._assembly_finished(response):
115-
sleep_time = response.data.get("info", {}).get("retryIn", 1)
117+
assembly_url = response_data.get("assembly_ssl_url")
118+
while not self._assembly_finished(response_data):
119+
sleep_time = response_data.get("info", {}).get("retryIn", 1)
116120
await asyncio.sleep(sleep_time)
117121
response = await self.transloadit.get_assembly(
118-
assembly_url=assembly_url or response.data.get("assembly_ssl_url")
122+
assembly_url=assembly_url or response_data.get("assembly_ssl_url")
119123
)
120-
assembly_url = response.data.get("assembly_ssl_url") or assembly_url
124+
response_data = self._response_data(response)
125+
if response_data is None:
126+
return response
127+
assembly_url = response_data.get("assembly_ssl_url") or assembly_url
121128

122129
return response
123130

124-
def _assembly_finished(self, response):
125-
status = response.data.get("ok")
131+
def _response_data(self, response):
132+
data = response.data
133+
return data if isinstance(data, dict) else None
134+
135+
def _assembly_finished(self, response_data):
136+
status = response_data.get("ok")
126137
is_aborted = status == "REQUEST_ABORTED"
127138
is_canceled = status == "ASSEMBLY_CANCELED"
128139
is_completed = status == "ASSEMBLY_COMPLETED"
129-
error = response.data.get("error")
140+
error = response_data.get("error")
130141
is_failed = error is not None
131142
is_fetch_rate_limit = error == "ASSEMBLY_STATUS_FETCHING_RATE_LIMIT_REACHED"
132143
is_submit_rate_limit = error == "RATE_LIMIT_REACHED"
133144
return is_aborted or is_canceled or is_completed or (is_failed and not (is_fetch_rate_limit or is_submit_rate_limit))
134145

135-
def _rate_limit_reached(self, response):
136-
return response.data.get("error") == "RATE_LIMIT_REACHED"
146+
def _rate_limit_reached(self, response_data):
147+
return response_data.get("error") == "RATE_LIMIT_REACHED"

transloadit/async_request.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ async def _ensure_session(self):
4444
return self._session
4545

4646
async def aclose(self):
47-
if self._session is not None and not self._session.closed and self._owns_session:
48-
await self._session.close()
47+
async with self._session_lock:
48+
if self._session is not None and not self._session.closed and self._owns_session:
49+
await self._session.close()
4950

5051
def _timeout(self, files=False):
5152
return aiohttp.ClientTimeout(

0 commit comments

Comments
 (0)