PageRenderTime 110ms CodeModel.GetById 47ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/gdata/photos/__init__.py

http://radioappz.googlecode.com/
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