Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assemblyai/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.9.0"
__version__ = "1.9.1"
4 changes: 3 additions & 1 deletion assemblyai/prerecorded/v2/async_transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ async def save_redacted_audio(self, filepath: str) -> None:
# The redacted audio lives behind a pre-signed URL, so it is fetched
# with a bare client rather than the API-authenticated one.
async with httpx.AsyncClient() as client:
async with client.stream(method="GET", url=url) as response:
async with client.stream(
method="GET", url=url, follow_redirects=True
) as response:
if response.status_code not in (
httpx.codes.OK,
httpx.codes.NOT_MODIFIED,
Expand Down
4 changes: 3 additions & 1 deletion assemblyai/prerecorded/v2/transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ def save_redacted_audio(self, filepath: str):
Args:
filepath: The path to save the redacted audio file to.
"""
with httpx.stream(method="GET", url=self.get_redacted_audio_url()) as response:
with httpx.stream(
method="GET", url=self.get_redacted_audio_url(), follow_redirects=True
) as response:
if response.status_code not in (httpx.codes.OK, httpx.codes.NOT_MODIFIED):
raise types.RedactedAudioUnavailableError(
f"Fetching redacted audio failed with status code {response.status_code}",
Expand Down
63 changes: 63 additions & 0 deletions tests/unit/test_redacted_audio_redirects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Signed redacted-audio downloads may redirect to a different host."""

import pytest

import assemblyai as aai
from tests.unit import factories


def prepare(httpx_mock, status):
data = factories.generate_dict_factory(
factories.TranscriptCompletedResponseFactory
)()
data.update(
redact_pii=True, redact_pii_audio=True, redact_pii_policies=["person_name"]
)
response = aai.types.TranscriptResponse(**data)
settings = aai.Settings(api_key="test")
httpx_mock.add_response(
url=f"{settings.base_url}/v2/transcript/{response.id}/redacted-audio",
json={
"status": "redacted_audio_ready",
"redacted_audio_url": "https://audio.example/redacted.wav",
},
)
httpx_mock.add_response(
url="https://audio.example/redacted.wav",
status_code=status,
headers={"location": "https://cdn.example/redacted.wav"},
)
httpx_mock.add_response(
url="https://cdn.example/redacted.wav", content=b"redacted audio"
)
return response, settings


@pytest.mark.parametrize("status", [301, 302, 307, 308])
def test_save_redacted_audio_follows_redirect(httpx_mock, tmp_path, status):
response, settings = prepare(httpx_mock, status)
transcript = aai.Transcript.from_response(
client=aai.Client(settings=settings), response=response
)
target = tmp_path / "redacted.wav"
transcript.save_redacted_audio(str(target))
assert target.read_bytes() == b"redacted audio"
assert all(
"authorization" not in request.headers
for request in httpx_mock.get_requests()[1:]
)


@pytest.mark.asyncio
@pytest.mark.parametrize("status", [301, 302, 307, 308])
async def test_async_save_redacted_audio_follows_redirect(httpx_mock, tmp_path, status):
response, settings = prepare(httpx_mock, status)
async with aai.AsyncClient(settings=settings) as client:
transcript = aai.AsyncTranscript.from_response(client=client, response=response)
target = tmp_path / "redacted.wav"
await transcript.save_redacted_audio(str(target))
assert target.read_bytes() == b"redacted audio"
assert all(
"authorization" not in request.headers
for request in httpx_mock.get_requests()[1:]
)