Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 491 lines (432 sloc) 18.385 kB
2b32f51 first commit
Steven Marshall authored
1 """
b3336f3 Added Github URL and updated version #.
Steven Marshall authored
2 Fire Eagle API Python module v0.6.1
2b32f51 first commit
Steven Marshall authored
3 by Steve Marshall <steve@nascentguruism.com>
4 <http://nascentguruism.com/>
5
b3336f3 Added Github URL and updated version #.
Steven Marshall authored
6 Source repo at <http://github.com/SteveMarshall/fire-eagle-python-binding/>
7
2b32f51 first commit
Steven Marshall authored
8 Example usage:
9
10 >>> from fireeagle_api import FireEagle
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
11 >>> from pprint import pprint
2b32f51 first commit
Steven Marshall authored
12 >>> fe = FireEagle( YOUR_CONSUMER_KEY, YOUR_CONSUMER_SECRET )
13 >>> application_token = fe.request_token()
14 >>> auth_url = fe.authorize( application_token )
15 >>> print auth_url
16 >>> pause( 'Please authorize the app at that URL!' )
17 >>> user_token = fe.access_token( application_token )
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
18 >>> pprint( fe.lookup( user_token, q='London, England' ) )
2b32f51 first commit
Steven Marshall authored
19 [{'name': 'London, England', 'place_id': '.2P4je.dBZgMyQ'}]
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
20 >>> pprint( fe.user( user_token ) )
2b32f51 first commit
Steven Marshall authored
21 [ { 'best_guess': True,
22 'georss:box': [ u'51.2613182068',
23 u'-0.5090100169',
24 u'51.6860313416',
25 u'0.2803600132'],
26 'level': 3,
27 'level_name': 'city',
28 'located_at': datetime.datetime(2008, 2, 29, 13, 22, 2),
29 'name': 'London, England',
30 'place_id': '.2P4je.dBZgMyQ'},
31 { 'best_guess': True,
32 'georss:box': [ u'49.8662185669',
33 u'-6.4506998062',
34 u'55.8111686707',
35 u'1.7633299828'],
36 'level': 5,
37 'level_name': 'state',
38 'located_at': datetime.datetime(2008, 2, 29, 13, 22, 2),
39 'name': 'England, United Kingdom',
40 'place_id': 'pn4MsiGbBZlXeplyXg'},
41 { 'best_guess': True,
42 'georss:box': [ u'49.1620903015',
43 u'-8.6495599747',
44 u'60.8606987',
45 u'1.7633399963'],
46 'level': 6,
47 'level_name': 'country',
48 'located_at': datetime.datetime(2008, 2, 29, 13, 22, 2),
49 'name': 'United Kingdom',
50 'place_id': 'DevLebebApj4RVbtaQ'}]
51
52 Copyright (c) 2008, Steve Marshall
53 All rights reserved.
54
55 Unless otherwise specified, redistribution and use of this software in
56 source and binary forms, with or without modification, are permitted
57 provided that the following conditions are met:
58
59 * Redistributions of source code must retain the above copyright
60 notice, this list of conditions and the following disclaimer.
61 * Redistributions in binary form must reproduce the above copyright
62 notice, this list of conditions and the following disclaimer in the
63 documentation and/or other materials provided with the distribution.
64 * The name of the author nor the names of any contributors may be
65 used to endorse or promote products derived from this software without
66 specific prior written permission.
67
68 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
69 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
70 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
71 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
72 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
73 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
74 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
75 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
76 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
77 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
78 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
79 """
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
80 import datetime, httplib, os.path, re, string
2b32f51 first commit
Steven Marshall authored
81 from xml.dom import minidom
82
83 import oauth
84
85 # General API setup
86 API_PROTOCOL = 'https'
87 API_SERVER = 'fireeagle.yahooapis.com'
88 API_VERSION = '0.1'
81f4c59 Updated to use HTTPS
Steven Marshall authored
89 FE_PROTOCOL = 'https'
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
90 FE_SERVER = 'fireeagle.yahoo.net'
2b32f51 first commit
Steven Marshall authored
91
92 # Calling templates
93 API_URL_TEMPLATE = string.Template(
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
94 '${server}/api/' + API_VERSION + '/${method}'
2b32f51 first commit
Steven Marshall authored
95 )
96 OAUTH_URL_TEMPLATE = string.Template(
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
97 '${server}/oauth/${method}'
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
98 )
99 AUTHORIZE_URL_TEMPLATE = string.Template(
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
100 '${server}/oauth/${method}?oauth_token=${token}'
2b32f51 first commit
Steven Marshall authored
101 )
102 POST_HEADERS = {
103 'Content-type': 'application/x-www-form-urlencoded',
104 'Accept' : 'text/plain'
105 }
106 LOCATION_PARAMETERS = [
107 'address', 'cid', 'city', 'country', 'geom', 'lac', 'lat',
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
108 'lon', 'mcc', 'mnc', 'place_id', 'postal', 'q', 'state', 'woeid'
2b32f51 first commit
Steven Marshall authored
109 ]
110
111 # Error templates
112 NULL_ARGUMENT_EXCEPTION = string.Template(
113 'Too few arguments were supplied for the method ${method}; required arguments are: ${args}'
114 )
115 # TODO: Allow specification of method name and call-stack?
116 SPECIFIED_ERROR_EXCEPTION = string.Template(
117 '${message} (Code ${code})'
118 )
119 UNSPECIFIED_ERROR_EXCEPTION = string.Template(
120 'An error occurred whilst trying to execute the requested method, and the server responded with status ${status}.'
121 )
122
123 # Attribute conversion functions
124 string = lambda s: s.encode('utf8')
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
125 boolean = lambda s: 'true' == s.lower()
126
127 def geo_str(s):
128 if 0 == len(s):
129 return None
130 # TODO: Would this be better served returning an array of floats?
81f4c59 Updated to use HTTPS
Steven Marshall authored
131 return [float(bit) for bit in s.split(' ')]
2b32f51 first commit
Steven Marshall authored
132
133 def date(s):
134 # 2008-02-08T10:49:03-08:00
135 bits = re.match(r"""
136 ^(\d{4}) # Year ($1)
137 -(\d{2}) # Month ($2)
138 -(\d{2}) # Day ($3)
139 T(\d{2}) # Hour ($4)
140 :(\d{2}) # Minute ($5)
141 :(\d{2}) # Second ($6)
142 [+-] # TODO: TZ offset dir ($7)
143 \d{2} # TODO: Offset hour ($8)
144 :\d{2} # TODO: Offset min ($9)
145 """, s, re.VERBOSE
146 ).groups()
147 bits = [bit for bit in bits if bit is not None]
148
149 # TODO: Generate fixed-offset tzinfo
150 return datetime.datetime(*map(int, bits))
151
152 # Return types
153 LOCATION = 'location', {
154 'name' : string,
155 'place_id': string,
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
156 'woeid' : string,
2b32f51 first commit
Steven Marshall authored
157 }
158
159 USER_LOCATION = 'location', {
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
160 'best_guess' : boolean,
2b32f51 first commit
Steven Marshall authored
161 # HACK: I'm not entirely happy using 'georss:box' as the key here
162 'georss:box' : geo_str,
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
163 'georss:point' : geo_str,
164 'level' : int,
165 'level_name' : string,
166 'located_at' : date,
167 'name' : string,
168 'place_id' : string,
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
169 'woeid' : string,
170 'query' : string,
2b32f51 first commit
Steven Marshall authored
171 }
172
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
173 USER = 'user', {
174 'token' : string,
e304fa5 Added support for _experimental_ within method.
Steven Marshall authored
175 'location': USER_LOCATION,
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
176 }
2b32f51 first commit
Steven Marshall authored
177 FIREEAGLE_METHODS = {
178 # OAuth methods
179 'access_token': {
180 'http_headers': None,
181 'http_method' : 'GET',
182 'optional' : [],
ebace6d @mojodna support for OAuth 1.0a
mojodna authored
183 'required' : ['oauth_verifier', 'token'],
2b32f51 first commit
Steven Marshall authored
184 'returns' : 'oauth_token',
185 'url_template': OAUTH_URL_TEMPLATE,
186 },
187 'authorize': {
188 'http_headers': None,
189 'http_method' : 'GET',
190 'optional' : [],
191 'required' : ['token'],
192 'returns' : 'request_url',
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
193 'url_template': AUTHORIZE_URL_TEMPLATE,
2b32f51 first commit
Steven Marshall authored
194 },
195 'request_token': {
196 'http_headers': None,
197 'http_method' : 'GET',
ebace6d @mojodna support for OAuth 1.0a
mojodna authored
198 'optional' : ['oauth_callback'],
2b32f51 first commit
Steven Marshall authored
199 'required' : [],
200 'returns' : 'oauth_token',
201 'url_template': OAUTH_URL_TEMPLATE,
202 },
203 # Fire Eagle methods
204 'lookup': {
205 'http_headers': None,
206 'http_method' : 'GET',
207 'optional' : LOCATION_PARAMETERS,
208 'required' : ['token'],
209 'returns' : LOCATION,
210 'url_template': API_URL_TEMPLATE,
211 },
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
212 'recent': {
213 'http_headers': None,
214 'http_method' : 'GET',
e304fa5 Added support for _experimental_ within method.
Steven Marshall authored
215 'optional' : ['per_page', 'page', 'time'],
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
216 'required' : ['token'],
217 'returns' : USER,
218 'url_template': API_URL_TEMPLATE,
219 },
2b32f51 first commit
Steven Marshall authored
220 'update': {
221 'http_headers': POST_HEADERS,
222 'http_method' : 'POST',
223 'optional' : LOCATION_PARAMETERS,
224 'required' : ['token'],
225 # We don't care about returns from update: HTTP 200 is success
226 'returns' : None,
227 'url_template': API_URL_TEMPLATE,
228 },
229 'user': {
230 'http_headers': None,
231 'http_method' : 'GET',
232 'optional' : [],
233 'required' : ['token'],
65c08d8 Added support for _experimental_ recent method.
Steven Marshall authored
234 'returns' : USER,
2b32f51 first commit
Steven Marshall authored
235 'url_template': API_URL_TEMPLATE,
236 },
e304fa5 Added support for _experimental_ within method.
Steven Marshall authored
237 'within': {
238 'http_headers': None,
239 'http_method' : 'GET',
240 # HACK: woe_id is ignored if place_id is present, so neither is
241 # strictly 'required'. Unfortunately, calling with neither
242 # returns an empty list
243 'optional' : ['place_id', 'woe_id'],
244 'required' : ['token'],
245 'returns' : USER,
246 'url_template': API_URL_TEMPLATE,
247 }
2b32f51 first commit
Steven Marshall authored
248 }
249
250 class FireEagleException( Exception ):
251 pass
252
e304fa5 Added support for _experimental_ within method.
Steven Marshall authored
253 # Used as a proxy for methods of the FireEagle class; when methods are called,
2b32f51 first commit
Steven Marshall authored
254 # __call__ in FireEagleAccumulator is called, ultimately calling the
255 # fireeagle_obj's callMethod()
256 class FireEagleAccumulator:
257 def __init__( self, fireeagle_obj, name ):
258 self.fireeagle_obj = fireeagle_obj
259 self.name = name
260
261 def __repr__( self ):
262 return self.name
263
264 def __call__( self, *args, **kw ):
265 return self.fireeagle_obj.call_method( self.name, *args, **kw )
266
267
268 class FireEagle:
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
269 def __init__( self, rc_or_consumer_key, consumer_secret=None ):
270 """
271 syntax: FireEagle( os.path.expanduser( "~/.fireeaglerc" ) )
272 or FireEagle( CONSUMER_KEY, CONSUMER_SECRET )
273 """
274
2b32f51 first commit
Steven Marshall authored
275 # Prepare object lifetime variables
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
276 self.read_config( rc_or_consumer_key, consumer_secret )
2b32f51 first commit
Steven Marshall authored
277 self.oauth_consumer = oauth.OAuthConsumer(
278 self.consumer_key,
279 self.consumer_secret
280 )
281 self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
282 proto, host, port = re.search(r"^(https?)://([a-z\.0-9]+)(?:\:(\d+))?$", self.api_server).groups()
283 self.http_connection = (proto == 'https' and httplib.HTTPSConnection or httplib.HTTPConnection)( host, port )
2b32f51 first commit
Steven Marshall authored
284
285 # Prepare the accumulators for each method
286 for method, _ in FIREEAGLE_METHODS.items():
287 if not hasattr( self, method ):
288 setattr( self, method, FireEagleAccumulator( self, method ))
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
289
290 def read_config( self, rc_or_consumer_key, consumer_secret ):
291 if consumer_secret is None:
292 info = {}
293 for line in open( rc_or_consumer_key ).readlines():
294 p = line.find( "#" )
295 if p != -1: line = line[:p]
296 line = line.strip()
297 if not line: continue
298 k, v = line.split("=", 1)
299 info[ k.strip() ] = v.strip()
300 else:
301 info = {
302 'consumer_key': rc_or_consumer_key,
303 'consumer_secret': consumer_secret,
304 }
305
306 info.setdefault("api_server", API_SERVER)
307 info.setdefault("api_protocol", API_PROTOCOL)
308 self.api_server = self._build_server_url(info, 'api')
309
310 info.setdefault("auth_server", FE_SERVER)
311 info.setdefault("auth_protocol", FE_PROTOCOL)
312 self.auth_server = self._build_server_url(info, 'auth')
313
314 self.consumer_key, self.consumer_secret = info['consumer_key'], info['consumer_secret']
315
316 def _build_server_url( self, info, role ):
317 proto = info['%s_protocol' % role]
318 default_port = (proto == 'https') and 443 or 80
319 port = int(info.get('%s_port' % role, default_port))
320 url = '%s://%s%s' % (
321 proto,
322 info['%s_server' % role],
323 (port != default_port) and (':%d' % port) or '',
324 )
325 return url
2b32f51 first commit
Steven Marshall authored
326
327 def fetch_response( self, http_method, url, \
2a4feda Added HTTP host header to all requests so that it works with Python v…
Ben Dowling authored
328 body = None, headers = {}):
2b32f51 first commit
Steven Marshall authored
329 """Pass a request to the server and return the response as a string"""
2a4feda Added HTTP host header to all requests so that it works with Python v…
Ben Dowling authored
330 # Explicity add a host header to prevent Python pre2.6 from adding the port
331 headers['host'] = API_SERVER
2b32f51 first commit
Steven Marshall authored
332 # Prepare the request
333 if ( body is not None ) or ( headers is not None ):
334 self.http_connection.request( http_method, url, body, headers )
335 else:
336 self.http_connection.request( http_method, url )
337
338 # Get the response
339 response = self.http_connection.getresponse()
340 response_body = response.read()
341
342 # If we've been informed of an error, raise it
343 if ( 200 != response.status ):
344 # Try to get the error message
345 try:
346 error_dom = minidom.parseString( response_body )
347 response_errors = error_dom.getElementsByTagName( 'err' )
348 except: # TODO: Naked except: make this explicit!
349 response_errors = None
350
351 # If we can't get the error message, just raise a generic one
352 if response_errors:
353 msg = SPECIFIED_ERROR_EXCEPTION.substitute( \
354 message = response_errors[0].getAttribute( 'msg' ),
355 code = response_errors[0].getAttribute( 'code' )
356 )
357 else:
358 msg = UNSPECIFIED_ERROR_EXCEPTION.substitute( \
359 status = response.status )
360
361 raise FireEagleException, msg
362
363 # Return the body of the response
364 return response_body
365
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
366 def build_return( self, dom_element, target_element_name, conversions):
367 results = []
368 for node in dom_element.getElementsByTagName( target_element_name ):
369 data = {}
370
371 for key, conversion in conversions.items():
372 node_key = key.replace( '_', '-' )
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
373 key = key.replace( ':', '_' )
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
374 data_elements = node.getElementsByTagName( node_key )
375
65c08d8 Added support for _experimental_ recent method.
Steven Marshall authored
376 # If conversion is a tuple, call build_return again
377 if isinstance( conversion, tuple ):
378 child_element, child_conversions = conversion
379 data[key] = self.build_return( \
380 node, child_element, child_conversions \
381 )
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
382 else:
65c08d8 Added support for _experimental_ recent method.
Steven Marshall authored
383 # If we've got multiple elements, build a
384 # list of conversions
385 if data_elements and ( len( data_elements ) > 1 ):
386 data_item = []
387 for data_element in data_elements:
388 data_item.append( conversion(
389 data_element.firstChild.data
390 ) )
391 # If we only have one element, assume text node
392 elif data_elements:
393 data_item = conversion( \
394 data_elements[0].firstChild.data
395 )
396 # If no elements are matched, convert the attribute
397 else:
398 data_item = conversion( \
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
399 node.getAttribute( node_key ) \
65c08d8 Added support for _experimental_ recent method.
Steven Marshall authored
400 )
ee8b220 Updated to include georss:point and properly convert boolean data.
Steven Marshall authored
401
402 if data_item is not None:
403 data[key] = data_item
270cce1 Extracted build_return method as prep for recursion.
Steven Marshall authored
404
405 results.append( data )
406
407 return results
408
2b32f51 first commit
Steven Marshall authored
409 def call_method( self, method, *args, **kw ):
410
411 # Theoretically, we might want to do 'does this method exits?' checks
412 # here, but as all the aggregators are being built in __init__(),
413 # we actually don't need to: Python handles it for us.
414 meta = FIREEAGLE_METHODS[method]
415
416 if args:
417 # Positional arguments are mapped to meta['required']
418 # and meta['optional'] in order of specification of those
419 # (with required first, obviously)
420 names = meta['required'] + meta['optional']
421 for i in range( len( args ) ):
422 kw[names[i]] = args[i]
423
424 # Check we have all required arguments
425 if len( set( meta['required'] ) - set( kw.keys() ) ) > 0:
426 raise FireEagleException, \
427 NULL_ARGUMENT_EXCEPTION.substitute( \
428 method = method, \
429 args = ', '.join( meta['required'] )
430 )
431
432 # Token shouldn't be handled as a normal arg, so strip it out
433 # (but make sure we have it, even if it's None)
434 if 'token' in kw:
435 token = kw['token']
436 del kw['token']
437 else:
438 token = None
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
439
440 # If the return type is the request_url, simply build the URL
441 # (without a signature) and return it witout executing
442 # anything.
443 if 'request_url' == meta['returns']:
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
444 return meta['url_template'].substitute( method=method, server=self.auth_server, token=token.key )
ebace6d @mojodna support for OAuth 1.0a
mojodna authored
445
446 if 'oauth_callback' in meta['optional'] and 'oauth_callback' not in kw:
447 kw['oauth_callback'] = "oob"
448
2b32f51 first commit
Steven Marshall authored
449 # Build and sign the oauth_request
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
450 # NOTE: If ( token == None ), it's handled silently
2b32f51 first commit
Steven Marshall authored
451 # when building/signing
452 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
453 self.oauth_consumer,
454 token = token,
455 http_method = meta['http_method'],
9f40e6d @myelin now you can store your key info in ~/.fireeaglerc
myelin authored
456 http_url = meta['url_template'].substitute( method=method, server=self.api_server ),
2b32f51 first commit
Steven Marshall authored
457 parameters = kw
458 )
459 oauth_request.sign_request(
460 self.signature_method,
461 self.oauth_consumer,
462 token
463 )
464
465 if 'POST' == meta['http_method']:
466 response = self.fetch_response( oauth_request.http_method, \
54cb8a0 @SteveMarshall Fixed httplib.request() use
authored
467 meta['url_template'].substitute( method=method, server=""), \
9c235b7 @SteveMarshall Ensure POST uses correct URL (without GET parameters).
authored
468 oauth_request.to_postdata(), \
2b32f51 first commit
Steven Marshall authored
469 meta['http_headers'] )
470 else:
471 response = self.fetch_response( oauth_request.http_method, \
54cb8a0 @SteveMarshall Fixed httplib.request() use
authored
472 meta['url_template'].substitute( method=method, server="") + "?" + oauth_request.to_postdata() )
2b32f51 first commit
Steven Marshall authored
473
474 # Method returns nothing, but finished fine
475 if not meta['returns']:
476 return True
477 # Return the oauth token
478 elif 'oauth_token' == meta['returns']:
479 return oauth.OAuthToken.from_string( response )
480
481 element, conversions = meta['returns']
482 response_dom = minidom.parseString( response )
d6e1d9b Removed extraneous print
Steven Marshall authored
483
a9a96f0 Tidied a couple things.
Steven Marshall authored
484 results = self.build_return( \
485 response_dom, element, conversions )
2b32f51 first commit
Steven Marshall authored
486
a9a96f0 Tidied a couple things.
Steven Marshall authored
487 return results
2b32f51 first commit
Steven Marshall authored
488
489
d19a70b @myelin updated fireeagle library to generate correct authorization urls and …
myelin authored
490 # TODO: Cached version
Something went wrong with that request. Please try again.