/gdata/photos/__init__.py
Python | 1112 lines | 1054 code | 21 blank | 37 comment | 22 complexity | 4c35c94c27b474ca0088f8b0c41d238f MD5 | raw file
1# -*-*- encoding: utf-8 -*-*- 2# 3# This is the base file for the PicasaWeb python client. 4# It is used for lower level operations. 5# 6# $Id: __init__.py 148 2007-10-28 15:09:19Z havard.gulldahl $ 7# 8# Copyright 2007 H?vard Gulldahl 9# Portions (C) 2006 Google Inc. 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"""This module provides a pythonic, gdata-centric interface to Google Photos 24(a.k.a. Picasa Web Services. 25 26It is modelled after the gdata/* interfaces from the gdata-python-client 27project[1] by Google. 28 29You'll find the user-friendly api in photos.service. Please see the 30documentation or live help() system for available methods. 31 32[1]: http://gdata-python-client.googlecode.com/ 33 34 """ 35 36__author__ = u'havard@gulldahl.no'# (H?vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__ 37__license__ = 'Apache License v2' 38__version__ = '$Revision: 164 $'[11:-2] 39 40import re 41try: 42 from xml.etree import cElementTree as ElementTree 43except ImportError: 44 try: 45 import cElementTree as ElementTree 46 except ImportError: 47 try: 48 from xml.etree import ElementTree 49 except ImportError: 50 from elementtree import ElementTree 51import atom 52import gdata 53 54# importing google photo submodules 55import gdata.media as Media, gdata.exif as Exif, gdata.geo as Geo 56 57# XML namespaces which are often used in Google Photo elements 58PHOTOS_NAMESPACE = 'http://schemas.google.com/photos/2007' 59MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/' 60EXIF_NAMESPACE = 'http://schemas.google.com/photos/exif/2007' 61OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/' 62GEO_NAMESPACE = 'http://www.w3.org/2003/01/geo/wgs84_pos#' 63GML_NAMESPACE = 'http://www.opengis.net/gml' 64GEORSS_NAMESPACE = 'http://www.georss.org/georss' 65PHEED_NAMESPACE = 'http://www.pheed.com/pheed/' 66BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch' 67 68 69class PhotosBaseElement(atom.AtomBase): 70 """Base class for elements in the PHOTO_NAMESPACE. To add new elements, 71 you only need to add the element tag name to self._tag 72 """ 73 74 _tag = '' 75 _namespace = PHOTOS_NAMESPACE 76 _children = atom.AtomBase._children.copy() 77 _attributes = atom.AtomBase._attributes.copy() 78 79 def __init__(self, name=None, extension_elements=None, 80 extension_attributes=None, text=None): 81 self.name = name 82 self.text = text 83 self.extension_elements = extension_elements or [] 84 self.extension_attributes = extension_attributes or {} 85 #def __str__(self): 86 #return str(self.text) 87 #def __unicode__(self): 88 #return unicode(self.text) 89 def __int__(self): 90 return int(self.text) 91 def bool(self): 92 return self.text == 'true' 93 94class GPhotosBaseFeed(gdata.GDataFeed, gdata.LinkFinder): 95 "Base class for all Feeds in gdata.photos" 96 _tag = 'feed' 97 _namespace = atom.ATOM_NAMESPACE 98 _attributes = gdata.GDataFeed._attributes.copy() 99 _children = gdata.GDataFeed._children.copy() 100 # We deal with Entry elements ourselves 101 del _children['{%s}entry' % atom.ATOM_NAMESPACE] 102 103 def __init__(self, author=None, category=None, contributor=None, 104 generator=None, icon=None, atom_id=None, link=None, logo=None, 105 rights=None, subtitle=None, title=None, updated=None, 106 entry=None, total_results=None, start_index=None, 107 items_per_page=None, extension_elements=None, 108 extension_attributes=None, text=None): 109 gdata.GDataFeed.__init__(self, author=author, category=category, 110 contributor=contributor, generator=generator, 111 icon=icon, atom_id=atom_id, link=link, 112 logo=logo, rights=rights, subtitle=subtitle, 113 title=title, updated=updated, entry=entry, 114 total_results=total_results, 115 start_index=start_index, 116 items_per_page=items_per_page, 117 extension_elements=extension_elements, 118 extension_attributes=extension_attributes, 119 text=text) 120 121 def kind(self): 122 "(string) Returns the kind" 123 try: 124 return self.category[0].term.split('#')[1] 125 except IndexError: 126 return None 127 128 def _feedUri(self, kind): 129 "Convenience method to return a uri to a feed of a special kind" 130 assert(kind in ('album', 'tag', 'photo', 'comment', 'user')) 131 here_href = self.GetSelfLink().href 132 if 'kind=%s' % kind in here_href: 133 return here_href 134 if not 'kind=' in here_href: 135 sep = '?' 136 if '?' in here_href: sep = '&' 137 return here_href + "%skind=%s" % (sep, kind) 138 rx = re.match('.*(kind=)(album|tag|photo|comment)', here_href) 139 return here_href[:rx.end(1)] + kind + here_href[rx.end(2):] 140 141 def _ConvertElementTreeToMember(self, child_tree): 142 """Re-implementing the method from AtomBase, since we deal with 143 Entry elements specially""" 144 category = child_tree.find('{%s}category' % atom.ATOM_NAMESPACE) 145 if category is None: 146 return atom.AtomBase._ConvertElementTreeToMember(self, child_tree) 147 namespace, kind = category.get('term').split('#') 148 if namespace != PHOTOS_NAMESPACE: 149 return atom.AtomBase._ConvertElementTreeToMember(self, child_tree) 150 ## TODO: is it safe to use getattr on gdata.photos? 151 entry_class = getattr(gdata.photos, '%sEntry' % kind.title()) 152 if not hasattr(self, 'entry') or self.entry is None: 153 self.entry = [] 154 self.entry.append(atom._CreateClassFromElementTree( 155 entry_class, child_tree)) 156 157class GPhotosBaseEntry(gdata.GDataEntry, gdata.LinkFinder): 158 "Base class for all Entry elements in gdata.photos" 159 _tag = 'entry' 160 _kind = '' 161 _namespace = atom.ATOM_NAMESPACE 162 _children = gdata.GDataEntry._children.copy() 163 _attributes = gdata.GDataEntry._attributes.copy() 164 165 def __init__(self, author=None, category=None, content=None, 166 atom_id=None, link=None, published=None, 167 title=None, updated=None, 168 extended_property=None, 169 extension_elements=None, extension_attributes=None, text=None): 170 gdata.GDataEntry.__init__(self, author=author, category=category, 171 content=content, atom_id=atom_id, link=link, 172 published=published, title=title, 173 updated=updated, text=text, 174 extension_elements=extension_elements, 175 extension_attributes=extension_attributes) 176 self.category.append( 177 atom.Category(scheme='http://schemas.google.com/g/2005#kind', 178 term = 'http://schemas.google.com/photos/2007#%s' % self._kind)) 179 180 def kind(self): 181 "(string) Returns the kind" 182 try: 183 return self.category[0].term.split('#')[1] 184 except IndexError: 185 return None 186 187 def _feedUri(self, kind): 188 "Convenience method to get the uri to this entry's feed of the some kind" 189 try: 190 href = self.GetFeedLink().href 191 except AttributeError: 192 return None 193 sep = '?' 194 if '?' in href: sep = '&' 195 return '%s%skind=%s' % (href, sep, kind) 196 197 198class PhotosBaseEntry(GPhotosBaseEntry): 199 pass 200 201class PhotosBaseFeed(GPhotosBaseFeed): 202 pass 203 204class GPhotosBaseData(object): 205 pass 206 207class Access(PhotosBaseElement): 208 """The Google Photo `Access' element. 209 210 The album's access level. Valid values are `public' or `private'. 211 In documentation, access level is also referred to as `visibility.'""" 212 213 _tag = 'access' 214def AccessFromString(xml_string): 215 return atom.CreateClassFromXMLString(Access, xml_string) 216 217class Albumid(PhotosBaseElement): 218 "The Google Photo `Albumid' element" 219 220 _tag = 'albumid' 221def AlbumidFromString(xml_string): 222 return atom.CreateClassFromXMLString(Albumid, xml_string) 223 224class BytesUsed(PhotosBaseElement): 225 "The Google Photo `BytesUsed' element" 226 227 _tag = 'bytesUsed' 228def BytesUsedFromString(xml_string): 229 return atom.CreateClassFromXMLString(BytesUsed, xml_string) 230 231class Client(PhotosBaseElement): 232 "The Google Photo `Client' element" 233 234 _tag = 'client' 235def ClientFromString(xml_string): 236 return atom.CreateClassFromXMLString(Client, xml_string) 237 238class Checksum(PhotosBaseElement): 239 "The Google Photo `Checksum' element" 240 241 _tag = 'checksum' 242def ChecksumFromString(xml_string): 243 return atom.CreateClassFromXMLString(Checksum, xml_string) 244 245class CommentCount(PhotosBaseElement): 246 "The Google Photo `CommentCount' element" 247 248 _tag = 'commentCount' 249def CommentCountFromString(xml_string): 250 return atom.CreateClassFromXMLString(CommentCount, xml_string) 251 252class CommentingEnabled(PhotosBaseElement): 253 "The Google Photo `CommentingEnabled' element" 254 255 _tag = 'commentingEnabled' 256def CommentingEnabledFromString(xml_string): 257 return atom.CreateClassFromXMLString(CommentingEnabled, xml_string) 258 259class Height(PhotosBaseElement): 260 "The Google Photo `Height' element" 261 262 _tag = 'height' 263def HeightFromString(xml_string): 264 return atom.CreateClassFromXMLString(Height, xml_string) 265 266class Id(PhotosBaseElement): 267 "The Google Photo `Id' element" 268 269 _tag = 'id' 270def IdFromString(xml_string): 271 return atom.CreateClassFromXMLString(Id, xml_string) 272 273class Location(PhotosBaseElement): 274 "The Google Photo `Location' element" 275 276 _tag = 'location' 277def LocationFromString(xml_string): 278 return atom.CreateClassFromXMLString(Location, xml_string) 279 280class MaxPhotosPerAlbum(PhotosBaseElement): 281 "The Google Photo `MaxPhotosPerAlbum' element" 282 283 _tag = 'maxPhotosPerAlbum' 284def MaxPhotosPerAlbumFromString(xml_string): 285 return atom.CreateClassFromXMLString(MaxPhotosPerAlbum, xml_string) 286 287class Name(PhotosBaseElement): 288 "The Google Photo `Name' element" 289 290 _tag = 'name' 291def NameFromString(xml_string): 292 return atom.CreateClassFromXMLString(Name, xml_string) 293 294class Nickname(PhotosBaseElement): 295 "The Google Photo `Nickname' element" 296 297 _tag = 'nickname' 298def NicknameFromString(xml_string): 299 return atom.CreateClassFromXMLString(Nickname, xml_string) 300 301class Numphotos(PhotosBaseElement): 302 "The Google Photo `Numphotos' element" 303 304 _tag = 'numphotos' 305def NumphotosFromString(xml_string): 306 return atom.CreateClassFromXMLString(Numphotos, xml_string) 307 308class Numphotosremaining(PhotosBaseElement): 309 "The Google Photo `Numphotosremaining' element" 310 311 _tag = 'numphotosremaining' 312def NumphotosremainingFromString(xml_string): 313 return atom.CreateClassFromXMLString(Numphotosremaining, xml_string) 314 315class Position(PhotosBaseElement): 316 "The Google Photo `Position' element" 317 318 _tag = 'position' 319def PositionFromString(xml_string): 320 return atom.CreateClassFromXMLString(Position, xml_string) 321 322class Photoid(PhotosBaseElement): 323 "The Google Photo `Photoid' element" 324 325 _tag = 'photoid' 326def PhotoidFromString(xml_string): 327 return atom.CreateClassFromXMLString(Photoid, xml_string) 328 329class Quotacurrent(PhotosBaseElement): 330 "The Google Photo `Quotacurrent' element" 331 332 _tag = 'quotacurrent' 333def QuotacurrentFromString(xml_string): 334 return atom.CreateClassFromXMLString(Quotacurrent, xml_string) 335 336class Quotalimit(PhotosBaseElement): 337 "The Google Photo `Quotalimit' element" 338 339 _tag = 'quotalimit' 340def QuotalimitFromString(xml_string): 341 return atom.CreateClassFromXMLString(Quotalimit, xml_string) 342 343class Rotation(PhotosBaseElement): 344 "The Google Photo `Rotation' element" 345 346 _tag = 'rotation' 347def RotationFromString(xml_string): 348 return atom.CreateClassFromXMLString(Rotation, xml_string) 349 350class Size(PhotosBaseElement): 351 "The Google Photo `Size' element" 352 353 _tag = 'size' 354def SizeFromString(xml_string): 355 return atom.CreateClassFromXMLString(Size, xml_string) 356 357class Snippet(PhotosBaseElement): 358 """The Google Photo `snippet' element. 359 360 When searching, the snippet element will contain a 361 string with the word you're looking for, highlighted in html markup 362 E.g. when your query is `hafjell', this element may contain: 363 `... here at <b>Hafjell</b>.' 364 365 You'll find this element in searches -- that is, feeds that combine the 366 `kind=photo' and `q=yoursearch' parameters in the request. 367 368 See also gphoto:truncated and gphoto:snippettype. 369 370 """ 371 372 _tag = 'snippet' 373def SnippetFromString(xml_string): 374 return atom.CreateClassFromXMLString(Snippet, xml_string) 375 376class Snippettype(PhotosBaseElement): 377 """The Google Photo `Snippettype' element 378 379 When searching, this element will tell you the type of element that matches. 380 381 You'll find this element in searches -- that is, feeds that combine the 382 `kind=photo' and `q=yoursearch' parameters in the request. 383 384 See also gphoto:snippet and gphoto:truncated. 385 386 Possible values and their interpretation: 387 o ALBUM_TITLE - The album title matches 388 o PHOTO_TAGS - The match is a tag/keyword 389 o PHOTO_DESCRIPTION - The match is in the photo's description 390 391 If you discover a value not listed here, please submit a patch to update this docstring. 392 393 """ 394 395 _tag = 'snippettype' 396def SnippettypeFromString(xml_string): 397 return atom.CreateClassFromXMLString(Snippettype, xml_string) 398 399class Thumbnail(PhotosBaseElement): 400 """The Google Photo `Thumbnail' element 401 402 Used to display user's photo thumbnail (hackergotchi). 403 404 (Not to be confused with the <media:thumbnail> element, which gives you 405 small versions of the photo object.)""" 406 407 _tag = 'thumbnail' 408def ThumbnailFromString(xml_string): 409 return atom.CreateClassFromXMLString(Thumbnail, xml_string) 410 411class Timestamp(PhotosBaseElement): 412 """The Google Photo `Timestamp' element 413 Represented as the number of milliseconds since January 1st, 1970. 414 415 416 Take a look at the convenience methods .isoformat() and .datetime(): 417 418 photo_epoch = Time.text # 1180294337000 419 photo_isostring = Time.isoformat() # '2007-05-27T19:32:17.000Z' 420 421 Alternatively: 422 photo_datetime = Time.datetime() # (requires python >= 2.3) 423 """ 424 425 _tag = 'timestamp' 426 def isoformat(self): 427 """(string) Return the timestamp as a ISO 8601 formatted string, 428 e.g. '2007-05-27T19:32:17.000Z' 429 """ 430 import time 431 epoch = float(self.text)/1000 432 return time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(epoch)) 433 434 def datetime(self): 435 """(datetime.datetime) Return the timestamp as a datetime.datetime object 436 437 Requires python 2.3 438 """ 439 import datetime 440 epoch = float(self.text)/1000 441 return datetime.datetime.fromtimestamp(epoch) 442def TimestampFromString(xml_string): 443 return atom.CreateClassFromXMLString(Timestamp, xml_string) 444 445class Truncated(PhotosBaseElement): 446 """The Google Photo `Truncated' element 447 448 You'll find this element in searches -- that is, feeds that combine the 449 `kind=photo' and `q=yoursearch' parameters in the request. 450 451 See also gphoto:snippet and gphoto:snippettype. 452 453 Possible values and their interpretation: 454 0 -- unknown 455 """ 456 457 _tag = 'Truncated' 458def TruncatedFromString(xml_string): 459 return atom.CreateClassFromXMLString(Truncated, xml_string) 460 461class User(PhotosBaseElement): 462 "The Google Photo `User' element" 463 464 _tag = 'user' 465def UserFromString(xml_string): 466 return atom.CreateClassFromXMLString(User, xml_string) 467 468class Version(PhotosBaseElement): 469 "The Google Photo `Version' element" 470 471 _tag = 'version' 472def VersionFromString(xml_string): 473 return atom.CreateClassFromXMLString(Version, xml_string) 474 475class Width(PhotosBaseElement): 476 "The Google Photo `Width' element" 477 478 _tag = 'width' 479def WidthFromString(xml_string): 480 return atom.CreateClassFromXMLString(Width, xml_string) 481 482class Weight(PhotosBaseElement): 483 """The Google Photo `Weight' element. 484 485 The weight of the tag is the number of times the tag 486 appears in the collection of tags currently being viewed. 487 The default weight is 1, in which case this tags is omitted.""" 488 _tag = 'weight' 489def WeightFromString(xml_string): 490 return atom.CreateClassFromXMLString(Weight, xml_string) 491 492class CommentAuthor(atom.Author): 493 """The Atom `Author' element in CommentEntry entries is augmented to 494 contain elements from the PHOTOS_NAMESPACE 495 496 http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 497 """ 498 _children = atom.Author._children.copy() 499 _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) 500 _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) 501 _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail) 502def CommentAuthorFromString(xml_string): 503 return atom.CreateClassFromXMLString(CommentAuthor, xml_string) 504 505########################## ################################ 506 507class AlbumData(object): 508 _children = {} 509 _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 510 _children['{%s}name' % PHOTOS_NAMESPACE] = ('name', Name) 511 _children['{%s}location' % PHOTOS_NAMESPACE] = ('location', Location) 512 _children['{%s}access' % PHOTOS_NAMESPACE] = ('access', Access) 513 _children['{%s}bytesUsed' % PHOTOS_NAMESPACE] = ('bytesUsed', BytesUsed) 514 _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp) 515 _children['{%s}numphotos' % PHOTOS_NAMESPACE] = ('numphotos', Numphotos) 516 _children['{%s}numphotosremaining' % PHOTOS_NAMESPACE] = \ 517 ('numphotosremaining', Numphotosremaining) 518 _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) 519 _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) 520 _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \ 521 ('commentingEnabled', CommentingEnabled) 522 _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \ 523 ('commentCount', CommentCount) 524 ## NOTE: storing media:group as self.media, to create a self-explaining api 525 gphoto_id = None 526 name = None 527 location = None 528 access = None 529 bytesUsed = None 530 timestamp = None 531 numphotos = None 532 numphotosremaining = None 533 user = None 534 nickname = None 535 commentingEnabled = None 536 commentCount = None 537 538class AlbumEntry(GPhotosBaseEntry, AlbumData): 539 """All metadata for a Google Photos Album 540 541 Take a look at AlbumData for metadata accessible as attributes to this object. 542 543 Notes: 544 To avoid name clashes, and to create a more sensible api, some 545 objects have names that differ from the original elements: 546 547 o media:group -> self.media, 548 o geo:where -> self.geo, 549 o photo:id -> self.gphoto_id 550 """ 551 552 _kind = 'album' 553 _children = GPhotosBaseEntry._children.copy() 554 _children.update(AlbumData._children.copy()) 555 # child tags only for Album entries, not feeds 556 _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where) 557 _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group) 558 media = Media.Group() 559 geo = Geo.Where() 560 561 def __init__(self, author=None, category=None, content=None, 562 atom_id=None, link=None, published=None, 563 title=None, updated=None, 564 #GPHOTO NAMESPACE: 565 gphoto_id=None, name=None, location=None, access=None, 566 timestamp=None, numphotos=None, user=None, nickname=None, 567 commentingEnabled=None, commentCount=None, thumbnail=None, 568 # MEDIA NAMESPACE: 569 media=None, 570 # GEORSS NAMESPACE: 571 geo=None, 572 extended_property=None, 573 extension_elements=None, extension_attributes=None, text=None): 574 GPhotosBaseEntry.__init__(self, author=author, category=category, 575 content=content, atom_id=atom_id, link=link, 576 published=published, title=title, 577 updated=updated, text=text, 578 extension_elements=extension_elements, 579 extension_attributes=extension_attributes) 580 581 ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id 582 self.gphoto_id = gphoto_id 583 self.name = name 584 self.location = location 585 self.access = access 586 self.timestamp = timestamp 587 self.numphotos = numphotos 588 self.user = user 589 self.nickname = nickname 590 self.commentingEnabled = commentingEnabled 591 self.commentCount = commentCount 592 self.thumbnail = thumbnail 593 self.extended_property = extended_property or [] 594 self.text = text 595 ## NOTE: storing media:group as self.media, and geo:where as geo, 596 ## to create a self-explaining api 597 self.media = media or Media.Group() 598 self.geo = geo or Geo.Where() 599 600 def GetAlbumId(self): 601 "Return the id of this album" 602 603 return self.GetFeedLink().href.split('/')[-1] 604 605 def GetPhotosUri(self): 606 "(string) Return the uri to this albums feed of the PhotoEntry kind" 607 return self._feedUri('photo') 608 609 def GetCommentsUri(self): 610 "(string) Return the uri to this albums feed of the CommentEntry kind" 611 return self._feedUri('comment') 612 613 def GetTagsUri(self): 614 "(string) Return the uri to this albums feed of the TagEntry kind" 615 return self._feedUri('tag') 616 617def AlbumEntryFromString(xml_string): 618 return atom.CreateClassFromXMLString(AlbumEntry, xml_string) 619 620class AlbumFeed(GPhotosBaseFeed, AlbumData): 621 """All metadata for a Google Photos Album, including its sub-elements 622 623 This feed represents an album as the container for other objects. 624 625 A Album feed contains entries of 626 PhotoEntry, CommentEntry or TagEntry, 627 depending on the `kind' parameter in the original query. 628 629 Take a look at AlbumData for accessible attributes. 630 631 """ 632 633 _children = GPhotosBaseFeed._children.copy() 634 _children.update(AlbumData._children.copy()) 635 636 def GetPhotosUri(self): 637 "(string) Return the uri to the same feed, but of the PhotoEntry kind" 638 639 return self._feedUri('photo') 640 641 def GetTagsUri(self): 642 "(string) Return the uri to the same feed, but of the TagEntry kind" 643 644 return self._feedUri('tag') 645 646 def GetCommentsUri(self): 647 "(string) Return the uri to the same feed, but of the CommentEntry kind" 648 649 return self._feedUri('comment') 650 651def AlbumFeedFromString(xml_string): 652 return atom.CreateClassFromXMLString(AlbumFeed, xml_string) 653 654 655class PhotoData(object): 656 _children = {} 657 ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id 658 _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 659 _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid) 660 _children['{%s}checksum' % PHOTOS_NAMESPACE] = ('checksum', Checksum) 661 _children['{%s}client' % PHOTOS_NAMESPACE] = ('client', Client) 662 _children['{%s}height' % PHOTOS_NAMESPACE] = ('height', Height) 663 _children['{%s}position' % PHOTOS_NAMESPACE] = ('position', Position) 664 _children['{%s}rotation' % PHOTOS_NAMESPACE] = ('rotation', Rotation) 665 _children['{%s}size' % PHOTOS_NAMESPACE] = ('size', Size) 666 _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp) 667 _children['{%s}version' % PHOTOS_NAMESPACE] = ('version', Version) 668 _children['{%s}width' % PHOTOS_NAMESPACE] = ('width', Width) 669 _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \ 670 ('commentingEnabled', CommentingEnabled) 671 _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \ 672 ('commentCount', CommentCount) 673 ## NOTE: storing media:group as self.media, exif:tags as self.exif, and 674 ## geo:where as self.geo, to create a self-explaining api 675 _children['{%s}tags' % EXIF_NAMESPACE] = ('exif', Exif.Tags) 676 _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where) 677 _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group) 678 # These elements show up in search feeds 679 _children['{%s}snippet' % PHOTOS_NAMESPACE] = ('snippet', Snippet) 680 _children['{%s}snippettype' % PHOTOS_NAMESPACE] = ('snippettype', Snippettype) 681 _children['{%s}truncated' % PHOTOS_NAMESPACE] = ('truncated', Truncated) 682 gphoto_id = None 683 albumid = None 684 checksum = None 685 client = None 686 height = None 687 position = None 688 rotation = None 689 size = None 690 timestamp = None 691 version = None 692 width = None 693 commentingEnabled = None 694 commentCount = None 695 snippet=None 696 snippettype=None 697 truncated=None 698 media = Media.Group() 699 geo = Geo.Where() 700 tags = Exif.Tags() 701 702class PhotoEntry(GPhotosBaseEntry, PhotoData): 703 """All metadata for a Google Photos Photo 704 705 Take a look at PhotoData for metadata accessible as attributes to this object. 706 707 Notes: 708 To avoid name clashes, and to create a more sensible api, some 709 objects have names that differ from the original elements: 710 711 o media:group -> self.media, 712 o exif:tags -> self.exif, 713 o geo:where -> self.geo, 714 o photo:id -> self.gphoto_id 715 """ 716 717 _kind = 'photo' 718 _children = GPhotosBaseEntry._children.copy() 719 _children.update(PhotoData._children.copy()) 720 721 def __init__(self, author=None, category=None, content=None, 722 atom_id=None, link=None, published=None, 723 title=None, updated=None, text=None, 724 # GPHOTO NAMESPACE: 725 gphoto_id=None, albumid=None, checksum=None, client=None, height=None, 726 position=None, rotation=None, size=None, timestamp=None, version=None, 727 width=None, commentCount=None, commentingEnabled=None, 728 # MEDIARSS NAMESPACE: 729 media=None, 730 # EXIF_NAMESPACE: 731 exif=None, 732 # GEORSS NAMESPACE: 733 geo=None, 734 extension_elements=None, extension_attributes=None): 735 GPhotosBaseEntry.__init__(self, author=author, category=category, 736 content=content, 737 atom_id=atom_id, link=link, published=published, 738 title=title, updated=updated, text=text, 739 extension_elements=extension_elements, 740 extension_attributes=extension_attributes) 741 742 743 ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id 744 self.gphoto_id = gphoto_id 745 self.albumid = albumid 746 self.checksum = checksum 747 self.client = client 748 self.height = height 749 self.position = position 750 self.rotation = rotation 751 self.size = size 752 self.timestamp = timestamp 753 self.version = version 754 self.width = width 755 self.commentingEnabled = commentingEnabled 756 self.commentCount = commentCount 757 ## NOTE: storing media:group as self.media, to create a self-explaining api 758 self.media = media or Media.Group() 759 self.exif = exif or Exif.Tags() 760 self.geo = geo or Geo.Where() 761 762 def GetPostLink(self): 763 "Return the uri to this photo's `POST' link (use it for updates of the object)" 764 765 return self.GetFeedLink() 766 767 def GetCommentsUri(self): 768 "Return the uri to this photo's feed of CommentEntry comments" 769 return self._feedUri('comment') 770 771 def GetTagsUri(self): 772 "Return the uri to this photo's feed of TagEntry tags" 773 return self._feedUri('tag') 774 775 def GetAlbumUri(self): 776 """Return the uri to the AlbumEntry containing this photo""" 777 778 href = self.GetSelfLink().href 779 return href[:href.find('/photoid')] 780 781def PhotoEntryFromString(xml_string): 782 return atom.CreateClassFromXMLString(PhotoEntry, xml_string) 783 784class PhotoFeed(GPhotosBaseFeed, PhotoData): 785 """All metadata for a Google Photos Photo, including its sub-elements 786 787 This feed represents a photo as the container for other objects. 788 789 A Photo feed contains entries of 790 CommentEntry or TagEntry, 791 depending on the `kind' parameter in the original query. 792 793 Take a look at PhotoData for metadata accessible as attributes to this object. 794 795 """ 796 _children = GPhotosBaseFeed._children.copy() 797 _children.update(PhotoData._children.copy()) 798 799 def GetTagsUri(self): 800 "(string) Return the uri to the same feed, but of the TagEntry kind" 801 802 return self._feedUri('tag') 803 804 def GetCommentsUri(self): 805 "(string) Return the uri to the same feed, but of the CommentEntry kind" 806 807 return self._feedUri('comment') 808 809def PhotoFeedFromString(xml_string): 810 return atom.CreateClassFromXMLString(PhotoFeed, xml_string) 811 812class TagData(GPhotosBaseData): 813 _children = {} 814 _children['{%s}weight' % PHOTOS_NAMESPACE] = ('weight', Weight) 815 weight=None 816 817class TagEntry(GPhotosBaseEntry, TagData): 818 """All metadata for a Google Photos Tag 819 820 The actual tag is stored in the .title.text attribute 821 822 """ 823 824 _kind = 'tag' 825 _children = GPhotosBaseEntry._children.copy() 826 _children.update(TagData._children.copy()) 827 828 def __init__(self, author=None, category=None, content=None, 829 atom_id=None, link=None, published=None, 830 title=None, updated=None, 831 # GPHOTO NAMESPACE: 832 weight=None, 833 extended_property=None, 834 extension_elements=None, extension_attributes=None, text=None): 835 GPhotosBaseEntry.__init__(self, author=author, category=category, 836 content=content, 837 atom_id=atom_id, link=link, published=published, 838 title=title, updated=updated, text=text, 839 extension_elements=extension_elements, 840 extension_attributes=extension_attributes) 841 842 self.weight = weight 843 844 def GetAlbumUri(self): 845 """Return the uri to the AlbumEntry containing this tag""" 846 847 href = self.GetSelfLink().href 848 pos = href.find('/photoid') 849 if pos == -1: 850 return None 851 return href[:pos] 852 853 def GetPhotoUri(self): 854 """Return the uri to the PhotoEntry containing this tag""" 855 856 href = self.GetSelfLink().href 857 pos = href.find('/tag') 858 if pos == -1: 859 return None 860 return href[:pos] 861 862def TagEntryFromString(xml_string): 863 return atom.CreateClassFromXMLString(TagEntry, xml_string) 864 865 866class TagFeed(GPhotosBaseFeed, TagData): 867 """All metadata for a Google Photos Tag, including its sub-elements""" 868 869 _children = GPhotosBaseFeed._children.copy() 870 _children.update(TagData._children.copy()) 871 872def TagFeedFromString(xml_string): 873 return atom.CreateClassFromXMLString(TagFeed, xml_string) 874 875class CommentData(GPhotosBaseData): 876 _children = {} 877 ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id 878 _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 879 _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid) 880 _children['{%s}photoid' % PHOTOS_NAMESPACE] = ('photoid', Photoid) 881 _children['{%s}author' % atom.ATOM_NAMESPACE] = ('author', [CommentAuthor,]) 882 gphoto_id=None 883 albumid=None 884 photoid=None 885 author=None 886 887class CommentEntry(GPhotosBaseEntry, CommentData): 888 """All metadata for a Google Photos Comment 889 890 The comment is stored in the .content.text attribute, 891 with a content type in .content.type. 892 893 894 """ 895 896 _kind = 'comment' 897 _children = GPhotosBaseEntry._children.copy() 898 _children.update(CommentData._children.copy()) 899 def __init__(self, author=None, category=None, content=None, 900 atom_id=None, link=None, published=None, 901 title=None, updated=None, 902 # GPHOTO NAMESPACE: 903 gphoto_id=None, albumid=None, photoid=None, 904 extended_property=None, 905 extension_elements=None, extension_attributes=None, text=None): 906 907 GPhotosBaseEntry.__init__(self, author=author, category=category, 908 content=content, 909 atom_id=atom_id, link=link, published=published, 910 title=title, updated=updated, 911 extension_elements=extension_elements, 912 extension_attributes=extension_attributes, 913 text=text) 914 915 self.gphoto_id = gphoto_id 916 self.albumid = albumid 917 self.photoid = photoid 918 919 def GetCommentId(self): 920 """Return the globally unique id of this comment""" 921 return self.GetSelfLink().href.split('/')[-1] 922 923 def GetAlbumUri(self): 924 """Return the uri to the AlbumEntry containing this comment""" 925 926 href = self.GetSelfLink().href 927 return href[:href.find('/photoid')] 928 929 def GetPhotoUri(self): 930 """Return the uri to the PhotoEntry containing this comment""" 931 932 href = self.GetSelfLink().href 933 return href[:href.find('/commentid')] 934 935def CommentEntryFromString(xml_string): 936 return atom.CreateClassFromXMLString(CommentEntry, xml_string) 937 938class CommentFeed(GPhotosBaseFeed, CommentData): 939 """All metadata for a Google Photos Comment, including its sub-elements""" 940 941 _children = GPhotosBaseFeed._children.copy() 942 _children.update(CommentData._children.copy()) 943 944def CommentFeedFromString(xml_string): 945 return atom.CreateClassFromXMLString(CommentFeed, xml_string) 946 947class UserData(GPhotosBaseData): 948 _children = {} 949 _children['{%s}maxPhotosPerAlbum' % PHOTOS_NAMESPACE] = ('maxPhotosPerAlbum', MaxPhotosPerAlbum) 950 _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) 951 _children['{%s}quotalimit' % PHOTOS_NAMESPACE] = ('quotalimit', Quotalimit) 952 _children['{%s}quotacurrent' % PHOTOS_NAMESPACE] = ('quotacurrent', Quotacurrent) 953 _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail) 954 _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) 955 _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 956 957 maxPhotosPerAlbum=None 958 nickname=None 959 quotalimit=None 960 quotacurrent=None 961 thumbnail=None 962 user=None 963 gphoto_id=None 964 965 966class UserEntry(GPhotosBaseEntry, UserData): 967 """All metadata for a Google Photos User 968 969 This entry represents an album owner and all appropriate metadata. 970 971 Take a look at at the attributes of the UserData for metadata available. 972 """ 973 _children = GPhotosBaseEntry._children.copy() 974 _children.update(UserData._children.copy()) 975 _kind = 'user' 976 977 def __init__(self, author=None, category=None, content=None, 978 atom_id=None, link=None, published=None, 979 title=None, updated=None, 980 # GPHOTO NAMESPACE: 981 gphoto_id=None, maxPhotosPerAlbum=None, nickname=None, quotalimit=None, 982 quotacurrent=None, thumbnail=None, user=None, 983 extended_property=None, 984 extension_elements=None, extension_attributes=None, text=None): 985 986 GPhotosBaseEntry.__init__(self, author=author, category=category, 987 content=content, 988 atom_id=atom_id, link=link, published=published, 989 title=title, updated=updated, 990 extension_elements=extension_elements, 991 extension_attributes=extension_attributes, 992 text=text) 993 994 995 self.gphoto_id=gphoto_id 996 self.maxPhotosPerAlbum=maxPhotosPerAlbum 997 self.nickname=nickname 998 self.quotalimit=quotalimit 999 self.quotacurrent=quotacurrent 1000 self.thumbnail=thumbnail 1001 self.user=user 1002 1003 def GetAlbumsUri(self): 1004 "(string) Return the uri to this user's feed of the AlbumEntry kind" 1005 return self._feedUri('album') 1006 1007 def GetPhotosUri(self): 1008 "(string) Return the uri to this user's feed of the PhotoEntry kind" 1009 return self._feedUri('photo') 1010 1011 def GetCommentsUri(self): 1012 "(string) Return the uri to this user's feed of the CommentEntry kind" 1013 return self._feedUri('comment') 1014 1015 def GetTagsUri(self): 1016 "(string) Return the uri to this user's feed of the TagEntry kind" 1017 return self._feedUri('tag') 1018 1019def UserEntryFromString(xml_string): 1020 return atom.CreateClassFromXMLString(UserEntry, xml_string) 1021 1022class UserFeed(GPhotosBaseFeed, UserData): 1023 """Feed for a User in the google photos api. 1024 1025 This feed represents a user as the container for other objects. 1026 1027 A User feed contains entries of 1028 AlbumEntry, PhotoEntry, CommentEntry, UserEntry or TagEntry, 1029 depending on the `kind' parameter in the original query. 1030 1031 The user feed itself also contains all of the metadata available 1032 as part of a UserData object.""" 1033 _children = GPhotosBaseFeed._children.copy() 1034 _children.update(UserData._children.copy()) 1035 1036 def GetAlbumsUri(self): 1037 """Get the uri to this feed, but with entries of the AlbumEntry kind.""" 1038 return self._feedUri('album') 1039 1040 def GetTagsUri(self): 1041 """Get the uri to this feed, but with entries of the TagEntry kind.""" 1042 return self._feedUri('tag') 1043 1044 def GetPhotosUri(self): 1045 """Get the uri to this feed, but with entries of the PhotosEntry kind.""" 1046 return self._feedUri('photo') 1047 1048 def GetCommentsUri(self): 1049 """Get the uri to this feed, but with entries of the CommentsEntry kind.""" 1050 return self._feedUri('comment') 1051 1052def UserFeedFromString(xml_string): 1053 return atom.CreateClassFromXMLString(UserFeed, xml_string) 1054 1055 1056 1057def AnyFeedFromString(xml_string): 1058 """Creates an instance of the appropriate feed class from the 1059 xml string contents. 1060 1061 Args: 1062 xml_string: str A string which contains valid XML. The root element 1063 of the XML string should match the tag and namespace of the desired 1064 class. 1065 1066 Returns: 1067 An instance of the target class with members assigned according to the 1068 contents of the XML - or a basic gdata.GDataFeed instance if it is 1069 impossible to determine the appropriate class (look for extra elements 1070 in GDataFeed's .FindExtensions() and extension_elements[] ). 1071 """ 1072 tree = ElementTree.fromstring(xml_string) 1073 category = tree.find('{%s}category' % atom.ATOM_NAMESPACE) 1074 if category is None: 1075 # TODO: is this the best way to handle this? 1076 return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree) 1077 namespace, kind = category.get('term').split('#') 1078 if namespace != PHOTOS_NAMESPACE: 1079 # TODO: is this the best way to handle this? 1080 return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree) 1081 ## TODO: is getattr safe this way? 1082 feed_class = getattr(gdata.photos, '%sFeed' % kind.title()) 1083 return atom._CreateClassFromElementTree(feed_class, tree) 1084 1085def AnyEntryFromString(xml_string): 1086 """Creates an instance of the appropriate entry class from the 1087 xml string contents. 1088 1089 Args: 1090 xml_string: str A string which contains valid XML. The root element 1091 of the XML string should match the tag and namespace of the desired 1092 class. 1093 1094 Returns: 1095 An instance of the target class with members assigned according to the 1096 contents of the XML - or a basic gdata.GDataEndry instance if it is 1097 impossible to determine the appropriate class (look for extra elements 1098 in GDataEntry's .FindExtensions() and extension_elements[] ). 1099 """ 1100 tree = ElementTree.fromstring(xml_string) 1101 category = tree.find('{%s}category' % atom.ATOM_NAMESPACE) 1102 if category is None: 1103 # TODO: is this the best way to handle this? 1104 return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree) 1105 namespace, kind = category.get('term').split('#') 1106 if namespace != PHOTOS_NAMESPACE: 1107 # TODO: is this the best way to handle this? 1108 return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree) 1109 ## TODO: is getattr safe this way? 1110 feed_class = getattr(gdata.photos, '%sEntry' % kind.title()) 1111 return atom._CreateClassFromElementTree(feed_class, tree) 1112