Package apiclient :: Module push
[hide private]
[frames] | no frames]

Source Code for Module apiclient.push

  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]
275