diff --git a/examples/call_flow.py b/examples/call_flow.py new file mode 100644 index 0000000..bea4191 --- /dev/null +++ b/examples/call_flow.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +import sys +import messagebird + +#ACCESS_KEY = '' +#CALL_FLOW_ID = '' + +try: + ACCESS_KEY +except NameError: + print('You need to set an ACCESS_KEY constant in this file') + sys.exit(1) + +try: + CALL_FLOW_ID +except NameError: + print('You need to set a CALL_FLOW_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 CallFlow object for the specified CALL_FLOW_ID. + call = client.call_flow(CALL_FLOW_ID) + + # Print the object information. + print('\nThe following information was returned as a CallFlow object:\n') + print(' id : {}'.format(call.id)) + print(' title : {}'.format(call.title)) + print(' steps : ') + for step in call.steps: + print(step) + + print(' record : {}'.format(call.record)) + print(' default : {}'.format(call.default)) + print(' updatedAt : {}'.format(call.updatedAt)) + print(' createdAt : {}'.format(call.createdAt)) + +except messagebird.client.ErrorException as e: + print('\nAn error occured while requesting a CallFlow object:\n') + + for error in e.errors: + print(' code : {}'.format(error.code)) + print(' description : {}'.format(error.description)) + print(' parameter : {}\n'.format(error.parameter)) diff --git a/messagebird/call_flow.py b/messagebird/call_flow.py new file mode 100644 index 0000000..8e61d08 --- /dev/null +++ b/messagebird/call_flow.py @@ -0,0 +1,137 @@ +from messagebird.base import Base +from messagebird.base_list import BaseList + + +class CallFlowList(BaseList): + + def __init__(self): + self._data = None + self._pagination = None + + super(CallFlowList, self).__init__(CallFlow) + + @property + def data(self): + return self._data + + @property + def pagination(self): + return self._pagination + + @pagination.setter + def pagination(self, value): + self._pagination = value + + @data.setter + def data(self, value): + """Create typed objects from the dicts.""" + items = [] + for item in value: + items.append(self.itemType().load(item)) + + self._data = items + + +class CallFlowNumberList(BaseList): + def __init__(self): + self._data = None + self._pagination = None + + super(CallFlowNumberList, self).__init__(CallFlowNumber) + + @property + def data(self): + return self._data + + @property + def pagination(self): + return self._pagination + + @pagination.setter + def pagination(self, value): + self._pagination = value + + @data.setter + def data(self, value): + """Create typed objects from the dicts.""" + items = [] + for item in value: + items.append(self.itemType().load(item)) + + self._data = items + +class CallFlow(Base): + + def __init__(self): + self.id = None + self.title = None + self.record = None + self.steps = None + self.default = None + self._createdAt = None + self._updatedAt = None + + @property + def createdAt(self): + return self._createdAt + + @createdAt.setter + def createdAt(self, value): + self._createdAt = self.value_to_time(value) + + @property + def updatedAt(self): + return self._updatedAt + + @updatedAt.setter + def updatedAt(self, value): + self._updatedAt = self.value_to_time(value) + + def load(self, data): + if data.get('data') is not None: + items = data.get('data')[0].items() + else: + items = list(data.items()) + + for name, value in items: + if hasattr(self, name) and not callable(getattr(self, name)): + setattr(self, name, value) + + return self + + +class CallFlowNumber(Base): + def __init__(self): + self.id = None + self.number = None + self.callFlowId = None + self._createdAt = None + self._updatedAt = None + + @property + def createdAt(self): + return self._createdAt + + @createdAt.setter + def createdAt(self, value): + self._createdAt = self.value_to_time(value) + + @property + def updatedAt(self): + return self._updatedAt + + @updatedAt.setter + def updatedAt(self, value): + self._updatedAt = self.value_to_time(value) + + def load(self, data): + if data.get('data') is not None: + items = data.get('data')[0].items() + else: + items = list(data.items()) + + for name, value in items: + if hasattr(self, name) and not callable(getattr(self, name)): + setattr(self, name, value) + + return self \ No newline at end of file diff --git a/messagebird/client.py b/messagebird/client.py index 143591f..ba6f50e 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -19,6 +19,7 @@ from messagebird.conversation import Conversation, ConversationList from messagebird.conversation_webhook import ConversationWebhook, ConversationWebhookList from messagebird.voice_recording import VoiceRecordingsList, VoiceRecording +from messagebird.call_flow import CallFlow, CallFlowList, CallFlowNumberList ENDPOINT = 'https://rest.messagebird.com' @@ -58,7 +59,7 @@ class Client(object): def __init__(self, access_key, http_client=None, features=[]): self.access_key = access_key self.http_client = http_client - + self.conversation_api_root = CONVERSATION_API_WHATSAPP_SANDBOX_ROOT if Feature.ENABLE_CONVERSATIONS_API_WHATSAPP_SANDBOX in features else CONVERSATION_API_ROOT def _get_http_client(self, type=REST_TYPE): @@ -67,7 +68,7 @@ def _get_http_client(self, type=REST_TYPE): if type == CONVERSATION_TYPE: return HttpClient(self.conversation_api_root, self.access_key, USER_AGENT) - + if type == VOICE_TYPE: return HttpClient(VOICE_API_ROOT, self.access_key, USER_AGENT) @@ -356,5 +357,30 @@ def voice_recording_download(self, call_id, leg_id, recording_id): recording_file = self.request_store_as_file(VOICE_API_ROOT + recording_file, recording_id + '.wav') return VOICE_API_ROOT + recording_file + def call_flow(self, id): + return CallFlow().load(self.request('call-flows/' + str(id), 'GET', None, VOICE_TYPE)) + + def call_flow_list(self, limit=10, offset=0): + query = self._format_query(limit, offset) + return CallFlowList().load(self.request('call-flows?' + query, 'GET', None, VOICE_TYPE)) + + def call_flow_create(self, title, steps, default=False, record=False): + params = {'title': title, 'steps': steps, 'default': default, 'record': record} + return CallFlow().load(self.request('call-flows', 'POST', params, VOICE_TYPE)) + + def call_flow_update(self, id, title, steps, default, record): + params = {'title': title, 'steps': steps, 'default': default, 'record': record} + return CallFlow().load(self.request('call-flows/' + str(id), 'PUT', params, VOICE_TYPE)) + + def call_flow_delete(self, id): + self.request_plain_text('call-flows/' + str(id), 'DELETE', None, VOICE_TYPE) + + def call_flow_numbers_list(self, call_flow_id): + return CallFlowNumberList().load(self.request('call-flows/' + str(call_flow_id) + '/numbers', 'GET', None, VOICE_TYPE)) + + def call_flow_numbers_add(self, call_flow_id, numbers=()): + params = {'numbers': numbers} + return CallFlowNumberList().load(self.request('call-flows/' + str(call_flow_id) + '/numbers', 'POST', params, VOICE_TYPE)) + def _format_query(self, limit, offset): return 'limit=' + str(limit) + '&offset=' + str(offset) diff --git a/tests/test_call_flow.py b/tests/test_call_flow.py new file mode 100644 index 0000000..53503a2 --- /dev/null +++ b/tests/test_call_flow.py @@ -0,0 +1,152 @@ +import unittest +from messagebird import Client + +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 TestCallFlow(unittest.TestCase): + + def test_get_flow(self): + http_client = Mock() + http_client.request.return_value = '''{ + "data": [ + { + "id": "de3ed163-d5fc-45f4-b8c4-7eea7458c635", + "title": "Updated call flow", + "record": false, + "steps": [ + { + "id": "3538a6b8-5a2e-4537-8745-f72def6bd393", + "action": "transfer", + "options": { + "destination": "31611223344" + } + } + ], + "createdAt": "2017-03-06T13:34:14Z", + "updatedAt": "2017-03-06T15:02:38Z" + } + ], + "_links": { + "self": "/call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635" + } +} + ''' + + call_flow = Client('', http_client).call_flow('de3ed163-d5fc-45f4-b8c4-7eea7458c635') + http_client.request.assert_called_once_with('call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635', 'GET', None) + + self.assertEqual('Updated call flow', call_flow.title) + self.assertIsNotNone(call_flow.steps) + + def test_get_flow_list(self): + http_client = Mock() + http_client.request.return_value = '''{ + "data": [ + { + "id": "de3ed163-d5fc-45f4-b8c4-7eea7458c635", + "title": "Forward call to 31612345678", + "record": false, + "steps": [ + { + "id": "3538a6b8-5a2e-4537-8745-f72def6bd393", + "action": "transfer", + "options": { + "destination": "31612345678" + } + } + ], + "createdAt": "2017-03-06T13:34:14Z", + "updatedAt": "2017-03-06T13:34:14Z", + "_links": { + "self": "/call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635" + } + }, + { + "id": "de3ed163-d5fc-45f4-b8c4-7eea7458c634", + "title": "Forward call to 0600123123", + "record": true, + "steps": [ + { + "id": "3538a6b8-5a2e-4537-8745-f72def6bd393", + "action": "transfer", + "options": { + "destination": "31612345678" + } + } + ], + "createdAt": "2017-03-06T13:34:14Z", + "updatedAt": "2017-03-06T13:34:14Z", + "_links": { + "self": "/call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c634" + } + } + ], + "_links": { + "self": "/call-flows?page=1" + }, + "pagination": { + "totalCount": 2, + "pageCount": 2, + "currentPage": 1, + "perPage": 10 + } +} + ''' + + call_flow_list = Client('', http_client).call_flow_list(20, 0) + + http_client.request.assert_called_once_with('call-flows?limit=20&offset=0', 'GET', None) + + self.assertEqual('Forward call to 0600123123', call_flow_list.data[1].title) + self.assertEqual(2, call_flow_list.pagination['totalCount']) + + def test_numbers_list(self): + http_client = Mock() + http_client.request.return_value = '''{ + "data": [ + { + "id": "13f38f34-7ff4-45b3-8783-8d5b1143f22b", + "number": "31611111111", + "callFlowId": "de3ed163-d5fc-45f4-b8c4-7eea7458c635", + "createdAt": "2017-03-16T13:49:24Z", + "updatedAt": "2017-09-12T08:59:50Z", + "_links": { + "self": "/numbers/13f38f34-7ff4-45b3-8783-8d5b1143f22b" + } + } + ], + "_links": { + "self": "/call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635/numbers?page=1" + }, + "pagination": { + "totalCount": 1, + "pageCount": 1, + "currentPage": 1, + "perPage": 10 + } +} + ''' + + number_list = Client('', http_client).call_flow_numbers_list('de3ed163-d5fc-45f4-b8c4-7eea7458c635') + + http_client.request.assert_called_once_with('call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635/numbers', 'GET', None) + + self.assertEqual('31611111111', number_list.data[0].number) + self.assertEqual(1, number_list.pagination['totalCount']) + + def test_numbers_add(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).call_flow_numbers_add('de3ed163-d5fc-45f4-b8c4-7eea7458c635', + ['31611111111', '31611111112']) + + params = {'numbers': ['31611111111', '31611111112']} + + http_client.request.assert_called_once_with('call-flows/de3ed163-d5fc-45f4-b8c4-7eea7458c635/numbers', 'POST', params) \ No newline at end of file