diff --git a/.travis.yml b/.travis.yml index 6869f26..5c9e0e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,14 @@ python: install: - pip install mock==2.0 - pip install requests + - pip install codecov + - pip install pytest pytest-cov - pip install . script: - - python -m unittest discover -s tests/ -p test_*.py -v + - coverage run --source=messagebird -m unittest discover -s tests/ + - coverage report --fail-under=80 matrix: allow_failures: - python: 'nightly' - python: 'pypy2.7' + - python: 'pypy3.5' diff --git a/examples/voice_recording_download.py b/examples/voice_recording_download.py new file mode 100644 index 0000000..770cac4 --- /dev/null +++ b/examples/voice_recording_download.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +import messagebird +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--accessKey', help='access key for MessageBird API', type=str, required=True) +parser.add_argument('--callID', help='identifier for the call', type=str, required=True) +parser.add_argument('--legID', help='identifier for the leg object you wish to list the recordings for', type=str, required=True) +parser.add_argument('--recordingID', help='identifier for the recording', type=str, required=True) +args = vars(parser.parse_args()) + +try: + client = messagebird.Client(args['accessKey']) + + voiceRecordingLocation = client.voice_recording_download(args['callID'], args['legID'], args['recordingID']) + + # Print the object information. + print('The following information was returned as a Voice Recording object:') + print(voiceRecordingLocation) + +except messagebird.client.ErrorException as e: + print('An error occured while requesting a Message object:') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s\n' % error.parameter) diff --git a/examples/voice_recording_view.py b/examples/voice_recording_view.py new file mode 100644 index 0000000..9551f0b --- /dev/null +++ b/examples/voice_recording_view.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +import messagebird +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--accessKey', help='access key for MessageBird API', type=str, required=True) +parser.add_argument('--callID', help='identifier for the call', type=str, required=True) +parser.add_argument('--legID', help='identifier for the leg object you wish to list the recordings for', type=str, required=True) +parser.add_argument('--recordingID', help='identifier for the recording', type=str, required=True) +args = vars(parser.parse_args()) + +try: + client = messagebird.Client(args['accessKey']) + + voiceRecording = client.voice_recording_view(args['callID'], args['legID'], args['recordingID']) + + # Print the object information. + print('The following information was returned as a Voice Recording object:') + print(voiceRecording) + +except messagebird.client.ErrorException as e: + print('An error occured while requesting a Message object:') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s\n' % error.parameter) diff --git a/examples/voice_recordings_list.py b/examples/voice_recordings_list.py new file mode 100644 index 0000000..985dba0 --- /dev/null +++ b/examples/voice_recordings_list.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +import messagebird +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--accessKey', help='access key for MessageBird API', type=str, required=True) +parser.add_argument('--callID', help='identifier for the call', type=str, required=True) +parser.add_argument('--legID', help='identifier for the leg object you wish to list the recordings for', type=str, required=True) +args = vars(parser.parse_args()) + +try: + client = messagebird.Client(args['accessKey']) + + voiceRecordingList = client.voice_recording_list_recordings(args['callID'], args['legID']) + + # Print the object information. + print('The following information was returned as a Voice Recordings List object:') + print(voiceRecordingList) + +except messagebird.client.ErrorException as e: + print('An error occured while requesting a Message object:') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s\n' % error.parameter) diff --git a/messagebird/client.py b/messagebird/client.py index e5eb805..11b4d66 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -1,5 +1,6 @@ import sys import json +import io from messagebird.balance import Balance from messagebird.contact import Contact, ContactList @@ -11,10 +12,11 @@ from messagebird.voicemessage import VoiceMessage from messagebird.lookup import Lookup from messagebird.verify import Verify -from messagebird.http_client import HttpClient +from messagebird.http_client import HttpClient, ResponseFormat from messagebird.conversation_message import ConversationMessage, ConversationMessageList from messagebird.conversation import Conversation, ConversationList from messagebird.conversation_webhook import ConversationWebhook, ConversationWebhookList +from messagebird.voice_recording import VoiceRecordingsList, VoiceRecording ENDPOINT = 'https://rest.messagebird.com' CLIENT_VERSION = '1.4.1' @@ -28,6 +30,11 @@ CONVERSATION_WEB_HOOKS_PATH = 'webhooks' CONVERSATION_TYPE = 'conversation' +VOICE_API_ROOT = 'https://voice.messagebird.com' +VOICE_PATH = 'calls' +VOICE_LEGS_PATH = 'legs' +VOICE_RECORDINGS_PATH = 'recordings' + class ErrorException(Exception): def __init__(self, errors): @@ -53,7 +60,6 @@ def _get_http_client(self, type=REST_TYPE): def request(self, path, method='GET', params=None, type=REST_TYPE): """Builds a request, gets a response and decodes it.""" response_text = self._get_http_client(type).request(path, method, params) - if not response_text: return response_text @@ -82,6 +88,18 @@ def request_plain_text(self, path, method='GET', params=None, type=REST_TYPE): return response_text + def request_store_as_file(self, path, filepath, method='GET', params=None, type=REST_TYPE): + """Builds a request, gets a response and decodes it.""" + response_binary = self._get_http_client(type).request(path, method, params, ResponseFormat.binary) + + if not response_binary: + return response_binary + + with io.open(filepath, 'wb') as f: + f.write(response_binary) + + return filepath + def balance(self): """Retrieve your balance.""" return Balance().load(self.request('balance')) @@ -296,5 +314,27 @@ def conversation_read_webhook(self, id): uri = CONVERSATION_WEB_HOOKS_PATH + '/' + str(id) return ConversationWebhook().load(self.request(uri, 'GET', None, CONVERSATION_TYPE)) + def voice_recording_list_recordings(self, call_id, leg_id): + uri = VOICE_API_ROOT + '/' + VOICE_PATH + '/' + str(call_id) + '/' + VOICE_LEGS_PATH + '/' + str(leg_id) + '/' + VOICE_RECORDINGS_PATH + return VoiceRecordingsList().load(self.request(uri, 'GET')) + + def voice_recording_view(self, call_id, leg_id, recording_id): + uri = VOICE_API_ROOT + '/' + VOICE_PATH + '/' + str(call_id) + '/' + VOICE_LEGS_PATH + '/' + str(leg_id) + '/' + VOICE_RECORDINGS_PATH + '/' + str(recording_id) + recording_response = self.request(uri, 'GET') + recording_links = recording_response.get('_links') + if recording_links is not None: + recording_response['data'][0]['_links'] = recording_links + return VoiceRecording().load(recording_response['data'][0]) + + def voice_recording_download(self, call_id, leg_id, recording_id): + uri = VOICE_API_ROOT + '/' + VOICE_PATH + '/' + str(call_id) + '/' + VOICE_LEGS_PATH + '/' + str(leg_id) + '/' + VOICE_RECORDINGS_PATH + '/' + str(recording_id) + recording_response = self.request(uri, 'GET') + recording_links = recording_response.get('_links') + if recording_links is None or recording_links.get('file') is None: + raise (ErrorException('There is no recording available')) + recording_file = recording_links.get('file') + recording_file = self.request_store_as_file(VOICE_API_ROOT + recording_file, recording_id + '.wav') + return VOICE_API_ROOT + recording_file + def _format_query(self, limit, offset): return 'limit=' + str(limit) + '&offset=' + str(offset) diff --git a/messagebird/http_client.py b/messagebird/http_client.py index 72123c3..f18f1b0 100644 --- a/messagebird/http_client.py +++ b/messagebird/http_client.py @@ -1,11 +1,16 @@ import json import requests +from enum import Enum try: from urllib.parse import urljoin except ImportError: from urlparse import urljoin +class ResponseFormat(Enum): + text = 1 + binary = 2 + class HttpClient(object): """Used for sending simple HTTP requests.""" @@ -17,7 +22,7 @@ def __init__(self, endpoint, access_key, user_agent): self.access_key = access_key self.user_agent = user_agent - def request(self, path, method='GET', params=None): + def request(self, path, method='GET', params=None, format=ResponseFormat.text): """Builds a request and gets a response.""" if params is None: params = {} url = urljoin(self.endpoint, path) @@ -29,22 +34,22 @@ def request(self, path, method='GET', params=None): 'Content-Type': 'application/json' } - if method == 'DELETE': - response = requests.delete(url, verify=True, headers=headers, data=json.dumps(params)) - elif method == 'GET': - response = requests.get(url, verify=True, headers=headers, params=params) - elif method == 'PATCH': - response = requests.patch(url, verify=True, headers=headers, data=json.dumps(params)) - elif method == 'POST': - response = requests.post(url, verify=True, headers=headers, data=json.dumps(params)) - elif method == 'PUT': - response = requests.put(url, verify=True, headers=headers, data=json.dumps(params)) - else: - raise ValueError(str(method) + ' is not a supported HTTP method') - - if response.status_code in self.__supported_status_codes: - response_text = response.text - else: + method_switcher = { + 'DELETE': requests.delete(url, verify=True, headers=headers, data=json.dumps(params)), + 'GET': requests.get(url, verify=True, headers=headers, params=params), + 'PATCH': requests.patch(url, verify=True, headers=headers, data=json.dumps(params)), + 'POST': requests.post(url, verify=True, headers=headers, data=json.dumps(params)), + 'PUT': requests.put(url, verify=True, headers=headers, data=json.dumps(params)) + } + response = method_switcher.get(method, str(method) + ' is not a supported HTTP method') + if isinstance(response, str): + raise ValueError(response) + + if response.status_code not in self.__supported_status_codes: response.raise_for_status() - return response_text + response_switcher = { + ResponseFormat.text: response.text, + ResponseFormat.binary: response.content + } + return response_switcher.get(format) diff --git a/messagebird/voice_recording.py b/messagebird/voice_recording.py new file mode 100644 index 0000000..a1c228e --- /dev/null +++ b/messagebird/voice_recording.py @@ -0,0 +1,71 @@ +from messagebird.base import Base + +class VoiceRecording(Base): + + def __init__(self): + self.id = None + self.format = None + self.type = None + self.legId = None + self.status = None + self.duration = None + self._createdDatetime = None + self._updatedDatetime = None + self._links = None + + @property + def createdDatetime(self): + return self._createdDatetime + + @createdDatetime.setter + def createdAt(self, value): + if value is not None: + self._createdDatetime = self.value_to_time(value, '%Y-%m-%dT%H:%M:%SZ') + + @property + def updatedDatetime(self): + return self._updatedDatetime + + @updatedDatetime.setter + def updatedAt(self, value): + if value is not None: + self._updatedDatetime = self.value_to_time(value, '%Y-%m-%dT%H:%M:%SZ') + + def __str__(self): + return "\n".join([ + 'recording id : %s' % self.id, + 'format : %s' % self.format, + 'type : %s' % self.type, + 'leg id : %s' % self.legId, + 'status : %s' % self.status, + 'duration : %s' % self.duration, + 'created date time : %s' % self._createdDatetime, + 'updated date time : %s' % self._updatedDatetime, + 'links : %s' % self._links + ]) + +class VoiceRecordingsList(Base): + def __init__(self): + self._items = None + + @property + def data(self): + return self._items + + @data.setter + def data(self, value): + if isinstance(value, list): + self._items = [] + for item in value: + self._items.append(VoiceRecording().load(item)) + + def __str__(self): + item_ids = [] + if self._items is not None: + for recording_item in self._items: + item_ids.append(recording_item.id) + + return "\n".join([ + 'items IDs : %s' % item_ids, + 'count : %s' % len(item_ids) + ]) diff --git a/tests/test_voice_recording.py b/tests/test_voice_recording.py new file mode 100644 index 0000000..ae7f125 --- /dev/null +++ b/tests/test_voice_recording.py @@ -0,0 +1,70 @@ +import unittest +from messagebird import Client, ErrorException +from datetime import datetime + +try: + from unittest.mock import Mock +except ImportError: + # mock was added to unittest in Python 3.3, but was an external library + # before. + from mock import Mock + + +class TestVoiceRecording(unittest.TestCase): + + def test_voice_recording_view(self): + http_client = Mock() + http_client.request.return_value = '{"data":[{"id":"12345678-9012-3456-7890-123456789012","format":"wav","legId":"87654321-0987-6543-2109-876543210987","status":"done","duration":32,"type":"transfer","createdAt":"2018-01-01T00:00:01Z","updatedAt":"2018-01-01T00:00:05Z","deletedAt":null}],"_links":{"file":"/calls/12348765-4321-0987-6543-210987654321/legs/87654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789012.wav","self":"/calls/12345678-9012-3456-7890-123456789012/legs/12348765-4321-0987-6543-210987654321/recordings/12345678-9012-3456-7890-123456789012"},"pagination":{"totalCount":0,"pageCount":0,"currentPage":0,"perPage":0}}' + + voice_recording = Client('', http_client).voice_recording_view('12348765-4321-0987-6543-210987654321', '87654321-0987-6543-2109-876543210987', '12345678-9012-3456-7890-123456789012') + + http_client.request.assert_called_once_with('https://voice.messagebird.com/calls/12348765-4321-0987-6543-210987654321/legs/87654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789012', 'GET', None) + + self.assertEqual('12345678-9012-3456-7890-123456789012', voice_recording.id) + self.assertEqual('done', voice_recording.status) + self.assertEqual('wav', voice_recording.format) + self.assertEqual(datetime(2018, 1, 1, 0, 0, 1), voice_recording.createdAt) + self.assertEqual(datetime(2018, 1, 1, 0, 0, 5), voice_recording.updatedAt) + self.assertEqual(2, len(voice_recording._links)) + self.assertIsInstance(str(voice_recording), str) + + def test_voice_recording_list(self): + http_client = Mock() + http_client.request.return_value = '{"data":[{"id":"12345678-9012-3456-7890-123456789012","format":"wav","legId":"87654321-0987-6543-2109-876543210987","status":"done","duration":32,"type":"transfer","createdAt":"2018-01-01T00:00:01Z","updatedAt":"2018-01-01T00:00:05Z","deletedAt":null,"_links":{"file":"/calls/12348765-4321-0987-6543-210987654321/legs/7654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789012.wav","self":"/calls/12348765-4321-0987-6543-210987654321/legs/7654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789012"}},{"id":"12345678-9012-3456-7890-123456789013","format":"wav","legId":"87654321-0987-6543-2109-876543210987","status":"done","duration":12,"type":"transfer","createdAt":"2019-01-01T00:00:01Z","updatedAt":"2019-01-01T00:00:05Z","deletedAt":null,"_links":{"file":"/calls/12348765-4321-0987-6543-210987654321/legs/7654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789013.wav","self":"/calls/12348765-4321-0987-6543-210987654321/legs/7654321-0987-6543-2109-876543210987/recordings/12345678-9012-3456-7890-123456789013"}}],"_links":{"self":"/calls/12348765-4321-0987-6543-210987654321/legs/7654321-0987-6543-2109-876543210987/recordings?page=1"},"pagination":{"totalCount":2,"pageCount":1,"currentPage":1,"perPage":10}}' + + voice_recordings = Client('', http_client).voice_recording_list_recordings('12348765-4321-0987-6543-210987654321', '87654321-0987-6543-2109-876543210987') + + http_client.request.assert_called_once_with('https://voice.messagebird.com/calls/12348765-4321-0987-6543-210987654321/legs/87654321-0987-6543-2109-876543210987/recordings', 'GET', None) + + recordings_check = { + '12345678-9012-3456-7890-123456789012': { "id": '12345678-9012-3456-7890-123456789012', "duration": 32, "year": 2018 }, + '12345678-9012-3456-7890-123456789013': { "id": '12345678-9012-3456-7890-123456789013', "duration": 12, "year": 2019 } + } + + for item in voice_recordings._items: + recording_specific = recordings_check.get(item.id) + self.assertEqual(recording_specific['id'], item.id) + self.assertEqual(recording_specific['duration'], item.duration) + self.assertEqual('done', item.status) + self.assertEqual('wav', item.format) + self.assertEqual(datetime(recording_specific['year'], 1, 1, 0, 0, 1), item.createdAt) + self.assertEqual(datetime(recording_specific['year'], 1, 1, 0, 0, 5), item.updatedAt) + self.assertEqual(2, len(item._links)) + self.assertIsInstance(str(voice_recordings), str) + + def test_voice_recording_download(self): + http_client = Mock() + http_client.request.return_value = '{"data":null,"errors":[{"message":"No recording found for ID `00000000-0000-0000-0000-000000000000`.","code":13}],"pagination":{"totalCount":0,"pageCount":0,"currentPage":0,"perPage":0}}' + + with self.assertRaises(ErrorException): + voice_recording = Client('', http_client).voice_recording_download('12348765-4321-0987-6543-210987654321', '87654321-0987-6543-2109-876543210987', '12345678-9012-3456-7890-123456789012') + + http_client.request.return_value = '{"data":[{"id":"12345678-9012-3456-7890-123456789012","format":"wav","legId":"87654321-0987-6543-2109-876543210987","status":"done","duration":32,"type":"transfer","createdAt":"2018-01-01T00:00:01Z","updatedAt":"2018-01-01T00:00:05Z","deletedAt":null}],"pagination":{"totalCount":0,"pageCount":0,"currentPage":0,"perPage":0}}' + + with self.assertRaises(ErrorException): + voice_recording = Client('', http_client).voice_recording_download('12348765-4321-0987-6543-210987654321', '87654321-0987-6543-2109-876543210987', '12345678-9012-3456-7890-123456789012') + + + +if __name__ == '__main__': + unittest.main()