SteveMarshall / fire-eagle-python-binding

A binding for the Fire Eagle API in Python

This URL has Read+Write access

mojodna (author)
Tue May 26 19:15:21 -0700 2009
commit  ebace6d8cdf1a5908550732e32f7a5b911c7de72
tree    9c3b0cae53310bd9e093bb9a9d3ca7cf1bd05c26
parent  7254b8af50a61f9bb5b5e6b19fdc3ddc8f16705f
fire-eagle-python-binding / fireeagle_api.py
2b32f51e » Steven Marshall 2008-03-04 first commit 1 """
b3336f3b » Steven Marshall 2008-03-25 Added Github URL and update... 2 Fire Eagle API Python module v0.6.1
2b32f51e » Steven Marshall 2008-03-04 first commit 3 by Steve Marshall <steve@nascentguruism.com>
4 <http://nascentguruism.com/>
5
b3336f3b » Steven Marshall 2008-03-25 Added Github URL and update... 6 Source repo at <http://github.com/SteveMarshall/fire-eagle-python-binding/>
7
2b32f51e » Steven Marshall 2008-03-04 first commit 8 Example usage:
9
10 >>> from fireeagle_api import FireEagle
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 11 >>> from pprint import pprint
2b32f51e » Steven Marshall 2008-03-04 first commit 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 )
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 18 >>> pprint( fe.lookup( user_token, q='London, England' ) )
2b32f51e » Steven Marshall 2008-03-04 first commit 19 [{'name': 'London, England', 'place_id': '.2P4je.dBZgMyQ'}]
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 20 >>> pprint( fe.user( user_token ) )
2b32f51e » Steven Marshall 2008-03-04 first commit 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 """
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 80 import datetime, httplib, os.path, re, string
2b32f51e » Steven Marshall 2008-03-04 first commit 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'
81f4c59b » Steven Marshall 2008-04-30 Updated to use HTTPS Comment 89 FE_PROTOCOL = 'https'
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 90 FE_SERVER = 'fireeagle.yahoo.net'
2b32f51e » Steven Marshall 2008-03-04 first commit 91
92 # Calling templates
93 API_URL_TEMPLATE = string.Template(
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 94 '${server}/api/' + API_VERSION + '/${method}'
2b32f51e » Steven Marshall 2008-03-04 first commit 95 )
96 OAUTH_URL_TEMPLATE = string.Template(
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 97 '${server}/oauth/${method}'
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 98 )
99 AUTHORIZE_URL_TEMPLATE = string.Template(
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 100 '${server}/oauth/${method}?oauth_token=${token}'
2b32f51e » Steven Marshall 2008-03-04 first commit 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',
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 108 'lon', 'mcc', 'mnc', 'place_id', 'postal', 'q', 'state', 'woeid'
2b32f51e » Steven Marshall 2008-03-04 first commit 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')
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 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?
81f4c59b » Steven Marshall 2008-04-30 Updated to use HTTPS Comment 131 return [float(bit) for bit in s.split(' ')]
2b32f51e » Steven Marshall 2008-03-04 first commit 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,
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 156 'woeid' : string,
2b32f51e » Steven Marshall 2008-03-04 first commit 157 }
158
159 USER_LOCATION = 'location', {
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 160 'best_guess' : boolean,
2b32f51e » Steven Marshall 2008-03-04 first commit 161 # HACK: I'm not entirely happy using 'georss:box' as the key here
162 'georss:box' : geo_str,
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 163 'georss:point' : geo_str,
164 'level' : int,
165 'level_name' : string,
166 'located_at' : date,
167 'name' : string,
168 'place_id' : string,
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 169 'woeid' : string,
170 'query' : string,
2b32f51e » Steven Marshall 2008-03-04 first commit 171 }
172
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 173 USER = 'user', {
174 'token' : string,
e304fa5e » Steven Marshall 2008-03-06 Added support for _experime... 175 'location': USER_LOCATION,
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 176 }
2b32f51e » Steven Marshall 2008-03-04 first commit 177 FIREEAGLE_METHODS = {
178 # OAuth methods
179 'access_token': {
180 'http_headers': None,
181 'http_method' : 'GET',
182 'optional' : [],
ebace6d8 » mojodna 2009-05-26 support for OAuth 1.0a 183 'required' : ['oauth_verifier', 'token'],
2b32f51e » Steven Marshall 2008-03-04 first commit 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',
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 193 'url_template': AUTHORIZE_URL_TEMPLATE,
2b32f51e » Steven Marshall 2008-03-04 first commit 194 },
195 'request_token': {
196 'http_headers': None,
197 'http_method' : 'GET',
ebace6d8 » mojodna 2009-05-26 support for OAuth 1.0a 198 'optional' : ['oauth_callback'],
2b32f51e » Steven Marshall 2008-03-04 first commit 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 },
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 212 'recent': {
213 'http_headers': None,
214 'http_method' : 'GET',
e304fa5e » Steven Marshall 2008-03-06 Added support for _experime... 215 'optional' : ['per_page', 'page', 'time'],
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 216 'required' : ['token'],
217 'returns' : USER,
218 'url_template': API_URL_TEMPLATE,
219 },
2b32f51e » Steven Marshall 2008-03-04 first commit 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'],
65c08d86 » Steven Marshall 2008-03-06 Added support for _experime... 234 'returns' : USER,
2b32f51e » Steven Marshall 2008-03-04 first commit 235 'url_template': API_URL_TEMPLATE,
236 },
e304fa5e » Steven Marshall 2008-03-06 Added support for _experime... 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 }
2b32f51e » Steven Marshall 2008-03-04 first commit 248 }
249
250 class FireEagleException( Exception ):
251 pass
252
e304fa5e » Steven Marshall 2008-03-06 Added support for _experime... 253 # Used as a proxy for methods of the FireEagle class; when methods are called,
2b32f51e » Steven Marshall 2008-03-04 first commit 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:
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 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
2b32f51e » Steven Marshall 2008-03-04 first commit 275 # Prepare object lifetime variables
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 276 self.read_config( rc_or_consumer_key, consumer_secret )
2b32f51e » Steven Marshall 2008-03-04 first commit 277 self.oauth_consumer = oauth.OAuthConsumer(
278 self.consumer_key,
279 self.consumer_secret
280 )
281 self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 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 )
2b32f51e » Steven Marshall 2008-03-04 first commit 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 ))
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 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
2b32f51e » Steven Marshall 2008-03-04 first commit 326
327 def fetch_response( self, http_method, url, \
328 body = None, headers = None ):
329 """Pass a request to the server and return the response as a string"""
330
331 # Prepare the request
332 if ( body is not None ) or ( headers is not None ):
333 self.http_connection.request( http_method, url, body, headers )
334 else:
335 self.http_connection.request( http_method, url )
336
337 # Get the response
338 response = self.http_connection.getresponse()
339 response_body = response.read()
340
341 # If we've been informed of an error, raise it
342 if ( 200 != response.status ):
343 # Try to get the error message
344 try:
345 error_dom = minidom.parseString( response_body )
346 response_errors = error_dom.getElementsByTagName( 'err' )
347 except: # TODO: Naked except: make this explicit!
348 response_errors = None
349
350 # If we can't get the error message, just raise a generic one
351 if response_errors:
352 msg = SPECIFIED_ERROR_EXCEPTION.substitute( \
353 message = response_errors[0].getAttribute( 'msg' ),
354 code = response_errors[0].getAttribute( 'code' )
355 )
356 else:
357 msg = UNSPECIFIED_ERROR_EXCEPTION.substitute( \
358 status = response.status )
359
360 raise FireEagleException, msg
361
362 # Return the body of the response
363 return response_body
364
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 365 def build_return( self, dom_element, target_element_name, conversions):
366 results = []
367 for node in dom_element.getElementsByTagName( target_element_name ):
368 data = {}
369
370 for key, conversion in conversions.items():
371 node_key = key.replace( '_', '-' )
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 372 key = key.replace( ':', '_' )
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 373 data_elements = node.getElementsByTagName( node_key )
374
65c08d86 » Steven Marshall 2008-03-06 Added support for _experime... 375 # If conversion is a tuple, call build_return again
376 if isinstance( conversion, tuple ):
377 child_element, child_conversions = conversion
378 data[key] = self.build_return( \
379 node, child_element, child_conversions \
380 )
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 381 else:
65c08d86 » Steven Marshall 2008-03-06 Added support for _experime... 382 # If we've got multiple elements, build a
383 # list of conversions
384 if data_elements and ( len( data_elements ) > 1 ):
385 data_item = []
386 for data_element in data_elements:
387 data_item.append( conversion(
388 data_element.firstChild.data
389 ) )
390 # If we only have one element, assume text node
391 elif data_elements:
392 data_item = conversion( \
393 data_elements[0].firstChild.data
394 )
395 # If no elements are matched, convert the attribute
396 else:
397 data_item = conversion( \
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 398 node.getAttribute( node_key ) \
65c08d86 » Steven Marshall 2008-03-06 Added support for _experime... 399 )
ee8b2202 » Steven Marshall 2008-03-25 Updated to include georss:p... 400
401 if data_item is not None:
402 data[key] = data_item
270cce1a » Steven Marshall 2008-03-06 Extracted build_return meth... 403
404 results.append( data )
405
406 return results
407
2b32f51e » Steven Marshall 2008-03-04 first commit 408 def call_method( self, method, *args, **kw ):
409
410 # Theoretically, we might want to do 'does this method exits?' checks
411 # here, but as all the aggregators are being built in __init__(),
412 # we actually don't need to: Python handles it for us.
413 meta = FIREEAGLE_METHODS[method]
414
415 if args:
416 # Positional arguments are mapped to meta['required']
417 # and meta['optional'] in order of specification of those
418 # (with required first, obviously)
419 names = meta['required'] + meta['optional']
420 for i in range( len( args ) ):
421 kw[names[i]] = args[i]
422
423 # Check we have all required arguments
424 if len( set( meta['required'] ) - set( kw.keys() ) ) > 0:
425 raise FireEagleException, \
426 NULL_ARGUMENT_EXCEPTION.substitute( \
427 method = method, \
428 args = ', '.join( meta['required'] )
429 )
430
431 # Token shouldn't be handled as a normal arg, so strip it out
432 # (but make sure we have it, even if it's None)
433 if 'token' in kw:
434 token = kw['token']
435 del kw['token']
436 else:
437 token = None
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 438
439 # If the return type is the request_url, simply build the URL
440 # (without a signature) and return it witout executing
441 # anything.
442 if 'request_url' == meta['returns']:
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 443 return meta['url_template'].substitute( method=method, server=self.auth_server, token=token.key )
ebace6d8 » mojodna 2009-05-26 support for OAuth 1.0a 444
445 if 'oauth_callback' in meta['optional'] and 'oauth_callback' not in kw:
446 kw['oauth_callback'] = "oob"
447
2b32f51e » Steven Marshall 2008-03-04 first commit 448 # Build and sign the oauth_request
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 449 # NOTE: If ( token == None ), it's handled silently
2b32f51e » Steven Marshall 2008-03-04 first commit 450 # when building/signing
451 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
452 self.oauth_consumer,
453 token = token,
454 http_method = meta['http_method'],
9f40e6d4 » myelin 2008-08-08 now you can store your key ... 455 http_url = meta['url_template'].substitute( method=method, server=self.api_server ),
2b32f51e » Steven Marshall 2008-03-04 first commit 456 parameters = kw
457 )
458 oauth_request.sign_request(
459 self.signature_method,
460 self.oauth_consumer,
461 token
462 )
463
464 if 'POST' == meta['http_method']:
465 response = self.fetch_response( oauth_request.http_method, \
9c235b7f » SteveMarshall 2009-05-21 Ensure POST uses correct UR... 466 oauth_request.get_normalized_http_url(), \
467 oauth_request.to_postdata(), \
2b32f51e » Steven Marshall 2008-03-04 first commit 468 meta['http_headers'] )
469 else:
470 response = self.fetch_response( oauth_request.http_method, \
471 oauth_request.to_url() )
472
473 # Method returns nothing, but finished fine
474 if not meta['returns']:
475 return True
476 # Return the oauth token
477 elif 'oauth_token' == meta['returns']:
478 return oauth.OAuthToken.from_string( response )
479
480 element, conversions = meta['returns']
481 response_dom = minidom.parseString( response )
d6e1d9bd » Steven Marshall 2008-03-25 Removed extraneous print 482
a9a96f0b » Steven Marshall 2008-03-06 Tidied a couple things. 483 results = self.build_return( \
484 response_dom, element, conversions )
2b32f51e » Steven Marshall 2008-03-04 first commit 485
a9a96f0b » Steven Marshall 2008-03-06 Tidied a couple things. 486 return results
2b32f51e » Steven Marshall 2008-03-04 first commit 487
488
d19a70b6 » myelin 2008-08-07 updated fireeagle library t... 489 # TODO: Cached version