Skip to content
This repository has been archived by the owner on Oct 27, 2022. It is now read-only.

Commit

Permalink
Merge branch 'develop' into release/0.2.x
Browse files Browse the repository at this point in the history
Conflicts:
	go_http/__init__.py
	setup.py
  • Loading branch information
hodgestar committed Feb 13, 2015
2 parents 34bcff9 + e0f5649 commit 2969265
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 16 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ install:
- "pip install coveralls --use-wheel"
- "python setup.py install"
script:
- "pep8 --repeat go_http"
- "pyflakes go_http"
- "flake8 go_http"
- "py.test --cov=go_http go_http"
after_success:
- coveralls
4 changes: 2 additions & 2 deletions go_http/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Vumi Go HTTP API client library."""

__version__ = "0.2.2"

from .send import HttpApiSender, LoggingSender

__version__ = "0.2.3"

__all__ = [
'HttpApiSender', 'LoggingSender',
]
68 changes: 62 additions & 6 deletions go_http/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,46 @@ def __init__(self, auth_token, api_url=None, session=None):
session = requests.Session()
self.session = session

def _api_request(self, method, api_collection, api_path, data=None):
def _api_request(
self, method, api_collection, api_path, data=None, params=None):
url = "%s/%s/%s" % (self.api_url, api_collection, api_path)
headers = {
"Content-Type": "application/json; charset=utf-8",
"Authorization": "Bearer %s" % (self.auth_token,),
}
if data is not None:
data = json.dumps(data)
r = self.session.request(method, url, data=data, headers=headers)
r = self.session.request(
method, url, data=data, headers=headers, params=params)
r.raise_for_status()
return r.json()

def contacts(self, start_cursor=None):
"""
Retrieve all contacts.
This uses the API's paginated contact download.
:param start_cursor:
An optional parameter that declares the cursor to start fetching
the contacts from.
:returns:
An iterator over all contacts.
"""
if start_cursor:
page = self._api_request(
"GET", "contacts", "?cursor=%s" % start_cursor)
else:
page = self._api_request("GET", "contacts", "")
while True:
for contact in page['data']:
yield contact
if page['cursor'] is None:
break
page = self._api_request(
"GET", "contacts", "?cursor=%s" % page['cursor'])

def create_contact(self, contact_data):
"""
Create a contact.
Expand All @@ -60,14 +88,42 @@ def create_contact(self, contact_data):
"""
return self._api_request("POST", "contacts", "", contact_data)

def get_contact(self, contact_key):
def _contact_by_key(self, contact_key):
return self._api_request("GET", "contacts", contact_key)

def _contact_by_field(self, field, value):
contact = self._api_request(
"GET", "contacts", "", params={'query': '%s=%s' % (field, value)})
return contact.get('data')[0]

def get_contact(self, *args, **kw):
"""
Get a contact.
Get a contact. May either be called as ``.get_contact(contact_key)``
to get a contact from its key, or ``.get_contact(field=value)``, to
get the contact from an address field ``field`` having a value
``value``.
Contact key example:
contact = api.get_contact('abcdef123456')
Field/value example:
contact = api.get_contact(msisdn='+12345')
:param str contact_key:
Key for the contact to get.
"""
return self._api_request("GET", "contacts", contact_key)
:param str field:
``field`` is the address field that is searched on (e.g. ``msisdn``
, ``twitter_handle``). The value of ``field`` is the value to
search for (e.g. ``+12345``, `@foobar``).
"""
if not kw and len(args) == 1:
return self._contact_by_key(args[0])
elif len(kw) == 1 and not args:
field, value = kw.items()[0]
return self._contact_by_field(field, value)
raise ValueError(
"get_contact may either be called as .get_contact(contact_key) or"
" .get_contact(field=value)")

def update_contact(self, contact_key, update_data):
"""
Expand Down
13 changes: 13 additions & 0 deletions go_http/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class UserOptedOutException(Exception):
"""
Exception raised if a message is sent to a recipient who has opted out.
Attributes:
to_addr - The address of the opted out recipient
message - The message content
reason - The error reason given by the API
"""
def __init__(self, to_addr, message, reason):
self.to_addr = to_addr
self.message = message
self.reason = reason
15 changes: 14 additions & 1 deletion go_http/send.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import uuid

import requests
from requests.exceptions import HTTPError

from go_http.exceptions import UserOptedOutException


class HttpApiSender(object):
Expand Down Expand Up @@ -66,7 +69,17 @@ def send_text(self, to_addr, content):
"content": content,
"to_addr": to_addr,
}
return self._api_request('messages.json', data)
try:
return self._api_request('messages.json', data)
except HTTPError as e:
response = e.response.json()
if (
e.response.status_code != 400 or
'opted out' not in response.get('reason', '') or
response.get('success')):
raise e
raise UserOptedOutException(
to_addr, content, response.get('reason'))

def fire_metric(self, metric, value, agg="last"):
""" Fire a value for a metric.
Expand Down
80 changes: 79 additions & 1 deletion go_http/tests/test_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ class TestContactsApiClient(TestCase):
API_URL = "http://example.com/go"
AUTH_TOKEN = "auth_token"

MAX_CONTACTS_PER_PAGE = 10

def setUp(self):
self.contacts_data = {}
self.groups_data = {}
self.contacts_backend = FakeContactsApi(
"go/", self.AUTH_TOKEN, self.contacts_data, self.groups_data)
"go/", self.AUTH_TOKEN, self.contacts_data, self.groups_data,
contacts_limit=self.MAX_CONTACTS_PER_PAGE)
self.session = TestSession()
adapter = FakeContactsApiAdapter(self.contacts_backend)
self.session.mount(self.API_URL, adapter)
Expand Down Expand Up @@ -124,6 +127,59 @@ def test_auth_failure(self):
contacts = self.make_client(auth_token="bogus_token")
self.assert_http_error(403, contacts.get_contact, "foo")

def test_contacts_single_page(self):
expected_contact = self.make_existing_contact({
u"msisdn": u"+15556483",
u"name": u"Arthur",
u"surname": u"of Camelot",
})
contacts_api = self.make_client()
[contact] = list(contacts_api.contacts())
self.assertEqual(contact, expected_contact)

def test_contacts_no_results(self):
contacts_api = self.make_client()
contacts = list(contacts_api.contacts())
self.assertEqual(contacts, [])

def test_contacts_multiple_pages(self):
expected_contacts = []
for i in range(self.MAX_CONTACTS_PER_PAGE + 1):
expected_contacts.append(self.make_existing_contact({
u"msisdn": u"+155564%d" % (i,),
u"name": u"Arthur",
u"surname": u"of Camelot",
}))
contacts_api = self.make_client()
contacts = list(contacts_api.contacts())

contacts.sort(key=lambda d: d['msisdn'])
expected_contacts.sort(key=lambda d: d['msisdn'])

self.assertEqual(contacts, expected_contacts)

def test_contacts_multiple_pages_with_cursor(self):
expected_contacts = []
for i in range(self.MAX_CONTACTS_PER_PAGE):
expected_contacts.append(self.make_existing_contact({
u"msisdn": u"+155564%d" % (i,),
u"name": u"Arthur",
u"surname": u"of Camelot",
}))
expected_contacts.append(self.make_existing_contact({
u"msisdn": u"+15556",
u"name": u"Arthur",
u"surname": u"of Camelot",
}))
contacts_api = self.make_client()
first_page = contacts_api._api_request("GET", "contacts", "")
cursor = first_page['cursor']
contacts = list(contacts_api.contacts(start_cursor=cursor))
contacts.extend(first_page['data'])
contacts.sort(key=lambda d: d['msisdn'])
expected_contacts.sort(key=lambda d: d['msisdn'])
self.assertEqual(contacts, expected_contacts)

def test_create_contact(self):
contacts = self.make_client()
contact_data = {
Expand Down Expand Up @@ -199,6 +255,28 @@ def test_get_missing_contact(self):
contacts = self.make_client()
self.assert_http_error(404, contacts.get_contact, "foo")

def test_get_contact_from_field(self):
contacts = self.make_client()
existing_contact = self.make_existing_contact({
u"msisdn": u"+15556483",
u"name": u"Arthur",
u"surname": u"of Camelot",
})

contact = contacts.get_contact(msisdn='+15556483')
self.assertEqual(contact, existing_contact)

def test_get_contact_from_field_missing(self):
contacts = self.make_client()
self.make_existing_contact({
u"msisdn": u"+15556483",
u"name": u"Arthur",
u"surname": u"of Camelot",
})

self.assert_http_error(
400, contacts.get_contact, msisdn='+12345')

def test_update_contact(self):
contacts = self.make_client()
existing_contact = self.make_existing_contact({
Expand Down
45 changes: 45 additions & 0 deletions go_http/tests/test_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from requests_testadapter import TestAdapter, TestSession

from go_http.send import HttpApiSender, LoggingSender
from go_http.exceptions import UserOptedOutException

from requests.exceptions import HTTPError


class RecordingAdapter(TestAdapter):
Expand Down Expand Up @@ -64,6 +67,48 @@ def test_send_text(self):
data={"content": "Hello!", "to_addr": "to-addr-1"},
headers={"Authorization": u'Basic YWNjLWtleTpjb252LXRva2Vu'})

def test_send_to_opted_out(self):
"""
UserOptedOutException raised for sending messages to opted out
recipients
"""
self.session.mount(
"http://example.com/api/v1/go/http_api_nostream/conv-key/"
"messages.json", TestAdapter(
json.dumps({
"success": False,
"reason": "Recipient with msisdn to-addr-1 has opted out"}
),
status=400))
try:
self.sender.send_text('to-addr-1', "foo")
except UserOptedOutException as e:
self.assertEqual(e.to_addr, 'to-addr-1')
self.assertEqual(e.message, 'foo')
self.assertEqual(
e.reason, 'Recipient with msisdn to-addr-1 has opted out')

def test_send_to_other_http_error(self):
"""
HTTP errors should not be raised as UserOptedOutExceptions if they are
not user opted out errors.
"""
self.session.mount(
"http://example.com/api/v1/go/http_api_nostream/conv-key/"
"messages.json", TestAdapter(
json.dumps({
"success": False,
"reason": "No unicorns were found"
}),
status=400))
try:
self.sender.send_text('to-addr-1', 'foo')
except HTTPError as e:
self.assertEqual(e.response.status_code, 400)
response = e.response.json()
self.assertFalse(response['success'])
self.assertEqual(response['reason'], "No unicorns were found")

def test_fire_metric(self):
adapter = RecordingAdapter(
json.dumps({"success": True, "reason": "Yay"}))
Expand Down
5 changes: 2 additions & 3 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
pep8
pyflakes
flake8
coverage
pytest
pytest-xdist
pytest-cov
requests_testadapter
fake-go-contacts>=0.1.3
fake-go-contacts>=0.1.8
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name="go_http",
version="0.2.2",
version="0.2.3",
url='http://github.com/praekelt/go-http-api',
license='BSD',
description="A client library for Vumi Go's HTTP API",
Expand Down

0 comments on commit 2969265

Please sign in to comment.