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.
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