Skip to content
Merged
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
27 changes: 27 additions & 0 deletions examples/voice_recording_download.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions examples/voice_recording_view.py
Original file line number Diff line number Diff line change
@@ -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)
26 changes: 26 additions & 0 deletions examples/voice_recordings_list.py
Original file line number Diff line number Diff line change
@@ -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)
44 changes: 42 additions & 2 deletions messagebird/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
import json
import io

from messagebird.balance import Balance
from messagebird.contact import Contact, ContactList
Expand All @@ -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'
Expand All @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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)
41 changes: 23 additions & 18 deletions messagebird/http_client.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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)
Expand All @@ -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)
71 changes: 71 additions & 0 deletions messagebird/voice_recording.py
Original file line number Diff line number Diff line change
@@ -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)
])
70 changes: 70 additions & 0 deletions tests/test_voice_recording.py
Original file line number Diff line number Diff line change
@@ -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()