diff --git a/.gitignore b/.gitignore index 1a30f4a..1b2be6e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ target/ # PyCharm .idea/ + +# pyvenv +venv/ \ No newline at end of file diff --git a/examples/call.py b/examples/call.py index c57bc58..c71d9cd 100644 --- a/examples/call.py +++ b/examples/call.py @@ -1,46 +1,48 @@ #!/usr/bin/env python -import sys, os +import os +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) import messagebird -#ACCESS_KEY = '' -#CALL_ID = '' +# ACCESS_KEY = '' +# CALL_ID = '' try: - ACCESS_KEY + ACCESS_KEY except NameError: - print('You need to set an ACCESS_KEY constant in this file') - sys.exit(1) + print('You need to set an ACCESS_KEY constant in this file') + sys.exit(1) try: - CALL_ID + CALL_ID except NameError: - print('You need to set a CALL_ID constant in this file') - sys.exit(1) + print('You need to set a CALL_ID constant in this file') + sys.exit(1) try: - # Create a MessageBird client with the specified ACCESS_KEY. - client = messagebird.Client(ACCESS_KEY) - - # Fetch the Message object for the specified MESSAGE_ID. - call = client.call(CALL_ID) - - # Print the object information. - print('\nThe following information was returned as a Message object:\n') - print(' id : %s' % call.data.id) - print(' status : %s' % call.data.status) - print(' source : %s' % call.data.source) - print(' destination : %s' % call.data.destination) - print(' createdAt : %s' % call.data.createdAt) - print(' updatedAt : %s' % call.data.updatedAt) - print(' endedAt : %s' % call.data.endedAt) - + # Create a MessageBird client with the specified ACCESS_KEY. + client = messagebird.Client(ACCESS_KEY) + + # Fetch the Call object for the specified CALL_ID. + call = client.call(CALL_ID) + + # Print the object information. + print('\nThe following information was returned as a', str(call.__class__), 'object:\n') + print(' id : %s' % call.data.id) + print(' status : %s' % call.data.status) + print(' source : %s' % call.data.source) + print(' destination : %s' % call.data.destination) + print(' createdAt : %s' % call.data.createdAt) + print(' updatedAt : %s' % call.data.updatedAt) + print(' endedAt : %s' % call.data.endedAt) + except messagebird.client.ErrorException as e: - print('\nAn error occured while requesting a Message object:\n') + print('\nAn error occurred while requesting a Message object:\n') - for error in e.errors: - print(' code : %d' % error.code) - print(' description : %s' % error.description) - print(' parameter : %s\n' % error.parameter) + 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/call_create.py b/examples/call_create.py new file mode 100644 index 0000000..e6cfa44 --- /dev/null +++ b/examples/call_create.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +import os +import sys +import json +import argparse +import requests + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import messagebird + +exampleCallFlow = '{"title":"Test Flow","steps":[{"action":"say","options":{"payload":"Hey, this is your first voice \ +call","language":"en-GB","voice":"female"}}]}' + +parser = argparse.ArgumentParser(usage='call_create.py\ + --accessKey="*******" \ + --destination=31612345678 \ + --source=31644556677 \ + --callFlow \'' + exampleCallFlow + '\'\ +') +parser.add_argument('--accessKey', help='access key for MessageBird API', type=str, required=True) +parser.add_argument('--source', help='The caller ID of the call.', type=str, required=True) +parser.add_argument('--destination', help='The number/address to be called.', type=str, required=True) +parser.add_argument('--callFlow', help='The call flow object to be executed when the call is answered.', type=str, required=False, default=exampleCallFlow) +parser.add_argument('--webhook', help='The webhook object containing the url & required token.', type=str, required=False, default='{}') +args = vars(parser.parse_args()) + +# arguments to parse as json +jsonArgs = ['callFlow', 'webhook'] + +for jsonArg in jsonArgs: + try: + args[jsonArg] = json.loads(str(args[jsonArg]).strip('\'')) + except json.decoder.JSONDecodeError as e: + parser.print_usage() + print('Invalid json provided for %s: %s' % (jsonArg, e)) + print('Provided %s json: %s' % (jsonArg, args[jsonArg])) + exit(1) + +try: + # Create a MessageBird client with the specified accessKey. + client = messagebird.Client(args['accessKey']) + del(args['accessKey']) + + # Create a call for the specified callID. + call = client.call_create(**args) + + # Print the object information. + print('\nThe following information was returned as a', str(call.__class__), 'object:\n') + print(' id : %s' % call.data.id) + print(' status : %s' % call.data.status) + print(' source : %s' % call.data.source) + print(' destination : %s' % call.data.destination) + print(' webhook : %s' % call.data.webhook) + print(' createdAt : %s' % call.data.createdAt) + print(' updatedAt : %s' % call.data.updatedAt) + print(' endedAt : %s' % call.data.endedAt) + +except messagebird.client.ErrorException as e: + print('\nAn error occurred while creating a call:\n') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s' % error.parameter) + print(' type : %s' % error.__class__) + +except requests.exceptions.HTTPError as e: + print('\nAn http exception occurred while creating a call:') + print(' ', e) + print(' Http request body: ', e.request.body) + print(' Http response status: ', e.response.status_code) + print(' Http response body: ', e.response.content.decode()) + +except Exception as e: + print('\nAn ', e.__class__, ' exception occurred while creating a call:') + print(e) + diff --git a/examples/call_delete.py b/examples/call_delete.py new file mode 100644 index 0000000..1b2fe5e --- /dev/null +++ b/examples/call_delete.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +import os +import sys +import json +import argparse +import requests + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import messagebird + +parser = argparse.ArgumentParser(usage='call_create.py\ + --accessKey="*******" \ + --callId=dda20377-72da-4846-9b2c-0fea3ad4bcb6 \ +') +parser.add_argument('--accessKey', help='Access key for MessageBird API.', type=str, required=True) +parser.add_argument('--callId', help='The ID of the MessageBird call to delete.', type=str, required=True) +args = vars(parser.parse_args()) + +try: + # Create a MessageBird client with the specified accessKey. + client = messagebird.Client(args['accessKey']) + + # Create a call for the specified callID. + call = client.call_delete(args['callId']) + + # If no error is thrown, means delete was successful. + print('\nDeleted call with id `%s` successfully!' % args['callId']) + +except messagebird.client.ErrorException as e: + print('\nAn error occurred while creating a call:\n') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s' % error.parameter) + print(' type : %s' % error.__class__) + +except requests.exceptions.HTTPError as e: + print('\nAn http exception occurred while deleting a call:') + print(' ', e) + print(' Http request body: ', e.request.body) + print(' Http response status: ', e.response.status_code) + print(' Http response body: ', e.response.content.decode()) + +except Exception as e: + print('\nAn ', e.__class__, ' exception occurred while deleting a call:') + print(e) + diff --git a/examples/call_list.py b/examples/call_list.py new file mode 100644 index 0000000..25e059a --- /dev/null +++ b/examples/call_list.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +import os +import sys +import argparse +import requests + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import messagebird + + +parser = argparse.ArgumentParser(usage='call_create.py\ + --accessKey="*******" \ + --page=1 \ +') +parser.add_argument('--accessKey', help='Access key for MessageBird API.', type=str, required=True) +parser.add_argument('--page', help='The page you wish to view.', type=str, required=False, default=1) +args = vars(parser.parse_args()) + +try: + # Create a MessageBird client with the specified accessKey. + client = messagebird.Client(args['accessKey']) + del(args['accessKey']) + + # Create a call for the specified callID. + callList = client.call_list(**args) + + # Print the object information. + print('\nThe following information was returned as a %s object:\n' % callList.__class__) + if callList.items is not None: + print(' Containing the the following items:') + for item in callList.items: + print(' {') + print(' id : %s' % item.id) + print(' status : %s' % item.status) + print(' source : %s' % item.source) + print(' destination : %s' % item.destination) + print(' createdAt : %s' % item.createdAt) + print(' updatedAt : %s' % item.updatedAt) + print(' endedAt : %s' % item.endedAt) + print(' },') + else: + print(' With an empty response.') + +except messagebird.client.ErrorException as e: + print('\nAn error occurred while listing calls:\n') + + for error in e.errors: + print(' code : %d' % error.code) + print(' description : %s' % error.description) + print(' parameter : %s' % error.parameter) + print(' type : %s' % error.__class__) + +except requests.exceptions.HTTPError as e: + print('\nAn HTTP exception occurred while listing calls:') + print(' ', e) + print(' Http request body: ', e.request.body) + print(' Http response status: ', e.response.status_code) + print(' Http response body: ', e.response.content.decode()) + +except Exception as e: + print('\nAn ', e.__class__, ' exception occurred while creating a call:') + print(e) + + diff --git a/messagebird/base_list.py b/messagebird/base_list.py index f77a290..0618170 100644 --- a/messagebird/base_list.py +++ b/messagebird/base_list.py @@ -43,3 +43,7 @@ def items(self, value): items.append(self.itemType().load(item)) self._items = items + + def __str__(self): + items_count = 0 if self.items is None else len(self.items) + return "%s with %d items.\n" % (str(self.__class__), items_count) diff --git a/messagebird/call.py b/messagebird/call.py index cd1d89f..4575d9d 100644 --- a/messagebird/call.py +++ b/messagebird/call.py @@ -19,3 +19,9 @@ def data(self): @data.setter def data(self, value): self._data = CallData().load(value[0]) + + def __str__(self): + return "\n".join([ + 'id : %s' % self.id, + 'data.'+'data.'.join(str(self._data).splitlines(True)), + ]) \ No newline at end of file diff --git a/messagebird/call_data.py b/messagebird/call_data.py index 01a8645..2cc730d 100644 --- a/messagebird/call_data.py +++ b/messagebird/call_data.py @@ -45,3 +45,15 @@ def webhook(self): @webhook.setter def webhook(self, value): self._webhook = Webhook.load(value) + + def __str__(self): + return "\n".join([ + 'id : %s' % self.id, + 'status : %s' % self.status, + 'source : %s' % self.source, + 'destination : %s' % self.destination, + 'webhook : %s' % self.webhook, + 'updatedAt : %s' % self.updatedAt, + 'createdAt : %s' % self.createdAt, + 'endedAt : %s' % self.endedAt, + ]) \ No newline at end of file diff --git a/messagebird/call_list.py b/messagebird/call_list.py new file mode 100644 index 0000000..0d2ec4f --- /dev/null +++ b/messagebird/call_list.py @@ -0,0 +1,42 @@ +from messagebird.base_list import BaseList +from messagebird.call_data import CallData + + +class CallList(BaseList): + def __init__(self): + # We're expecting items of type CallData + super(CallList, self).__init__(CallData) + self.perPage = None + self.currentPage = None + self.pageCount = None + self._pagination = None + + @property + def data(self): + return self.items + + @property + def pagination(self): + return { + "totalCount": self.totalCount, + "pageCount": self.pageCount, + "currentPage": self.currentPage, + "perPage": self.perPage + } + + @pagination.setter + def pagination(self, value): + if isinstance(value, dict): + self.totalCount = value['totalCount'] + self.pageCount = value['pageCount'] + self.currentPage = value['currentPage'] + self.perPage = value['perPage'] + self.limit = self.perPage * self.currentPage + self.offset = self.perPage * (self.currentPage - 1) + + @data.setter + def data(self, value): + if isinstance(value, list): + self.count = len(value) + self.items = value + diff --git a/messagebird/client.py b/messagebird/client.py index 143591f..fe9d1ec 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -5,6 +5,7 @@ from messagebird.balance import Balance from messagebird.call import Call +from messagebird.call_list import CallList from messagebird.contact import Contact, ContactList from messagebird.error import Error from messagebird.group import Group, GroupList @@ -50,6 +51,10 @@ def __init__(self, errors): message = ' '.join([str(e) for e in self.errors]) super(ErrorException, self).__init__(message) +class SignleErrorException(Exception): + def __init__(self, errorMessage): + super(SignleErrorException, self).__init__(errorMessage) + class Feature(enum.Enum): ENABLE_CONVERSATIONS_API_WHATSAPP_SANDBOX = 1 @@ -124,6 +129,44 @@ def call(self,id): """Retrieve the information of a specific call""" return Call().load(self.request('calls/' + str(id), 'GET', None, VOICE_TYPE)) + def call_list(self, page=1): + """Listing calls + + Args: + page(int) : The page to list. + Raises: + ErrorException : On api returning errors + + Returns: + CallList(object) : The list of calls requested & their status.""" + return CallList().load(self.request('calls/?page=' + str(page), 'GET', None, VOICE_TYPE)) + + def call_create(self, source, destination, callFlow, webhook): + """Creating a call + + Args: + source(str) : The caller ID of the call. + destination(string) : The number/address to be called. + callFlow(object) : The call flow object to be executed when the call is answered. + webhook(object) : The webhook object containing the url & required token. + Raises: + ErrorException : On api returning errors + + Returns: + Call(object) : The Call object just created.""" + + params = locals() + del(params['self']) + return Call().load(self.request('calls', 'POST', params, VOICE_TYPE)) + + def call_delete(self, id): + """Delete an existing call object.""" + response = self.request_plain_text('calls/' + str(id), 'DELETE', None, VOICE_TYPE) + + # successful delete should be empty + if len(response) > 0: + raise SignleErrorException(response) + def hlr(self, id): """Retrieve the information of a specific HLR lookup.""" return HLR().load(self.request('hlr/' + str(id))) @@ -155,11 +198,11 @@ def message_delete(self, id): self.request_plain_text('messages/' + str(id), 'DELETE') def mms_create(self, originator, recipients, body, mediaUrls, subject = None, reference = None, scheduledDatetime = None): - ''' Send bulk mms. + """ Send bulk mms. Args: originator(str): name of the originator - recipients(str/list(str)): comma seperated numbers or list of numbers in E164 format + recipients(str/list(str)): comma separated numbers or list of numbers in E164 format body(str) : text message body mediaUrl(str) : list of URL's of attachments of the MMS message. subject(str) : utf-encoded subject @@ -169,8 +212,8 @@ def mms_create(self, originator, recipients, body, mediaUrls, subject = None, re ErrorException: On api returning errors Returns: - MMS: On success an MMS instance instantiated with succcess response - ''' + MMS: On success an MMS instance instantiated with success response + """ if isinstance(recipients,list): recipients = ','.join(recipients) if isinstance(mediaUrls,str): diff --git a/messagebird/webhook.py b/messagebird/webhook.py index 1f962f4..28c0e7b 100644 --- a/messagebird/webhook.py +++ b/messagebird/webhook.py @@ -6,3 +6,9 @@ class Webhook(Base): def __init__(self): self.url = None self.token = None + + def __str__(self): + return "\n".join([ + 'url : %s' % self.url, + 'token : %s' % self.token, + ]) diff --git a/tests/test_call.py b/tests/test_call.py index e0ac112..22ee6f4 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -1,5 +1,8 @@ +import json import unittest from messagebird import Client, ErrorException +from messagebird.base import Base +from messagebird.client import VOICE_TYPE try: from unittest.mock import Mock @@ -13,10 +16,174 @@ class TestCall(unittest.TestCase): def test_call(self): http_client = Mock() - http_client.request.return_value = '{"data":[{"id":"call-id","status":"ended","source":"16479311111","destination":"1416555555","createdAt":"2019-08-06T13:17:06Z","updatedAt":"2019-08-06T13:17:39Z","endedAt":"2019-08-06T13:17:39Z"}],"_links":{"legs":"/calls/66bd9f08-a8af-40fe-a830-652d8dabc057/legs","self":"/calls/66bd9f08-a8af-40fe-a830-652d8bca357"},"pagination":{"totalCount":0,"pageCount":0,"currentPage":0,"perPage":0}}' + http_client.request.return_value = """ + { + "data":[ + { + "id":"call-id", + "status":"ended", + "source":"16479311111", + "destination":"1416555555", + "createdAt":"2019-08-06T13:17:06Z", + "updatedAt":"2019-08-06T13:17:39Z", + "endedAt":"2019-08-06T13:17:39Z" + } + ], + "_links":{ + "legs":"/calls/66bd9f08-a8af-40fe-a830-652d8dabc057/legs", + "self":"/calls/66bd9f08-a8af-40fe-a830-652d8bca357" + }, + "pagination":{ + "totalCount":0, + "pageCount":0, + "currentPage":0, + "perPage":0 + } + } + """ call = Client('', http_client).call('call-id') http_client.request.assert_called_once_with('calls/call-id', 'GET', None) - self.assertEqual('ended', call.data.status) \ No newline at end of file + self.assertEqual('ended', call.data.status) + + def test_call_list(self): + http_client = Mock() + http_client.request.return_value = """ + { + "data":[ + { + "id":"dda20377-72da-4846-9b2c-0fea3ad4bcb6", + "status":"no_answer", + "source":"16479311111", + "destination":"1416555555", + "createdAt":"2019-08-06T13:17:06Z", + "updatedAt":"2019-08-06T13:17:39Z", + "endedAt":"2019-08-06T13:17:39Z", + "_links":{ + "legs":"/calls/dda20377-72da-4846-9b2c-0fea3ad4bcb6/legs", + "self":"/calls/dda20377-72da-4846-9b2c-0fea3ad4bcb6" + } + }, + { + "id":"1541535b-9b80-4002-bde5-ed05b5ebed76", + "status":"ended", + "source":"16479311111", + "destination":"1416555556", + "createdAt":"2019-08-06T13:17:06Z", + "updatedAt":"2019-08-06T13:17:39Z", + "endedAt":"2019-08-06T13:17:39Z", + "_links":{ + "legs":"/calls/1541535b-9b80-4002-bde5-ed05b5ebed76/legs", + "self":"/calls/1541535b-9b80-4002-bde5-ed05b5ebed76" + } + } + ], + "_links": { + "self": "/calls?page=1" + }, + "pagination":{ + "totalCount":2, + "pageCount":1, + "currentPage":1, + "perPage":10 + } + } + """ + + callList = Client('', http_client).call_list(page=1) + + http_client.request.assert_called_once_with('calls/?page=1', 'GET', None) + + # check data is processed + self.assertEqual('no_answer', callList.data[0].status) + self.assertEqual('ended', callList.data[1].status) + + # check pagination is passed to object + self.assertEqual(2, callList.totalCount) + self.assertEqual(1, callList.pageCount) + self.assertEqual(1, callList.currentPage) + self.assertEqual(10, callList.perPage) + self.assertEqual(10, callList.pagination['perPage'], 'Check it also supports API pagination format.') + + self.assertEqual(0, callList.offset, 'Check it correctly calculates offset.') + self.assertEqual(10, callList.limit, 'Check it correctly calculates limit.') + + def test_call_create(self): + api_response = { + "data": [ + { + "id": "21025ed1-cc1d-4554-ac05-043fa6c84e00", + "status": "queued", + "source": "31644556677", + "destination": "31612345678", + "createdAt": "2017-08-30T07:35:37Z", + "updatedAt": "2017-08-30T07:35:37Z", + "endedAt": None + } + ], + "_links": { + "self": "/calls/21025ed1-cc1d-4554-ac05-043fa6c84e00" + } + } + + params = { + "source": "31644556677", + "destination": "31612345678", + "callFlow": { + "title": "Say message", + "steps": [ + { + "action": "say", + "options": { + "payload": "This is a journey into sound. Good bye!", + "voice": "male", + "language": "en-US" + } + } + ] + }, + "webhook": { + "url": "https://example.com", + "token": "token_to_sign_the_call_events_with", + } + } + + http_client = Mock() + http_client.request.return_value = json.dumps(api_response) + + call_creation_response = Client('', http_client).call_create(**params) + + http_client.request.assert_called_once_with('calls', 'POST', params) + + # check all api response data is outputted + expected_data = self.create_expected_call_data_based_on_api_response(api_response) + response_data = call_creation_response.data.__dict__ + self.assertEqual(expected_data, response_data, 'Check client response contains the API response data.') + + # check it can be formatted as string + self.assertTrue(len(str(call_creation_response)) > 0, 'Check returned call can be formatted as string.') + + def test_call_delete(self): + http_client = Mock() + http_client.request.return_value = '' + call_id_to_delete = '21025ed1-cc1d-4554-ac05-043fa6c84e00' + Client('', http_client).call_delete(call_id_to_delete) + + http_client.request.assert_called_once_with('calls/%s' % call_id_to_delete, 'DELETE', None) + + @staticmethod + def create_expected_call_data_based_on_api_response(api_response): + expected_data = api_response['data'][0] + + # convert dates + expected_data['_createdAt'] = Base.value_to_time(expected_data['createdAt'], '%Y-%m-%dT%H:%M:%SZ') + expected_data['_updatedAt'] = Base.value_to_time(expected_data['updatedAt'], '%Y-%m-%dT%H:%M:%SZ') + expected_data['_endedAt'] = Base.value_to_time(expected_data['endedAt'], '%Y-%m-%dT%H:%M:%SZ') + del (expected_data['createdAt'], expected_data['updatedAt'], expected_data['endedAt']) + + # add generated data + expected_data.setdefault('_webhook', None) + + return expected_data