/gdata/photos/service.py
Python | 680 lines | 615 code | 10 blank | 55 comment | 2 complexity | 6ac8258b43b46a835fd77db4efdbc619 MD5 | raw file
1#!/usr/bin/env python 2# -*-*- encoding: utf-8 -*-*- 3# 4# This is the service file for the Google Photo python client. 5# It is used for higher level operations. 6# 7# $Id: service.py 144 2007-10-25 21:03:34Z havard.gulldahl $ 8# 9# Copyright 2007 H?vard Gulldahl 10# 11# Licensed under the Apache License, Version 2.0 (the "License"); 12# you may not use this file except in compliance with the License. 13# You may obtain a copy of the License at 14# 15# http://www.apache.org/licenses/LICENSE-2.0 16# 17# Unless required by applicable law or agreed to in writing, software 18# distributed under the License is distributed on an "AS IS" BASIS, 19# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20# See the License for the specific language governing permissions and 21# limitations under the License. 22 23"""Google PhotoService provides a human-friendly interface to 24Google Photo (a.k.a Picasa Web) services[1]. 25 26It extends gdata.service.GDataService and as such hides all the 27nasty details about authenticating, parsing and communicating with 28Google Photos. 29 30[1]: http://code.google.com/apis/picasaweb/gdata.html 31 32Example: 33 import gdata.photos, gdata.photos.service 34 pws = gdata.photos.service.PhotosService() 35 pws.ClientLogin(username, password) 36 #Get all albums 37 albums = pws.GetUserFeed().entry 38 # Get all photos in second album 39 photos = pws.GetFeed(albums[1].GetPhotosUri()).entry 40 # Get all tags for photos in second album and print them 41 tags = pws.GetFeed(albums[1].GetTagsUri()).entry 42 print [ tag.summary.text for tag in tags ] 43 # Get all comments for the first photos in list and print them 44 comments = pws.GetCommentFeed(photos[0].GetCommentsUri()).entry 45 print [ c.summary.text for c in comments ] 46 47 # Get a photo to work with 48 photo = photos[0] 49 # Update metadata 50 51 # Attributes from the <gphoto:*> namespace 52 photo.summary.text = u'A nice view from my veranda' 53 photo.title.text = u'Verandaview.jpg' 54 55 # Attributes from the <media:*> namespace 56 photo.media.keywords.text = u'Home, Long-exposure, Sunset' # Comma-separated 57 58 # Adding attributes to media object 59 60 # Rotate 90 degrees clockwise 61 photo.rotation = gdata.photos.Rotation(text='90') 62 63 # Submit modified photo object 64 photo = pws.UpdatePhotoMetadata(photo) 65 66 # Make sure you only modify the newly returned object, else you'll get 67 # versioning errors. See Optimistic-concurrency 68 69 # Add comment to a picture 70 comment = pws.InsertComment(photo, u'I wish the water always was this warm') 71 72 # Remove comment because it was silly 73 print "*blush*" 74 pws.Delete(comment.GetEditLink().href) 75 76""" 77 78__author__ = u'havard@gulldahl.no'# (H?vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__ 79__license__ = 'Apache License v2' 80__version__ = '$Revision: 176 $'[11:-2] 81 82 83import sys, os.path, StringIO 84import time 85import gdata.service 86import gdata 87import atom.service 88import atom 89import gdata.photos 90 91SUPPORTED_UPLOAD_TYPES = ('bmp', 'jpeg', 'jpg', 'gif', 'png') 92 93UNKOWN_ERROR=1000 94GPHOTOS_BAD_REQUEST=400 95GPHOTOS_CONFLICT=409 96GPHOTOS_INTERNAL_SERVER_ERROR=500 97GPHOTOS_INVALID_ARGUMENT=601 98GPHOTOS_INVALID_CONTENT_TYPE=602 99GPHOTOS_NOT_AN_IMAGE=603 100GPHOTOS_INVALID_KIND=604 101 102class GooglePhotosException(Exception): 103 def __init__(self, response): 104 105 self.error_code = response['status'] 106 self.reason = response['reason'].strip() 107 if '<html>' in str(response['body']): #general html message, discard it 108 response['body'] = "" 109 self.body = response['body'].strip() 110 self.message = "(%(status)s) %(body)s -- %(reason)s" % response 111 112 #return explicit error codes 113 error_map = { '(12) Not an image':GPHOTOS_NOT_AN_IMAGE, 114 'kind: That is not one of the acceptable values': 115 GPHOTOS_INVALID_KIND, 116 117 } 118 for msg, code in error_map.iteritems(): 119 if self.body == msg: 120 self.error_code = code 121 break 122 self.args = [self.error_code, self.reason, self.body] 123 124class PhotosService(gdata.service.GDataService): 125 userUri = '/data/feed/api/user/%s' 126 127 def __init__(self, email=None, password=None, source=None, 128 server='picasaweb.google.com', additional_headers=None, 129 **kwargs): 130 """Creates a client for the Google Photos service. 131 132 Args: 133 email: string (optional) The user's email address, used for 134 authentication. 135 password: string (optional) The user's password. 136 source: string (optional) The name of the user's application. 137 server: string (optional) The name of the server to which a connection 138 will be opened. Default value: 'picasaweb.google.com'. 139 **kwargs: The other parameters to pass to gdata.service.GDataService 140 constructor. 141 """ 142 self.email = email 143 self.client = source 144 gdata.service.GDataService.__init__( 145 self, email=email, password=password, service='lh2', source=source, 146 server=server, additional_headers=additional_headers, **kwargs) 147 148 def GetFeed(self, uri, limit=None, start_index=None): 149 """Get a feed. 150 151 The results are ordered by the values of their `updated' elements, 152 with the most recently updated entry appearing first in the feed. 153 154 Arguments: 155 uri: the uri to fetch 156 limit (optional): the maximum number of entries to return. Defaults to what 157 the server returns. 158 159 Returns: 160 one of gdata.photos.AlbumFeed, 161 gdata.photos.UserFeed, 162 gdata.photos.PhotoFeed, 163 gdata.photos.CommentFeed, 164 gdata.photos.TagFeed, 165 depending on the results of the query. 166 Raises: 167 GooglePhotosException 168 169 See: 170 http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual 171 """ 172 if limit is not None: 173 uri += '&max-results=%s' % limit 174 if start_index is not None: 175 uri += '&start-index=%s' % start_index 176 try: 177 return self.Get(uri, converter=gdata.photos.AnyFeedFromString) 178 except gdata.service.RequestError, e: 179 raise GooglePhotosException(e.args[0]) 180 181 def GetEntry(self, uri, limit=None, start_index=None): 182 """Get an Entry. 183 184 Arguments: 185 uri: the uri to the entry 186 limit (optional): the maximum number of entries to return. Defaults to what 187 the server returns. 188 189 Returns: 190 one of gdata.photos.AlbumEntry, 191 gdata.photos.UserEntry, 192 gdata.photos.PhotoEntry, 193 gdata.photos.CommentEntry, 194 gdata.photos.TagEntry, 195 depending on the results of the query. 196 Raises: 197 GooglePhotosException 198 """ 199 if limit is not None: 200 uri += '&max-results=%s' % limit 201 if start_index is not None: 202 uri += '&start-index=%s' % start_index 203 try: 204 return self.Get(uri, converter=gdata.photos.AnyEntryFromString) 205 except gdata.service.RequestError, e: 206 raise GooglePhotosException(e.args[0]) 207 208 def GetUserFeed(self, kind='album', user='default', limit=None): 209 """Get user-based feed, containing albums, photos, comments or tags; 210 defaults to albums. 211 212 The entries are ordered by the values of their `updated' elements, 213 with the most recently updated entry appearing first in the feed. 214 215 Arguments: 216 kind: the kind of entries to get, either `album', `photo', 217 `comment' or `tag', or a python list of these. Defaults to `album'. 218 user (optional): whose albums we're querying. Defaults to current user. 219 limit (optional): the maximum number of entries to return. 220 Defaults to everything the server returns. 221 222 223 Returns: 224 gdata.photos.UserFeed, containing appropriate Entry elements 225 226 See: 227 http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual 228 http://googledataapis.blogspot.com/2007/07/picasa-web-albums-adds-new-api-features.html 229 """ 230 if isinstance(kind, (list, tuple) ): 231 kind = ",".join(kind) 232 233 uri = '/data/feed/api/user/%s?kind=%s' % (user, kind) 234 return self.GetFeed(uri, limit=limit) 235 236 def GetTaggedPhotos(self, tag, user='default', limit=None): 237 """Get all photos belonging to a specific user, tagged by the given keyword 238 239 Arguments: 240 tag: The tag you're looking for, e.g. `dog' 241 user (optional): Whose images/videos you want to search, defaults 242 to current user 243 limit (optional): the maximum number of entries to return. 244 Defaults to everything the server returns. 245 246 Returns: 247 gdata.photos.UserFeed containing PhotoEntry elements 248 """ 249 # Lower-casing because of 250 # http://code.google.com/p/gdata-issues/issues/detail?id=194 251 uri = '/data/feed/api/user/%s?kind=photo&tag=%s' % (user, tag.lower()) 252 return self.GetFeed(uri, limit) 253 254 def SearchUserPhotos(self, query, user='default', limit=100): 255 """Search through all photos for a specific user and return a feed. 256 This will look for matches in file names and image tags (a.k.a. keywords) 257 258 Arguments: 259 query: The string you're looking for, e.g. `vacation' 260 user (optional): The username of whose photos you want to search, defaults 261 to current user. 262 limit (optional): Don't return more than `limit' hits, defaults to 100 263 264 Only public photos are searched, unless you are authenticated and 265 searching through your own photos. 266 267 Returns: 268 gdata.photos.UserFeed with PhotoEntry elements 269 """ 270 uri = '/data/feed/api/user/%s?kind=photo&q=%s' % (user, query) 271 return self.GetFeed(uri, limit=limit) 272 273 def SearchCommunityPhotos(self, query, limit=100): 274 """Search through all public photos and return a feed. 275 This will look for matches in file names and image tags (a.k.a. keywords) 276 277 Arguments: 278 query: The string you're looking for, e.g. `vacation' 279 limit (optional): Don't return more than `limit' hits, defaults to 100 280 281 Returns: 282 gdata.GDataFeed with PhotoEntry elements 283 """ 284 uri='/data/feed/api/all?q=%s' % query 285 return self.GetFeed(uri, limit=limit) 286 287 def GetContacts(self, user='default', limit=None): 288 """Retrieve a feed that contains a list of your contacts 289 290 Arguments: 291 user: Username of the user whose contacts you want 292 293 Returns 294 gdata.photos.UserFeed, with UserEntry entries 295 296 See: 297 http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 298 """ 299 uri = '/data/feed/api/user/%s/contacts?kind=user' % user 300 return self.GetFeed(uri, limit=limit) 301 302 def SearchContactsPhotos(self, user='default', search=None, limit=None): 303 """Search over your contacts' photos and return a feed 304 305 Arguments: 306 user: Username of the user whose contacts you want 307 search (optional): What to search for (photo title, description and keywords) 308 309 Returns 310 gdata.photos.UserFeed, with PhotoEntry elements 311 312 See: 313 http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 314 """ 315 316 uri = '/data/feed/api/user/%s/contacts?kind=photo&q=%s' % (user, search) 317 return self.GetFeed(uri, limit=limit) 318 319 def InsertAlbum(self, title, summary, location=None, access='public', 320 commenting_enabled='true', timestamp=None): 321 """Add an album. 322 323 Needs authentication, see self.ClientLogin() 324 325 Arguments: 326 title: Album title 327 summary: Album summary / description 328 access (optional): `private' or `public'. Public albums are searchable 329 by everyone on the internet. Defaults to `public' 330 commenting_enabled (optional): `true' or `false'. Defaults to `true'. 331 timestamp (optional): A date and time for the album, in milliseconds since 332 Unix epoch[1] UTC. Defaults to now. 333 334 Returns: 335 The newly created gdata.photos.AlbumEntry 336 337 See: 338 http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed 339 340 [1]: http://en.wikipedia.org/wiki/Unix_epoch 341 """ 342 album = gdata.photos.AlbumEntry() 343 album.title = atom.Title(text=title, title_type='text') 344 album.summary = atom.Summary(text=summary, summary_type='text') 345 if location is not None: 346 album.location = gdata.photos.Location(text=location) 347 album.access = gdata.photos.Access(text=access) 348 if commenting_enabled in ('true', 'false'): 349 album.commentingEnabled = gdata.photos.CommentingEnabled(text=commenting_enabled) 350 if timestamp is None: 351 timestamp = '%i' % int(time.time() * 1000) 352 album.timestamp = gdata.photos.Timestamp(text=timestamp) 353 try: 354 return self.Post(album, uri=self.userUri % self.email, 355 converter=gdata.photos.AlbumEntryFromString) 356 except gdata.service.RequestError, e: 357 raise GooglePhotosException(e.args[0]) 358 359 def InsertPhoto(self, album_or_uri, photo, filename_or_handle, 360 content_type='image/jpeg'): 361 """Add a PhotoEntry 362 363 Needs authentication, see self.ClientLogin() 364 365 Arguments: 366 album_or_uri: AlbumFeed or uri of the album where the photo should go 367 photo: PhotoEntry to add 368 filename_or_handle: A file-like object or file name where the image/video 369 will be read from 370 content_type (optional): Internet media type (a.k.a. mime type) of 371 media object. Currently Google Photos supports these types: 372 o image/bmp 373 o image/gif 374 o image/jpeg 375 o image/png 376 377 Images will be converted to jpeg on upload. Defaults to `image/jpeg' 378 379 """ 380 381 try: 382 assert(isinstance(photo, gdata.photos.PhotoEntry)) 383 except AssertionError: 384 raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, 385 'body':'`photo` must be a gdata.photos.PhotoEntry instance', 386 'reason':'Found %s, not PhotoEntry' % type(photo) 387 }) 388 try: 389 majtype, mintype = content_type.split('/') 390 assert(mintype in SUPPORTED_UPLOAD_TYPES) 391 except (ValueError, AssertionError): 392 raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE, 393 'body':'This is not a valid content type: %s' % content_type, 394 'reason':'Accepted content types: %s' % \ 395 ['image/'+t for t in SUPPORTED_UPLOAD_TYPES] 396 }) 397 if isinstance(filename_or_handle, (str, unicode)) and \ 398 os.path.exists(filename_or_handle): # it's a file name 399 mediasource = gdata.MediaSource() 400 mediasource.setFile(filename_or_handle, content_type) 401 elif hasattr(filename_or_handle, 'read'):# it's a file-like resource 402 if hasattr(filename_or_handle, 'seek'): 403 filename_or_handle.seek(0) # rewind pointer to the start of the file 404 # gdata.MediaSource needs the content length, so read the whole image 405 file_handle = StringIO.StringIO(filename_or_handle.read()) 406 name = 'image' 407 if hasattr(filename_or_handle, 'name'): 408 name = filename_or_handle.name 409 mediasource = gdata.MediaSource(file_handle, content_type, 410 content_length=file_handle.len, file_name=name) 411 else: #filename_or_handle is not valid 412 raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, 413 'body':'`filename_or_handle` must be a path name or a file-like object', 414 'reason':'Found %s, not path name or object with a .read() method' % \ 415 type(filename_or_handle) 416 }) 417 418 if isinstance(album_or_uri, (str, unicode)): # it's a uri 419 feed_uri = album_or_uri 420 elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object 421 feed_uri = album_or_uri.GetFeedLink().href 422 423 try: 424 return self.Post(photo, uri=feed_uri, media_source=mediasource, 425 converter=gdata.photos.PhotoEntryFromString) 426 except gdata.service.RequestError, e: 427 raise GooglePhotosException(e.args[0]) 428 429 def InsertPhotoSimple(self, album_or_uri, title, summary, filename_or_handle, 430 content_type='image/jpeg', keywords=None): 431 """Add a photo without constructing a PhotoEntry. 432 433 Needs authentication, see self.ClientLogin() 434 435 Arguments: 436 album_or_uri: AlbumFeed or uri of the album where the photo should go 437 title: Photo title 438 summary: Photo summary / description 439 filename_or_handle: A file-like object or file name where the image/video 440 will be read from 441 content_type (optional): Internet media type (a.k.a. mime type) of 442 media object. Currently Google Photos supports these types: 443 o image/bmp 444 o image/gif 445 o image/jpeg 446 o image/png 447 448 Images will be converted to jpeg on upload. Defaults to `image/jpeg' 449 keywords (optional): a 1) comma separated string or 2) a python list() of 450 keywords (a.k.a. tags) to add to the image. 451 E.g. 1) `dog, vacation, happy' 2) ['dog', 'happy', 'vacation'] 452 453 Returns: 454 The newly created gdata.photos.PhotoEntry or GooglePhotosException on errors 455 456 See: 457 http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed 458 [1]: http://en.wikipedia.org/wiki/Unix_epoch 459 """ 460 461 metadata = gdata.photos.PhotoEntry() 462 metadata.title=atom.Title(text=title) 463 metadata.summary = atom.Summary(text=summary, summary_type='text') 464 if keywords is not None: 465 if isinstance(keywords, list): 466 keywords = ','.join(keywords) 467 metadata.media.keywords = gdata.media.Keywords(text=keywords) 468 return self.InsertPhoto(album_or_uri, metadata, filename_or_handle, 469 content_type) 470 471 def UpdatePhotoMetadata(self, photo): 472 """Update a photo's metadata. 473 474 Needs authentication, see self.ClientLogin() 475 476 You can update any or all of the following metadata properties: 477 * <title> 478 * <media:description> 479 * <gphoto:checksum> 480 * <gphoto:client> 481 * <gphoto:rotation> 482 * <gphoto:timestamp> 483 * <gphoto:commentingEnabled> 484 485 Arguments: 486 photo: a gdata.photos.PhotoEntry object with updated elements 487 488 Returns: 489 The modified gdata.photos.PhotoEntry 490 491 Example: 492 p = GetFeed(uri).entry[0] 493 p.title.text = u'My new text' 494 p.commentingEnabled.text = 'false' 495 p = UpdatePhotoMetadata(p) 496 497 It is important that you don't keep the old object around, once 498 it has been updated. See 499 http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency 500 """ 501 try: 502 return self.Put(data=photo, uri=photo.GetEditLink().href, 503 converter=gdata.photos.PhotoEntryFromString) 504 except gdata.service.RequestError, e: 505 raise GooglePhotosException(e.args[0]) 506 507 508 def UpdatePhotoBlob(self, photo_or_uri, filename_or_handle, 509 content_type = 'image/jpeg'): 510 """Update a photo's binary data. 511 512 Needs authentication, see self.ClientLogin() 513 514 Arguments: 515 photo_or_uri: a gdata.photos.PhotoEntry that will be updated, or a 516 `edit-media' uri pointing to it 517 filename_or_handle: A file-like object or file name where the image/video 518 will be read from 519 content_type (optional): Internet media type (a.k.a. mime type) of 520 media object. Currently Google Photos supports these types: 521 o image/bmp 522 o image/gif 523 o image/jpeg 524 o image/png 525 Images will be converted to jpeg on upload. Defaults to `image/jpeg' 526 527 Returns: 528 The modified gdata.photos.PhotoEntry 529 530 Example: 531 p = GetFeed(PhotoUri) 532 p = UpdatePhotoBlob(p, '/tmp/newPic.jpg') 533 534 It is important that you don't keep the old object around, once 535 it has been updated. See 536 http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency 537 """ 538 539 try: 540 majtype, mintype = content_type.split('/') 541 assert(mintype in SUPPORTED_UPLOAD_TYPES) 542 except (ValueError, AssertionError): 543 raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE, 544 'body':'This is not a valid content type: %s' % content_type, 545 'reason':'Accepted content types: %s' % \ 546 ['image/'+t for t in SUPPORTED_UPLOAD_TYPES] 547 }) 548 549 if isinstance(filename_or_handle, (str, unicode)) and \ 550 os.path.exists(filename_or_handle): # it's a file name 551 photoblob = gdata.MediaSource() 552 photoblob.setFile(filename_or_handle, content_type) 553 elif hasattr(filename_or_handle, 'read'):# it's a file-like resource 554 if hasattr(filename_or_handle, 'seek'): 555 filename_or_handle.seek(0) # rewind pointer to the start of the file 556 # gdata.MediaSource needs the content length, so read the whole image 557 file_handle = StringIO.StringIO(filename_or_handle.read()) 558 name = 'image' 559 if hasattr(filename_or_handle, 'name'): 560 name = filename_or_handle.name 561 mediasource = gdata.MediaSource(file_handle, content_type, 562 content_length=file_handle.len, file_name=name) 563 else: #filename_or_handle is not valid 564 raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, 565 'body':'`filename_or_handle` must be a path name or a file-like object', 566 'reason':'Found %s, not path name or an object with .read() method' % \ 567 type(filename_or_handle) 568 }) 569 570 if isinstance(photo_or_uri, (str, unicode)): 571 entry_uri = photo_or_uri # it's a uri 572 elif hasattr(photo_or_uri, 'GetEditMediaLink'): 573 entry_uri = photo_or_uri.GetEditMediaLink().href 574 try: 575 return self.Put(photoblob, entry_uri, 576 converter=gdata.photos.PhotoEntryFromString) 577 except gdata.service.RequestError, e: 578 raise GooglePhotosException(e.args[0]) 579 580 def InsertTag(self, photo_or_uri, tag): 581 """Add a tag (a.k.a. keyword) to a photo. 582 583 Needs authentication, see self.ClientLogin() 584 585 Arguments: 586 photo_or_uri: a gdata.photos.PhotoEntry that will be tagged, or a 587 `post' uri pointing to it 588 (string) tag: The tag/keyword 589 590 Returns: 591 The new gdata.photos.TagEntry 592 593 Example: 594 p = GetFeed(PhotoUri) 595 tag = InsertTag(p, 'Beautiful sunsets') 596 597 """ 598 tag = gdata.photos.TagEntry(title=atom.Title(text=tag)) 599 if isinstance(photo_or_uri, (str, unicode)): 600 post_uri = photo_or_uri # it's a uri 601 elif hasattr(photo_or_uri, 'GetEditMediaLink'): 602 post_uri = photo_or_uri.GetPostLink().href 603 try: 604 return self.Post(data=tag, uri=post_uri, 605 converter=gdata.photos.TagEntryFromString) 606 except gdata.service.RequestError, e: 607 raise GooglePhotosException(e.args[0]) 608 609 610 def InsertComment(self, photo_or_uri, comment): 611 """Add a comment to a photo. 612 613 Needs authentication, see self.ClientLogin() 614 615 Arguments: 616 photo_or_uri: a gdata.photos.PhotoEntry that is about to be commented 617 , or a `post' uri pointing to it 618 (string) comment: The actual comment 619 620 Returns: 621 The new gdata.photos.CommentEntry 622 623 Example: 624 p = GetFeed(PhotoUri) 625 tag = InsertComment(p, 'OOOH! I would have loved to be there. 626 Who's that in the back?') 627 628 """ 629 comment = gdata.photos.CommentEntry(content=atom.Content(text=comment)) 630 if isinstance(photo_or_uri, (str, unicode)): 631 post_uri = photo_or_uri # it's a uri 632 elif hasattr(photo_or_uri, 'GetEditMediaLink'): 633 post_uri = photo_or_uri.GetPostLink().href 634 try: 635 return self.Post(data=comment, uri=post_uri, 636 converter=gdata.photos.CommentEntryFromString) 637 except gdata.service.RequestError, e: 638 raise GooglePhotosException(e.args[0]) 639 640 def Delete(self, object_or_uri, *args, **kwargs): 641 """Delete an object. 642 643 Re-implementing the GDataService.Delete method, to add some 644 convenience. 645 646 Arguments: 647 object_or_uri: Any object that has a GetEditLink() method that 648 returns a link, or a uri to that object. 649 650 Returns: 651 ? or GooglePhotosException on errors 652 """ 653 try: 654 uri = object_or_uri.GetEditLink().href 655 except AttributeError: 656 uri = object_or_uri 657 try: 658 return gdata.service.GDataService.Delete(self, uri, *args, **kwargs) 659 except gdata.service.RequestError, e: 660 raise GooglePhotosException(e.args[0]) 661 662def GetSmallestThumbnail(media_thumbnail_list): 663 """Helper function to get the smallest thumbnail of a list of 664 gdata.media.Thumbnail. 665 Returns gdata.media.Thumbnail """ 666 r = {} 667 for thumb in media_thumbnail_list: 668 r[int(thumb.width)*int(thumb.height)] = thumb 669 keys = r.keys() 670 keys.sort() 671 return r[keys[0]] 672 673def ConvertAtomTimestampToEpoch(timestamp): 674 """Helper function to convert a timestamp string, for instance 675 from atom:updated or atom:published, to milliseconds since Unix epoch 676 (a.k.a. POSIX time). 677 678 `2007-07-22T00:45:10.000Z' -> """ 679 return time.mktime(time.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.000Z')) 680 ## TODO: Timezone aware