1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """
16 Common Tasks
17 ============
18 - Authentication
19 - Google's implementation of OAuth is covered in the documentation for the
20 Accounts API: http://code.google.com/apis/accounts/docs/OAuth_ref.html
21 - Getting the request token::
22 import buzz
23 client = buzz.Client()
24 client.build_oauth_consumer('your-app.appspot.com', 'consumer_secret')
25 client.oauth_scopes.append(buzz.FULL_ACCESS_SCOPE)
26 request_token = \\
27 client.fetch_oauth_request_token('http://example.com/callback/')
28 # Persist the request_token
29 authorization_url = client.build_oauth_authorization_url()
30 self.redirect(authorization_url)
31 - Exchanging a request token for an access token::
32 import buzz
33 client = buzz.Client()
34 client.build_oauth_consumer('your-app.appspot.com', 'consumer_secret')
35 client.oauth_scopes.append(buzz.FULL_ACCESS_SCOPE)
36 # Retrieve the persisted request token
37 client.build_oauth_request_token(
38 request_token.key, request_token.secret
39 )
40 verifier = self.request.get('oauth_verifier')
41 access_token = \\
42 client.fetch_oauth_access_token(verifier)
43 # Persist the access_token
44 - Reusing an access token::
45 import buzz
46 client = buzz.Client()
47 client.build_oauth_consumer('your-app.appspot.com', 'consumer_secret')
48 client.oauth_scopes.append(buzz.FULL_ACCESS_SCOPE)
49 # Retrieve the persisted access token
50 client.build_oauth_access_token(
51 access_token.key, access_token.secret
52 )
53 - Getting streams
54 - Signed-in user's consumption stream::
55 results = client.posts(user_id='@me', type_id='@consumption')
56 - Signed-in user's published posts::
57 results = client.posts(user_id='@me', type_id='@self')
58 - Another user's public posts::
59 results = client.posts(user_id='googlebuzz', type_id='@public')
60 - Larger result pages::
61 results = client.posts(
62 user_id='googlebuzz', type_id='@public', max_results=100
63 )
64 - Creating a post
65 - Simple::
66 post = buzz.Post(
67 content="This is some example content."
68 )
69 client.create_post(post)
70 - Post with a link::
71 attachment = buzz.Attachment(
72 type='article',
73 title='Google Buzz',
74 uri='http://www.google.com/buzz'
75 )
76 post = buzz.Post(
77 content="Google Buzz is really cool.",
78 attachments=[attachment]
79 )
80 client.create_post(post)
81 - Post with a geocode::
82 post = buzz.Post(
83 content="Google Buzz is really cool.",
84 geocode=('37.421776', '-122.084155')
85 )
86 client.create_post(post)
87 """
88
89 import os
90 import sys
91 import urlparse
92 import cgi
93 import httplib
94 import string
95 import urllib
96 import re
97
98 import logging
99
100 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
101
102 try:
103 import oauth.oauth as oauth
104 except (ImportError):
105 import oauth
106
107 try:
108
109 from django.utils import simplejson
110 except (ImportError):
111 import simplejson
112
113 default_path = os.path.join(
114 os.path.dirname(__file__), 'buzz_python_client.yaml'
115 )
116 if not os.path.exists(default_path):
117 default_path = 'buzz_python_client.yaml'
118 CONFIG_PATH = os.environ.get('BUZZ_CONFIG_PATH', default_path)
119 if os.path.exists(CONFIG_PATH):
120
121 try:
122 import yaml
123 except (ImportError):
124 sys.stderr.write('Please install PyYAML.\n')
125 exit(1)
126 CLIENT_CONFIG = yaml.load(open(CONFIG_PATH).read())
127 API_PREFIX = CLIENT_CONFIG.get('api_prefix') or \
128 "https://www.googleapis.com/buzz/v1"
129 else:
130 CLIENT_CONFIG = {}
131 API_PREFIX = "https://www.googleapis.com/buzz/v1"
132
133 READONLY_SCOPE = 'https://www.googleapis.com/auth/buzz.readonly'
134 FULL_ACCESS_SCOPE = 'https://www.googleapis.com/auth/buzz'
135
136 OAUTH_REQUEST_TOKEN_URI = \
137 'https://www.google.com/accounts/OAuthGetRequestToken'
138 OAUTH_ACCESS_TOKEN_URI = \
139 'https://www.google.com/accounts/OAuthGetAccessToken'
140 OAUTH_AUTHORIZATION_URI = \
141 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken'
142
143 if CLIENT_CONFIG.has_key('debug') and CLIENT_CONFIG.get('debug'):
144 DEBUG = True
145 logging.basicConfig(level=logging.DEBUG)
146 else:
147 DEBUG = False
148
149 DEFAULT_PAGE_SIZE = 20
152 """
153 This exception gets raised if there was some kind of HTTP or network error
154 while accessing the API.
155 """
156 - def __init__(self, message=None, uri=None, json=None, exception=None):
157 if not message and exception:
158 message = str(exception)
159 self._uri = uri
160 self._message = message
161 self._json = json
162
164 return 'Could not retrieve \'%s\': %s' % (self._uri, self._message)
165
167 """
168 This exception gets raised if the API sends data that does not match
169 what the client was expecting. If this exception is raised, it's typically
170 a bug.
171 """
172 - def __init__(self, message=None, json=None, uri=None, exception=None):
173 if not message and exception:
174 message = str(exception)
175 self._message = message
176 self._uri = uri
177 self._json = json
178 self._exception = exception
179
181 if self._uri:
182 if self._exception and isinstance(self._exception, KeyError):
183 return 'Parse failed for \'%s\': KeyError(%s) on %s' % (
184 self._uri, str(self._exception), self._json
185 )
186 else:
187 return 'Parse failed for \'%s\': %s' % (self._uri, self._json)
188 else:
189 if self._exception and isinstance(self._exception, KeyError):
190 return 'Parse failed: KeyError(%s) on %s' % (
191 str(self._exception), self._json
192 )
193 else:
194 return 'Parse failed: %s' % (self._json)
195
197
198 if isinstance(json, dict):
199 if isinstance(json, dict) and json.get('data'):
200 json = json['data']
201 if isinstance(json, dict) and json.get('entry'):
202 json = json['entry']
203 if isinstance(json, dict) and json.get('results'):
204 json = json['results']
205 if isinstance(json, dict) and json.get('items'):
206 json = json['items']
207 else:
208 raise TypeError('Expected dict: \'%s\'' % str(json))
209 return json
210
212
213 if not isinstance(json, dict):
214 raise TypeError(
215 'Expected dict as arg but received %s: %s' % type(json), json
216 )
217 links = []
218 if json.get('links'):
219 json = json.get('links')
220 if json:
221 for link_obj in json:
222 if isinstance(link_obj, basestring):
223
224
225 link_list = json[link_obj]
226 for link_json in link_list:
227 links.append(Link(link_json, rel=link_obj))
228 elif isinstance(link_obj, dict):
229
230 link_json = link_obj
231 links.append(Link(link_json))
232 else:
233 raise TypeError('Expected dict: \'%s\'' % str(link_obj))
234 return links
235
237
238 if ' ' in geocode:
239 lat, lon = geocode.split(' ')
240 elif ',' in geocode:
241 lat, lon = geocode.split(',')
242 else:
243 raise ValueError('Bogus geocode.')
244 return (lat, lon)
245
247 """
248 The Buzz API L{Client} object is the primary method of making calls against
249 the Buzz API. It can be used with or without authentication. It attempts to
250 reuse HTTP connections whenever possible. Currently, authentication is done
251 via OAuth.
252 """
254
255
256 parsed = urlparse.urlparse(API_PREFIX)
257 authority = parsed[1].split(':')
258 if len(authority) == 1:
259
260
261 self._host = authority[0]
262 self._port = None
263 else:
264 self._host, self._port = authority
265 if not self._port:
266 if parsed[0] == 'https':
267 self._port = 443
268 else:
269 self._port = 80
270
271 self._http_connection = None
272
273
274 self.oauth_scopes = []
275 self._oauth_http_connection = None
276 self.oauth_consumer = None
277 self.oauth_request_token = None
278 self.oauth_access_token = None
279 self.oauth_display_name = None
280 self._oauth_token_authorized = False
281 self._oauth_signature_method_hmac_sha1 = \
282 oauth.OAuthSignatureMethod_HMAC_SHA1()
283
284 @property
286
287
288 if not self._http_connection:
289 if self._port == 443:
290 self._http_connection = httplib.HTTPSConnection(self._host)
291 elif self._port == 80:
292 self._http_connection = httplib.HTTPConnection(self._host)
293 else:
294 self._http_connection = httplib.HTTPConnection(self._host, self._port)
295 return self._http_connection
296
298 """
299 This method sets the consumer key and secret to 'anonymous'. It can also
300 optionally set the C{xoauth_displayname} parameter. This method is
301 primarily intended for use with installed applications.
302
303 @type oauth_display_name: string
304 @param oauth_display_name: The display name for the application
305 """
306 self.oauth_consumer = oauth.OAuthConsumer('anonymous', 'anonymous')
307 if oauth_display_name:
308 self.oauth_display_name = oauth_display_name
309
311 """
312 This method sets the consumer key and secret. If you do not already have
313 them, these can be obtained by U{registering your web application <
314 http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html
315 >}.
316
317 @type key: string
318 @param key: Your consumer key. This will be your hostname.
319 @type secret: string
320 @param secret: Your consumer secret. This is issued to you by Google.
321 """
322 self.oauth_consumer = oauth.OAuthConsumer(key, secret)
323
325 """
326 This method sets the request token key and secret. This allows you to
327 load a request token into the client from persistent storage.
328
329 @type key: string
330 @param key: The request token key.
331 @type secret: string
332 @param secret: The request token secret.
333 """
334 self.oauth_request_token = oauth.OAuthToken(key, secret)
335
337 self.oauth_access_token = oauth.OAuthToken(key, secret)
338
339 @property
341 if not self._oauth_http_connection:
342 self._oauth_http_connection = httplib.HTTPSConnection('www.google.com')
343 if self._oauth_http_connection.host != 'www.google.com':
344 raise ValueError("OAuth HTTPS Connection must be for 'www.google.com'.")
345
346
347 return self._oauth_http_connection
348
350 """Sends a signed request to Google's Accounts API."""
351
352 if oauth_request.http_method != 'POST':
353 raise ValueError("OAuthRequest HTTP method must be POST.")
354 try:
355 self.oauth_http_connection.request(
356 oauth_request.http_method,
357 oauth_request.http_url,
358 body=oauth_request.to_postdata(),
359 headers={
360 'Content-Type': 'application/x-www-form-urlencoded'
361 }
362 )
363 response = self.oauth_http_connection.getresponse()
364 except (httplib.BadStatusLine, httplib.CannotSendRequest):
365
366 if self._oauth_http_connection:
367 self._oauth_http_connection.close()
368 self._oauth_http_connection = None
369
370 self.oauth_http_connection.request(
371 oauth_request.http_method,
372 oauth_request.http_url,
373 body=oauth_request.to_postdata(),
374 headers={
375 'Content-Type': 'application/x-www-form-urlencoded'
376 }
377 )
378 response = self.oauth_http_connection.getresponse()
379 return response
380
382 """Obtains an OAuth request token from Google's Accounts API."""
383 if not self.oauth_request_token:
384
385 parameters = {
386 'oauth_consumer_key': self.oauth_consumer.key,
387 'oauth_timestamp': oauth.generate_timestamp(),
388 'oauth_nonce': oauth.generate_nonce(),
389 'oauth_version': oauth.OAuthRequest.version,
390 'oauth_callback': callback_uri,
391 'scope': ' '.join(self.oauth_scopes)
392 }
393 if self.oauth_display_name:
394 parameters['xoauth_displayname'] = self.oauth_display_name
395 oauth_request = oauth.OAuthRequest(
396 'POST',
397 OAUTH_REQUEST_TOKEN_URI,
398 parameters
399 )
400 oauth_request.sign_request(
401 self._oauth_signature_method_hmac_sha1,
402 self.oauth_consumer,
403 token=None
404 )
405 response = self.fetch_oauth_response(oauth_request)
406 if response.status == 200:
407
408 self.oauth_request_token = oauth.OAuthToken.from_string(
409 response.read()
410 )
411 else:
412 raise Exception('Failed to obtain request token:\n' + response.read())
413 return self.oauth_request_token
414
416 if not token:
417 token = self.oauth_request_token
418 if not self.oauth_consumer:
419 raise ValueError("Client is missing consumer.")
420 auth_uri = OAUTH_AUTHORIZATION_URI + \
421 "?oauth_token=" + token.key + \
422 "&domain=" + self.oauth_consumer.key + \
423 "&scope=" + '%20'.join(self.oauth_scopes)
424 return auth_uri
425
427 """Obtains an OAuth access token from Google's Accounts API."""
428 if not self.oauth_access_token:
429 if not token:
430 token = self.oauth_request_token
431 if not token:
432 raise ValueError("A request token must be supplied.")
433
434 parameters = {
435 'oauth_consumer_key': self.oauth_consumer.key,
436 'oauth_timestamp': oauth.generate_timestamp(),
437 'oauth_nonce': oauth.generate_nonce(),
438 'oauth_version': oauth.OAuthRequest.version,
439 'oauth_token': token.key,
440 'oauth_verifier': verifier
441 }
442 oauth_request = oauth.OAuthRequest(
443 'POST',
444 OAUTH_ACCESS_TOKEN_URI,
445 parameters
446 )
447 oauth_request.sign_request(
448 self._oauth_signature_method_hmac_sha1,
449 self.oauth_consumer,
450 token=token
451 )
452 response = self.fetch_oauth_response(oauth_request)
453 if response.status == 200:
454
455 self.oauth_access_token = oauth.OAuthToken.from_string(
456 response.read()
457 )
458 else:
459 raise Exception('Failed to obtain access token:\n' + response.read())
460 return self.oauth_access_token
461
463
464
465 query = urlparse.urlparse(http_uri)[4]
466 if query:
467 qs_parser = None
468 if hasattr(urlparse, 'parse_qs'):
469 qs_parser = urlparse.parse_qs
470 else:
471
472 qs_parser = cgi.parse_qs
473
474
475 parameters = qs_parser(
476 query,
477 keep_blank_values=True,
478 strict_parsing=False
479 )
480 for k, v in parameters.iteritems():
481 parameters[k] = v[0]
482 else:
483 parameters = {}
484
485 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
486 self.oauth_consumer,
487 token=self.oauth_access_token,
488 http_method=http_method,
489 http_url=http_uri,
490 parameters=parameters
491 )
492 oauth_request.sign_request(
493 self._oauth_signature_method_hmac_sha1,
494 self.oauth_consumer,
495 token=self.oauth_access_token
496 )
497 return oauth_request
498
499 - def fetch_api_response(self, http_method, http_uri, http_headers={}, \
500 http_connection=None, http_body=''):
501 if not http_connection:
502 http_connection = self.http_connection
503 if not self.oauth_consumer and http_headers.get('Authorization'):
504 del http_headers['Authorization']
505 http_headers.update({
506 'Content-Length': str(len(http_body))
507 })
508 if http_body:
509 http_headers.update({
510 'Content-Type': 'application/json'
511 })
512 if self.oauth_consumer and self.oauth_access_token:
513
514 oauth_request = self.build_oauth_request(http_method, http_uri)
515 http_headers.update(oauth_request.to_header())
516 try:
517 try:
518 http_connection.request(
519 http_method, http_uri,
520 headers=http_headers,
521 body=http_body
522 )
523 response = http_connection.getresponse()
524 except (httplib.BadStatusLine, httplib.CannotSendRequest):
525 if http_connection and http_connection == self.http_connection:
526
527 http_connection.close()
528 http_connection = None
529 self._http_connection = None
530 http_connection = self.http_connection
531
532 http_connection.request(
533 http_method, http_uri,
534 headers=http_headers,
535 body=http_body
536 )
537 response = http_connection.getresponse()
538 except Exception, e:
539 if e.__class__.__name__ == 'ApplicationError' or \
540 e.__class__.__name__ == 'DownloadError':
541 if "5" in e.message:
542 message = "Request timed out"
543 else:
544 message = "Request failed"
545 elif e.__class__.__name__ == 'TypeError':
546 message = "Network failure"
547 else:
548 message = str(e)
549 json = None
550
551 if hasattr(e, '_json'):
552 json = e._json
553 raise RetrieveError(
554 uri=http_uri,
555 message="%s: %s" % (e.__class__.__name__, message),
556 json=json
557 )
558 return response
559
560
561
563 api_endpoint = API_PREFIX + "/people/search?alt=json"
564 if query:
565 api_endpoint += "&q=" + urllib.quote_plus(query)
566 logging.info(api_endpoint)
567 return Result(self, 'GET', api_endpoint, result_type=Person)
568
571 api_endpoint = API_PREFIX + "/activities/search/@people?alt=json"
572 if query:
573 api_endpoint += "&q=" + urllib.quote_plus(query)
574 if (latitude is not None) and (longitude is not None):
575 api_endpoint += "&lat=" + urllib.quote(latitude)
576 api_endpoint += "&lon=" + urllib.quote(longitude)
577 if radius is not None:
578 api_endpoint += "&radius=" + urllib.quote(str(radius))
579 return Result(self, 'GET', api_endpoint, result_type=Person)
580
581 - def person(self, user_id='@me'):
582 if isinstance(user_id, Person):
583
584
585
586 user_id = user_id.id
587 if self.oauth_access_token:
588 api_endpoint = API_PREFIX + ("/people/%s/@self" % user_id)
589 api_endpoint += "?alt=json"
590 return Result(
591 self, 'GET', api_endpoint, result_type=Person, singular=True
592 )
593 else:
594 raise ValueError("This client doesn't have an authenticated user.")
595
597 if isinstance(user_id, Person):
598 user_id = user_id.id
599 api_endpoint = API_PREFIX + ("/people/%s/@groups/@followers" % user_id)
600 api_endpoint += "?alt=json"
601 return Result(self, 'GET', api_endpoint, result_type=Person)
602
604 if isinstance(user_id, Person):
605 user_id = user_id.id
606 api_endpoint = API_PREFIX + ("/people/%s/@groups/@following" % user_id)
607 api_endpoint += "?alt=json"
608 return Result(self, 'GET', api_endpoint, result_type=Person)
609
611 if isinstance(user_id, Person):
612 user_id = user_id.id
613 if self.oauth_access_token:
614 api_endpoint = API_PREFIX + (
615 "/people/@me/@groups/@following/%s" % user_id
616 )
617 api_endpoint += "?alt=json"
618 return Result(self, 'PUT', api_endpoint, result_type=None).data
619 else:
620 raise ValueError("This client doesn't have an authenticated user.")
621
623 if isinstance(user_id, Person):
624 user_id = user_id.id
625 if self.oauth_access_token:
626 api_endpoint = API_PREFIX + (
627 "/people/@me/@groups/@following/%s" % user_id
628 )
629 api_endpoint += "?alt=json"
630 return Result(self, 'DELETE', api_endpoint, result_type=None).data
631 else:
632 raise ValueError("This client doesn't have an authenticated user.")
633
634
635
636 - def search(self, query=None, latitude=None, longitude=None, radius=None):
637 api_endpoint = API_PREFIX + "/activities/search?alt=json"
638 if query:
639 api_endpoint += "&q=" + urllib.quote_plus(query)
640 if (latitude is not None) and (longitude is not None):
641 api_endpoint += "&lat=" + urllib.quote(latitude)
642 api_endpoint += "&lon=" + urllib.quote(longitude)
643 if radius is not None:
644 api_endpoint += "&radius=" + urllib.quote(str(radius))
645 return Result(self, 'GET', api_endpoint, result_type=Post)
646
647 - def posts(self, type_id='@self', user_id='@me', max_results=20):
648 if isinstance(user_id, Person):
649 user_id = user_id.id
650 api_endpoint = API_PREFIX + "/activities/" + str(user_id) + "/" + type_id
651 api_endpoint += "?alt=json"
652 if max_results:
653 api_endpoint += "&max-results=" + str(max_results)
654 return Result(self, 'GET', api_endpoint, result_type=Post)
655
656 - def post(self, post_id, actor_id='0'):
657 if isinstance(actor_id, Person):
658 actor_id = actor_id.id
659 if isinstance(post_id, Post):
660 post_id = post_id.id
661 api_endpoint = API_PREFIX + "/activities/" + str(actor_id) + \
662 "/@self/" + post_id
663 api_endpoint += "?alt=json"
664 return Result(self, 'GET', api_endpoint, result_type=Post, singular=True)
665
666 - def create_post(self, post):
667 api_endpoint = API_PREFIX + "/activities/@me/@self"
668 api_endpoint += "?alt=json"
669 json_string = simplejson.dumps({'data': post._json_output})
670 return Result(
671 self, 'POST', api_endpoint, http_body=json_string, result_type=None
672 ).data
673
674 - def update_post(self, post):
675 if not post.id:
676 raise ValueError('Post must have a valid id to update.')
677 api_endpoint = API_PREFIX + "/activities/@me/@self/" + post.id
678 api_endpoint += "?alt=json"
679 json_string = simplejson.dumps({'data': post._json_output})
680 return Result(
681 self, 'PUT', api_endpoint, http_body=json_string, result_type=None
682 ).data
683
684 - def delete_post(self, post):
685 if not post.id:
686 raise ValueError('Post must have a valid id to delete.')
687 api_endpoint = API_PREFIX + "/activities/@me/@self/" + post.id
688 api_endpoint += "?alt=json"
689 return Result(self, 'DELETE', api_endpoint, result_type=None).data
690
702
713
727
738
740 """Returns a collection of posts that the user has commented on."""
741 return self.posts(type_id='@comments', user_id=user_id)
742
743
744
756
757
758
759 - def likers(self, post_id, actor_id='0', max_results=20):
760 if isinstance(actor_id, Person):
761 actor_id = actor_id.id
762 if isinstance(post_id, Post):
763 post_id = post_id.id
764 api_endpoint = API_PREFIX + "/activities/" + actor_id + \
765 "/@self/" + post_id + "/@liked"
766 api_endpoint += "?alt=json"
767 if max_results:
768 api_endpoint += "&max-results=" + str(max_results)
769 return Result(self, 'GET', api_endpoint, result_type=Person)
770
771 - def liked_posts(self, user_id='@me'):
772 """Returns a collection of posts that a user has liked."""
773 return self.posts(type_id='@liked', user_id=user_id)
774
775 - def like_post(self, post_id):
776 """
777 Likes a post.
778 """
779 if isinstance(post_id, Post):
780 post_id = post_id.id
781 api_endpoint = API_PREFIX + "/activities/@me/@liked/" + post_id
782 api_endpoint += "?alt=json"
783 return Result(
784 self, 'PUT', api_endpoint, result_type=None, singular=True
785 ).data
786
787 - def unlike_post(self, post_id):
788 """
789 Unlikes a post.
790 """
791 if isinstance(post_id, Post):
792 post_id = post_id.id
793 api_endpoint = API_PREFIX + "/activities/@me/@liked/" + post_id
794 api_endpoint += "?alt=json"
795 return Result(
796 self, 'DELETE', api_endpoint, result_type=None, singular=True
797 ).data
798
799
800
801 - def mute_post(self, post_id):
802 """
803 Mutes a post.
804 """
805 if isinstance(post_id, Post):
806 post_id = post_id.id
807 api_endpoint = API_PREFIX + "/activities/@me/@muted/" + post_id
808 api_endpoint += "?alt=json"
809 return Result(
810 self, 'PUT', api_endpoint, result_type=None, singular=True
811 ).data
812
813 - def unmute_post(self, post_id):
814 """
815 Unmutes a post.
816 """
817 if isinstance(post_id, Post):
818 post_id = post_id.id
819 api_endpoint = API_PREFIX + "/activities/@me/@muted/" + post_id
820 api_endpoint += "?alt=json"
821 return Result(
822 self, 'DELETE', api_endpoint, result_type=None, singular=True
823 ).data
824
826 """
827 Returns information about the number of times a URI has been shared.
828 """
829 api_endpoint = API_PREFIX + "/activities/count?alt=json"
830 api_endpoint += "&url=" + urllib.quote(uri)
831 result = Result(
832 self, 'GET', api_endpoint, result_type=None, singular=True
833 )
834 result.data
835 json = result._json
836 try:
837 if json.get('error'):
838 self.parse_error(json)
839 json = _prune_json_envelope(json)
840 return int(json['counts'][uri][0]['count'])
841 except KeyError, e:
842 raise JSONParseError(
843 uri=api_endpoint,
844 json=json,
845 exception=e
846 )
847
848
849
851 """
852 Returns information about the client's current access token.
853
854 Allows a developer to verify that their token is valid.
855 """
856 api_endpoint = "https://www.google.com/accounts/AuthSubTokenInfo"
857 if not self.oauth_access_token:
858 raise ValueError("Client is missing access token.")
859 response = self.fetch_api_response(
860 'GET',
861 api_endpoint,
862 http_connection=self.oauth_http_connection
863 )
864 return response.read()
865
867 """
868 The L{Post} object represents a post within Buzz. A post has an actor and
869 content, and may have zero or more comments and likes. An L{Attachment} may
870 be associated with the post by appending to the attachments list.
871 """
872 - def __init__(self, json=None, client=None,
873 content=None, annotation=None, uri=None, verb=None, actor=None,
874 geocode=None, place_id=None,
875 attachments=None):
876 self.client = client
877 self.json = json
878 self.id = None
879 self.object = None
880 self.type=None
881 self.place_name=None
882 self.visibility=None
883 self.published=None
884 self.updated=None
885 self.source=None
886
887
888 self.content = content
889 self.annotation = annotation
890 self.uri = uri
891 self.verb = verb
892 self.actor = actor
893 self.geocode = geocode
894 self.place_id = place_id
895 self.attachments = attachments
896
897 self._likers = None
898 self.liker_count = 0
899 self._comments = None
900 self.comment_count = 0
901
902 if self.json:
903
904
905 try:
906 json = _prune_json_envelope(json)
907 if json.get('error'):
908 raise JSONParseError(json=json)
909 self.id = json['id']
910 if isinstance(json.get('content'), dict):
911 self.content = json['content']['value']
912 elif json.get('content'):
913 self.content = json['content']
914 elif json.get('object') and json['object'].get('content'):
915 self.content = json['object']['content']
916 if json.get('annotation'):
917 self.annotation = json['annotation']
918 if isinstance(json['title'], dict):
919 self.title = json['title']['value']
920 else:
921 self.title = json['title']
922 if json.get('object'):
923 self.object = json['object']
924 if json.get('links'):
925 self.links = _parse_links(json.get('links'))
926 self.replies = []
927 self.liked = []
928 if self.links:
929 for link in self.links:
930 if link.rel == "alternate":
931 self.link = link
932 self.uri = self.link.uri
933 elif link.rel == "replies":
934 self.replies.append(link)
935 elif link.rel == "liked":
936 self.liked.append(link)
937 if self.replies:
938 for reply in self.replies:
939 if reply.count:
940 self.comment_count += reply.count
941 if self.liked:
942 for liker in self.liked:
943 if liker.count:
944 self.liker_count += liker.count
945 if isinstance(json.get('verb'), list):
946 self.verb = json['verb'][0]
947 elif json.get('verb'):
948 self.verb = json['verb']
949 if json.get('published'):
950 self.published = json['published']
951 if json.get('updated'):
952 self.updated = json['updated']
953 if isinstance(json.get('type'), list):
954 self.type = json['type'][0]
955 elif json.get('type'):
956 self.type = json['type']
957 elif self.object and self.object.get('type'):
958 self.type = self.object['type']
959 if json.get('author'):
960 self.actor = Person(json['author'], client=self.client)
961 elif json.get('actor'):
962 self.actor = Person(json['actor'], client=self.client)
963 if self.object and self.object.get('attachments'):
964 self.attachments = [
965 Attachment(attachment_json, client=self.client)
966 for attachment_json
967 in self.object['attachments']
968 ]
969 else:
970 self.attachments = []
971 if json.get('geocode'):
972 self.geocode = _parse_geocode(json['geocode'])
973 if json.get('placeName'):
974 self.place_name = json['placeName']
975 if json.get('visibility'):
976 self.visibility = json['visibility']
977 if isinstance(self.visibility, dict) and \
978 self.visibility.get('entries'):
979 self.visibility = self.visibility.get('entries')
980 if json.get('source') and json['source'].get('title'):
981 self.source = json['source']['title']
982 except KeyError, e:
983 raise JSONParseError(
984 json=json,
985 exception=e
986 )
987
988 - def __repr__(self):
989 if not self.public:
990 return (u'<Post[%s] (private)>' % self.id).encode(
991 'ASCII', 'ignore'
992 )
993 else:
994 return (u'<Post[%s]>' % self.id).encode(
995 'ASCII', 'ignore'
996 )
997
998 @property
1000 if self.visibility:
1001 public_visibilities = [
1002 entry for entry in self.visibility
1003 if entry.get('id') == 'tag:google.com,2010:buzz-group:@me:@public'
1004 ]
1005 return not not public_visibilities
1006 else:
1007
1008 return True
1009
1010 @property
1011 - def _json_output(self):
1012 output = {
1013 'object': {}
1014 }
1015 if self.id:
1016 output['id'] = self.id
1017 if self.uri:
1018 output['links'] = {
1019 u'alternate': [{u'href': self.uri, u'type': u'text/html'}]
1020 }
1021 output['object']['links'] = {
1022 u'alternate': [{u'href': self.uri, u'type': u'text/html'}]
1023 }
1024 if self.content:
1025 output['object']['content'] = self.content
1026 if self.annotation:
1027 output['annotation'] = self.annotation
1028 if self.type:
1029 output['object']['type'] = self.type
1030 else:
1031 output['object']['type'] = 'note'
1032 if self.verb:
1033 output['verb'] = self.verb
1034 if self.geocode:
1035 output['geocode'] = '%s %s' % (
1036 str(self.geocode[0]), str(self.geocode[1])
1037 )
1038 if self.place_id:
1039 output['placeId'] = self.place_id
1040 if self.attachments:
1041 output['object']['attachments'] = [
1042 attachment._json_output for attachment in self.attachments
1043 ]
1044 return output
1045
1047 """Syntactic sugar for `client.comments(post)`."""
1048 if not client:
1049 client = self.client
1050 return self.client.comments(post_id=self.id, actor_id=self.actor.id)
1051
1053 """Syntactic sugar for `client.related_links(post)`."""
1054 if not client:
1055 client = self.client
1056 return self.client.related_links(post_id=self.id, actor_id=self.actor.id)
1057
1058 - def likers(self, client=None):
1059 """Syntactic sugar for `client.likers(post)`."""
1060 if not client:
1061 client = self.client
1062 return self.client.likers(post_id=self.id, actor_id=self.actor.id)
1063
1064 - def like(self, client=None):
1065 """Syntactic sugar for `client.like_post(post)`."""
1066 if not client:
1067 client = self.client
1068 return client.like_post(post_id=self.id)
1069
1070 - def unlike(self, client=None):
1071 """Syntactic sugar for `client.unlike_post(post)`."""
1072 if not client:
1073 client = self.client
1074 return client.unlike_post(post_id=self.id)
1075
1076 - def mute(self, client=None):
1077 """Syntactic sugar for `client.mute_post(post)`."""
1078 if not client:
1079 client = self.client
1080 return client.mute_post(post_id=self.id)
1081
1082 - def unmute(self, client=None):
1083 """Syntactic sugar for `client.unmute_post(post)`."""
1084 if not client:
1085 client = self.client
1086 return client.unmute_post(post_id=self.id)
1087
1168
1170 """
1171 The L{Link} object represents a hyperlink. It encapsulates both the URI of
1172 the hyperlink itself, as well as metadata such as MIME type and rel-value.
1173 """
1174 - def __init__(self, json=None,
1175 id=None, rel=None, type=None, title=None, summary=None,
1176 count=None, uri=None):
1177 self.json = json
1178 self.id = id
1179 self.rel = rel
1180 self.type = type
1181 self.title = title
1182 self.summary = summary
1183 self.count = count
1184 self.uri = uri
1185 if json:
1186 try:
1187 json = _prune_json_envelope(json)
1188 if json.get('error'):
1189 raise JSONParseError(json=json)
1190 if json.get('ref'):
1191 self.id = json['ref']
1192 elif json.get('id'):
1193 self.id = json['id']
1194 self.rel = json.get('rel') or self.rel
1195 self.type = json.get('type') or self.type
1196 if json.get('title'):
1197 if isinstance(json['title'], dict):
1198 self.title = json['title']['value']
1199 else:
1200 self.title = json['title']
1201 else:
1202 self.title = None
1203 if isinstance(json.get('summary'), dict):
1204 self.summary = json['summary']['value']
1205 elif json.get('summary'):
1206 self.summary = json['summary']
1207 elif isinstance(json.get('content'), dict):
1208 self.summary = json['content']['value']
1209 elif json.get('content'):
1210 self.summary = json['content']
1211 else:
1212 self.summary = None
1213 if json.get('count'):
1214 self.count = json['count']
1215 if json.get('href'):
1216 self.uri = json['href']
1217 elif json.get('uri'):
1218 self.uri = json['uri']
1219 except KeyError, e:
1220 raise JSONParseError(
1221 json=json,
1222 exception=e
1223 )
1224
1226 return (u'<Link[%s]>' % self.uri).encode(
1227 'ASCII', 'ignore'
1228 )
1229
1230 @property
1232 output = {}
1233 if self.id:
1234 output['ref'] = self.id
1235 output['rel'] = self.rel or 'alternate'
1236 output['type'] = self.type or 'text/html'
1237 if self.title:
1238 output['title'] = self.title
1239 if self.summary:
1240 output['summary'] = self.summary
1241 if self.uri:
1242 output['href'] = self.uri
1243 return output
1244
1246 """
1247 The L{Attachment} object represents an attachment to a L{Post} within Buzz.
1248 It may contain rich media types such as video, audio, pictures, or just
1249 simple hyperlinks.
1250 """
1251 - def __init__(self, json=None, client=None,
1252 type=None, title=None, content=None, uri=None,
1253 preview=None, enclosure=None):
1254 self.client = client
1255 self.json = json
1256 self.type = type
1257 self.title = title
1258 self.content = content
1259 self.uri = uri
1260 self.link = None
1261 self.links = []
1262 self.preview = preview
1263 self.enclosure = enclosure
1264 if json:
1265 try:
1266 json = _prune_json_envelope(json)
1267 if json.get('error'):
1268 raise JSONParseError(json=json)
1269 if isinstance(json.get('content'), dict):
1270 self.content = json['content']['value']
1271 elif json.get('content'):
1272 self.content = json['content']
1273 if json.get('title'):
1274 if isinstance(json['title'], dict):
1275 self.title = json['title']['value']
1276 else:
1277 self.title = json['title']
1278 else:
1279 self.title = None
1280 if json.get('links'):
1281 self.links = _parse_links(json.get('links'))
1282 if self.links:
1283 for link in self.links:
1284 if link.rel == "alternate":
1285 self.link = link
1286 self.uri = self.link.uri
1287 elif link.rel == "preview":
1288 self.preview = link
1289 elif link.rel == "enclosure":
1290 self.enclosure = link
1291 self.type = json['type']
1292 except KeyError, e:
1293 raise JSONParseError(
1294 json=json,
1295 exception=e
1296 )
1297
1299 return (u'<Attachment[%s]>' % self.uri).encode(
1300 'ASCII', 'ignore'
1301 )
1302
1303 @property
1305 output = {}
1306 if self.type:
1307 output['type'] = self.type
1308 if self.title:
1309 output['title'] = self.title
1310 if self.content:
1311 output['content'] = self.content
1312 if self.uri:
1313 output['links'] = {
1314 u'alternate': [{u'href': self.uri, u'type': u'text/html'}]
1315 }
1316 if self.preview:
1317 output['links'] = {
1318 u'preview': [{u'href': self.preview.uri}]
1319 }
1320 if self.enclosure:
1321 output['links'] = {
1322 u'enclosure': [{u'href': self.enclosure.uri}]
1323 }
1324 return output
1325
1327 """
1328 The L{Person} object represents a Buzz user. L{Person} objects may be
1329 associated with L{Post} or L{Comment} objects as authors, or with other
1330 L{Person} objects as followers.
1331 """
1332 - def __init__(self, json, client=None):
1333 self.client = client
1334 self.json = json
1335 self.profile_name = None
1336
1337 try:
1338 json = _prune_json_envelope(json)
1339 if json.get('error'):
1340 raise JSONParseError(json=json)
1341 self.uri = \
1342 json.get('uri') or json.get('profileUrl')
1343 if self.uri == "":
1344 self.uri = None
1345 if json.get('id'):
1346 self.id = json.get('id')
1347 elif self.uri:
1348 self.id = re.search('/([^/]*?)$', self.uri).group(1)
1349 self.name = \
1350 json.get('name') or json.get('displayName')
1351 self.photo = \
1352 json.get('photoUrl') or json.get('thumbnailUrl')
1353 if self.photo and self.photo.startswith('/photos/public/'):
1354 self.photo = 'http://www.google.com/s2' + self.photo
1355 if json.get('urls'):
1356 self.uris = json.get('urls')
1357 if json.get('photos'):
1358 self.photos = json.get('photos')
1359 if self.uri and \
1360 not re.search('^\\d+$', re.search('/([^/]*?)$', self.uri).group(1)):
1361 self.profile_name = re.search('/([^/]*?)$', self.uri).group(1)
1362 except KeyError, e:
1363 raise JSONParseError(
1364 json=json,
1365 exception=e
1366 )
1367
1369 return (u'<Person[%s, %s]>' % (self.name, self.id)).encode(
1370 'ASCII', 'ignore'
1371 )
1372
1373 @property
1375 output = {}
1376 if self.id:
1377 output['id'] = self.id
1378 if self.name:
1379 output['name'] = self.name
1380 if self.uri:
1381 output['profileUrl'] = self.uri
1382 if self.photo:
1383 output['thumbnailUrl'] = self.photo
1384 return output
1385
1386 - def follow(self, client=None):
1387 """Syntactic sugar for `client.follow(person)`."""
1388 if not client:
1389 client = self.client
1390 return client.follow(user_id=self.id)
1391
1393 """Syntactic sugar for `client.unfollow(person)`."""
1394 if not client:
1395 client = self.client
1396 return client.unfollow(user_id=self.id)
1397
1398 - def posts(self, client=None):
1399 """Syntactic sugar for `client.posts(person)`."""
1400 if not client:
1401 client = self.client
1402 return client.posts(user_id=self.id)
1403
1405 """
1406 The L{Result} object encapsulates each result returned from the API.
1407 """
1408 - def __init__(self, client, http_method, http_uri, http_headers={}, \
1409 http_body='', result_type=Post, singular=False):
1410 self.client = client
1411 self.result_type = result_type
1412 self.singular = singular
1413
1414
1415 self._response = None
1416
1417 self._body = None
1418
1419 self._json = None
1420
1421 self._data = None
1422
1423 self._next_uri = None
1424
1425 self._http_method = http_method
1426 self._http_uri = http_uri
1427 self._http_headers = http_headers
1428 self._http_body = http_body
1429 self.poco_count = 0
1430
1433
1434 @property
1436 if not self._data:
1437 if not self._response:
1438 self.reload()
1439 if not (self._response.status >= 200 and self._response.status < 300):
1440
1441 self._parse_error(self._json)
1442 if self.result_type == Post and self.singular:
1443 self._data = self._parse_post(self._json)
1444 elif self.result_type == Post and not self.singular:
1445 self._data = self._parse_posts(self._json)
1446 elif self.result_type == Comment and self.singular:
1447 self._data = self._parse_comment(self._json)
1448 elif self.result_type == Comment and not self.singular:
1449 self._data = self._parse_comments(self._json)
1450 elif self.result_type == Person and self.singular:
1451 self._data = self._parse_person(self._json)
1452 elif self.result_type == Person and not self.singular:
1453 self._data = self._parse_people(self._json)
1454 elif self.result_type == Link and self.singular:
1455 self._data = self._parse_link(self._json)
1456 elif self.result_type == Link and not self.singular:
1457 self._data = self._parse_links(self._json)
1458 return self._data
1459
1461 if DEBUG:
1462 logging.debug('URI to fetch is %s' % self._http_uri)
1463 logging.debug('Headers are: %s' % str(self._http_headers))
1464 self._data = None
1465 self._response = self.client.fetch_api_response(
1466 http_method=self._http_method,
1467 http_uri=self._http_uri,
1468 http_headers=self._http_headers,
1469 http_body=self._http_body
1470 )
1471 self._body = self._response.read()
1472 try:
1473 if self._body == '':
1474 self._json = None
1475 else:
1476 self._json = simplejson.loads(self._body)
1477 except Exception, e:
1478 raise JSONParseError(
1479 json=(self._json or self._body),
1480 uri=self._http_uri,
1481 exception=e
1482 )
1483
1485 if self.next_uri:
1486 self._http_uri = self.next_uri
1487
1488 self._next_uri = None
1489 self._response = None
1490 self._body = None
1491 self._json = None
1492 self._data = None
1493 else:
1494 raise ValueError('Cannot load next page, next page not present.')
1495
1496 @property
1498 if not self._next_uri:
1499 if self.singular:
1500 return None
1501 else:
1502 if not self._json:
1503 self.reload()
1504 semi_pruned_json = self._json.get('data') or self._json
1505
1506
1507 if semi_pruned_json.get('kind') == 'buzz#peopleFeed':
1508 total_results = semi_pruned_json.get('totalResults')
1509 if semi_pruned_json.get('startIndex') < total_results:
1510 if 'c=' in self._http_uri:
1511 old_param = '&c=%s' % self.poco_count
1512 self.poco_count += DEFAULT_PAGE_SIZE
1513 new_param = '&c=%s' % self.poco_count
1514 self._next_uri = self._http_uri.replace(old_param, new_param)
1515 else:
1516 self._next_uri = self._http_uri + '&c=%s' % DEFAULT_PAGE_SIZE
1517 self.poco_count = DEFAULT_PAGE_SIZE
1518 if self.poco_count >= total_results:
1519
1520 return None
1521 return self._next_uri
1522 else:
1523 links = semi_pruned_json.get('links')
1524 if not links:
1525 return None
1526 next_link = links.get('next')
1527 if not next_link:
1528 return None
1529 self._next_uri = next_link[0].get('href')
1530 if not self._next_uri:
1531 return None
1532 return self._next_uri
1533
1534 - def _parse_post(self, json):
1535 """Helper method for converting a post JSON structure."""
1536 try:
1537 if json.get('error'):
1538 self.parse_error(json)
1539 json = _prune_json_envelope(json)
1540 if isinstance(json, list) and len(json) == 1:
1541 json = json[0]
1542 return Post(json, client=self.client)
1543 except KeyError, e:
1544 raise JSONParseError(
1545 uri=self._http_uri,
1546 json=json,
1547 exception=e
1548 )
1549
1550 - def _parse_posts(self, json):
1551 """Helper method for converting a set of post JSON structures."""
1552 try:
1553 if json.get('error'):
1554 self.parse_error(json)
1555 json = _prune_json_envelope(json)
1556 if isinstance(json, list):
1557 return [
1558 Post(post_json, client=self.client) for post_json in json
1559 ]
1560 else:
1561
1562 return []
1563 except KeyError, e:
1564 raise JSONParseError(
1565 uri=self._http_uri,
1566 json=json,
1567 exception=e
1568 )
1569
1570 def _parse_comment(self, json):
1571 """Helper method for converting a comment JSON structure."""
1572 try:
1573 if json.get('error'):
1574 self.parse_error(json)
1575 json = _prune_json_envelope(json)
1576 if isinstance(json, list) and len(json) == 1:
1577 json = json[0]
1578 return Comment(json, client=self.client)
1579 except KeyError, e:
1580 raise JSONParseError(
1581 uri=self._http_uri,
1582 json=json,
1583 exception=e
1584 )
1585
1605
1607 """Helper method for converting a person JSON structure."""
1608 try:
1609 if json.get('error'):
1610 self.parse_error(json)
1611 json = _prune_json_envelope(json)
1612 if isinstance(json, list) and len(json) == 1:
1613 json = json[0]
1614 return Person(json, client=self.client)
1615 except KeyError, e:
1616 raise JSONParseError(
1617 uri=self._http_uri,
1618 json=json,
1619 exception=e
1620 )
1621
1623 """Helper method for converting a set of person JSON structures."""
1624 try:
1625 if json.get('error'):
1626 self.parse_error(json)
1627 json = _prune_json_envelope(json)
1628 if isinstance(json, list):
1629 return [
1630 Person(person_json, client=self.client) for person_json in json
1631 ]
1632 else:
1633
1634 return []
1635 except KeyError, e:
1636 raise JSONParseError(
1637 uri=self._http_uri,
1638 json=json,
1639 exception=e
1640 )
1641
1643 """Helper method for converting a person JSON structure."""
1644 try:
1645 if json.get('error'):
1646 self.parse_error(json)
1647 json = _prune_json_envelope(json)
1648 if isinstance(json, list) and len(json) == 1:
1649 json = json[0]
1650 return Link(json)
1651 except KeyError, e:
1652 raise JSONParseError(
1653 uri=self._http_uri,
1654 json=json,
1655 exception=e
1656 )
1657
1659 """Helper method for converting a set of person JSON structures."""
1660 try:
1661 if json.get('error'):
1662 self.parse_error(json)
1663 json = _prune_json_envelope(json)
1664 if isinstance(json, list):
1665 return [
1666 Link(link_json) for link_json in json
1667 ]
1668 else:
1669
1670 return []
1671 except KeyError, e:
1672 raise JSONParseError(
1673 uri=self._http_uri,
1674 json=json,
1675 exception=e
1676 )
1677
1679 """Helper method for converting an error response to an exception."""
1680 if json:
1681 raise RetrieveError(
1682 uri=self._http_uri,
1683 message=json['error'].get('message'),
1684 json=json
1685 )
1686 else:
1687 raise RetrieveError(
1688 uri=self._http_uri,
1689 message='Unknown error'
1690 )
1691
1694 """
1695 A L{ResultIterator} allows iteration over a result set.
1696 """
1698 self.result = result
1699 self.cursor = 0
1700 self.start_index = 0
1701
1704
1705 @property
1707 return self.cursor - self.start_index
1708
1710 if self.local_index >= len(self.result.data):
1711 if self.result.next_uri:
1712 self.start_index += len(self.result.data)
1713 self.result.load_next()
1714 else:
1715 raise StopIteration('No more results.')
1716
1717 value = self.result.data[self.local_index]
1718 self.cursor += 1
1719 return value
1720