|
3 | 3 | import httplib |
4 | 4 | import functools |
5 | 5 | from sys import hexversion |
| 6 | +import requests |
| 7 | +from pyapns import _json as json |
| 8 | +from pyapns.model import Notification, DisconnectionEvent |
| 9 | + |
| 10 | + |
| 11 | +class ClientError(Exception): |
| 12 | + def __init__(self, message, response): |
| 13 | + super(ClientError, self).__init__(message) |
| 14 | + self.message = message |
| 15 | + self.response = response |
| 16 | + |
| 17 | + |
| 18 | +class Client(object): |
| 19 | + @property |
| 20 | + def connection(self): |
| 21 | + con = getattr(self, '_connection', None) |
| 22 | + if con is None: |
| 23 | + con = self._connection = requests.Session() |
| 24 | + return con |
| 25 | + |
| 26 | + def __init__(self, host='http://localhost', port=8088, timeout=20): |
| 27 | + self.host = host.strip('/') |
| 28 | + self.port = port |
| 29 | + self.timeout = timeout |
| 30 | + |
| 31 | + def provision(self, app_id, environment, certificate, timeout=15): |
| 32 | + """ |
| 33 | + Tells the pyapns server that we want set up an app to receive |
| 34 | + notifications from us. |
| 35 | +
|
| 36 | + :param app_id: An app id, you can use anything but it's |
| 37 | + recommended to just use the bundle identifier used in your app. |
| 38 | + :type app_id: string |
| 39 | + |
| 40 | + :param environment: Which environment are you using? This value |
| 41 | + must be either "production" or "sandbox". |
| 42 | + :type environment: string |
| 43 | +
|
| 44 | + :param certificate: A path to a encoded, password-free .pem file |
| 45 | + on the pyapns host. This must be a path local to the host! You |
| 46 | + can also read an entire .pem file in and send it in this value |
| 47 | + as well. |
| 48 | + :type certificate: string |
| 49 | +
|
| 50 | + :returns: Dictionary-representation of the App record |
| 51 | + :rtype: dict |
| 52 | + """ |
| 53 | + status, resp = self._request( |
| 54 | + 'POST', 'apps/{}/{}'.format(app_id, environment), |
| 55 | + data={'certificate': certificate, 'timeout': timeout} |
| 56 | + ) |
| 57 | + if status != 201: |
| 58 | + raise ClientError('Unable to provision app id', resp) |
| 59 | + return resp['response'] |
| 60 | + |
| 61 | + def notify(self, app_id, environment, notifications): |
| 62 | + """ |
| 63 | + Sends notifications to clients via the pyapns server. The |
| 64 | + `app_id` and `environment` must be previously provisioned |
| 65 | + values--either by using the :py:meth:`provision` method or |
| 66 | + having been bootstrapped on the server. |
| 67 | +
|
| 68 | + `notifications` is a list of notification dictionaries that all |
| 69 | + must have the following keys: |
| 70 | +
|
| 71 | + * `payload` is the actual notification dict to be jsonified |
| 72 | + * `token` is the hexlified device token you scraped from |
| 73 | + the client |
| 74 | + * `identifier` is a unique id specific to this id. for this |
| 75 | + you may use any value--pyapns will generate its own |
| 76 | + internal ID to track it. The APN gateway only allows for |
| 77 | + this to be 4 bytes. |
| 78 | + * `expiry` is how long the notification should be retried |
| 79 | + for if for some reason the apple servers can not contact |
| 80 | + the device |
| 81 | +
|
| 82 | + You can also construct a :py:class:`Notification` object--the |
| 83 | + dict and class representations are interchangable here. |
| 84 | +
|
| 85 | + :param app_id: Which app id to use |
| 86 | + :type app_id: string |
| 87 | +
|
| 88 | + :param environmenet: The environment for the app_id |
| 89 | + :type environment: string |
| 90 | +
|
| 91 | + :param notifications: A list of notification dictionaries |
| 92 | + (see the discussion above) |
| 93 | + :type notifications: list |
| 94 | +
|
| 95 | + :returns: Empty response--this method doesn't return anything |
| 96 | + :rtype: dict |
| 97 | + """ |
| 98 | + notes = [] |
| 99 | + for note in notifications: |
| 100 | + if isinstance(note, dict): |
| 101 | + notes.append(Notification.from_simple(note)) |
| 102 | + elif isinstance(note, Notification): |
| 103 | + notes.append(note) |
| 104 | + else: |
| 105 | + raise ValueError('Unknown notification: {}'.format(repr(note))) |
| 106 | + data = [n.to_simple() for n in notes] |
| 107 | + |
| 108 | + status, resp = self._request( |
| 109 | + 'POST', 'apps/{}/{}/notifications'.format(app_id, environment), |
| 110 | + data=data |
| 111 | + ) |
| 112 | + if status != 201: |
| 113 | + raise ClientError('Could not send notifications', resp) |
| 114 | + return resp['response'] |
| 115 | + |
| 116 | + def feedback(self, app_id, environment): |
| 117 | + """ |
| 118 | + """ |
| 119 | + status, feedbacks = self._request( |
| 120 | + 'GET', 'apps/{}/{}/feedback'.format(app_id, environment) |
| 121 | + ) |
| 122 | + if status != 200: |
| 123 | + raise ClientError('Could not fetch feedbacks', resp) |
| 124 | + return feedbacks['response'] |
| 125 | + |
| 126 | + def disconnections(self, app_id, environment): |
| 127 | + """ |
| 128 | + Retrieves a list of the 5000 most recent disconnection events |
| 129 | + recorded by pyapns. Each time apple severs the connection with |
| 130 | + pyapns it will try to send back an error packet describing which |
| 131 | + notification caused the error and the error that occurred. |
| 132 | + """ |
| 133 | + status, disconnects = self._request( |
| 134 | + 'GET', 'apps/{}/{}/disconnections'.format(app_id, environment) |
| 135 | + ) |
| 136 | + if status != 200: |
| 137 | + raise ClientError('Could not retrieve disconnections') |
| 138 | + ret = [] |
| 139 | + for evt in disconnects['response']: |
| 140 | + ret.append(DisconnectionEvent.from_simple(evt)) |
| 141 | + return ret |
| 142 | + |
| 143 | + def _request(self, method, path, args=None, data=None): |
| 144 | + url = '{}:{}/{}'.format(self.host, self.port, path) |
| 145 | + kwargs = {'timeout': self.timeout} |
| 146 | + if args is not None: |
| 147 | + kwargs['params'] = args |
| 148 | + if data is not None: |
| 149 | + kwargs['data'] = json.dumps(data) |
| 150 | + |
| 151 | + func = getattr(self.connection, method.lower()) |
| 152 | + resp = func(url, **kwargs) |
| 153 | + if resp.headers['content-type'].startswith('application/json'): |
| 154 | + resp_data = json.loads(resp.content) |
| 155 | + else: |
| 156 | + resp_data = None |
| 157 | + return resp.status_code, resp_data |
| 158 | + |
| 159 | + |
| 160 | +## OLD XML-RPC INTERFACE ------------------------------------------------------ |
6 | 161 |
|
7 | 162 | OPTIONS = {'CONFIGURED': False, 'TIMEOUT': 20} |
8 | 163 |
|
|
0 commit comments