Skip to content

Commit

Permalink
add method for streaming an http request to a file, add Voice.downloa…
Browse files Browse the repository at this point in the history
…d_recording
  • Loading branch information
maxkahan committed Nov 22, 2024
1 parent bd4fcb0 commit da1f990
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 3 deletions.
17 changes: 17 additions & 0 deletions http_client/src/vonage_http_client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ def __init__(self, response: Response, content_type: str):
super().__init__(response, content_type)


class FileStreamingError(HttpRequestError):
"""Exception indicating an error occurred while streaming a file in a Vonage SDK
request.
Args:
response (requests.Response): The HTTP response object.
content_type (str): The response content type.
Attributes (inherited from HttpRequestError parent exception):
response (requests.Response): The HTTP response object.
message (str): The returned error message.
"""

def __init__(self, response: Response, content_type: str):
super().__init__(response, content_type)


class ServerError(HttpRequestError):
"""Exception indicating an error was returned by a Vonage server in response to a
Vonage SDK request.
Expand Down
31 changes: 31 additions & 0 deletions http_client/src/vonage_http_client/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from vonage_http_client.auth import Auth
from vonage_http_client.errors import (
AuthenticationError,
FileStreamingError,
ForbiddenError,
HttpRequestError,
InvalidHttpClientOptionsError,
Expand Down Expand Up @@ -250,6 +251,34 @@ def make_request(
with self._session.request(**request_params) as response:
return self._parse_response(response)

def download_file_stream(self, url: str, file_path: str) -> bytes:
"""Download a file from a URL and save it to a local file. This method streams the
file to disk.
Args:
url (str): The URL of the file to download.
file_path (str): The local path to save the file to.
Returns:
bytes: The content of the file.
"""
headers = {
'User-Agent': self.user_agent,
'Authorization': self.auth.create_jwt_auth_string(),
}

logger.debug(
f'Downloading file by streaming from {url} to local location: {file_path}, with headers: {self._headers}'
)
try:
with self._session.get(url, headers=headers, stream=True) as response:
with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=4096):
f.write(chunk)
except Exception as e:
logger.error(f'Error downloading file from {url}: {e}')
raise FileStreamingError(f'Error downloading file from {url}: {e}') from e

def append_to_user_agent(self, string: str):
"""Append a string to the User-Agent header.
Expand All @@ -267,6 +296,8 @@ def _parse_response(self, response: Response) -> Union[dict, None]:
try:
return response.json()
except JSONDecodeError:
if hasattr(response.headers, 'Content-Type'):
return response.content
return None
if response.status_code >= 400:
content_type = response.headers['Content-Type'].split(';', 1)[0]
Expand Down
Binary file added http_client/tests/data/file_stream.mp3
Binary file not shown.
44 changes: 43 additions & 1 deletion http_client/tests/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from vonage_http_client.auth import Auth
from vonage_http_client.errors import (
AuthenticationError,
FileStreamingError,
ForbiddenError,
HttpRequestError,
InvalidHttpClientOptionsError,
Expand All @@ -16,7 +17,7 @@
)
from vonage_http_client.http_client import HttpClient

from testutils import build_response
from testutils import build_response, get_mock_jwt_auth

path = abspath(__file__)

Expand Down Expand Up @@ -250,3 +251,44 @@ def test_append_to_user_agent():
client = HttpClient(Auth())
client.append_to_user_agent('TestAgent')
assert 'TestAgent' in client.user_agent


@responses.activate
def test_download_file_stream():
build_response(
path,
'GET',
'https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
'file_stream.mp3',
)

client = HttpClient(get_mock_jwt_auth())
client.download_file_stream(
url='https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
file_path='file.mp3',
)

with open('file.mp3', 'rb') as file:
file_content = file.read()
assert file_content.startswith(b'ID3')


@responses.activate
def test_download_file_stream_error():
build_response(
path,
'GET',
'https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
status_code=400,
)

client = HttpClient(get_mock_jwt_auth())
try:
client.download_file_stream(
url='https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
file_path='file.mp3',
)
except FileStreamingError as err:
assert '400 response from' in err.message
assert err.response.status_code == 400
assert err.response.json()['title'] == 'Bad Request'
1 change: 1 addition & 0 deletions sample-6s.mp3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><Error><Code>InvalidArgument</Code><Message></Message><RequestId>tx000006a4510e18f9bfaee-006740722a-12e0a6-fra</RequestId><HostId>12e0a6-fra-default</HostId></Error>
8 changes: 6 additions & 2 deletions testutils/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
def _load_mock_data(caller_file_path: str, mock_path: str):
"""Load mock data from a file."""

with open(join(dirname(caller_file_path), 'data', mock_path)) as file:
return file.read()
try:
with open(join(dirname(caller_file_path), 'data', mock_path)) as file:
return file.read()
except UnicodeDecodeError:
with open(join(dirname(caller_file_path), 'data', mock_path), 'rb') as file:
return file.read()


def _filter_none_values(data: dict) -> dict:
Expand Down
13 changes: 13 additions & 0 deletions voice/src/vonage_voice/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,16 @@ def play_dtmf_into_call(self, uuid: str, dtmf: Dtmf) -> CallMessage:
)

return CallMessage(**response)

@validate_call
def download_recording(self, url: str, file_path: str) -> bytes:
"""Downloads a call recording from the specified URL and saves it to a local file.
Args:
url (str): The URL of the recording to get.
file_path (str): The path to save the recording to.
Returns:
bytes: The recording data.
"""
self._http_client.download_file_stream(url=url, file_path=file_path)

0 comments on commit da1f990

Please sign in to comment.