/gdata/contacts/service.py
Python | 427 lines | 377 code | 14 blank | 36 comment | 1 complexity | 34e41ced316941233a74fb17fc278eb1 MD5 | raw file
1#!/usr/bin/env python 2# 3# Copyright 2009 Google Inc. All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""ContactsService extends the GDataService for Google Contacts operations. 18 19 ContactsService: Provides methods to query feeds and manipulate items. 20 Extends GDataService. 21 22 DictionaryToParamList: Function which converts a dictionary into a list of 23 URL arguments (represented as strings). This is a 24 utility function used in CRUD operations. 25""" 26 27__author__ = 'dbrattli (Dag Brattli)' 28 29 30import gdata 31import gdata.calendar 32import gdata.service 33 34 35DEFAULT_BATCH_URL = ('http://www.google.com/m8/feeds/contacts/default/full' 36 '/batch') 37DEFAULT_PROFILES_BATCH_URL = ('http://www.google.com' 38 '/m8/feeds/profiles/default/full/batch') 39 40GDATA_VER_HEADER = 'GData-Version' 41 42 43class Error(Exception): 44 pass 45 46 47class RequestError(Error): 48 pass 49 50 51class ContactsService(gdata.service.GDataService): 52 """Client for the Google Contacts service.""" 53 54 def __init__(self, email=None, password=None, source=None, 55 server='www.google.com', additional_headers=None, 56 contact_list='default', **kwargs): 57 """Creates a client for the Contacts service. 58 59 Args: 60 email: string (optional) The user's email address, used for 61 authentication. 62 password: string (optional) The user's password. 63 source: string (optional) The name of the user's application. 64 server: string (optional) The name of the server to which a connection 65 will be opened. Default value: 'www.google.com'. 66 contact_list: string (optional) The name of the default contact list to 67 use when no URI is specified to the methods of the service. 68 Default value: 'default' (the logged in user's contact list). 69 **kwargs: The other parameters to pass to gdata.service.GDataService 70 constructor. 71 """ 72 73 self.contact_list = contact_list 74 gdata.service.GDataService.__init__( 75 self, email=email, password=password, service='cp', source=source, 76 server=server, additional_headers=additional_headers, **kwargs) 77 78 def GetFeedUri(self, kind='contacts', contact_list=None, projection='full', 79 scheme=None): 80 """Builds a feed URI. 81 82 Args: 83 kind: The type of feed to return, typically 'groups' or 'contacts'. 84 Default value: 'contacts'. 85 contact_list: The contact list to return a feed for. 86 Default value: self.contact_list. 87 projection: The projection to apply to the feed contents, for example 88 'full', 'base', 'base/12345', 'full/batch'. Default value: 'full'. 89 scheme: The URL scheme such as 'http' or 'https', None to return a 90 relative URI without hostname. 91 92 Returns: 93 A feed URI using the given kind, contact list, and projection. 94 Example: '/m8/feeds/contacts/default/full'. 95 """ 96 contact_list = contact_list or self.contact_list 97 if kind == 'profiles': 98 contact_list = 'domain/%s' % contact_list 99 prefix = scheme and '%s://%s' % (scheme, self.server) or '' 100 return '%s/m8/feeds/%s/%s/%s' % (prefix, kind, contact_list, projection) 101 102 def GetContactsFeed(self, uri=None): 103 uri = uri or self.GetFeedUri() 104 return self.Get(uri, converter=gdata.contacts.ContactsFeedFromString) 105 106 def GetContact(self, uri): 107 return self.Get(uri, converter=gdata.contacts.ContactEntryFromString) 108 109 def CreateContact(self, new_contact, insert_uri=None, url_params=None, 110 escape_params=True): 111 """Adds an new contact to Google Contacts. 112 113 Args: 114 new_contact: atom.Entry or subclass A new contact which is to be added to 115 Google Contacts. 116 insert_uri: the URL to post new contacts to the feed 117 url_params: dict (optional) Additional URL parameters to be included 118 in the insertion request. 119 escape_params: boolean (optional) If true, the url_parameters will be 120 escaped before they are included in the request. 121 122 Returns: 123 On successful insert, an entry containing the contact created 124 On failure, a RequestError is raised of the form: 125 {'status': HTTP status code from server, 126 'reason': HTTP reason from the server, 127 'body': HTTP body of the server's response} 128 """ 129 insert_uri = insert_uri or self.GetFeedUri() 130 return self.Post(new_contact, insert_uri, url_params=url_params, 131 escape_params=escape_params, 132 converter=gdata.contacts.ContactEntryFromString) 133 134 def UpdateContact(self, edit_uri, updated_contact, url_params=None, 135 escape_params=True): 136 """Updates an existing contact. 137 138 Args: 139 edit_uri: string The edit link URI for the element being updated 140 updated_contact: string, atom.Entry or subclass containing 141 the Atom Entry which will replace the contact which is 142 stored at the edit_url 143 url_params: dict (optional) Additional URL parameters to be included 144 in the update request. 145 escape_params: boolean (optional) If true, the url_parameters will be 146 escaped before they are included in the request. 147 148 Returns: 149 On successful update, a httplib.HTTPResponse containing the server's 150 response to the PUT request. 151 On failure, a RequestError is raised of the form: 152 {'status': HTTP status code from server, 153 'reason': HTTP reason from the server, 154 'body': HTTP body of the server's response} 155 """ 156 return self.Put(updated_contact, self._CleanUri(edit_uri), 157 url_params=url_params, 158 escape_params=escape_params, 159 converter=gdata.contacts.ContactEntryFromString) 160 161 def DeleteContact(self, edit_uri, extra_headers=None, 162 url_params=None, escape_params=True): 163 """Removes an contact with the specified ID from Google Contacts. 164 165 Args: 166 edit_uri: string The edit URL of the entry to be deleted. Example: 167 '/m8/feeds/contacts/default/full/xxx/yyy' 168 url_params: dict (optional) Additional URL parameters to be included 169 in the deletion request. 170 escape_params: boolean (optional) If true, the url_parameters will be 171 escaped before they are included in the request. 172 173 Returns: 174 On successful delete, a httplib.HTTPResponse containing the server's 175 response to the DELETE request. 176 On failure, a RequestError is raised of the form: 177 {'status': HTTP status code from server, 178 'reason': HTTP reason from the server, 179 'body': HTTP body of the server's response} 180 """ 181 return self.Delete(self._CleanUri(edit_uri), 182 url_params=url_params, escape_params=escape_params) 183 184 def GetGroupsFeed(self, uri=None): 185 uri = uri or self.GetFeedUri('groups') 186 return self.Get(uri, converter=gdata.contacts.GroupsFeedFromString) 187 188 def CreateGroup(self, new_group, insert_uri=None, url_params=None, 189 escape_params=True): 190 insert_uri = insert_uri or self.GetFeedUri('groups') 191 return self.Post(new_group, insert_uri, url_params=url_params, 192 escape_params=escape_params, 193 converter=gdata.contacts.GroupEntryFromString) 194 195 def UpdateGroup(self, edit_uri, updated_group, url_params=None, 196 escape_params=True): 197 return self.Put(updated_group, self._CleanUri(edit_uri), 198 url_params=url_params, 199 escape_params=escape_params, 200 converter=gdata.contacts.GroupEntryFromString) 201 202 def DeleteGroup(self, edit_uri, extra_headers=None, 203 url_params=None, escape_params=True): 204 return self.Delete(self._CleanUri(edit_uri), 205 url_params=url_params, escape_params=escape_params) 206 207 def ChangePhoto(self, media, contact_entry_or_url, content_type=None, 208 content_length=None): 209 """Change the photo for the contact by uploading a new photo. 210 211 Performs a PUT against the photo edit URL to send the binary data for the 212 photo. 213 214 Args: 215 media: filename, file-like-object, or a gdata.MediaSource object to send. 216 contact_entry_or_url: ContactEntry or str If it is a ContactEntry, this 217 method will search for an edit photo link URL and 218 perform a PUT to the URL. 219 content_type: str (optional) the mime type for the photo data. This is 220 necessary if media is a file or file name, but if media 221 is a MediaSource object then the media object can contain 222 the mime type. If media_type is set, it will override the 223 mime type in the media object. 224 content_length: int or str (optional) Specifying the content length is 225 only required if media is a file-like object. If media 226 is a filename, the length is determined using 227 os.path.getsize. If media is a MediaSource object, it is 228 assumed that it already contains the content length. 229 """ 230 if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): 231 url = contact_entry_or_url.GetPhotoEditLink().href 232 else: 233 url = contact_entry_or_url 234 if isinstance(media, gdata.MediaSource): 235 payload = media 236 # If the media object is a file-like object, then use it as the file 237 # handle in the in the MediaSource. 238 elif hasattr(media, 'read'): 239 payload = gdata.MediaSource(file_handle=media, 240 content_type=content_type, content_length=content_length) 241 # Assume that the media object is a file name. 242 else: 243 payload = gdata.MediaSource(content_type=content_type, 244 content_length=content_length, file_path=media) 245 return self.Put(payload, url) 246 247 def GetPhoto(self, contact_entry_or_url): 248 """Retrives the binary data for the contact's profile photo as a string. 249 250 Args: 251 contact_entry_or_url: a gdata.contacts.ContactEntry objecr or a string 252 containing the photo link's URL. If the contact entry does not 253 contain a photo link, the image will not be fetched and this method 254 will return None. 255 """ 256 # TODO: add the ability to write out the binary image data to a file, 257 # reading and writing a chunk at a time to avoid potentially using up 258 # large amounts of memory. 259 url = None 260 if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): 261 photo_link = contact_entry_or_url.GetPhotoLink() 262 if photo_link: 263 url = photo_link.href 264 else: 265 url = contact_entry_or_url 266 if url: 267 return self.Get(url, converter=str) 268 else: 269 return None 270 271 def DeletePhoto(self, contact_entry_or_url): 272 url = None 273 if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): 274 url = contact_entry_or_url.GetPhotoEditLink().href 275 else: 276 url = contact_entry_or_url 277 if url: 278 self.Delete(url) 279 280 def GetProfilesFeed(self, uri=None): 281 """Retrieves a feed containing all domain's profiles. 282 283 Args: 284 uri: string (optional) the URL to retrieve the profiles feed, 285 for example /m8/feeds/profiles/default/full 286 287 Returns: 288 On success, a ProfilesFeed containing the profiles. 289 On failure, raises a RequestError. 290 """ 291 292 uri = uri or self.GetFeedUri('profiles') 293 return self.Get(uri, 294 converter=gdata.contacts.ProfilesFeedFromString) 295 296 def GetProfile(self, uri): 297 """Retrieves a domain's profile for the user. 298 299 Args: 300 uri: string the URL to retrieve the profiles feed, 301 for example /m8/feeds/profiles/default/full/username 302 303 Returns: 304 On success, a ProfileEntry containing the profile for the user. 305 On failure, raises a RequestError 306 """ 307 return self.Get(uri, 308 converter=gdata.contacts.ProfileEntryFromString) 309 310 def UpdateProfile(self, edit_uri, updated_profile, url_params=None, 311 escape_params=True): 312 """Updates an existing profile. 313 314 Args: 315 edit_uri: string The edit link URI for the element being updated 316 updated_profile: string atom.Entry or subclass containing 317 the Atom Entry which will replace the profile which is 318 stored at the edit_url. 319 url_params: dict (optional) Additional URL parameters to be included 320 in the update request. 321 escape_params: boolean (optional) If true, the url_params will be 322 escaped before they are included in the request. 323 324 Returns: 325 On successful update, a httplib.HTTPResponse containing the server's 326 response to the PUT request. 327 On failure, raises a RequestError. 328 """ 329 return self.Put(updated_profile, self._CleanUri(edit_uri), 330 url_params=url_params, escape_params=escape_params, 331 converter=gdata.contacts.ProfileEntryFromString) 332 333 def ExecuteBatch(self, batch_feed, url, 334 converter=gdata.contacts.ContactsFeedFromString): 335 """Sends a batch request feed to the server. 336 337 Args: 338 batch_feed: gdata.contacts.ContactFeed A feed containing batch 339 request entries. Each entry contains the operation to be performed 340 on the data contained in the entry. For example an entry with an 341 operation type of insert will be used as if the individual entry 342 had been inserted. 343 url: str The batch URL to which these operations should be applied. 344 converter: Function (optional) The function used to convert the server's 345 response to an object. The default value is ContactsFeedFromString. 346 347 Returns: 348 The results of the batch request's execution on the server. If the 349 default converter is used, this is stored in a ContactsFeed. 350 """ 351 return self.Post(batch_feed, url, converter=converter) 352 353 def ExecuteBatchProfiles(self, batch_feed, url, 354 converter=gdata.contacts.ProfilesFeedFromString): 355 """Sends a batch request feed to the server. 356 357 Args: 358 batch_feed: gdata.profiles.ProfilesFeed A feed containing batch 359 request entries. Each entry contains the operation to be performed 360 on the data contained in the entry. For example an entry with an 361 operation type of insert will be used as if the individual entry 362 had been inserted. 363 url: string The batch URL to which these operations should be applied. 364 converter: Function (optional) The function used to convert the server's 365 response to an object. The default value is 366 gdata.profiles.ProfilesFeedFromString. 367 368 Returns: 369 The results of the batch request's execution on the server. If the 370 default converter is used, this is stored in a ProfilesFeed. 371 """ 372 return self.Post(batch_feed, url, converter=converter) 373 374 def _CleanUri(self, uri): 375 """Sanitizes a feed URI. 376 377 Args: 378 uri: The URI to sanitize, can be relative or absolute. 379 380 Returns: 381 The given URI without its http://server prefix, if any. 382 Keeps the leading slash of the URI. 383 """ 384 url_prefix = 'http://%s' % self.server 385 if uri.startswith(url_prefix): 386 uri = uri[len(url_prefix):] 387 return uri 388 389class ContactsQuery(gdata.service.Query): 390 391 def __init__(self, feed=None, text_query=None, params=None, 392 categories=None, group=None): 393 self.feed = feed or '/m8/feeds/contacts/default/full' 394 if group: 395 self._SetGroup(group) 396 gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, 397 params=params, categories=categories) 398 399 def _GetGroup(self): 400 if 'group' in self: 401 return self['group'] 402 else: 403 return None 404 405 def _SetGroup(self, group_id): 406 self['group'] = group_id 407 408 group = property(_GetGroup, _SetGroup, 409 doc='The group query parameter to find only contacts in this group') 410 411class GroupsQuery(gdata.service.Query): 412 413 def __init__(self, feed=None, text_query=None, params=None, 414 categories=None): 415 self.feed = feed or '/m8/feeds/groups/default/full' 416 gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, 417 params=params, categories=categories) 418 419 420class ProfilesQuery(gdata.service.Query): 421 """Constructs a query object for the profiles feed.""" 422 423 def __init__(self, feed=None, text_query=None, params=None, 424 categories=None): 425 self.feed = feed or '/m8/feeds/profiles/default/full' 426 gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, 427 params=params, categories=categories)