Skip to content
Merged
17 changes: 17 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
language: python
python:
- 'pypy2.7'
- 'pypy3.5'
- '2.7'
- '3.3'
- '3.6'
- 'nightly'
install:
- pip install mock==2.0
- pip install requests
script:
- python -m unittest discover -s tests/ -p test_*.py -v
matrix:
allow_failures:
- python: 'nightly'
- python: 'pypy2.7'
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion messagebird/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from messagebird.client import Client
from messagebird.client import Client, ErrorException
45 changes: 45 additions & 0 deletions messagebird/base_list.py
Original file line number Diff line number Diff line change
@@ -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
76 changes: 49 additions & 27 deletions messagebird/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
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.contact import Contact, ContactList
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
Expand All @@ -19,6 +15,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):
Expand All @@ -29,35 +26,41 @@ 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]

if http_client is None:
self.http_client = HttpClient(ENDPOINT, access_key, USER_AGENT)
else:
self.http_client = http_client

def request(self, path, method='GET', params=None):
if params is None: params = {}
url = urljoin(ENDPOINT, path)
"""Builds a request, gets a response and decodes it."""
response_text = self.http_client.request(path, method, params)
response_json = json.loads(response_text)

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 'errors' in response_json:
raise(ErrorException([Error().load(e) for e in response_json['errors']]))

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))
return response_json

if response.status_code in self._supported_status_codes:
json_response = response.json()
else:
response.raise_for_status()
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 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']]))
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 json_response
return response_text

def balance(self):
"""Retrieve your balance."""
Expand Down Expand Up @@ -125,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_plain_text('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))
87 changes: 87 additions & 0 deletions messagebird/contact.py
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions messagebird/http_client.py
Original file line number Diff line number Diff line change
@@ -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
Empty file added tests/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions tests/test_balance.py
Original file line number Diff line number Diff line change
@@ -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)
Loading