Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 44 additions & 32 deletions gcm/gcm.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import requests

import json
from collections import defaultdict
import time
import random

import time
import urllib
try:
from urllib import quote_plus
import urllib.request as urllib2
except ImportError:
from urllib.parse import quote_plus
import urllib2
from collections import defaultdict


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


class GCMException(Exception):

"""GCM Exception."""
pass


Expand Down Expand Up @@ -74,8 +76,8 @@ def group_response(response, registration_ids, key):
grouping = dict(filtered)
else:
grouping = defaultdict(list)
for k, v in filtered:
grouping[v].append(k)
for k, value in filtered:
grouping[value].append(k)

return grouping or None

Expand All @@ -89,8 +91,8 @@ def urlencode_utf8(params):
params = params.items()
params = (
'='.join((
quote_plus(k.encode('utf8'), safe='/'),
quote_plus(v.encode('utf8'), safe='/')
urllib.quote_plus(k.encode('utf8'), safe='/'),
urllib.quote_plus(v.encode('utf8'), safe='/')
)) for k, v in params
)
return '&'.join(params)
Expand Down Expand Up @@ -122,9 +124,9 @@ def __init__(self, api_key, url=GCM_URL, proxy=None):
'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 @@ -135,7 +137,7 @@ def construct_payload(self, registration_ids, data=None, collapse_key=None,
"""

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

payload = {}
Expand Down Expand Up @@ -172,41 +174,47 @@ def make_request(self, data, is_json=True):

:param data: return value from construct_payload method
:raises GCMMalformedJsonException: if malformed JSON request found
:raises GCMAuthenticationException: if there was a problem with authentication, invalid api key
:raises GCMAuthenticationException: if there was a problem with
authentication, invalid api key
:raises GCMConnectionException: if GCM is screwed
"""

# Default Content-Type is
# application/x-www-form-urlencoded;charset=UTF-8
if is_json:
self.headers['Content-Type'] = 'application/json'

data = urllib.urlencode(data)
if not is_json:
data = urlencode_utf8(data)

response = requests.post(
self.url, data=data, headers=self.headers,
proxies=self.proxy
)
request = urllib2.Request(self.url, data)
request.add_header('Authorization', self.headers['Authorization'])
response = None
try:
response_object = urllib2.urlopen(request)
response = response_object.read()
response_object.close()
except urllib2.HTTPError:
raise GCMAuthenticationException(
"There was an error authenticating the sender account")

# Successful response
if response.status_code == 200:
if response_object.getcode() == 200:
if is_json:
response = response.json()
else:
response = response.content
return json.loads(response)
return response

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

def raise_error(self, error):
Expand Down Expand Up @@ -258,13 +266,15 @@ def extract_unsent_reg_ids(self, info):
return []

def plaintext_request(self, registration_id, data=None, collapse_key=None,
delay_while_idle=False, time_to_live=None, retries=5, dry_run=False):
delay_while_idle=False, time_to_live=None, retries=5,
dry_run=False):
"""
Makes a plaintext request to GCM servers

:param registration_id: string of the registration id
:param data: dict mapping of key-value pairs of messages
:return dict of response body from Google including multicast_id, success, failure, canonical_ids, etc
:return dict of response body from Google including multicast_id,
success, failure, canonical_ids, etc
:raises GCMMissingRegistrationException: if registration_id is not provided
"""

Expand All @@ -291,13 +301,15 @@ def plaintext_request(self, registration_id, data=None, collapse_key=None,
raise IOError("Could not make request after %d attempts" % attempt)

def json_request(self, registration_ids, data=None, collapse_key=None,
delay_while_idle=False, time_to_live=None, retries=5, dry_run=False):
delay_while_idle=False, time_to_live=None, retries=5,
dry_run=False):
"""
Makes a JSON request to GCM servers

:param registration_ids: list of the registration ids
:param data: dict mapping of key-value pairs of messages
:return dict of response body from Google including multicast_id, success, failure, canonical_ids, etc
:return dict of response body from Google including multicast_id,
success, failure, canonical_ids, etc
:raises GCMMissingRegistrationException: if the list of registration_ids is empty
:raises GCMTooManyRegIdsException: if the list of registration_ids exceeds 1000 items
"""
Expand All @@ -309,7 +321,7 @@ def json_request(self, registration_ids, data=None, collapse_key=None,
"Exceded number of registration_ids")

backoff = self.BACKOFF_INITIAL_DELAY
for attempt in range(retries):
for _ in range(retries):
payload = self.construct_payload(
registration_ids, data, collapse_key,
delay_while_idle, time_to_live, True, dry_run
Expand Down
79 changes: 47 additions & 32 deletions gcm/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,16 @@ def test_gcm_proxy(self):
def test_construct_payload(self):
res = self.gcm.construct_payload(
registration_ids=['1', '2'], data=self.data, collapse_key='foo',
delay_while_idle=True, time_to_live=3600, is_json=True, dry_run = True
delay_while_idle=True, time_to_live=3600, is_json=True, dry_run=True
)
payload = json.loads(res)
for arg in ['registration_ids', 'data', 'collapse_key', 'delay_while_idle', 'time_to_live', 'dry_run']:
self.assertIn(arg, payload)

def test_json_payload(self):
reg_ids = ['12', '145', '56']
json_payload = self.gcm.construct_payload(registration_ids=reg_ids, data=self.data)
json_payload = self.gcm.construct_payload(
registration_ids=reg_ids, data=self.data)
payload = json.loads(json_payload)

self.assertIn('registration_ids', payload)
Expand Down Expand Up @@ -178,53 +179,53 @@ def test_handle_plaintext_response(self):
res = self.gcm.handle_plaintext_response(response)
self.assertEqual(res, '3456')

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

mock_request.return_value.status_code = 200
mock_request.return_value.content = "OK"
mock_request().read = lambda: '{}'
mock_request().close = lambda: True
mock_request().getcode = lambda: 200
# Perform request
self.gcm.make_request(
result = 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)

'application/json')
self.assertEqual(result, {})

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

mock_request.return_value.status_code = 200
mock_request.return_value.content = "OK"
mock_request().read = lambda: 'OK'
mock_request().close = lambda: True
mock_request().getcode = lambda: 200
# Perform request
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)
self.assertEqual(response, "OK")

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

mock_request.return_value.status_code = 401
mock_request().getcode = lambda: 401
with self.assertRaises(GCMAuthenticationException):
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)

mock_request.return_value.status_code = 503
mock_request().getcode = lambda: 503
with self.assertRaises(GCMUnavailableException):
response = self.gcm.make_request(
{'message': 'test'}, is_json=False
)

@patch('requests.api.request')
@patch('urllib2.Request')
def test_make_request_unicode(self, mock_request):
""" Test make_request with unicode payload. """
data = {
Expand All @@ -236,51 +237,65 @@ def test_make_request_unicode(self, mock_request):
pass
self.assertTrue(mock_request.called)
self.assertEqual(
mock_request.call_args[1]['data'],
mock_request.call_args[0][1],
'message=%C2%80abc'
)

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

self.gcm.make_request = MagicMock(side_effect=create_side_effect(returns))
res = self.gcm.plaintext_request(registration_id='1234', data=self.data)
self.gcm.make_request = MagicMock(
side_effect=create_side_effect(returns))
res = self.gcm.plaintext_request(
registration_id='1234', data=self.data)

self.assertIsNone(res)
self.assertEqual(self.gcm.make_request.call_count, 3)

def test_retry_plaintext_request_fail(self):
returns = [GCMUnavailableException(), GCMUnavailableException(), GCMUnavailableException()]
returns = [
GCMUnavailableException(), GCMUnavailableException(), GCMUnavailableException()]

self.gcm.make_request = MagicMock(side_effect=create_side_effect(returns))
self.gcm.make_request = MagicMock(
side_effect=create_side_effect(returns))
with self.assertRaises(IOError):
self.gcm.plaintext_request(registration_id='1234', data=self.data, retries=2)
self.gcm.plaintext_request(
registration_id='1234', data=self.data, retries=2)

self.assertEqual(self.gcm.make_request.call_count, 2)

def test_retry_json_request_ok(self):
returns = [self.mock_response_1, self.mock_response_2, self.mock_response_3]
returns = [
self.mock_response_1, self.mock_response_2, self.mock_response_3]

self.gcm.make_request = MagicMock(side_effect=create_side_effect(returns))
res = self.gcm.json_request(registration_ids=['1', '2'], data=self.data)
self.gcm.make_request = MagicMock(
side_effect=create_side_effect(returns))
res = self.gcm.json_request(
registration_ids=['1', '2'], data=self.data)

self.assertEqual(self.gcm.make_request.call_count, 3)
self.assertNotIn('errors', res)

def test_retry_json_request_fail(self):
returns = [self.mock_response_1, self.mock_response_2, self.mock_response_3]
returns = [
self.mock_response_1, self.mock_response_2, self.mock_response_3]

self.gcm.make_request = MagicMock(side_effect=create_side_effect(returns))
res = self.gcm.json_request(registration_ids=['1', '2'], data=self.data, retries=2)
self.gcm.make_request = MagicMock(
side_effect=create_side_effect(returns))
res = self.gcm.json_request(
registration_ids=['1', '2'], data=self.data, retries=2)

self.assertEqual(self.gcm.make_request.call_count, 2)
self.assertIn('Unavailable', res['errors'])
self.assertEqual(res['errors']['Unavailable'][0], '1')

def test_retry_exponential_backoff(self):
returns = [GCMUnavailableException(), GCMUnavailableException(), 'id=123456789']
returns = [
GCMUnavailableException(), GCMUnavailableException(), 'id=123456789']

self.gcm.make_request = MagicMock(side_effect=create_side_effect(returns))
self.gcm.make_request = MagicMock(
side_effect=create_side_effect(returns))
self.gcm.plaintext_request(registration_id='1234', data=self.data)

# time.sleep is actually mock object.
Expand Down