OLD | NEW |
(Empty) | |
| 1 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 2 # you may not use this file except in compliance with the License. |
| 3 # You may obtain a copy of the License at |
| 4 # |
| 5 # http://www.apache.org/licenses/LICENSE-2.0 |
| 6 # |
| 7 # Unless required by applicable law or agreed to in writing, software |
| 8 # distributed under the License is distributed on an "AS IS" BASIS, |
| 9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 10 # See the License for the specific language governing permissions and |
| 11 # limitations under the License. |
| 12 |
| 13 """Push notifications support. |
| 14 |
| 15 This code is based on experimental APIs and is subject to change. |
| 16 """ |
| 17 |
| 18 __author__ = 'afshar@google.com (Ali Afshar)' |
| 19 |
| 20 import binascii |
| 21 import collections |
| 22 import os |
| 23 import urllib |
| 24 |
| 25 SUBSCRIBE = 'X-GOOG-SUBSCRIBE' |
| 26 SUBSCRIPTION_ID = 'X-GOOG-SUBSCRIPTION-ID' |
| 27 TOPIC_ID = 'X-GOOG-TOPIC-ID' |
| 28 TOPIC_URI = 'X-GOOG-TOPIC-URI' |
| 29 CLIENT_TOKEN = 'X-GOOG-CLIENT-TOKEN' |
| 30 EVENT_TYPE = 'X-GOOG-EVENT-TYPE' |
| 31 UNSUBSCRIBE = 'X-GOOG-UNSUBSCRIBE' |
| 32 |
| 33 |
| 34 class InvalidSubscriptionRequestError(ValueError): |
| 35 """The request cannot be subscribed.""" |
| 36 |
| 37 |
| 38 def new_token(): |
| 39 """Gets a random token for use as a client_token in push notifications. |
| 40 |
| 41 Returns: |
| 42 str, a new random token. |
| 43 """ |
| 44 return binascii.hexlify(os.urandom(32)) |
| 45 |
| 46 |
| 47 class Channel(object): |
| 48 """Base class for channel types.""" |
| 49 |
| 50 def __init__(self, channel_type, channel_args): |
| 51 """Create a new Channel. |
| 52 |
| 53 You probably won't need to create this channel manually, since there are |
| 54 subclassed Channel for each specific type with a more customized set of |
| 55 arguments to pass. However, you may wish to just create it manually here. |
| 56 |
| 57 Args: |
| 58 channel_type: str, the type of channel. |
| 59 channel_args: dict, arguments to pass to the channel. |
| 60 """ |
| 61 self.channel_type = channel_type |
| 62 self.channel_args = channel_args |
| 63 |
| 64 def as_header_value(self): |
| 65 """Create the appropriate header for this channel. |
| 66 |
| 67 Returns: |
| 68 str encoded channel description suitable for use as a header. |
| 69 """ |
| 70 return '%s?%s' % (self.channel_type, urllib.urlencode(self.channel_args)) |
| 71 |
| 72 def write_header(self, headers): |
| 73 """Write the appropriate subscribe header to a headers dict. |
| 74 |
| 75 Args: |
| 76 headers: dict, headers to add subscribe header to. |
| 77 """ |
| 78 headers[SUBSCRIBE] = self.as_header_value() |
| 79 |
| 80 |
| 81 class WebhookChannel(Channel): |
| 82 """Channel for registering web hook notifications.""" |
| 83 |
| 84 def __init__(self, url, app_engine=False): |
| 85 """Create a new WebhookChannel |
| 86 |
| 87 Args: |
| 88 url: str, URL to post notifications to. |
| 89 app_engine: bool, default=False, whether the destination for the |
| 90 notifications is an App Engine application. |
| 91 """ |
| 92 super(WebhookChannel, self).__init__( |
| 93 channel_type='web_hook', |
| 94 channel_args={ |
| 95 'url': url, |
| 96 'app_engine': app_engine and 'true' or 'false', |
| 97 } |
| 98 ) |
| 99 |
| 100 |
| 101 class Headers(collections.defaultdict): |
| 102 """Headers for managing subscriptions.""" |
| 103 |
| 104 |
| 105 ALL_HEADERS = set([SUBSCRIBE, SUBSCRIPTION_ID, TOPIC_ID, TOPIC_URI, |
| 106 CLIENT_TOKEN, EVENT_TYPE, UNSUBSCRIBE]) |
| 107 |
| 108 def __init__(self): |
| 109 """Create a new subscription configuration instance.""" |
| 110 collections.defaultdict.__init__(self, str) |
| 111 |
| 112 def __setitem__(self, key, value): |
| 113 """Set a header value, ensuring the key is an allowed value. |
| 114 |
| 115 Args: |
| 116 key: str, the header key. |
| 117 value: str, the header value. |
| 118 Raises: |
| 119 ValueError if key is not one of the accepted headers. |
| 120 """ |
| 121 normal_key = self._normalize_key(key) |
| 122 if normal_key not in self.ALL_HEADERS: |
| 123 raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) |
| 124 else: |
| 125 return collections.defaultdict.__setitem__(self, normal_key, value) |
| 126 |
| 127 def __getitem__(self, key): |
| 128 """Get a header value, normalizing the key case. |
| 129 |
| 130 Args: |
| 131 key: str, the header key. |
| 132 Returns: |
| 133 String header value. |
| 134 Raises: |
| 135 KeyError if the key is not one of the accepted headers. |
| 136 """ |
| 137 normal_key = self._normalize_key(key) |
| 138 if normal_key not in self.ALL_HEADERS: |
| 139 raise ValueError('Header name must be one of %s.' % self.ALL_HEADERS) |
| 140 else: |
| 141 return collections.defaultdict.__getitem__(self, normal_key) |
| 142 |
| 143 def _normalize_key(self, key): |
| 144 """Normalize a header name for use as a key.""" |
| 145 return key.upper() |
| 146 |
| 147 def items(self): |
| 148 """Generator for each header.""" |
| 149 for header in self.ALL_HEADERS: |
| 150 value = self[header] |
| 151 if value: |
| 152 yield header, value |
| 153 |
| 154 def write(self, headers): |
| 155 """Applies the subscription headers. |
| 156 |
| 157 Args: |
| 158 headers: dict of headers to insert values into. |
| 159 """ |
| 160 for header, value in self.items(): |
| 161 headers[header.lower()] = value |
| 162 |
| 163 def read(self, headers): |
| 164 """Read from headers. |
| 165 |
| 166 Args: |
| 167 headers: dict of headers to read from. |
| 168 """ |
| 169 for header in self.ALL_HEADERS: |
| 170 if header.lower() in headers: |
| 171 self[header] = headers[header.lower()] |
| 172 |
| 173 |
| 174 class Subscription(object): |
| 175 """Information about a subscription.""" |
| 176 |
| 177 def __init__(self): |
| 178 """Create a new Subscription.""" |
| 179 self.headers = Headers() |
| 180 |
| 181 @classmethod |
| 182 def for_request(cls, request, channel, client_token=None): |
| 183 """Creates a subscription and attaches it to a request. |
| 184 |
| 185 Args: |
| 186 request: An http.HttpRequest to modify for making a subscription. |
| 187 channel: A apiclient.push.Channel describing the subscription to |
| 188 create. |
| 189 client_token: (optional) client token to verify the notification. |
| 190 |
| 191 Returns: |
| 192 New subscription object. |
| 193 """ |
| 194 subscription = cls.for_channel(channel=channel, client_token=client_token) |
| 195 subscription.headers.write(request.headers) |
| 196 if request.method != 'GET': |
| 197 raise InvalidSubscriptionRequestError( |
| 198 'Can only subscribe to requests which are GET.') |
| 199 request.method = 'POST' |
| 200 |
| 201 def _on_response(response, subscription=subscription): |
| 202 """Called with the response headers. Reads the subscription headers.""" |
| 203 subscription.headers.read(response) |
| 204 |
| 205 request.add_response_callback(_on_response) |
| 206 return subscription |
| 207 |
| 208 @classmethod |
| 209 def for_channel(cls, channel, client_token=None): |
| 210 """Alternate constructor to create a subscription from a channel. |
| 211 |
| 212 Args: |
| 213 channel: A apiclient.push.Channel describing the subscription to |
| 214 create. |
| 215 client_token: (optional) client token to verify the notification. |
| 216 |
| 217 Returns: |
| 218 New subscription object. |
| 219 """ |
| 220 subscription = cls() |
| 221 channel.write_header(subscription.headers) |
| 222 if client_token is None: |
| 223 client_token = new_token() |
| 224 subscription.headers[SUBSCRIPTION_ID] = new_token() |
| 225 subscription.headers[CLIENT_TOKEN] = client_token |
| 226 return subscription |
| 227 |
| 228 def verify(self, headers): |
| 229 """Verifies that a webhook notification has the correct client_token. |
| 230 |
| 231 Args: |
| 232 headers: dict of request headers for a push notification. |
| 233 |
| 234 Returns: |
| 235 Boolean value indicating whether the notification is verified. |
| 236 """ |
| 237 new_subscription = Subscription() |
| 238 new_subscription.headers.read(headers) |
| 239 return new_subscription.client_token == self.client_token |
| 240 |
| 241 @property |
| 242 def subscribe(self): |
| 243 """Subscribe header value.""" |
| 244 return self.headers[SUBSCRIBE] |
| 245 |
| 246 @property |
| 247 def subscription_id(self): |
| 248 """Subscription ID header value.""" |
| 249 return self.headers[SUBSCRIPTION_ID] |
| 250 |
| 251 @property |
| 252 def topic_id(self): |
| 253 """Topic ID header value.""" |
| 254 return self.headers[TOPIC_ID] |
| 255 |
| 256 @property |
| 257 def topic_uri(self): |
| 258 """Topic URI header value.""" |
| 259 return self.headers[TOPIC_URI] |
| 260 |
| 261 @property |
| 262 def client_token(self): |
| 263 """Client Token header value.""" |
| 264 return self.headers[CLIENT_TOKEN] |
| 265 |
| 266 @property |
| 267 def event_type(self): |
| 268 """Event Type header value.""" |
| 269 return self.headers[EVENT_TYPE] |
| 270 |
| 271 @property |
| 272 def unsubscribe(self): |
| 273 """Unsuscribe header value.""" |
| 274 return self.headers[UNSUBSCRIBE] |
OLD | NEW |