Dreamstime

Monday 5 September 2016

Customized gdata For PicasaWeb Uploads

Without going into details, I briefly mentioned that I added a few new functions into the class GDataService and PhotoService in order to get the interfacing between 'Google GData Photo API' and RavenPlus working with Google OAuth2 authentication.

Here are the technical details, plus the Python code.

As this is a rather long post, I have broken it down into two parts. In this post, I will only discuss the changes that I have made to the gdata library. In a future blog post, I will show how I used this customised gdata library for uploading pictures to Google's PicasaWeb from RavenPlus.

Some Background Information

Since Python is an object-oriented programming language, the most obvious way to customised gdata is via 'inheritance'. I tried and failed because of some local private variables being referenced in the base class from my derived class. Like I have said before, I am no expert in Python. In desperation, I used what seems to be the easiest way out: I made local copies of what's needed from gdata and customised them to fit my requirements. The important thing here is it works.

Customized gdata Within RavenPlus Source Tree

File And Directory Structure

The picture above shows the file and directory structure that I have made to the RavenPlus source code tree.

I created a directory called 'gdataExtend' under 'zoundry/blogpub/blogger'. I then copied the file 'service.py' (from the Python gdata library), renamed it 'gdata_service.py' and place it in 'gdataExtend'. I then created a file called '__init__.py' in 'gdataExtend' that contains only 1 line of code (RavenPlus wouldn't compile otherwise):

__all__ = []

I also created the directory 'photos' under 'gDataExtend', which mirrors the directory structure from the Python gdata library. I copied the two files '__init__.py' and 'service.py' from 'gdata/photos' and place it in 'gDataExtend/photos'.

Source Code

File 1: zoundry/blogpub/blogger/gdataExtend/gdata_service.py


These are the 3 new functions (PostOrPutRaven, PostRaven and PutRaven) that I have added into the class GDataService. The source code for these functions are listed below.


==== BEGIN ====

  def PostOrPutRaven(self, verb, data, uri, extra_headers=None, url_params=None, 
           escape_params=True, redirects_remaining=4, media_source=None, 
           converter=None):
    # Function is identical to gdata.service.PostOrPut() except that it returns
    # the raw Picasa Web XML meta-data unconverted.

    if extra_headers is None:
      extra_headers = {}

    if self.__gsessionid is not None:
      if uri.find('gsessionid=') < 0:
        if url_params is None:
          url_params = {}
        url_params['gsessionid'] = self.__gsessionid

    if data and media_source:
      if ElementTree.iselement(data):
        data_str = ElementTree.tostring(data)
      else:
        data_str = str(data)
        
      multipart = []
      multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \
          'Content-Type: application/atom+xml\r\n\r\n')
      multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \
          media_source.content_type+'\r\n\r\n')
      multipart.append('\r\n--END_OF_PART--\r\n')
        
      extra_headers['MIME-version'] = '1.0'
      extra_headers['Content-Length'] = str(len(multipart[0]) +
          len(multipart[1]) + len(multipart[2]) +
          len(data_str) + media_source.content_length)

      extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART'
      server_response = self.request(verb, uri, 
          data=[multipart[0], data_str, multipart[1], media_source.file_handle,
              multipart[2]], headers=extra_headers, url_params=url_params)
      result_body = server_response.read()
      
    elif media_source or isinstance(data, gdata.MediaSource):

      if isinstance(data, gdata.MediaSource):
        media_source = data
      extra_headers['Content-Length'] = str(media_source.content_length)
      extra_headers['Content-Type'] = media_source.content_type
      server_response = self.request(verb, uri, 
          data=media_source.file_handle, headers=extra_headers,
          url_params=url_params)
      result_body = server_response.read()

    else:
      
      http_data = data
      if 'Content-Type' not in extra_headers:
        content_type = 'application/atom+xml'
        extra_headers['Content-Type'] = content_type
      server_response = self.request(verb, uri, data=http_data,
          headers=extra_headers, url_params=url_params)
      result_body = server_response.read()
      
    # Chuah TC  26-9-2015
    # print "--- RESULT BODY ---"
    # print result_body
    # print "--- RESULT BODY ---"
    #

    # Server returns 201 for most post requests, but when performing a batch
    # request the server responds with a 200 on success.
    if server_response.status == 201 or server_response.status == 200:
      return result_body
      # Original (commented out) code below.
      #
      # if converter:
      #  return converter(result_body)
      # feed = gdata.GDataFeedFromString(result_body)
      # if not feed:
      #   entry = gdata.GDataEntryFromString(result_body)
      #   if not entry:
      #     return result_body
      #   return entry
      # return feed
      #

    elif server_response.status == 302:
      if redirects_remaining > 0:
        location = (server_response.getheader('Location')
                    or server_response.getheader('location'))
        if location is not None:
          m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location)
          if m is not None:
            self.__gsessionid = m.group(1) 
          return GDataService.PostOrPutRaven(self, verb, data, location, 
              extra_headers, url_params, escape_params, 
              redirects_remaining - 1, media_source, converter=converter)
        else:
          raise RequestError, {'status': server_response.status,
              'reason': '302 received without Location header',
              'body': result_body}
      else:
        raise RequestError, {'status': server_response.status,
            'reason': 'Redirect received, but redirects_remaining <= 0',
            'body': result_body}
    else:
      raise RequestError, {'status': server_response.status,
          'reason': server_response.reason, 'body': result_body}

  # end PostOrPutRaven()


  def PostRaven(self, data, uri, extra_headers=None, url_params=None,
           escape_params=True, redirects_remaining=4, media_source=None,
           converter=None):
    # Function is identical to gdata.service.Post() except that it calls
    # GDataService.PostOrPutRaven() instead of GDataService.PostOrPut().

    return GDataService.PostOrPutRaven(self, 'POST', data, uri, 
        extra_headers=extra_headers, url_params=url_params, 
        escape_params=escape_params, redirects_remaining=redirects_remaining,
        media_source=media_source, converter=converter)
  # end PostRaven()


  def PutRaven(self, data, uri, extra_headers=None, url_params=None, 
          escape_params=True, redirects_remaining=3, media_source=None,
          converter=None):
    # Function is identical to gdata.service.Post() except that it calls
    # GDataService.PostOrPutRaven() instead of GDataService.PostOrPut().

    return GDataService.PostOrPutRaven(self, 'PUT', data, uri, 
        extra_headers=extra_headers, url_params=url_params, 
        escape_params=escape_params, redirects_remaining=redirects_remaining,
        media_source=media_source, converter=converter)
  # end PutRaven()

==== END ====


File 2: zoundry/blogpub/blogger/gdataExtend/photos/service.py


Replace all references of "gdata.photos" with "zoundry.blogpub.blogger.gdataExtend.photos", and likewise, replace all references of "gdata.service" with "zoundry.blogpub.blogger.gdataExtend.gdata_service".

Reason for the above find-and-replace: we want to use our customized gdata now and NOT gdata from the standard Python library.

I also added two new functions (InsertPhotoRaven and UpdatePhotoBlogRaven) into the class PhotosService. The source code for both functions are listed below.


==== BEGIN ====

  def InsertPhotoRaven(self, album_or_uri, photo, filename_or_handle, content_type='image/jpeg'):
    # Function is identical to gdata.photos.service.PhotosService.InsertPhoto except that it calls
    # self.PostRaven() instead of self.Post(). Reason: Raven+ needs the raw XML meta-data from
    # Picasa Web unconverted.
    #
    # Chuah TC  15-10-2015

    try:
      # assert(isinstance(photo, gdata.photos.PhotoEntry))
      assert(isinstance(photo, zoundry.blogpub.blogger.gdataExtend.photos.PhotoEntry))
    except AssertionError:
      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
        # 'body':'`photo` must be a gdata.photos.PhotoEntry instance',
        'body':'`photo` must be a zoundry.blogpub.blogger.gdataExtend.photos.PhotoEntry instance',
        'reason':'Found %s, not PhotoEntry' % type(photo)
        })
    try:
      majtype, mintype = content_type.split('/')
      assert(mintype in SUPPORTED_UPLOAD_TYPES)
    except (ValueError, AssertionError):
      raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
        'body':'This is not a valid content type: %s' % content_type,
        'reason':'Accepted content types: %s' % \
          ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
        })
    if isinstance(filename_or_handle, (str, unicode)) and \
      os.path.exists(filename_or_handle): # it's a file name
      mediasource = gdata.MediaSource()
      mediasource.setFile(filename_or_handle, content_type)
    elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
      if hasattr(filename_or_handle, 'seek'):
        filename_or_handle.seek(0) # rewind pointer to the start of the file
      # gdata.MediaSource needs the content length, so read the whole image 
      file_handle = StringIO.StringIO(filename_or_handle.read()) 
      name = 'image'
      if hasattr(filename_or_handle, 'name'):
        name = filename_or_handle.name
      mediasource = gdata.MediaSource(file_handle, content_type,
        content_length=file_handle.len, file_name=name)
    else: #filename_or_handle is not valid
      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
        'body':'`filename_or_handle` must be a path name or a file-like object',
        'reason':'Found %s, not path name or object with a .read() method' % \
          filename_or_handle
        })
    
    if isinstance(album_or_uri, (str, unicode)): # it's a uri
      feed_uri = album_or_uri
    elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object
      feed_uri = album_or_uri.GetFeedLink().href
  
    try:
      # return self.PostRaven(photo, uri=feed_uri, media_source=mediasource, converter=gdata.photos.PhotoEntryFromString)
      return self.PostRaven(photo, uri=feed_uri, media_source=mediasource)
    # except gdata.service.RequestError, e:
    except zoundry.blogpub.blogger.gdataExtend.gdata_service.RequestError, e:
      raise GooglePhotosException(e.args[0])
  
  # end InsertPhotoRaven()

  def UpdatePhotoBlobRaven(self, photo_or_uri, filename_or_handle,
                      content_type = 'image/jpeg'):
    # Function is identical to gdata.photos.service.PhotosService.UpdatePhotoBlob except that it calls
    # self.PutRaven() instead of self.Put(). Reason: Raven+ needs the raw XML meta-data from
    # Picasa Web unconverted.
    #
    # Chuah TC  19-10-2015


    try:  
      majtype, mintype = content_type.split('/')
      assert(mintype in SUPPORTED_UPLOAD_TYPES)
    except (ValueError, AssertionError):
      raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
        'body':'This is not a valid content type: %s' % content_type,
        'reason':'Accepted content types: %s' % \
          ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
        })
    
    if isinstance(filename_or_handle, (str, unicode)) and \
      os.path.exists(filename_or_handle): # it's a file name
      photoblob = gdata.MediaSource()
      photoblob.setFile(filename_or_handle, content_type)
    elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
      if hasattr(filename_or_handle, 'seek'):
        filename_or_handle.seek(0) # rewind pointer to the start of the file
      # gdata.MediaSource needs the content length, so read the whole image 
      file_handle = StringIO.StringIO(filename_or_handle.read()) 
      name = 'image'
      if hasattr(filename_or_handle, 'name'):
        name = filename_or_handle.name
      mediasource = gdata.MediaSource(file_handle, content_type,
        content_length=file_handle.len, file_name=name)
    else: #filename_or_handle is not valid
      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
        'body':'`filename_or_handle` must be a path name or a file-like object',
        'reason':'Found %s, not path name or an object with .read() method' % \
          type(filename_or_handle)
        })
    
    if isinstance(photo_or_uri, (str, unicode)):
      entry_uri = photo_or_uri # it's a uri
    elif hasattr(photo_or_uri, 'GetEditMediaLink'):
      entry_uri = photo_or_uri.GetEditMediaLink().href
    try:
      # return self.Put(photoblob, entry_uri, converter=gdata.photos.PhotoEntryFromString)
      return self.PutRaven(photoblob, entry_uri, converter=zoundry.blogpub.blogger.gdataExtend.photos.PhotoEntryFromString)
    # except gdata.service.RequestError, e:
    except zoundry.blogpub.blogger.gdataExtend.gdata_service.RequestError, e:
      raise GooglePhotosException(e.args[0])

  # end UpdatePhotoBlobRaven()


==== END ====

File 3: zoundry/blogpub/blogger/gdataExtend/photos/__init__.py


Replace all instance of "gdata.photos" with "zoundry.blogpub.blogger.gdataExtend.photos".

As in File 2: above, we want to use our customized gdata and NOT gdata from the standard Python library.

Also, add the following line somewhere at the top of the file (mine is at line 55), otherwise 'getattr' will fail:

import zoundry.blogpub.blogger.gdataExtend.photos


0 comments:

Post a Comment