From 2c5dd2df593956d7f795b37c7686ca44728a34a5 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:47:17 +0200 Subject: [PATCH 01/13] Refactor Client for easier mocking --- messagebird/client.py | 42 ++++++++++++-------------------------- messagebird/http_client.py | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 messagebird/http_client.py diff --git a/messagebird/client.py b/messagebird/client.py index 6ffceeb..64c25bf 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -1,16 +1,11 @@ import sys import json -import requests - -try: - from urllib.parse import urljoin -except ImportError: - from urlparse import urljoin from messagebird.base import Base from messagebird.balance import Balance from messagebird.error import Error from messagebird.hlr import HLR +from messagebird.http_client import HttpClient from messagebird.message import Message from messagebird.voicemessage import VoiceMessage from messagebird.lookup import Lookup @@ -19,6 +14,7 @@ ENDPOINT = 'https://rest.messagebird.com' CLIENT_VERSION = '1.2.1' PYTHON_VERSION = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], sys.version_info[2]) +USER_AGENT = 'MessageBird/ApiClient/%s Python/%s' % (CLIENT_VERSION, PYTHON_VERSION) class ErrorException(Exception): @@ -29,35 +25,23 @@ def __init__(self, errors): class Client(object): - def __init__(self, access_key): + def __init__(self, access_key, http_client=None): self.access_key = access_key - self._supported_status_codes = [200, 201, 204, 401, 404, 405, 422] - - def request(self, path, method='GET', params=None): - if params is None: params = {} - url = urljoin(ENDPOINT, path) - headers = { - 'Accept' : 'application/json', - 'Authorization' : 'AccessKey ' + self.access_key, - 'User-Agent' : 'MessageBird/ApiClient/%s Python/%s' % (CLIENT_VERSION, PYTHON_VERSION), - 'Content-Type' : 'application/json' - } - - if method == 'GET': - response = requests.get(url, verify=True, headers=headers, params=params) + if http_client is None: + self.http_client = HttpClient(ENDPOINT, access_key, USER_AGENT) else: - response = requests.post(url, verify=True, headers=headers, data=json.dumps(params)) + self.http_client = http_client - if response.status_code in self._supported_status_codes: - json_response = response.json() - else: - response.raise_for_status() + def request(self, path, method='GET', params=None): + """Builds a request, gets a response and decodes it.""" + response_text = self.http_client.request(path, method, params) + response_json = json.loads(response_text) - if 'errors' in json_response: - raise(ErrorException([Error().load(e) for e in json_response['errors']])) + if 'errors' in response_json: + raise(ErrorException([Error().load(e) for e in response_json['errors']])) - return json_response + return response_json def balance(self): """Retrieve your balance.""" diff --git a/messagebird/http_client.py b/messagebird/http_client.py new file mode 100644 index 0000000..e07a10c --- /dev/null +++ b/messagebird/http_client.py @@ -0,0 +1,41 @@ +import requests + +try: + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + + +class HttpClient(object): + """Used for sending simple HTTP requests.""" + + def __init__(self, endpoint, access_key, user_agent): + self.__supported_status_codes = [200, 201, 204, 401, 404, 405, 422] + + self.endpoint = endpoint + self.access_key = access_key + self.user_agent = user_agent + + def request(self, path, method='GET', params=None): + """Builds a request and gets a response.""" + if params is None: params = {} + url = urljoin(self.endpoint, path) + + headers = { + 'Accept': 'application/json', + 'Authorization': 'AccessKey ' + self.access_key, + 'User-Agent': self.user_agent, + 'Content-Type': 'application/json' + } + + if method == 'GET': + response = requests.get(url, verify=True, headers=headers, params=params) + else: + response = requests.post(url, verify=True, headers=headers, data=json.dumps(params)) + + if response.status_code in self.__supported_status_codes: + response_text = response.text + else: + response.raise_for_status() + + return response_text \ No newline at end of file From ed694bac6a3a9cbd13ae632cede3aa1348038d7e Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:48:30 +0200 Subject: [PATCH 02/13] Add simple Balance test for demonstration --- tests/__init__.py | 0 tests/test_balance.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_balance.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_balance.py b/tests/test_balance.py new file mode 100644 index 0000000..ca025ee --- /dev/null +++ b/tests/test_balance.py @@ -0,0 +1,23 @@ +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 TestBalance(unittest.TestCase): + + def test_balance(self): + http_client = Mock() + http_client.request.return_value = '{"payment": "prepaid","type": "credits","amount": 9.2}' + + balance = Client('', http_client).balance() + + http_client.request.assert_called_once_with('balance', 'GET', None) + + self.assertEqual('prepaid', balance.payment) + self.assertEqual('credits', balance.type) From 7872e196fe02c1945e262b8adb6303a1ea57fdc8 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:55:56 +0200 Subject: [PATCH 03/13] Add Travis integration --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..520f16b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +python: + - 'pypy2.7' + - 'pypy3.5' + - '2.7' + - '3.3' + - '3.7' + - 'nightly' +install: + - pip install mock==2.0 +matrix: + allow_failures: + - python: 'nightly' From 4ae7d794f2ebfb0fb086159e69fbc2a808d5dccb Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 15:58:00 +0200 Subject: [PATCH 04/13] Add test script to .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 520f16b..e6e855c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ python: - 'nightly' install: - pip install mock==2.0 +script: + - python -m unittest discover -s tests/ -p test_*.py -v matrix: allow_failures: - python: 'nightly' From cab74893315c256d4b5a1934c71b2329a98abd60 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:04:00 +0200 Subject: [PATCH 05/13] Install requests before running tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e6e855c..6169c3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: - 'nightly' install: - pip install mock==2.0 + - pip install requests script: - python -m unittest discover -s tests/ -p test_*.py -v matrix: From 2a5a6311b5c5568260dafe7f34f521f0ac829150 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:07:50 +0200 Subject: [PATCH 06/13] Use Python 3.6 instead of 3.7: latter doesn't seem to be supported by Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6169c3b..7e32f0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - 'pypy3.5' - '2.7' - '3.3' - - '3.7' + - '3.6' - 'nightly' install: - pip install mock==2.0 From b844c64556aed84e10c673eaa92884039d97670b Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:14:15 +0200 Subject: [PATCH 07/13] Allow pypy2.7 to fail: Travis can not find virtualenv script --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7e32f0d..2aebb87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,3 +14,4 @@ script: matrix: allow_failures: - python: 'nightly' + - python: 'pypy2.7' From b6cf05bff2dd20cf6d4c2a6dd88ec98ad7fd4215 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Wed, 15 Aug 2018 16:17:57 +0200 Subject: [PATCH 08/13] Add Travis badge to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4a51510..5adac28 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ MessageBird's REST API for Python ================================= This repository contains the open source Python client for MessageBird's REST API. Documentation can be found at: https://developers.messagebird.com/. +[![Build Status](https://travis-ci.org/messagebird/python-rest-api.svg?branch=master)](https://travis-ci.org/messagebird/python-rest-api) + Requirements ------------ - [Sign up](https://www.messagebird.com/en/signup) for a free MessageBird account From e93a241cc204cd2c3ea9a9db6057846e077a6f35 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Fri, 17 Aug 2018 10:25:41 +0200 Subject: [PATCH 09/13] Add tests for other endpoints --- tests/test_hlr.py | 30 ++++++++++++++++++++++++++++ tests/test_lookup.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/test_message.py | 30 ++++++++++++++++++++++++++++ tests/test_verify.py | 40 ++++++++++++++++++++++++++++++++++++++ tests/test_voicemessage.py | 30 ++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 tests/test_hlr.py create mode 100644 tests/test_lookup.py create mode 100644 tests/test_message.py create mode 100644 tests/test_verify.py create mode 100644 tests/test_voicemessage.py diff --git a/tests/test_hlr.py b/tests/test_hlr.py new file mode 100644 index 0000000..5f2fa2b --- /dev/null +++ b/tests/test_hlr.py @@ -0,0 +1,30 @@ +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 TestHLR(unittest.TestCase): + + def test_hlr(self): + http_client = Mock() + http_client.request.return_value = '{"id":"hlr-id","href":"https://rest.messagebird.com/hlr/hlr-id","msisdn":31612345678,"network":20406,"reference":"MyReference","status": "sent","createdDatetime": "2015-01-04T13:14:08+00:00","statusDatetime": "2015-01-04T13:14:09+00:00"}' + + hlr = Client('', http_client).hlr('hlr-id') + + http_client.request.assert_called_once_with('hlr/hlr-id', 'GET', None) + + self.assertEqual('hlr-id', hlr.id) + + def test_hlr_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).hlr_create(31612345678, 'MyReference') + + http_client.request.assert_called_once_with('hlr', 'POST', {'msisdn': 31612345678, 'reference': 'MyReference'}) diff --git a/tests/test_lookup.py b/tests/test_lookup.py new file mode 100644 index 0000000..2d2d534 --- /dev/null +++ b/tests/test_lookup.py @@ -0,0 +1,40 @@ +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 TestLookup(unittest.TestCase): + + def test_lookup(self): + http_client = Mock() + http_client.request.return_value = '{"href": "https://rest.messagebird.com/lookup/31612345678","countryCode": "NL","countryPrefix": 31,"phoneNumber": 31612345678,"type": "mobile","formats": {"e164": "+31612345678","international": "+31 6 12345678","national": "06 12345678","rfc3966": "tel:+31-6-12345678"},"hlr": {"id": "hlr-id","network": 20416,"reference": "reference2000","status": "active","createdDatetime": "2015-12-15T08:19:24+00:00","statusDatetime": "2015-12-15T08:19:25+00:00"}}' + + lookup = Client('', http_client).lookup('0612345678', {'countryCode': 'NL'}) + + http_client.request.assert_called_once_with('lookup/0612345678', 'GET', {'countryCode': 'NL'}) + + self.assertEqual('mobile', lookup.type) + + def test_lookup_hlr(self): + http_client = Mock() + http_client.request.return_value = '{"id": "hlr-id","network": 20416,"reference": "reference2000","status": "active","createdDatetime": "2015-12-15T08:19:24+00:00","statusDatetime": "2015-12-15T08:19:25+00:00"}' + + lookup_hlr = Client('', http_client).lookup_hlr(31612345678, {'reference': 'reference2000'}) + + http_client.request.assert_called_once_with('lookup/31612345678/hlr', 'GET', {'reference': 'reference2000'}) + + self.assertEqual(lookup_hlr.status, 'active') + + def test_lookup_hlr_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).lookup_hlr_create(31612345678, {'reference': 'MyReference'}) + + http_client.request.assert_called_once_with('lookup/31612345678/hlr', 'POST', {'reference': 'MyReference'}) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 0000000..a81003e --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,30 @@ +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 TestMessage(unittest.TestCase): + + def test_message(self): + http_client = Mock() + http_client.request.return_value = '{"body": "Hello World","createdDatetime": "2015-01-05T10:02:59+00:00","datacoding": "plain","direction": "mt","gateway": 239,"href": "https://rest.messagebird.com/messages/message-id","id": "message-id","mclass": 1,"originator": "TestName","recipients": {"items": [{"recipient": 31612345678,"status": "sent","statusDatetime": "2015-01-05T10:02:59+00:00"}],"totalCount": 1,"totalDeliveredCount": 0,"totalDeliveryFailedCount": 0,"totalSentCount": 1},"reference": null,"scheduledDatetime": null,"type": "sms","typeDetails": {},"validity": null}' + + message = Client('', http_client).message('message-id') + + http_client.request.assert_called_once_with('messages/message-id', 'GET', None) + + self.assertEqual('mt', message.direction) + + def test_message_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).message_create('MessageBird', ['31612345678', '31687654321'], 'Hello World', {'datacoding': 'unicode'}) + + http_client.request.assert_called_once_with('messages', 'POST', {'datacoding': 'unicode', 'originator': 'MessageBird', 'body': 'Hello World', 'recipients': '31612345678,31687654321' }) diff --git a/tests/test_verify.py b/tests/test_verify.py new file mode 100644 index 0000000..eee9760 --- /dev/null +++ b/tests/test_verify.py @@ -0,0 +1,40 @@ +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 TestVerify(unittest.TestCase): + + def test_verify(self): + http_client = Mock() + http_client.request.return_value = '{"id": "verify-id","href": "https://rest.messagebird.com/verify/verify-id","recipient": 31612345678,"reference": "MyReference","messages": {"href": "https://rest.messagebird.com/messages/message-id"},"status": "verified","createdDatetime": "2017-05-30T12:39:50+00:00","validUntilDatetime": "2017-05-30T12:40:20+00:00"}' + + verify = Client('', http_client).verify('verify-id') + + http_client.request.assert_called_once_with('verify/verify-id', 'GET', None) + + self.assertEqual('verify-id', verify.id) + + def test_verify_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).verify_create('31612345678', {}) + + http_client.request.assert_called_once_with('verify', 'POST', {'recipient': '31612345678'}) + + def test_verify_verify(self): + http_client = Mock() + http_client.request.return_value = '{"id": "verify-id","href": "https://rest.messagebird.com/verify/verify-id","recipient": 31612345678,"reference": "MyReference","messages": {"href": "https://rest.messagebird.com/messages/63b168423592d681641eb07b76226648"},"status": "verified","createdDatetime": "2017-05-30T12:39:50+00:00","validUntilDatetime": "2017-05-30T12:40:20+00:00"}' + + verify = Client('', http_client).verify_verify('verify-id', 'secret') + + http_client.request.assert_called_once_with('verify/verify-id', 'GET', {'token': 'secret'}) + + self.assertEqual('verified', verify.status) diff --git a/tests/test_voicemessage.py b/tests/test_voicemessage.py new file mode 100644 index 0000000..c7f2a14 --- /dev/null +++ b/tests/test_voicemessage.py @@ -0,0 +1,30 @@ +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 TestVoicemessage(unittest.TestCase): + + def test_voicemessage(self): + http_client = Mock() + http_client.request.return_value = '{"body": "Hello World","createdDatetime": "2015-01-05T16:11:24+00:00","href": "https://rest.messagebird.com/voicemessages/voicemessage-id","id": "voicemessage-id","ifMachine": "continue","language": "en-gb","originator": "MessageBird","recipients": {"items": [{"recipient": 31612345678,"status": "calling","statusDatetime": "2015-01-05T16:11:24+00:00"}],"totalCount": 1,"totalDeliveredCount": 0,"totalDeliveryFailedCount": 0,"totalSentCount": 1},"reference": null,"repeat": 1,"scheduledDatetime": null,"voice": "female"}' + + voice_message = Client('', http_client).voice_message('voicemessage-id') + + http_client.request.assert_called_once_with('voicemessages/voicemessage-id', 'GET', None) + + self.assertEqual('voicemessage-id', voice_message.id) + + def test_voicemessage_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).voice_message_create(['31612345678', '31687654321'], 'Hello World', { 'reference': 'MyReference' }) + + http_client.request.assert_called_once_with('voicemessages', 'POST', {'body': 'Hello World', 'recipients': '31612345678,31687654321', 'reference': 'MyReference'}) From a343fe9512da3e4fbe954b18e0a7f6d80781cc3a Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Mon, 20 Aug 2018 12:21:21 +0200 Subject: [PATCH 10/13] Export ErrorException from messagebird --- messagebird/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messagebird/__init__.py b/messagebird/__init__.py index 9ea5a5a..cf57a14 100644 --- a/messagebird/__init__.py +++ b/messagebird/__init__.py @@ -1 +1 @@ -from messagebird.client import Client +from messagebird.client import Client, ErrorException From 24fbcbe62608a8c26d4696788fde8f48fc0b08ac Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Mon, 20 Aug 2018 12:22:51 +0200 Subject: [PATCH 11/13] Add Contact objects --- messagebird/base_list.py | 45 +++++++++++++++++++++ messagebird/contact.py | 87 ++++++++++++++++++++++++++++++++++++++++ tests/test_contact.py | 70 ++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 messagebird/base_list.py create mode 100644 messagebird/contact.py create mode 100644 tests/test_contact.py diff --git a/messagebird/base_list.py b/messagebird/base_list.py new file mode 100644 index 0000000..f77a290 --- /dev/null +++ b/messagebird/base_list.py @@ -0,0 +1,45 @@ +from messagebird.base import Base + + +class Links(Base): + + def __init__(self): + self.first = None + self.previous = None + self.next = None + self.last = None + + +class BaseList(Base): + + def __init__(self, item_type): + """When setting items, they are instantiated as objects of type item_type.""" + self.limit = None + self.offset = None + self.count = None + self.totalCount = None + self._links = None + self._items = None + + self.itemType = item_type + + @property + def links(self): + return self._links + + @links.setter + def links(self, value): + self._links = Links().load(value) + + @property + def items(self): + return self._items + + @items.setter + def items(self, value): + """Create typed objects from the dicts.""" + items = [] + for item in value: + items.append(self.itemType().load(item)) + + self._items = items diff --git a/messagebird/contact.py b/messagebird/contact.py new file mode 100644 index 0000000..e9428fa --- /dev/null +++ b/messagebird/contact.py @@ -0,0 +1,87 @@ +from messagebird.base import Base +from messagebird.base_list import BaseList + + +class CustomDetails(Base): + + def __init__(self): + self.custom1 = None + self.custom2 = None + self.custom3 = None + self.custom4 = None + + +class GroupReference(Base): + + def __init__(self): + self.href = None + self.totalCount = None + + +class MessageReference(Base): + + def __init__(self): + self.href = None + self.totalCount = None + + +class ContactList(BaseList): + + def __init__(self): + # Signal the BaseList that we're expecting items of type Contact... + super(ContactList, self).__init__(Contact) + + +class Contact(Base): + + def __init__(self): + self.id = None + self.href = None + self.msisdn = None + self.firstName = None + self.lastName = None + self._customDetails = None + self._groups = None + self._messages = None + self._createdDatetime = None + self._updatedDatetime = None + + @property + def customDetails(self): + return self._customDetails + + @customDetails.setter + def customDetails(self, value): + self._customDetails = CustomDetails().load(value) + + @property + def groups(self): + return self._groups + + @groups.setter + def groups(self, value): + self._groups = GroupReference().load(value) + + @property + def messages(self): + return self._messages + + @messages.setter + def messages(self, value): + self._messages = MessageReference().load(value) + + @property + def createdDatetime(self): + return self._createdDatetime + + @createdDatetime.setter + def createdDatetime(self, value): + self._createdDatetime = self.value_to_time(value) + + @property + def updatedDatetime(self): + return self._updatedDatetime + + @updatedDatetime.setter + def updatedDatetime(self, value): + self._updatedDatetime = self.value_to_time(value) diff --git a/tests/test_contact.py b/tests/test_contact.py new file mode 100644 index 0000000..8de461d --- /dev/null +++ b/tests/test_contact.py @@ -0,0 +1,70 @@ +import unittest +from messagebird import Client, ErrorException + +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 TestContact(unittest.TestCase): + + def test_contact(self): + http_client = Mock() + http_client.request.return_value = '{"id": "contact-id","href": "https://rest.messagebird.com/contacts/contact-id","msisdn": 31612345678,"firstName": "Foo","lastName": "Bar","customDetails": {"custom1": "First","custom2": "Second","custom3": "Third","custom4": "Fourth"},"groups": {"totalCount": 3,"href": "https://rest.messagebird.com/contacts/contact-id/groups"},"messages": {"totalCount": 5,"href": "https://rest.messagebird.com/contacts/contact-id/messages"},"createdDatetime": "2018-07-13T10:34:08+00:00","updatedDatetime": "2018-07-13T10:44:08+00:00"}' + + contact = Client('', http_client).contact('contact-id') + + http_client.request.assert_called_once_with('contacts/contact-id', 'GET', None) + + self.assertEqual(31612345678, contact.msisdn) + self.assertEqual('First', contact.customDetails.custom1) + self.assertEqual(3, contact.groups.totalCount) + self.assertEqual('https://rest.messagebird.com/contacts/contact-id/messages', contact.messages.href) + + def test_contact_create(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).contact_create(31612345678, {'firstName': 'Foo', 'custom3': 'Third'}) + + http_client.request.assert_called_once_with('contacts', 'POST', {'msisdn': 31612345678, 'firstName': 'Foo', 'custom3': 'Third'}) + + def test_contact_delete(self): + http_client = Mock() + http_client.request.return_value = '' + + Client('', http_client).contact_delete('contact-id') + + http_client.request.assert_called_once_with('contacts/contact-id', 'DELETE', None) + + def test_contact_delete_invalid(self): + http_client = Mock() + http_client.request.return_value = '{"errors": [{"code": 20,"description": "contact not found","parameter": null}]}' + + with self.assertRaises(ErrorException): + Client('', http_client).contact_delete('non-existent-contact-id') + + http_client.request.assert_called_once_with('contacts/non-existent-contact-id', 'DELETE', None) + + def test_contact_update(self): + http_client = Mock() + http_client.request.return_value = '{}' + + Client('', http_client).contact_update('contact-id', {'msisdn': 31687654321, 'custom4': 'fourth'}) + + http_client.request.assert_called_once_with('contacts/contact-id', 'PATCH', {'msisdn': 31687654321, 'custom4': 'fourth'}) + + def test_contact_list(self): + http_client = Mock() + http_client.request.return_value = '{"offset": 0,"limit": 20,"count": 2,"totalCount": 2,"links": {"first": "https://rest.messagebird.com/contacts?offset=0","previous": null,"next": null,"last": "https://rest.messagebird.com/contacts?offset=0"},"items": [{"id": "first-id","href": "https://rest.messagebird.com/contacts/first-id","msisdn": 31612345678,"firstName": "Foo","lastName": "Bar","customDetails": {"custom1": null,"custom2": null,"custom3": null,"custom4": null},"groups": {"totalCount": 0,"href": "https://rest.messagebird.com/contacts/first-id/groups"},"messages": {"totalCount": 0,"href": "https://rest.messagebird.com/contacts/first-id/messages"},"createdDatetime": "2018-07-13T10:34:08+00:00","updatedDatetime": "2018-07-13T10:34:08+00:00"},{"id": "second-id","href": "https://rest.messagebird.com/contacts/second-id","msisdn": 49612345678,"firstName": "Hello","lastName": "World","customDetails": {"custom1": null,"custom2": null,"custom3": null,"custom4": null},"groups": {"totalCount": 0,"href": "https://rest.messagebird.com/contacts/second-id/groups"},"messages": {"totalCount": 0,"href": "https://rest.messagebird.com/contacts/second-id/messages"},"createdDatetime": "2018-07-13T10:33:52+00:00","updatedDatetime": null}]}' + + contact_list = Client('', http_client).contact_list(10, 20) + + http_client.request.assert_called_once_with('contacts?limit=10&offset=20', 'GET', None) + + self.assertEqual(2, contact_list.totalCount) + self.assertEqual('https://rest.messagebird.com/contacts?offset=0', contact_list.links.first) + self.assertEqual('https://rest.messagebird.com/contacts/first-id/groups', contact_list.items[0].groups.href) From dbf1d252530b8a6a253ccb9d1ded58ca2df871ef Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Mon, 20 Aug 2018 12:23:15 +0200 Subject: [PATCH 12/13] Add Contact endpoints --- messagebird/client.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/messagebird/client.py b/messagebird/client.py index 64c25bf..ac329e3 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -3,6 +3,7 @@ from messagebird.base import Base from messagebird.balance import Balance +from messagebird.contact import Contact, ContactList from messagebird.error import Error from messagebird.hlr import HLR from messagebird.http_client import HttpClient @@ -43,6 +44,24 @@ def request(self, path, method='GET', params=None): return response_json + def request_plain_text(self, path, method='GET', params=None): + """Builds a request, gets a response and returns the body.""" + response_text = self.http_client.request(path, method, params) + + try: + # Try to decode the response to JSON to see if the API returned any + # errors. + response_json = json.loads(response_text) + + if 'errors' in response_json: + raise (ErrorException([Error().load(e) for e in response_json['errors']])) + except ValueError: + # Do nothing: json.loads throws if the input string is not valid JSON, + # which is expected. We'll just return the response body below. + pass + + return response_text + def balance(self): """Retrieve your balance.""" return Balance().load(self.request('balance')) @@ -109,3 +128,22 @@ def verify_create(self, recipient, params=None): def verify_verify(self, id, token): """Verify the token of a specific verification.""" return Verify().load(self.request('verify/' + str(id), params={'token': token})) + + def contact(self, id): + """Retrieve the information of a specific contact.""" + return Contact().load(self.request('contacts/' + str(id))) + + def contact_create(self, phonenumber, params=None): + if params is None: params = {} + params.update({'msisdn': phonenumber}) + return Contact().load(self.request('contacts', 'POST', params)) + + def contact_delete(self, id): + self.request_plain_text('contacts/' + str(id), 'DELETE') + + def contact_update(self, id, params=None): + self.request('contacts/' + str(id), 'PATCH', params) + + def contact_list(self, limit=0, offset=0): + query = 'limit='+str(limit)+'&offset='+str(offset) + return ContactList().load(self.request('contacts?'+query, 'GET', None)) From e0e8278b388d26c96c9bcfb21157ae9e88c343e4 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Mon, 20 Aug 2018 16:53:31 +0200 Subject: [PATCH 13/13] Fix contact_update to not expect a JSON response --- messagebird/client.py | 2 +- tests/test_contact.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/messagebird/client.py b/messagebird/client.py index ac329e3..0d3dabd 100644 --- a/messagebird/client.py +++ b/messagebird/client.py @@ -142,7 +142,7 @@ def contact_delete(self, id): self.request_plain_text('contacts/' + str(id), 'DELETE') def contact_update(self, id, params=None): - self.request('contacts/' + str(id), 'PATCH', params) + self.request_plain_text('contacts/' + str(id), 'PATCH', params) def contact_list(self, limit=0, offset=0): query = 'limit='+str(limit)+'&offset='+str(offset) diff --git a/tests/test_contact.py b/tests/test_contact.py index 8de461d..12c265c 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -51,7 +51,7 @@ def test_contact_delete_invalid(self): def test_contact_update(self): http_client = Mock() - http_client.request.return_value = '{}' + http_client.request.return_value = '' Client('', http_client).contact_update('contact-id', {'msisdn': 31687654321, 'custom4': 'fourth'})