Module buzz
[hide private]
[frames] | no frames]

Source Code for Module buzz

   1  # Copyright 2010 Google Inc. 
   2  # 
   3  # Licensed under the Apache License, Version 2.0 (the "License"); 
   4  # you may not use this file except in compliance with the License. 
   5  # You may obtain a copy of the License at 
   6  # 
   7  #      http://www.apache.org/licenses/LICENSE-2.0 
   8  # 
   9  # Unless required by applicable law or agreed to in writing, software 
  10  # distributed under the License is distributed on an "AS IS" BASIS, 
  11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  12  # See the License for the specific language governing permissions and 
  13  # limitations under the License. 
  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    # This is where simplejson lives on App Engine 
 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    # Allow optional configuration file to be loaded 
 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 
150 151 -class RetrieveError(Exception):
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
163 - def __str__(self):
164 return 'Could not retrieve \'%s\': %s' % (self._uri, self._message)
165
166 -class JSONParseError(Exception):
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
180 - def __str__(self):
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
196 -def _prune_json_envelope(json):
197 # Follow Postel's law 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 235
236 -def _parse_geocode(geocode):
237 # Follow Postel's law 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
246 -class Client:
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 """
253 - def __init__(self):
254 # Make sure we're always getting the right HTTP connection, even if 255 # API_PREFIX changes 256 parsed = urlparse.urlparse(API_PREFIX) 257 authority = parsed[1].split(':') 258 if len(authority) == 1: 259 # Incidentally, this is why unpacking shouldn't complain about 260 # size mismatch on the array. Bad Python. Stop trying to protect me! 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 # OAuth state 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
285 - def http_connection(self):
286 # if not self._http_connection: 287 # self._http_connection = httplib.HTTPSConnection('www.google.com') 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
297 - def use_anonymous_oauth_consumer(self, oauth_display_name=None):
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
310 - def build_oauth_consumer(self, key, secret):
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
324 - def build_oauth_request_token(self, key, secret):
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
336 - def build_oauth_access_token(self, key, secret):
337 self.oauth_access_token = oauth.OAuthToken(key, secret)
338 339 @property
340 - def oauth_http_connection(self):
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 # if self._oauth_http_connection.port != 443: 346 # raise ValueError("OAuth HTTPS Connection must be for port 443.") 347 return self._oauth_http_connection
348
349 - def fetch_oauth_response(self, oauth_request):
350 """Sends a signed request to Google's Accounts API.""" 351 # Transmit the OAuth request to Google 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 # Reset the connection 366 if self._oauth_http_connection: 367 self._oauth_http_connection.close() 368 self._oauth_http_connection = None 369 # Retry once 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
381 - def fetch_oauth_request_token(self, callback_uri):
382 """Obtains an OAuth request token from Google's Accounts API.""" 383 if not self.oauth_request_token: 384 # Build and sign an OAuth request 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 # Create the token from the response 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
415 - def build_oauth_authorization_url(self, token=None):
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
426 - def fetch_oauth_access_token(self, verifier=None, token=None):
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 # Build and sign an OAuth request 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 # Create the token from the response 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
462 - def build_oauth_request(self, http_method, http_uri):
463 # Query parameters have to be signed, and the OAuth library isn't smart 464 # enough to do this automatically 465 query = urlparse.urlparse(http_uri)[4] # Query is 4th element of the tuple 466 if query: 467 qs_parser = None 468 if hasattr(urlparse, 'parse_qs'): 469 qs_parser = urlparse.parse_qs 470 else: 471 # Deprecated in 2.6 472 qs_parser = cgi.parse_qs 473 # Buzz gives non-strict conforming next uris, like: 474 # https://www.googleapis.com/buzz/v1/activities/search?q&lon=1123&lat=456&max-results=2&c=2 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 # Build the OAuth request, add in our parameters, and sign it 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 # Build OAuth request and add OAuth header if we've got an access token 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 # Reset the connection 527 http_connection.close() 528 http_connection = None 529 self._http_connection = None 530 http_connection = self.http_connection 531 # Retry once 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 # If the raw JSON of the error is available, we don't want to lose it. 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 # People APIs 561
562 - def people_search(self, query=None):
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
569 - def people_search_by_topic(self, \ 570 query=None, latitude=None, longitude=None, radius=None):
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 # You'd think we could just return directly here, but sometimes a 584 # Person object is incomplete, in which case this operation would 585 # 'upgrade' to the full Person object. 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
596 - def followers(self, user_id='@me'):
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
603 - def following(self, user_id='@me'):
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
610 - def follow(self, user_id):
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
622 - def unfollow(self, user_id):
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 # Post APIs 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
691 - def comments(self, post_id, actor_id='0', max_results=20):
692 if isinstance(actor_id, Person): 693 actor_id = actor_id.id 694 if isinstance(post_id, Post): 695 post_id = post_id.id 696 api_endpoint = API_PREFIX + "/activities/" + actor_id + \ 697 "/@self/" + post_id + "/@comments" 698 api_endpoint += "?alt=json" 699 if max_results: 700 api_endpoint += "&max-results=" + str(max_results) 701 return Result(self, 'GET', api_endpoint, result_type=Comment)
702
703 - def create_comment(self, comment):
704 api_endpoint = API_PREFIX + ("/activities/%s/@self/%s/@comments" % ( 705 comment.post(client=self).actor.id, 706 comment.post(client=self).id 707 )) 708 api_endpoint += "?alt=json" 709 json_string = simplejson.dumps({'data': comment._json_output}) 710 return Result( 711 self, 'POST', api_endpoint, http_body=json_string, result_type=None 712 ).data
713
714 - def update_comment(self, comment):
715 if not comment.id: 716 raise ValueError('Comment must have a valid id to update.') 717 api_endpoint = API_PREFIX + ("/activities/%s/@self/%s/@comments/%s" % ( 718 comment.actor.id, 719 comment.post(client=self).id, 720 comment.id 721 )) 722 api_endpoint += "?alt=json" 723 json_string = simplejson.dumps({'data': comment._json_output}) 724 return Result( 725 self, 'PUT', api_endpoint, http_body=json_string, result_type=None 726 ).data
727
728 - def delete_comment(self, comment):
729 if not comment.id: 730 raise ValueError('Comment must have a valid id to update.') 731 api_endpoint = API_PREFIX + ("/activities/%s/@self/%s/@comments/%s" % ( 732 comment.actor.id, 733 comment.post(client=self).id, 734 comment.id 735 )) 736 api_endpoint += "?alt=json" 737 return Result(self, 'DELETE', api_endpoint, result_type=None).data
738
739 - def commented_posts(self, user_id='@me'):
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 # Related Links 744 756 757 # Likes 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 # Mutes 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
825 - def share_count(self, uri):
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 # OAuth debugging 849
850 - def oauth_token_info(self):
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
866 -class Post:
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 # Construct the post piece-wise. 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 # Parse the incoming JSON 904 # Follow Postel's law 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
999 - def public(self):
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 # If there's no visibility attribute it's public 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
1046 - def comments(self, client=None):
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 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
1088 -class Comment:
1089 """ 1090 The L{Comment} object represents a comment on a L{Post} within Buzz. A 1091 comment always has an actor and content associated with it. 1092 """
1093 - def __init__(self, json=None, client=None, 1094 post=None, post_id=None, content=None):
1095 self.client = client 1096 self.json = json 1097 self.id = None 1098 self.content = content 1099 self.actor = None 1100 self.links = [] 1101 self._post = post 1102 self._post_id = post_id 1103 self.published=None 1104 self.updated=None 1105 1106 if json: 1107 # Follow Postel's law 1108 try: 1109 json = _prune_json_envelope(json) 1110 if json.get('error'): 1111 raise JSONParseError(json=json) 1112 self.id = json['id'] 1113 if isinstance(json.get('content'), dict): 1114 self.content = json['content']['value'] 1115 elif json.get('content'): 1116 self.content = json['content'] 1117 elif json.get('object') and json['object'].get('content'): 1118 self.content = json['object']['content'] 1119 if json.get('author'): 1120 self.actor = Person(json['author'], client=self.client) 1121 elif json.get('actor'): 1122 self.actor = Person(json['actor'], client=self.client) 1123 if json.get('links'): 1124 self.links = _parse_links(json.get('links')) 1125 if self.links: 1126 for link in self.links: 1127 if link.rel == "inReplyTo": 1128 self._post_id = link.id 1129 break 1130 if json.get('published'): 1131 self.published = json['published'] 1132 if json.get('updated'): 1133 self.updated = json['updated'] 1134 except KeyError, e: 1135 raise JSONParseError( 1136 json=json, 1137 exception=e 1138 )
1139
1140 - def __repr__(self):
1141 return (u'<Comment[%s]>' % self.id).encode( 1142 'ASCII', 'ignore' 1143 )
1144 1145 @property
1146 - def _json_output(self):
1147 output = {} 1148 if self.id: 1149 output['id'] = self.id 1150 if self.content: 1151 output['content'] = self.content 1152 return output
1153
1154 - def post(self, client=None):
1155 """Syntactic sugar for `client.post(post)`.""" 1156 if not self._post: 1157 if not self._post_id: 1158 raise ValueError('Could not determine comment\'s parent post.') 1159 if not client: 1160 client = self.client 1161 if self.actor: 1162 self._post = \ 1163 client.post(post_id=self._post_id, actor_id=self.actor.id).data 1164 else: 1165 self._post = \ 1166 client.post(post_id=self._post_id).data 1167 return self._post
1168 1244
1245 -class Attachment:
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
1298 - def __repr__(self):
1299 return (u'<Attachment[%s]>' % self.uri).encode( 1300 'ASCII', 'ignore' 1301 )
1302 1303 @property
1304 - def _json_output(self):
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
1326 -class Person:
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 # Follow Postel's law 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
1368 - def __repr__(self):
1369 return (u'<Person[%s, %s]>' % (self.name, self.id)).encode( 1370 'ASCII', 'ignore' 1371 )
1372 1373 @property
1374 - def _json_output(self):
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
1392 - def unfollow(self, client=None):
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
1404 -class Result:
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 # The HTTP response for the current page 1415 self._response = None 1416 # The HTTP response body for the current page 1417 self._body = None 1418 # The raw JSON data for the current page 1419 self._json = None 1420 # The parsed data for the current page 1421 self._data = None 1422 # The URI of the next page of results 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
1431 - def __iter__(self):
1432 return ResultIterator(self)
1433 1434 @property
1435 - def data(self):
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 # Response was not a 2xx class status 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
1460 - def reload(self):
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
1484 - def load_next(self):
1485 if self.next_uri: 1486 self._http_uri = self.next_uri 1487 # Reset all of these 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
1497 - def next_uri(self):
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 # Portable Contacts feeds have different pagination rules 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 # Finished processing PoCo 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 # The entire key is omitted when there are no results 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
1586 - def _parse_comments(self, json):
1587 """Helper method for converting a set of comment JSON structures.""" 1588 try: 1589 if json.get('error'): 1590 self.parse_error(json) 1591 json = _prune_json_envelope(json) 1592 if isinstance(json, list): 1593 return [ 1594 Comment(comment_json, client=self.client) for comment_json in json 1595 ] 1596 else: 1597 # The entire key is omitted when there are no results 1598 return [] 1599 except KeyError, e: 1600 raise JSONParseError( 1601 uri=self._http_uri, 1602 json=json, 1603 exception=e 1604 )
1605
1606 - def _parse_person(self, json):
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
1622 - def _parse_people(self, json):
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 # The entire key is omitted when there are no results 1634 return [] 1635 except KeyError, e: 1636 raise JSONParseError( 1637 uri=self._http_uri, 1638 json=json, 1639 exception=e 1640 )
1641 1657 1677
1678 - def _parse_error(self, json):
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
1692 1693 -class ResultIterator:
1694 """ 1695 A L{ResultIterator} allows iteration over a result set. 1696 """
1697 - def __init__(self, result):
1698 self.result = result 1699 self.cursor = 0 1700 self.start_index = 0
1701
1702 - def __iter__(self):
1703 return self
1704 1705 @property
1706 - def local_index(self):
1707 return self.cursor - self.start_index
1708
1709 - def next(self):
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 # The local_index is in range of the current page 1717 value = self.result.data[self.local_index] 1718 self.cursor += 1 1719 return value
1720