gcm-client-0.1.4/0000755000076500000240000000000012227774753014155 5ustar sardarstaff00000000000000gcm-client-0.1.4/gcm_client.egg-info/0000755000076500000240000000000012227774753017753 5ustar sardarstaff00000000000000gcm-client-0.1.4/gcm_client.egg-info/dependency_links.txt0000644000076500000240000000000112227774753024021 0ustar sardarstaff00000000000000 gcm-client-0.1.4/gcm_client.egg-info/PKG-INFO0000644000076500000240000000526512227774753021060 0ustar sardarstaff00000000000000Metadata-Version: 1.0 Name: gcm-client Version: 0.1.4 Summary: Python client for Google Cloud Messaging (GCM) Home-page: https://bitbucket.org/sardarnl/gcm-client Author: Sardar Yumatov Author-email: ja.doma@gmail.com License: Apache 2.0 Description: gcm-client ========== Python client for `Google Cloud Messaging (GCM) `_. Check `documentation `_ to learn how to use it. Check the client with similar interface for `Apple Push Notification service `_. Requirements ------------ - `requests `_ - HTTP request, handles proxies etc. - `omnijson `_ if you use Python 2.5 or older. Alternatives ------------ Th only alternative library known at the time of writing was `python-gcm `_. This library differs in the following design decisions: - *Predictable execution time*. Do not automatically retry request on failure. According to Google's recommendations, each retry has to wait exponential back-off delay. We use Celery back-end, where the best way to retry after some delay will be scheduling the task with ``countdown=delay``. Sleeping while in Celery worker hurts your concurrency. - *Do not forget results if you need to retry*. This sounds obvious, but ``python-gcm`` drops important results, such as canonical ID mapping if request needs to be (partially) retried. - *Clean pythonic API*. No need to borrow all Java like exceptions etc. - *Do not hard-code validation, let GCM fail*. This decision makes library a little bit more future proof. Support ------- GCM client was created by `Sardar Yumatov `_, contact me if you find any bugs or need help. Contact `Getlogic `_ if you need a full-featured push notification service for all popular platforms. You can view outstanding issues on the `GCM Bitbucket page `_. Keywords: gcm push notification google cloud messaging android Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules gcm-client-0.1.4/gcm_client.egg-info/requires.txt0000644000076500000240000000001012227774753022342 0ustar sardarstaff00000000000000requestsgcm-client-0.1.4/gcm_client.egg-info/SOURCES.txt0000644000076500000240000000041112227774753021633 0ustar sardarstaff00000000000000LICENSE MANIFEST.in README.rst setup.py gcm_client.egg-info/PKG-INFO gcm_client.egg-info/SOURCES.txt gcm_client.egg-info/dependency_links.txt gcm_client.egg-info/requires.txt gcm_client.egg-info/top_level.txt gcmclient/__init__.py gcmclient/gcm.py gcmclient/test.pygcm-client-0.1.4/gcm_client.egg-info/top_level.txt0000644000076500000240000000001212227774753022476 0ustar sardarstaff00000000000000gcmclient gcm-client-0.1.4/gcmclient/0000755000076500000240000000000012227774753016122 5ustar sardarstaff00000000000000gcm-client-0.1.4/gcmclient/__init__.py0000644000076500000240000000155612122170623020216 0ustar sardarstaff00000000000000# Copyright 2013 Getlogic BV, Sardar Yumatov # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. __title__ = 'GCM client' __version__ = "0.1" __author__ = "Sardar Yumatov" __contact__ = "ja.doma@gmail.com" __license__ = "Apache 2.0" __homepage__ = "https://bitbucket.org/sardarnl/gcm-client/" __copyright__ = 'Copyright 2013 Getlogic BV, Sardar Yumatov' from gcmclient.gcm import * gcm-client-0.1.4/gcmclient/gcm.py0000644000076500000240000004537512227774406017253 0ustar sardarstaff00000000000000# Copyright 2013 Getlogic BV, Sardar Yumatov # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import random import requests try: import json except ImportError: import omnijson as json # all you need __all__ = ('GCMAuthenticationError', 'JSONMessage', 'PlainTextMessage', 'GCM', 'Result') # More info: http://developer.android.com/google/gcm/gcm.html #: Default URL to GCM service. GCM_URL = 'https://android.googleapis.com/gcm/send' class GCMAuthenticationError(ValueError): """ Raised if your Google API key is rejected. """ pass class Message(object): """ Base message class. """ # recognized options, read GCM manual for more info. OPTIONS = { 'collapse_key': lambda v: v if isinstance(v, basestring) else str(v), 'time_to_live': int, 'delay_while_idle': bool, 'restricted_package_name': lambda v: v if isinstance(v, basestring) else str(v), 'dry_run': bool, } def __init__(self, data=None, options=None): """ Abstract message. :Arguments: - `data` (dict): key-value pairs, payload of this message. - `options` (dict): GCM options. Refer to `GCM `_ for more explanation on available options. :Options: - `collapse_key` (str): collapse key/bucket. - `time_to_live` (int): message TTL in seconds. - `delay_while_idle` (bool): hold message if device is off-line. - `restricted_package_name` (str): declare package name. - `dry_run` (bool): pretend sending message to devices. """ self.data = data self.options = options or {} def _prepare_data(self, payload, data=None): """ Hook to format message data payload. """ data = data or self.data if data: payload['data'] = data def _prepare_payload(self, data=None, options=None): """ Hook to prepare all message options. """ options = options or self.options payload = {} # set options for opt, flt in self.OPTIONS.iteritems(): val = options.get(opt, None) if val is not None: val = flt(val) if val or isinstance(val, (int, long)): payload[opt] = val self._prepare_data(payload, data=data) return payload def _prepare(self, headers, data=None, options=None): """ Prepare message for HTTP request. The method should at least set 'Content-Type' header. If message is using URL encoding (plain-text HTTP request), then method should return key-value pairs in a dict. Arguments: headers (dict): HTTP headers. Returns: HTTP payload (str or dict). """ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8' # return dict, will be URLencoded by requests return self._prepare_payload(data=data, options=options) def _parse_response(self, response): """ Parse GCM response. Subclasses must override this method. """ raise NotImplementedError class JSONMessage(Message): """ JSON formatted message. """ def __init__(self, registration_ids, data=None, **options): """ Multicast message, uses JSON format. :Arguments: - `registration_ids` (list): registration ID's of target devices. - `data` (dict): key-value pairs of message payload. - `options` (kwargs): GCM options, see :class:`Message` for more info. """ if not registration_ids: raise ValueError("Empty registration_ids list") super(JSONMessage, self).__init__(data, options) self._registration_ids = registration_ids @property def registration_ids(self): """ Target registration ID's. """ return self._registration_ids def _prepare_payload(self, data=None, options=None): """ Prepare message payload. """ payload = super(JSONMessage, self)._prepare_payload(data=data, options=options) registration_ids = self._registration_ids if not isinstance(registration_ids, (list, tuple)): registration_ids = list(registration_ids) payload['registration_ids'] = registration_ids return payload def _prepare(self, headers, data=None, options=None): """ Serializes messaget to JSON. """ headers['Content-Type'] = 'application/json' payload = self._prepare_payload(data=data, options=options) return json.dumps(payload) def _parse_response(self, response): """ Parse JSON response. """ if not isinstance(response, basestring): # requests.Response object response = response.content data = json.loads(response) # raises ValueError if 'results' not in data or len(data.get('results')) != len(self.registration_ids): raise ValueError("Invalid response") success = {} canonicals = {} unavailable = [] not_registered = [] errors = {} for reg_id, res in zip(self.registration_ids, data['results']): if 'message_id' in res: success[reg_id] = res['message_id'] if 'registration_id' in res: canonicals[reg_id] = res['registration_id'] else: if res['error'] == "Unavailable" or res['error'] == "InternalServerError": unavailable.append(reg_id) elif res['error'] == "NotRegistered": not_registered.append(reg_id) else: errors[reg_id] = res['error'] return { 'multicast_id': data['multicast_id'], 'success': success, 'canonicals': canonicals, 'unavailable': unavailable, 'not_registered': not_registered, 'failed': errors, } def _retry(self, unavailable): """ Create new message for given unavailable ID's list. """ return JSONMessage(unavailable, self.data, **self.options) def __getstate__(self): """ Returns ``dict`` with ``__init__`` arguments. If you use ``pickle``, then simply pickle/unpickle the message object. If you use something else, like JSON, then:: # obtain state dict from message state = message.__getstate__() # send/store the state # recover state and restore message. you have to pick the right class message_copy = JSONMessage(**state) :Returns: `kwargs` for `JSONMessage` constructor. """ ret = dict((key, getattr(self, key)) for key in ('registration_ids', 'data')) if self.options: ret.update(self.options) return ret def __setstate__(self, state): """ Overwrite message state with given kwargs. """ self.options = {} for key, val in state.iteritems(): if key == 'registration_ids': self._registration_ids = val elif key == 'data': self.data = val else: self.options[key] = val class PlainTextMessage(Message): """ Plain-text unicast message. """ def __init__(self, registration_id, data=None, **options): """ Unicast message, uses plain text format. All values in the data payload must be URL encodable scalars. :Arguments: - `registration_id` (str): registration ID of target device. - `data` (dict): key-value pairs of message payload. - `options` (kwargs): GCM options, see :class:`Message` for more info. """ if not registration_id: raise ValueError("registration_id is required") super(PlainTextMessage, self).__init__(data, options) self._registration_id = registration_id @property def registration_id(self): """ Target registration ID. """ return self._registration_id def _prepare_data(self, payload, data=None): """ Prepare data key-value pairs for URL encoding. """ data = data or self.data if data: for k,v in data.iteritems(): if v is not None: # FIXME: maybe we should check here if v is scalar. URL encoding # does not support complex values. payload['data.%s' % k] = v def _prepare_payload(self, data=None, options=None): """ Prepare message payload. """ payload = super(PlainTextMessage, self)._prepare_payload(data=data, options=options) payload['registration_id'] = self.registration_id return payload def _parse_response(self, response): """ Parse plain-text response. """ success = {} canonicals = {} not_registered = [] errors = {} if not isinstance(response, basestring): # requests.Response object response = response.content lines = response.strip().split('\n') if lines[0].startswith("Error="): error_code = lines[0][6:].strip() if error_code == "NotRegistered": not_registered.append(self.registration_id) else: # Plain-text requests will never return Unavailable as the error code, # they would have returned a 500 HTTP status instead errors[self.registration_id] = error_code elif lines[0].startswith("id="): success[self.registration_id] = lines[0][3:].strip() if len(lines) > 1: if lines[1].startswith("registration_id="): canonicals[self.registration_id] = lines[1][16:] else: raise ValueError("Can not parse second line of response body: {0}".format(lines[1])) else: raise ValueError("Can not parse response body: {0}".format(response)) return { 'success': success, 'canonicals': canonicals, 'not_registered': not_registered, 'failed': errors, } def _retry(self, unavailable): """ Create new message for given unavailable ID's list. """ if len(unavailable) != 1: raise ValueError("Plain-text messages are unicast.") return PlainTextMessage(unavailable[0], self.data, **self.options) def __getstate__(self): """ Returns ``dict`` with ``__init__`` arguments. If you use ``pickle``, then simply pickle/unpickle the message object. If you use something else, like JSON, then:: # obtain state dict from message state = message.__getstate__() # send/store the state # recover state and restore message. you have to pick the right class message_copy = PlainTextMessage(**state) :Returns: `kwargs` for `PlainTextMessage` constructor. """ ret = dict((key, getattr(self, key)) for key in ('registration_id', 'data')) if self.options: ret.update(self.options) return ret def __setstate__(self, state): """ Overwrite message state with given kwargs. """ self.options = {} for key, val in state.iteritems(): if key == 'registration_id': self._registration_id = val elif key == 'data': self.data = val else: self.options[key] = val class Result(object): """ Result of send operation. You should check :func:`canonical` for any registration ID's that should be updated. If the whole message or some registration ID's have recoverably failed, then :func:`retry` will provide you with new message. You have to wait :func:`delay` seconds before attempting a new request. """ def __init__(self, message, response, backoff): self.message = message self._random = None self._backoff = backoff try: # on failures, retry-after self.retry_after = response.headers.get('Retry-After', 0) if self.retry_after < 1: self.retry_after = None except ValueError: self.retry_after = None if response.status_code != 200: # For all 5xx Google says "you may retry" self._retry_message = message self._success_ids = {} self._canonical_ids = {} self._not_registered_ids = [] self._failed_ids = {} else: info = message._parse_response(response) self._success_ids = info['success'] self._canonical_ids = info['canonicals'] self._not_registered_ids = info['not_registered'] self._failed_ids = info['failed'] # user has to retry anyway, so pre-create unavailable = info.get('unavailable', None) if unavailable: self._retry_message = message._retry(unavailable) else: self._retry_message = None @property def success(self): """ Successfully processed registration ID's as mapping ``{registration_id: message_id}``. """ return self._success_ids @property def canonical(self): """ New registration ID's as mapping ``{registration_id: canonical_id}``. You have to update registration ID's of your subscribers by replacing them with corresponding canonical ID. Read more `here `_. """ return self._canonical_ids @property def not_registered(self): """ List all registration ID's that GCM reports as ``NotRegistered``. You should remove them from your database. """ return self._not_registered_ids @property def failed(self): """ Unrecoverably failed regisration ID's as mapping ``{registration_id: error code}``. This method lists devices, that have failed with something else than: - ``Unavailable`` -- look for :func:`retry` instead. - ``NotRegistered`` -- look for :attr:`not_registered` instead. Read more about possible `error codes `_. """ return self._failed_ids def needs_retry(self): """ True if :func:`retry` will return message. """ return self._retry_message is not None def retry(self): """ Construct new message that will unicast/multicast to remaining recoverably failed registration ID's. Method returns None if there is nothing to retry. Do not forget to wait for :func:`delay` seconds before new attempt. """ return self._retry_message def delay(self, retry=0): """ Time to wait in seconds before attempting a retry as a float number. This method will return value of Retry-After header if it is provided by GCM. Otherwise, it will return (backoff * 2^retry) with some random shift. Google may black list your server if you do not honor Retry-After hint and do not use exponential backoff. """ if self.retry_after: return self.retry_after return self.backoff(retry=retry) def backoff(self, retry=0): """ Computes exponential backoff for given retry number. """ if self._random is None: self._random = random.Random() base = (self._backoff << retry) - (self._backoff >> 1) return (base + self._random.randrange(self._backoff)) / 1000.0 class GCM(object): """ GCM client. """ # Initial backoff in milliseconds INITIAL_BACKOFF = 1000 def __init__(self, api_key, url=GCM_URL, backoff=INITIAL_BACKOFF, **options): """ Create new connection. :Arguments: - `api_key` (str): Google API key. - `url` (str): GCM server URL. - `backoff` (int): initial backoff in milliseconds. - `options` (kwargs): options for `requests `_ such as ``proxies``. """ if not api_key: raise ValueError("Google API key is required") self.api_key = api_key self.url = url self.backoff = backoff self.requests_options = options def send(self, message): """ Send message. The message may contain various options, such as ``time_to_live``. Your request might be rejected, because some of your options might be invalid. In this case a ``ValueError`` with explanation will be raised. :Arguments: `message` (:class:`Message`): plain text or JSON message. :Returns: :class:`Result` interpreting the results. :Raises: - ``requests.exceptions.RequestException`` on any network problem. - ``ValueError`` if your GCM request or response is rejected. - :class:`GCMAuthenticationError` your API key is invalid. """ headers = { 'Authorization': 'key=%s' % self.api_key, } # construct HTTP message data = message._prepare(headers) # raises requests.exceptions.RequestException on timeouts, connection and # other problems. response = requests.post(self.url, data=data, headers=headers, **self.requests_options) # invalid JSON. Body contains explanation. Happens if options are invalid. if response.status_code == 400: raise ValueError(response.content) if response.status_code == 401: raise GCMAuthenticationError("Authentication Error") # either request is accepted or rejected with possibility for retry if response.status_code == 200 or (response.status_code >= 500 and response.status_code <= 599): return Result(message, response, self.backoff) assert False, "Unknown status code: {0}".format(response.status_code) gcm-client-0.1.4/gcmclient/test.py0000644000076500000240000001236712136244674017455 0ustar sardarstaff00000000000000if __name__ == '__main__': import os.path, sys sys.path.append(os.path.dirname(os.path.dirname(__file__))) import unittest import json import pickle from gcmclient.gcm import * class GCMClientTest(unittest.TestCase): """ API tests. """ def setUp(self): self.gcm = GCM('my_api_key') def test_plain_text(self): msg = PlainTextMessage('target_device', { 'str': 'string', 'int': 90, 'bool': True, }, collapse_key='collapse.key', time_to_live=90, delay_while_idle=True, dry_run=True) headers = {} data = msg._prepare(headers) # will be URL encoded by requests ex_data = {'collapse_key': 'collapse.key', 'time_to_live': 90, 'delay_while_idle': True, 'dry_run': True, 'registration_id': 'target_device', 'data.bool': True, 'data.int': 90, 'data.str': 'string', } ex_headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'} self.assertEqual(data, ex_data) self.assertEqual(headers, ex_headers) # responses rsp = msg._parse_response("id=1:2342\nregistration_id=32\n") ex_rsp = { 'canonicals': {'target_device': '32'}, 'failed': {}, 'not_registered': [], 'success': {'target_device': '1:2342'}, } self.assertEqual(rsp, ex_rsp) rsp = msg._parse_response("Error=InvalidRegistration") ex_rsp = { 'canonicals': {}, 'failed': {'target_device': 'InvalidRegistration'}, 'not_registered': [], 'success': {} } self.assertEqual(rsp, ex_rsp) # retry (must be ['target_device'], but we test here the difference) msg2 = msg._retry(['target_device_retry']) self.assertEqual(msg2.registration_id, 'target_device_retry') self.assertEqual(msg2.options, msg.options) self.assertEqual(msg2.data, msg.data) # pickle pmsg = pickle.dumps(msg) pmsg = pickle.loads(pmsg) self.assertEqual(pmsg.registration_id, msg.registration_id) self.assertEqual(pmsg.options, msg.options) self.assertEqual(pmsg.data, msg.data) pstate = msg.__getstate__() pmsg = PlainTextMessage(**pstate) self.assertEqual(pmsg.registration_id, msg.registration_id) self.assertEqual(pmsg.options, msg.options) self.assertEqual(pmsg.data, msg.data) def test_json_message(self): msg = JSONMessage(['A', 'B', 'C', 'D', 'E'], { 'str': 'string', 'int': 90, 'bool': True, }, collapse_key='collapse.key', time_to_live=90, delay_while_idle=True, dry_run=True) headers = {} data = msg._prepare(headers) # will be URL encoded by requests ex_data = json.dumps({'collapse_key': 'collapse.key', 'time_to_live': 90, 'delay_while_idle': True, 'dry_run': True, 'registration_ids': ['A', 'B', 'C', 'D', 'E'], 'data': { 'str': 'string', 'int': 90, 'bool': True, } }) ex_headers = {'Content-Type': 'application/json'} self.assertEqual(data, ex_data) self.assertEqual(headers, ex_headers) # responses response = json.dumps({ "multicast_id": 1, "success": 2, "failure": 3, "canonical_ids": 1, "results": [ { "message_id": "1:0408" }, { "error": "Unavailable" }, { "error": "InvalidRegistration" }, { "message_id": "1:2342", "registration_id": "32" }, { "error": "NotRegistered"} ] }) rsp = msg._parse_response(response) ex_rsp = { 'multicast_id': 1, 'canonicals': {'D': u'32'}, 'failed': {'C': u'InvalidRegistration'}, 'not_registered': ['E'], 'success': {'A': u'1:0408', 'D': u'1:2342'}, 'unavailable': ['B'] } self.assertEqual(rsp, ex_rsp) # retry (must be ['target_device'], but we test here the difference) msg2 = msg._retry(['B']) self.assertEqual(msg2.registration_ids, ['B']) self.assertEqual(msg2.options, msg.options) self.assertEqual(msg2.data, msg.data) # pickle pmsg = pickle.dumps(msg) pmsg = pickle.loads(pmsg) self.assertEqual(pmsg.registration_ids, msg.registration_ids) self.assertEqual(pmsg.options, msg.options) self.assertEqual(pmsg.data, msg.data) pstate = msg.__getstate__() pmsg = JSONMessage(**pstate) self.assertEqual(pmsg.registration_ids, msg.registration_ids) self.assertEqual(pmsg.options, msg.options) self.assertEqual(pmsg.data, msg.data) if __name__ == '__main__': unittest.main() gcm-client-0.1.4/LICENSE0000644000076500000240000002613512120321766015151 0ustar sardarstaff00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gcm-client-0.1.4/MANIFEST.in0000644000076500000240000000003312120322104015652 0ustar sardarstaff00000000000000include README.rst LICENSE gcm-client-0.1.4/PKG-INFO0000644000076500000240000000526512227774753015262 0ustar sardarstaff00000000000000Metadata-Version: 1.0 Name: gcm-client Version: 0.1.4 Summary: Python client for Google Cloud Messaging (GCM) Home-page: https://bitbucket.org/sardarnl/gcm-client Author: Sardar Yumatov Author-email: ja.doma@gmail.com License: Apache 2.0 Description: gcm-client ========== Python client for `Google Cloud Messaging (GCM) `_. Check `documentation `_ to learn how to use it. Check the client with similar interface for `Apple Push Notification service `_. Requirements ------------ - `requests `_ - HTTP request, handles proxies etc. - `omnijson `_ if you use Python 2.5 or older. Alternatives ------------ Th only alternative library known at the time of writing was `python-gcm `_. This library differs in the following design decisions: - *Predictable execution time*. Do not automatically retry request on failure. According to Google's recommendations, each retry has to wait exponential back-off delay. We use Celery back-end, where the best way to retry after some delay will be scheduling the task with ``countdown=delay``. Sleeping while in Celery worker hurts your concurrency. - *Do not forget results if you need to retry*. This sounds obvious, but ``python-gcm`` drops important results, such as canonical ID mapping if request needs to be (partially) retried. - *Clean pythonic API*. No need to borrow all Java like exceptions etc. - *Do not hard-code validation, let GCM fail*. This decision makes library a little bit more future proof. Support ------- GCM client was created by `Sardar Yumatov `_, contact me if you find any bugs or need help. Contact `Getlogic `_ if you need a full-featured push notification service for all popular platforms. You can view outstanding issues on the `GCM Bitbucket page `_. Keywords: gcm push notification google cloud messaging android Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules gcm-client-0.1.4/README.rst0000644000076500000240000000344612123137106015627 0ustar sardarstaff00000000000000gcm-client ========== Python client for `Google Cloud Messaging (GCM) `_. Check `documentation `_ to learn how to use it. Check the client with similar interface for `Apple Push Notification service `_. Requirements ------------ - `requests `_ - HTTP request, handles proxies etc. - `omnijson `_ if you use Python 2.5 or older. Alternatives ------------ Th only alternative library known at the time of writing was `python-gcm `_. This library differs in the following design decisions: - *Predictable execution time*. Do not automatically retry request on failure. According to Google's recommendations, each retry has to wait exponential back-off delay. We use Celery back-end, where the best way to retry after some delay will be scheduling the task with ``countdown=delay``. Sleeping while in Celery worker hurts your concurrency. - *Do not forget results if you need to retry*. This sounds obvious, but ``python-gcm`` drops important results, such as canonical ID mapping if request needs to be (partially) retried. - *Clean pythonic API*. No need to borrow all Java like exceptions etc. - *Do not hard-code validation, let GCM fail*. This decision makes library a little bit more future proof. Support ------- GCM client was created by `Sardar Yumatov `_, contact me if you find any bugs or need help. Contact `Getlogic `_ if you need a full-featured push notification service for all popular platforms. You can view outstanding issues on the `GCM Bitbucket page `_. gcm-client-0.1.4/setup.cfg0000644000076500000240000000007312227774753015776 0ustar sardarstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 gcm-client-0.1.4/setup.py0000644000076500000240000000153612227774454015672 0ustar sardarstaff00000000000000try: from setuptools import setup except ImportError: from distutils.core import setup setup( name='gcm-client', version='0.1.4', author='Sardar Yumatov', author_email='ja.doma@gmail.com', url='https://bitbucket.org/sardarnl/gcm-client', description='Python client for Google Cloud Messaging (GCM)', long_description=open('README.rst').read(), packages=['gcmclient'], license="Apache 2.0", keywords='gcm push notification google cloud messaging android', install_requires=['requests'], classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules'] )