Skip to content

Commit 810ebb1

Browse files
committed
Merge pull request #60 from geeknam/requests
Add requests as a new requirement.
2 parents 0af0b12 + fc4c999 commit 810ebb1

File tree

5 files changed

+136
-102
lines changed

5 files changed

+136
-102
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
language: python
22
python:
33
- "2.7"
4+
- "3.4"
45
install:
56
- pip install mock
67
- pip install coveralls

README.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
python-gcm
22
======================
3-
3+
.. image:: https://img.shields.io/pypi/v/python-gcm.svg
4+
:target: https://pypi.python.org/pypi/python-gcm
5+
.. image:: https://img.shields.io/pypi/dm/python-gcm.svg
6+
:target: https://pypi.python.org/pypi/python-gcm
47
.. image:: https://secure.travis-ci.org/geeknam/python-gcm.png?branch=master
58
:alt: Build Status
69
:target: http://travis-ci.org/geeknam/python-gcm
710
.. image:: https://landscape.io/github/geeknam/python-gcm/master/landscape.png
811
:target: https://landscape.io/github/geeknam/python-gcm/master
912
:alt: Code Health
10-
13+
.. image:: https://coveralls.io/repos/geeknam/python-gcm/badge.svg?branch=master
14+
:target: https://coveralls.io/r/geeknam/python-gcm
15+
.. image:: https://img.shields.io/gratipay/geeknam.svg
16+
:target: https://gratipay.com/geeknam/
1117

1218
Python client for Google Cloud Messaging for Android (GCM)
1319

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

2936
Usage
3037
------------

gcm/gcm.py

+54-53
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import urllib
2-
import urllib2
1+
import requests
32
import json
43
from collections import defaultdict
54
import time
65
import random
76

7+
try:
8+
from urllib import quote_plus
9+
except ImportError:
10+
from urllib.parse import quote_plus
11+
12+
813
GCM_URL = 'https://android.googleapis.com/gcm/send'
914

1015

@@ -80,17 +85,14 @@ def urlencode_utf8(params):
8085
UTF-8 safe variant of urllib.urlencode.
8186
http://stackoverflow.com/a/8152242
8287
"""
83-
8488
if hasattr(params, 'items'):
8589
params = params.items()
86-
8790
params = (
8891
'='.join((
89-
urllib.quote_plus(k.encode('utf8'), safe='/'),
90-
urllib.quote_plus(v.encode('utf8'), safe='/')
92+
quote_plus(k.encode('utf8'), safe='/'),
93+
quote_plus(v.encode('utf8'), safe='/')
9194
)) for k, v in params
9295
)
93-
9496
return '&'.join(params)
9597

9698

@@ -99,6 +101,8 @@ class GCM(object):
99101
# Timeunit is milliseconds.
100102
BACKOFF_INITIAL_DELAY = 1000
101103
MAX_BACKOFF_DELAY = 1024000
104+
# TTL in seconds
105+
GCM_TTL = 2419200
102106

103107
def __init__(self, api_key, url=GCM_URL, proxy=None):
104108
""" api_key : google api key
@@ -107,18 +111,20 @@ def __init__(self, api_key, url=GCM_URL, proxy=None):
107111
"""
108112
self.api_key = api_key
109113
self.url = url
110-
if proxy:
111-
if isinstance(proxy, basestring):
112-
protocol = url.split(':')[0]
113-
proxy = {protocol: proxy}
114114

115-
auth = urllib2.HTTPBasicAuthHandler()
116-
opener = urllib2.build_opener(
117-
urllib2.ProxyHandler(proxy), auth, urllib2.HTTPHandler)
118-
urllib2.install_opener(opener)
115+
if isinstance(proxy, str):
116+
protocol = url.split(':')[0]
117+
self.proxy = {protocol: proxy}
118+
else:
119+
self.proxy = proxy
120+
121+
self.headers = {
122+
'Authorization': 'key=%s' % self.api_key,
123+
}
124+
119125

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

131137
if time_to_live:
132-
four_weeks_in_secs = 2419200
133-
if not (0 <= time_to_live <= four_weeks_in_secs):
138+
if not (0 <= time_to_live <= self.GCM_TTL):
134139
raise GCMInvalidTtlException("Invalid time to live value")
135140

141+
payload = {}
136142
if is_json:
137-
payload = {'registration_ids': registration_ids}
143+
payload['registration_ids'] = registration_ids
138144
if data:
139145
payload['data'] = data
140146
else:
141-
payload = {'registration_id': registration_ids}
147+
payload['registration_id'] = registration_ids
142148
if data:
143-
plaintext_data = data.copy()
144-
for k in plaintext_data.keys():
145-
plaintext_data['data.%s' % k] = plaintext_data.pop(k)
146-
payload.update(plaintext_data)
149+
for key, value in data.items():
150+
payload['data.%s' % key] = value
147151

148152
if delay_while_idle:
149153
payload['delay_while_idle'] = delay_while_idle
@@ -172,39 +176,38 @@ def make_request(self, data, is_json=True):
172176
:raises GCMConnectionException: if GCM is screwed
173177
"""
174178

175-
headers = {
176-
'Authorization': 'key=%s' % self.api_key,
177-
}
178-
# Default Content-Type is defaulted to
179+
# Default Content-Type is
179180
# application/x-www-form-urlencoded;charset=UTF-8
180181
if is_json:
181-
headers['Content-Type'] = 'application/json'
182+
self.headers['Content-Type'] = 'application/json'
182183

183184
if not is_json:
184185
data = urlencode_utf8(data)
185-
req = urllib2.Request(self.url, data, headers)
186-
187-
try:
188-
response = urllib2.urlopen(req).read()
189-
except urllib2.HTTPError as e:
190-
if e.code == 400:
191-
raise GCMMalformedJsonException(
192-
"The request could not be parsed as JSON")
193-
elif e.code == 401:
194-
raise GCMAuthenticationException(
195-
"There was an error authenticating the sender account")
196-
elif e.code == 503:
197-
raise GCMUnavailableException("GCM service is unavailable")
198-
else:
199-
error = "GCM service error: %d" % e.code
200-
raise GCMUnavailableException(error)
201-
except urllib2.URLError as e:
202-
raise GCMConnectionException(
203-
"There was an internal error in the GCM server while trying to process the request")
204186

205-
if is_json:
206-
response = json.loads(response)
207-
return response
187+
response = requests.post(
188+
self.url, data=data, headers=self.headers,
189+
proxies=self.proxy
190+
)
191+
# Successful response
192+
if response.status_code == 200:
193+
if is_json:
194+
response = response.json()
195+
else:
196+
response = response.content
197+
return response
198+
199+
# Failures
200+
if response.status_code == 400:
201+
raise GCMMalformedJsonException(
202+
"The request could not be parsed as JSON")
203+
elif response.status_code == 401:
204+
raise GCMAuthenticationException(
205+
"There was an error authenticating the sender account")
206+
elif response.status_code == 503:
207+
raise GCMUnavailableException("GCM service is unavailable")
208+
else:
209+
error = "GCM service error: %d" % response.status_code
210+
raise GCMUnavailableException(error)
208211

209212
def raise_error(self, error):
210213
if error == 'InvalidRegistration':
@@ -305,7 +308,6 @@ def json_request(self, registration_ids, data=None, collapse_key=None,
305308
raise GCMTooManyRegIdsException(
306309
"Exceded number of registration_ids")
307310

308-
attempt = 0
309311
backoff = self.BACKOFF_INITIAL_DELAY
310312
for attempt in range(retries):
311313
payload = self.construct_payload(
@@ -324,5 +326,4 @@ def json_request(self, registration_ids, data=None, collapse_key=None,
324326
backoff *= 2
325327
else:
326328
break
327-
328329
return info

gcm/test.py

+66-44
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,6 @@ def side_effect(*args, **kwargs):
1515
return side_effect
1616

1717

18-
class MockResponse(object):
19-
"""
20-
Mock urllib2.urlopen response.
21-
http://stackoverflow.com/a/2276727
22-
"""
23-
def __init__(self, resp_data, code=200, msg='OK'):
24-
self.resp_data = resp_data
25-
self.code = code
26-
self.msg = msg
27-
self.headers = {'content-type': 'text/xml; charset=utf-8'}
28-
29-
def read(self):
30-
return self.resp_data
31-
32-
def getcode(self):
33-
return self.code
34-
35-
3618
class GCMTest(unittest.TestCase):
3719

3820
def setUp(self):
@@ -71,6 +53,21 @@ def setUp(self):
7153
}
7254
time.sleep = MagicMock()
7355

56+
def test_gcm_proxy(self):
57+
self.gcm = GCM('123api', proxy='http://domain.com:8888')
58+
self.assertEqual(self.gcm.proxy, {
59+
'https': 'http://domain.com:8888'
60+
})
61+
62+
self.gcm = GCM('123api', proxy={
63+
'http': 'http://domain.com:8888',
64+
'https': 'https://domain.com:8888'
65+
})
66+
self.assertEqual(self.gcm.proxy, {
67+
'http': 'http://domain.com:8888',
68+
'https': 'https://domain.com:8888'
69+
})
70+
7471
def test_construct_payload(self):
7572
res = self.gcm.construct_payload(
7673
registration_ids=['1', '2'], data=self.data, collapse_key='foo',
@@ -90,8 +87,9 @@ def test_json_payload(self):
9087
self.assertEqual(payload['registration_ids'], reg_ids)
9188

9289
def test_plaintext_payload(self):
93-
result = self.gcm.construct_payload(registration_ids='1234', data=self.data, is_json=False)
94-
90+
result = self.gcm.construct_payload(
91+
registration_ids='1234', data=self.data, is_json=False
92+
)
9593
self.assertIn('registration_id', result)
9694
self.assertIn('data.param1', result)
9795
self.assertIn('data.param2', result)
@@ -180,43 +178,67 @@ def test_handle_plaintext_response(self):
180178
res = self.gcm.handle_plaintext_response(response)
181179
self.assertEqual(res, '3456')
182180

183-
@patch('urllib2.urlopen')
184-
def test_make_request_plaintext(self, urlopen_mock):
181+
@patch('requests.post')
182+
def test_make_request_header(self, mock_request):
185183
""" Test plaintext make_request. """
186184

187-
# Set mock value for urlopen return value
188-
urlopen_mock.return_value = MockResponse('blah')
189-
185+
mock_request.return_value.status_code = 200
186+
mock_request.return_value.content = "OK"
190187
# Perform request
191-
response = self.gcm.make_request({'message': 'test'}, is_json=False)
188+
self.gcm.make_request(
189+
{'message': 'test'}, is_json=True
190+
)
191+
self.assertEqual(self.gcm.headers['Content-Type'],
192+
'application/json'
193+
)
194+
self.assertTrue(mock_request.return_value.json.called)
192195

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

197-
# Test encoded data
198-
encoded_data = request.get_data()
199-
self.assertEquals(
200-
encoded_data, 'message=test'
201-
)
197+
@patch('requests.post')
198+
def test_make_request_plaintext(self, mock_request):
199+
""" Test plaintext make_request. """
202200

203-
# Assert return value
204-
self.assertEquals(
205-
response,
206-
'blah'
201+
mock_request.return_value.status_code = 200
202+
mock_request.return_value.content = "OK"
203+
# Perform request
204+
response = self.gcm.make_request(
205+
{'message': 'test'}, is_json=False
207206
)
207+
self.assertEqual(response, "OK")
208208

209+
mock_request.return_value.status_code = 400
210+
with self.assertRaises(GCMMalformedJsonException):
211+
response = self.gcm.make_request(
212+
{'message': 'test'}, is_json=False
213+
)
209214

210-
@patch('urllib2.urlopen')
211-
def test_make_request_unicode(self, urlopen_mock):
212-
""" Regression: Test make_request with unicode payload. """
215+
mock_request.return_value.status_code = 401
216+
with self.assertRaises(GCMAuthenticationException):
217+
response = self.gcm.make_request(
218+
{'message': 'test'}, is_json=False
219+
)
213220

214-
# Unicode character in data
221+
mock_request.return_value.status_code = 503
222+
with self.assertRaises(GCMUnavailableException):
223+
response = self.gcm.make_request(
224+
{'message': 'test'}, is_json=False
225+
)
226+
227+
@patch('requests.api.request')
228+
def test_make_request_unicode(self, mock_request):
229+
""" Test make_request with unicode payload. """
215230
data = {
216231
'message': u'\x80abc'
217232
}
218-
219-
self.gcm.make_request(data, is_json=False)
233+
try:
234+
self.gcm.make_request(data, is_json=False)
235+
except:
236+
pass
237+
self.assertTrue(mock_request.called)
238+
self.assertEqual(
239+
mock_request.call_args[1]['data'],
240+
'message=%C2%80abc'
241+
)
220242

221243
def test_retry_plaintext_request_ok(self):
222244
returns = [GCMUnavailableException(), GCMUnavailableException(), 'id=123456789']

setup.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
setup(
44
name='python-gcm',
5-
version='0.1.4',
5+
version='0.2',
66
packages=['gcm'],
77
license=open('LICENSE').read(),
8-
author='Minh Nam Ngo',
9-
author_email='nam@namis.me',
8+
author='Nam Ngo',
9+
author_email='nam@kogan.com.au',
1010
url='http://blog.namis.me/python-gcm/',
1111
description='Python client for Google Cloud Messaging for Android (GCM)',
1212
long_description=open('README.rst').read(),
1313
keywords='android gcm push notification google cloud messaging',
14+
install_requires=[
15+
'requests',
16+
],
1417
tests_require = ['mock'],
1518
)

0 commit comments

Comments
 (0)