Package modules :: Module contacts_google
[hide private]
[frames] | no frames]

Source Code for Module modules.contacts_google

  1  """ 
  2  Syncronize with Google Contacts 
  3   
  4  This file is part of Pisi. 
  5   
  6  Google provides a really straight forward API for accessing their contact information (gdata).  
  7  U{http://code.google.com/p/gdata-python-client/}. It is used for this implementation - the site package  
  8  has to be installed. 
  9   
 10  Pisi is free software: you can redistribute it and/or modify 
 11  it under the terms of the GNU General Public License as published by 
 12  the Free Software Foundation, either version 3 of the License, or 
 13  (at your option) any later version. 
 14   
 15  Pisi is distributed in the hope that it will be useful, 
 16  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 18  GNU General Public License for more details. 
 19   
 20  You should have received a copy of the GNU General Public License 
 21  along with Pisi.  If not, see <http://www.gnu.org/licenses/>. 
 22  """ 
 23   
 24  import sys,os,datetime 
 25  # Allows us to import event 
 26  sys.path.insert(0,os.path.abspath(__file__+"/../..")) 
 27  from contacts import contacts 
 28  from pisiconstants import * 
 29  import pisiprogress 
 30  import pisitools 
 31   
 32  import atom 
 33  import gdata.contacts 
 34  import gdata.contacts.service 
 35   
36 -class SynchronizationModule(contacts.AbstractContactSynchronizationModule):
37 """ 38 The implementation of the interface L{contacts.AbstractContactSynchronizationModule} for the Google Contacts backend 39 """ 40
41 - def __init__( self, modulesString, config, configsection, folder, verbose=False, soft=False):
42 """ 43 Constructor 44 45 Super class constructor (L{contacts.AbstractContactSynchronizationModule.__init__}) is called. 46 Local variables are initialized. 47 The settings from the configuration file are loaded. 48 The connection to the Google Gdata backend is established. 49 50 @param modulesString: is a small string to help you make a unique id. It is the two modules configuration-names concatinated. 51 @param config: is the configuration from ~/.pisi/conf. Use like config.get(configsection,'user') 52 @param configsection: is the configuration from ~/.pisi/conf. Use like config.get(configsection,'user') 53 @param folder: is a string with the location for where your module can save its own files if necessary 54 @param verbose: should make your module "talk" more 55 @param soft: should tell you if you should make changes (ie. save) 56 """ 57 contacts.AbstractContactSynchronizationModule.__init__(self, verbose, soft, modulesString, config, configsection, "Google contacts") 58 self.verbose = verbose 59 self._user = config.get(configsection,'user') 60 self._password = config.get(configsection,'password') 61 pisiprogress.getCallback().verbose("Google contacts module loaded") 62 63 self._idMappingInternalGlobal = {} 64 self._idMappingGlobalInternal = {} 65 66 self._gd_client = gdata.contacts.service.ContactsService() 67 self._gd_client.email = self._user 68 self._gd_client.password = self._password 69 self._gd_client.source = GOOGLE_CONTACTS_APPNAME 70 self._gd_client.ProgrammaticLogin()
71
72 - def _unpackGoogleTitle(self, atts, gtitle):
73 """ 74 Takes the components of a Google Title apart (all names) 75 76 The whole thing is about guessing - check code for details. 77 """ 78 pisiprogress.getCallback().verbose("Google Contacts: Loading") 79 title, first, last, middle = pisitools.parseFullName(gtitle) 80 atts['title'] = title 81 atts['firstname'] = first 82 atts['lastname'] = last 83 atts['middlename'] = middle
84
85 - def _unpackPostalCity(self, line):
86 """ 87 Takes one address line apart and identifies postal code and locality 88 89 This is really tricky - you never know how many parts belong to which side - and it's all in one line. 90 We do it this way: 91 - only one item indicates city only 92 - two items one each 93 - more than two - depending on the length of the first item, another item is applied to the postal code; all the remaining stuff is city 94 """ 95 items = line.strip().split(' ') 96 if len(items) == 1: # only city - no postal code given 97 city = items[0] 98 postalCode = '' 99 elif len(items) == 2: 100 postalCode = items[0] 101 city = items[1] 102 else: 103 if len(items[0]) > 4: 104 postalCode = items[0] 105 else: 106 postalCode = items[0] + " " + items[1] 107 city = line[len(postalCode):].strip() # all stuff behind postal code (cities might have several components) 108 # print ('%s \t> %s : %s' %(line, postalCode, city)) 109 return city, postalCode
110
111 - def _unpackGooglePostalAddress(self, atts, addressText, type):
112 """ 113 Takes the components of a Google address apart 114 115 The whole thing is about guessing - check code for details. 116 """ 117 text = addressText.strip() 118 lines = text.split("\n") 119 try: 120 street = lines[0].strip() 121 if type == 'home': 122 atts['homeStreet'] = street 123 elif type == 'work': 124 atts['businessStreet'] = street 125 del lines[0] 126 city, postalCode = self._unpackPostalCity(lines[0]) 127 if type == 'home': 128 atts['homeCity'] = city 129 if postalCode: 130 atts['homePostalCode'] = postalCode 131 elif type == 'work': 132 atts['businessCity'] = city 133 if postalCode: 134 atts['businessPostalCode'] = postalCode 135 del lines[0] 136 if len(lines)>1: # we assume, if there is another two lines, one will be the state; the other one the country - one line left indicates that we only have country information 137 state = lines[0].strip() 138 if type == 'home': 139 atts['homeState'] = state 140 elif type == 'work': 141 atts['businessState'] = state 142 del lines[0] 143 country = lines[0].strip() 144 if type == 'home': 145 atts['homeCountry'] = country 146 elif type == 'work': 147 atts['businessCountry'] = country 148 149 except IndexError: 150 pass # that's fine - we cannot have everything
151
152 - def load(self):
153 """ 154 Load all data from backend 155 156 A single query is performed and the result set is parsed afterwards. 157 """ 158 query = gdata.contacts.service.ContactsQuery() 159 query.max_results = GOOGLE_CONTACTS_MAXRESULTS 160 feed = self._gd_client.GetContactsFeed(query.ToUri()) 161 for i, entry in enumerate(feed.entry): 162 atts = {} 163 if not entry.title or not entry.title.text: 164 pisiprogress.getCallback().verbose('** In Googlecontacts account is an entry with no title - I cannot process this account and will skip it.') 165 continue # an entry without a title cannot be processed by PISI as the name is the 'primary key' 166 self._unpackGoogleTitle(atts, entry.title.text.decode("utf-8")) 167 168 if len(entry.email) > 0: 169 email = entry.email[0] 170 # for email in entry.email: 171 if email.primary and email.primary == 'true': # for now we only support one email address 172 if email.address: 173 atts['email'] = email.address.decode("utf-8") 174 175 for phone in entry.phone_number: 176 if phone.rel == gdata.contacts.PHONE_HOME: 177 atts['phone'] = phone.text 178 if phone.rel == gdata.contacts.PHONE_MOBILE: 179 atts['mobile'] = phone.text 180 if phone.rel == gdata.contacts.PHONE_WORK: 181 atts['officePhone'] = phone.text 182 if phone.rel == gdata.contacts.PHONE_WORK_FAX: 183 atts['fax'] = phone.text 184 185 for address in entry.postal_address: 186 if address.text: 187 value = address.text.decode("utf-8") 188 type = None 189 if address.rel == gdata.contacts.REL_HOME: 190 type = 'home' 191 elif address.rel == gdata.contacts.REL_WORK: 192 type = 'work' 193 if type: 194 self._unpackGooglePostalAddress(atts, value, type) 195 196 if entry.organization: 197 if entry.organization.org_name and entry.organization.org_name.text: 198 atts['businessOrganisation'] = entry.organization.org_name.text.decode("utf-8") 199 if entry.organization.org_title and entry.organization.org_title.text: 200 atts['businessDepartment'] = entry.organization.org_title.text.decode("utf-8") # well - that doesn't map fully - but better than nothing :) 201 202 id = contacts.assembleID(atts) 203 c = contacts.Contact(id, atts) 204 self._allContacts[id] = c 205 206 self._idMappingGlobalInternal[id] = entry.GetEditLink().href 207 self._idMappingInternalGlobal[entry.GetEditLink().href] = id
208
209 - def _assembleTitle(self, contactEntry):
210 """ 211 Assembles all information from a contact entry for the packed version of a title in google contacts 212 """ 213 return pisitools.assembleFullName(contactEntry)
214
215 - def _savePhones(self, contact, new_contact):
216 """ 217 Integrates all saving tranformations for phone attributes 218 """ 219 if contact.attributes.has_key('phone'): 220 phone = contact.attributes['phone'] 221 if phone and phone != '': 222 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=phone, rel=gdata.contacts.PHONE_HOME)) 223 if contact.attributes.has_key('mobile'): 224 mobile = contact.attributes['mobile'] 225 if mobile and mobile != '': 226 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=mobile, rel=gdata.contacts.PHONE_MOBILE)) 227 if contact.attributes.has_key('officePhone'): 228 phone = contact.attributes['officePhone'] 229 if phone and phone != '': 230 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=phone, rel=gdata.contacts.PHONE_WORK)) 231 if contact.attributes.has_key('fax'): 232 fax = contact.attributes['fax'] 233 if fax and fax != '': 234 new_contact.phone_number.append(gdata.contacts.PhoneNumber(text=fax, rel=gdata.contacts.PHONE_WORK_FAX))
235
236 - def _saveAddresses(self, contact, new_contact):
237 """ 238 Integrates all saving tranformations for address attributes 239 """ 240 # we assume, if somebody supplies information about an address, first the city (locality) name would be provided 241 if contact.attributes.has_key('homeCity'): 242 homeCity = contact.attributes['homeCity'] 243 if homeCity and homeCity != None: 244 text = '' 245 if contact.attributes.has_key('homeStreet'): 246 homeStreet = contact.attributes['homeStreet'] 247 if homeStreet and homeStreet != '': 248 text += homeStreet + "\n" 249 if contact.attributes.has_key('homePostalCode'): 250 homePostalCode = contact.attributes['homePostalCode'] 251 if homePostalCode and homePostalCode != '': 252 text += homePostalCode + " " 253 text += homeCity +"\n" 254 if contact.attributes.has_key('homeState'): 255 homeState = contact.attributes['homeState'] 256 if homeState and homeState != '': 257 text += homeState + "\n" 258 if contact.attributes.has_key('homeCountry'): 259 homeCountry = contact.attributes['homeCountry'] 260 if homeCountry and homeCountry != '': 261 text += homeCountry + "\n" 262 new_contact.postal_address.append(gdata.contacts.PostalAddress(text=text, rel=gdata.contacts.REL_HOME)) 263 264 if contact.attributes.has_key('businessCity'): 265 businessCity = contact.attributes['businessCity'] 266 if businessCity and businessCity != None: 267 text = '' 268 if contact.attributes.has_key('businessStreet'): 269 businessStreet = contact.attributes['businessStreet'] 270 if businessStreet and businessStreet != '': 271 text += businessStreet + "\n" 272 if contact.attributes.has_key('businessPostalCode'): 273 businessPostalCode = contact.attributes['businessPostalCode'] 274 if businessPostalCode and businessPostalCode != '': 275 text += businessPostalCode + " " 276 text += businessCity +"\n" 277 if contact.attributes.has_key('businessState'): 278 businessState = contact.attributes['businessState'] 279 if businessState and businessState != '': 280 text += businessState + "\n" 281 if contact.attributes.has_key('businessCountry'): 282 businessCountry = contact.attributes['businessCountry'] 283 if businessCountry and businessCountry != '': 284 text += businessCountry + "\n" 285 new_contact.postal_address.append(gdata.contacts.PostalAddress(text=text, rel=gdata.contacts.REL_WORK))
286
287 - def _saveBusinessDetails(self, c, new_contact):
288 """ 289 Creates an entry for business organzation und unit. 290 """ 291 if c.attributes.has_key('businessOrganisation'): 292 o = c.attributes['businessOrganisation'] 293 if o and o != '': 294 ou = None 295 if c.attributes.has_key('businessDepartment'): 296 ou = c.attributes['businessDepartment'] 297 298 new_contact.organization = gdata.contacts.Organization(org_name = gdata.contacts.OrgName(o), org_title = gdata.contacts.OrgTitle(ou))
299
300 - def _saveOperationAdd(self, id):
301 """ 302 Save all changes to Google contacts that have come by 303 """ 304 contact = self.getContact(id) 305 new_contact = gdata.contacts.ContactEntry(title=atom.Title(text=self._assembleTitle(contact))) 306 if contact.attributes.has_key('email'): 307 email = contact.attributes['email'] 308 if email and email != '': 309 new_contact.email.append(gdata.contacts.Email(address=email,primary='true', rel=gdata.contacts.REL_HOME)) 310 self._savePhones(contact, new_contact) 311 self._saveAddresses(contact, new_contact) 312 self._saveBusinessDetails(contact, new_contact) 313 314 contact_entry = self._gd_client.CreateContact(new_contact)
315
316 - def _saveOperationDelete(self, id):
317 """ 318 Finally deletes the contact entry identified by its ID in the google contact backend 319 """ 320 link = self._idMappingGlobalInternal[id] 321 self._gd_client.DeleteContact(link)
322
323 - def _saveOperationModify(self, id):
324 """ 325 Applies changes for an entry to the Google contacts backend 326 327 In order to keep things simple the old value is simply erased using the method L{_saveOperationDelete} and a new one is inserted (L{_saveOperationAdd}). 328 """ 329 self._saveOperationDelete(id) 330 self._saveOperationAdd(id)
331
332 - def saveModifications(self ):
333 """ 334 Save whatever changes have come by 335 336 The L{_history} variable is iterated. The corresponding function is called for each action. 337 """ 338 i=0 339 for listItem in self._history: 340 action = listItem[0] 341 id = listItem[1] 342 if action == ACTIONID_ADD: 343 pisiprogress.getCallback().verbose("\t\t<google contacts> adding %s" %(id)) 344 self._saveOperationAdd(id) 345 elif action == ACTIONID_DELETE: 346 pisiprogress.getCallback().verbose("\t\t<google contacts> deleting %s" %(id)) 347 self._saveOperationDelete(id) 348 elif action == ACTIONID_MODIFY: 349 self._saveOperationModify(id) 350 pisiprogress.getCallback().verbose("\t\t<google contacts> replacing %s" %(id)) 351 i+=1 352 pisiprogress.getCallback().progress.setProgress(i * 100 / len(self._history)) 353 pisiprogress.getCallback().update('Storing')
354