Skip to content

Commit

Permalink
Merge pull request #60 from geeknam/requests
Browse files Browse the repository at this point in the history
Add requests as a new requirement.
  • Loading branch information
geeknam committed May 29, 2015
2 parents 0af0b12 + fc4c999 commit 810ebb1
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 102 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
language: python
python:
- "2.7"
- "3.4"
install:
- pip install mock
- pip install coveralls
Expand Down
11 changes: 9 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
python-gcm
======================

.. image:: https://img.shields.io/pypi/v/python-gcm.svg
:target: https://pypi.python.org/pypi/python-gcm
.. image:: https://img.shields.io/pypi/dm/python-gcm.svg
:target: https://pypi.python.org/pypi/python-gcm
.. image:: https://secure.travis-ci.org/geeknam/python-gcm.png?branch=master
:alt: Build Status
:target: http://travis-ci.org/geeknam/python-gcm
.. image:: https://landscape.io/github/geeknam/python-gcm/master/landscape.png
:target: https://landscape.io/github/geeknam/python-gcm/master
:alt: Code Health

.. image:: https://coveralls.io/repos/geeknam/python-gcm/badge.svg?branch=master
:target: https://coveralls.io/r/geeknam/python-gcm
.. image:: https://img.shields.io/gratipay/geeknam.svg
:target: https://gratipay.com/geeknam/

Python client for Google Cloud Messaging for Android (GCM)

Expand All @@ -25,6 +31,7 @@ Features
* Resend messages using exponential back-off
* Proxy support
* Easily handle errors
* Uses `requests` from version > 0.2

Usage
------------
Expand Down
107 changes: 54 additions & 53 deletions gcm/gcm.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import urllib
import urllib2
import requests
import json
from collections import defaultdict
import time
import random

try:
from urllib import quote_plus
except ImportError:
from urllib.parse import quote_plus


GCM_URL = 'https://android.googleapis.com/gcm/send'


Expand Down Expand Up @@ -80,17 +85,14 @@ def urlencode_utf8(params):
UTF-8 safe variant of urllib.urlencode.
http://stackoverflow.com/a/8152242
"""

if hasattr(params, 'items'):
params = params.items()

params = (
'='.join((
urllib.quote_plus(k.encode('utf8'), safe='/'),
urllib.quote_plus(v.encode('utf8'), safe='/')
quote_plus(k.encode('utf8'), safe='/'),
quote_plus(v.encode('utf8'), safe='/')
)) for k, v in params
)

return '&'.join(params)


Expand All @@ -99,6 +101,8 @@ class GCM(object):
# Timeunit is milliseconds.
BACKOFF_INITIAL_DELAY = 1000
MAX_BACKOFF_DELAY = 1024000
# TTL in seconds
GCM_TTL = 2419200

def __init__(self, api_key, url=GCM_URL, proxy=None):
""" api_key : google api key
Expand All @@ -107,18 +111,20 @@ def __init__(self, api_key, url=GCM_URL, proxy=None):
"""
self.api_key = api_key
self.url = url
if proxy:
if isinstance(proxy, basestring):
protocol = url.split(':')[0]
proxy = {protocol: proxy}

auth = urllib2.HTTPBasicAuthHandler()
opener = urllib2.build_opener(
urllib2.ProxyHandler(proxy), auth, urllib2.HTTPHandler)
urllib2.install_opener(opener)
if isinstance(proxy, str):
protocol = url.split(':')[0]
self.proxy = {protocol: proxy}
else:
self.proxy = proxy

self.headers = {
'Authorization': 'key=%s' % self.api_key,
}


def construct_payload(self, registration_ids, data=None, collapse_key=None,
delay_while_idle=False, time_to_live=None, is_json=True, dry_run=False):
delay_while_idle=False, time_to_live=None, is_json=True, dry_run=False):
"""
Construct the dictionary mapping of parameters.
Encodes the dictionary into JSON if for json requests.
Expand All @@ -129,21 +135,19 @@ def construct_payload(self, registration_ids, data=None, collapse_key=None,
"""

if time_to_live:
four_weeks_in_secs = 2419200
if not (0 <= time_to_live <= four_weeks_in_secs):
if not (0 <= time_to_live <= self.GCM_TTL):
raise GCMInvalidTtlException("Invalid time to live value")

payload = {}
if is_json:
payload = {'registration_ids': registration_ids}
payload['registration_ids'] = registration_ids
if data:
payload['data'] = data
else:
payload = {'registration_id': registration_ids}
payload['registration_id'] = registration_ids
if data:
plaintext_data = data.copy()
for k in plaintext_data.keys():
plaintext_data['data.%s' % k] = plaintext_data.pop(k)
payload.update(plaintext_data)
for key, value in data.items():
payload['data.%s' % key] = value

if delay_while_idle:
payload['delay_while_idle'] = delay_while_idle
Expand Down Expand Up @@ -172,39 +176,38 @@ def make_request(self, data, is_json=True):
:raises GCMConnectionException: if GCM is screwed
"""

headers = {
'Authorization': 'key=%s' % self.api_key,
}
# Default Content-Type is defaulted to
# Default Content-Type is
# application/x-www-form-urlencoded;charset=UTF-8
if is_json:
headers['Content-Type'] = 'application/json'
self.headers['Content-Type'] = 'application/json'

if not is_json:
data = urlencode_utf8(data)
req = urllib2.Request(self.url, data, headers)

try:
response = urllib2.urlopen(req).read()
except urllib2.HTTPError as e:
if e.code == 400:
raise GCMMalformedJsonException(
"The request could not be parsed as JSON")
elif e.code == 401:
raise GCMAuthenticationException(
"There was an error authenticating the sender account")
elif e.code == 503:
raise GCMUnavailableException("GCM service is unavailable")
else:
error = "GCM service error: %d" % e.code
raise GCMUnavailableException(error)
except urllib2.URLError as e:
raise GCMConnectionException(
"There was an internal error in the GCM server while trying to process the request")

if is_json:
response = json.loads(response)
return response
response = requests.post(
self.url, data=data, headers=self.headers,
proxies=self.proxy
)
# Successful response
if response.status_code == 200:
if is_json:
response = response.json()
else:
response = response.content
return response

# Failures
if response.status_code == 400:
raise GCMMalformedJsonException(
"The request could not be parsed as JSON")
elif response.status_code == 401:
raise GCMAuthenticationException(
"There was an error authenticating the sender account")
elif response.status_code == 503:
raise GCMUnavailableException("GCM service is unavailable")
else:
error = "GCM service error: %d" % response.status_code
raise GCMUnavailableException(error)

def raise_error(self, error):
if error == 'InvalidRegistration':
Expand Down Expand Up @@ -305,7 +308,6 @@ def json_request(self, registration_ids, data=None, collapse_key=None,
raise GCMTooManyRegIdsException(
"Exceded number of registration_ids")

attempt = 0
backoff = self.BACKOFF_INITIAL_DELAY
for attempt in range(retries):
payload = self.construct_payload(
Expand All @@ -324,5 +326,4 @@ def json_request(self, registration_ids, data=None, collapse_key=None,
backoff *= 2
else:
break

return info
110 changes: 66 additions & 44 deletions gcm/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,6 @@ def side_effect(*args, **kwargs):
return side_effect


class MockResponse(object):
"""
Mock urllib2.urlopen response.
http://stackoverflow.com/a/2276727
"""
def __init__(self, resp_data, code=200, msg='OK'):
self.resp_data = resp_data
self.code = code
self.msg = msg
self.headers = {'content-type': 'text/xml; charset=utf-8'}

def read(self):
return self.resp_data

def getcode(self):
return self.code


class GCMTest(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -71,6 +53,21 @@ def setUp(self):
}
time.sleep = MagicMock()

def test_gcm_proxy(self):
self.gcm = GCM('123api', proxy='http://domain.com:8888')
self.assertEqual(self.gcm.proxy, {
'https': 'http://domain.com:8888'
})

self.gcm = GCM('123api', proxy={
'http': 'http://domain.com:8888',
'https': 'https://domain.com:8888'
})
self.assertEqual(self.gcm.proxy, {
'http': 'http://domain.com:8888',
'https': 'https://domain.com:8888'
})

def test_construct_payload(self):
res = self.gcm.construct_payload(
registration_ids=['1', '2'], data=self.data, collapse_key='foo',
Expand All @@ -90,8 +87,9 @@ def test_json_payload(self):
self.assertEqual(payload['registration_ids'], reg_ids)

def test_plaintext_payload(self):
result = self.gcm.construct_payload(registration_ids='1234', data=self.data, is_json=False)

result = self.gcm.construct_payload(
registration_ids='1234', data=self.data, is_json=False
)
self.assertIn('registration_id', result)
self.assertIn('data.param1', result)
self.assertIn('data.param2', result)
Expand Down Expand Up @@ -180,43 +178,67 @@ def test_handle_plaintext_response(self):
res = self.gcm.handle_plaintext_response(response)
self.assertEqual(res, '3456')

@patch('urllib2.urlopen')
def test_make_request_plaintext(self, urlopen_mock):
@patch('requests.post')
def test_make_request_header(self, mock_request):
""" Test plaintext make_request. """

# Set mock value for urlopen return value
urlopen_mock.return_value = MockResponse('blah')

mock_request.return_value.status_code = 200
mock_request.return_value.content = "OK"
# Perform request
response = self.gcm.make_request({'message': 'test'}, is_json=False)
self.gcm.make_request(
{'message': 'test'}, is_json=True
)
self.assertEqual(self.gcm.headers['Content-Type'],
'application/json'
)
self.assertTrue(mock_request.return_value.json.called)

# Get request (first positional argument to urlopen)
# Ref: http://www.voidspace.org.uk/python/mock/mock.html#mock.Mock.call_args
request = urlopen_mock.call_args[0][0]

# Test encoded data
encoded_data = request.get_data()
self.assertEquals(
encoded_data, 'message=test'
)
@patch('requests.post')
def test_make_request_plaintext(self, mock_request):
""" Test plaintext make_request. """

# Assert return value
self.assertEquals(
response,
'blah'
mock_request.return_value.status_code = 200
mock_request.return_value.content = "OK"
# Perform request
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)
self.assertEqual(response, "OK")

mock_request.return_value.status_code = 400
with self.assertRaises(GCMMalformedJsonException):
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)

@patch('urllib2.urlopen')
def test_make_request_unicode(self, urlopen_mock):
""" Regression: Test make_request with unicode payload. """
mock_request.return_value.status_code = 401
with self.assertRaises(GCMAuthenticationException):
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)

# Unicode character in data
mock_request.return_value.status_code = 503
with self.assertRaises(GCMUnavailableException):
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)

@patch('requests.api.request')
def test_make_request_unicode(self, mock_request):
""" Test make_request with unicode payload. """
data = {
'message': u'\x80abc'
}

self.gcm.make_request(data, is_json=False)
try:
self.gcm.make_request(data, is_json=False)
except:
pass
self.assertTrue(mock_request.called)
self.assertEqual(
mock_request.call_args[1]['data'],
'message=%C2%80abc'
)

def test_retry_plaintext_request_ok(self):
returns = [GCMUnavailableException(), GCMUnavailableException(), 'id=123456789']
Expand Down
9 changes: 6 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

setup(
name='python-gcm',
version='0.1.4',
version='0.2',
packages=['gcm'],
license=open('LICENSE').read(),
author='Minh Nam Ngo',
author_email='nam@namis.me',
author='Nam Ngo',
author_email='nam@kogan.com.au',
url='http://blog.namis.me/python-gcm/',
description='Python client for Google Cloud Messaging for Android (GCM)',
long_description=open('README.rst').read(),
keywords='android gcm push notification google cloud messaging',
install_requires=[
'requests',
],
tests_require = ['mock'],
)

0 comments on commit 810ebb1

Please sign in to comment.