Skip to content

Commit da1f990

Browse files
committed
add method for streaming an http request to a file, add Voice.download_recording
1 parent bd4fcb0 commit da1f990

File tree

7 files changed

+111
-3
lines changed

7 files changed

+111
-3
lines changed

http_client/src/vonage_http_client/errors.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,23 @@ def __init__(self, response: Response, content_type: str):
122122
super().__init__(response, content_type)
123123

124124

125+
class FileStreamingError(HttpRequestError):
126+
"""Exception indicating an error occurred while streaming a file in a Vonage SDK
127+
request.
128+
129+
Args:
130+
response (requests.Response): The HTTP response object.
131+
content_type (str): The response content type.
132+
133+
Attributes (inherited from HttpRequestError parent exception):
134+
response (requests.Response): The HTTP response object.
135+
message (str): The returned error message.
136+
"""
137+
138+
def __init__(self, response: Response, content_type: str):
139+
super().__init__(response, content_type)
140+
141+
125142
class ServerError(HttpRequestError):
126143
"""Exception indicating an error was returned by a Vonage server in response to a
127144
Vonage SDK request.

http_client/src/vonage_http_client/http_client.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from vonage_http_client.auth import Auth
1111
from vonage_http_client.errors import (
1212
AuthenticationError,
13+
FileStreamingError,
1314
ForbiddenError,
1415
HttpRequestError,
1516
InvalidHttpClientOptionsError,
@@ -250,6 +251,34 @@ def make_request(
250251
with self._session.request(**request_params) as response:
251252
return self._parse_response(response)
252253

254+
def download_file_stream(self, url: str, file_path: str) -> bytes:
255+
"""Download a file from a URL and save it to a local file. This method streams the
256+
file to disk.
257+
258+
Args:
259+
url (str): The URL of the file to download.
260+
file_path (str): The local path to save the file to.
261+
262+
Returns:
263+
bytes: The content of the file.
264+
"""
265+
headers = {
266+
'User-Agent': self.user_agent,
267+
'Authorization': self.auth.create_jwt_auth_string(),
268+
}
269+
270+
logger.debug(
271+
f'Downloading file by streaming from {url} to local location: {file_path}, with headers: {self._headers}'
272+
)
273+
try:
274+
with self._session.get(url, headers=headers, stream=True) as response:
275+
with open(file_path, 'wb') as f:
276+
for chunk in response.iter_content(chunk_size=4096):
277+
f.write(chunk)
278+
except Exception as e:
279+
logger.error(f'Error downloading file from {url}: {e}')
280+
raise FileStreamingError(f'Error downloading file from {url}: {e}') from e
281+
253282
def append_to_user_agent(self, string: str):
254283
"""Append a string to the User-Agent header.
255284
@@ -267,6 +296,8 @@ def _parse_response(self, response: Response) -> Union[dict, None]:
267296
try:
268297
return response.json()
269298
except JSONDecodeError:
299+
if hasattr(response.headers, 'Content-Type'):
300+
return response.content
270301
return None
271302
if response.status_code >= 400:
272303
content_type = response.headers['Content-Type'].split(';', 1)[0]
17.4 KB
Binary file not shown.

http_client/tests/test_http_client.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from vonage_http_client.auth import Auth
99
from vonage_http_client.errors import (
1010
AuthenticationError,
11+
FileStreamingError,
1112
ForbiddenError,
1213
HttpRequestError,
1314
InvalidHttpClientOptionsError,
@@ -16,7 +17,7 @@
1617
)
1718
from vonage_http_client.http_client import HttpClient
1819

19-
from testutils import build_response
20+
from testutils import build_response, get_mock_jwt_auth
2021

2122
path = abspath(__file__)
2223

@@ -250,3 +251,44 @@ def test_append_to_user_agent():
250251
client = HttpClient(Auth())
251252
client.append_to_user_agent('TestAgent')
252253
assert 'TestAgent' in client.user_agent
254+
255+
256+
@responses.activate
257+
def test_download_file_stream():
258+
build_response(
259+
path,
260+
'GET',
261+
'https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
262+
'file_stream.mp3',
263+
)
264+
265+
client = HttpClient(get_mock_jwt_auth())
266+
client.download_file_stream(
267+
url='https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
268+
file_path='file.mp3',
269+
)
270+
271+
with open('file.mp3', 'rb') as file:
272+
file_content = file.read()
273+
assert file_content.startswith(b'ID3')
274+
275+
276+
@responses.activate
277+
def test_download_file_stream_error():
278+
build_response(
279+
path,
280+
'GET',
281+
'https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
282+
status_code=400,
283+
)
284+
285+
client = HttpClient(get_mock_jwt_auth())
286+
try:
287+
client.download_file_stream(
288+
url='https://api.nexmo.com/v1/files/aaaaaaaa-bbbb-cccc-dddd-0123456789ab',
289+
file_path='file.mp3',
290+
)
291+
except FileStreamingError as err:
292+
assert '400 response from' in err.message
293+
assert err.response.status_code == 400
294+
assert err.response.json()['title'] == 'Bad Request'

sample-6s.mp3

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>

testutils/testutils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
def _load_mock_data(caller_file_path: str, mock_path: str):
99
"""Load mock data from a file."""
1010

11-
with open(join(dirname(caller_file_path), 'data', mock_path)) as file:
12-
return file.read()
11+
try:
12+
with open(join(dirname(caller_file_path), 'data', mock_path)) as file:
13+
return file.read()
14+
except UnicodeDecodeError:
15+
with open(join(dirname(caller_file_path), 'data', mock_path), 'rb') as file:
16+
return file.read()
1317

1418

1519
def _filter_none_values(data: dict) -> dict:

voice/src/vonage_voice/voice.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,16 @@ def play_dtmf_into_call(self, uuid: str, dtmf: Dtmf) -> CallMessage:
261261
)
262262

263263
return CallMessage(**response)
264+
265+
@validate_call
266+
def download_recording(self, url: str, file_path: str) -> bytes:
267+
"""Downloads a call recording from the specified URL and saves it to a local file.
268+
269+
Args:
270+
url (str): The URL of the recording to get.
271+
file_path (str): The path to save the recording to.
272+
273+
Returns:
274+
bytes: The recording data.
275+
"""
276+
self._http_client.download_file_stream(url=url, file_path=file_path)

0 commit comments

Comments
 (0)