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 |