Module pisicli
[hide private]
[frames] | no frames]

Source Code for Module pisicli

  1  """ 
  2  Command Line Interface to PISI - one implementation of user interaction 
  3   
  4  This file is part of Pisi. 
  5   
  6  This module provides the CLI interface to PISI: 
  7      - controlling the entire CLI application 
  8      - Checking of Arguments 
  9       
 10  A callback is defined as well, which handles all the output coming from the application core. 
 11   
 12  Pisi is free software: you can redistribute it and/or modify 
 13  it under the terms of the GNU General Public License as published by 
 14  the Free Software Foundation, either version 3 of the License, or 
 15  (at your option) any later version. 
 16   
 17  Pisi is distributed in the hope that it will be useful, 
 18  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 19  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 20  GNU General Public License for more details. 
 21   
 22  You should have received a copy of the GNU General Public License 
 23  along with Pisi.  If not, see <http://www.gnu.org/licenses/>. 
 24  """ 
 25  from pisiconstants import * 
 26  import pisiprogress 
 27  import pisi 
 28  import sys 
 29  import os 
 30  import ConfigParser 
 31  from thirdparty.epydocutil import TerminalController  
 32  import warnings 
 33   
34 -class CLICallback(pisiprogress.AbstractCallback):
35 """ 36 Command Line Interface to PISI 37 """ 38
39 - def __init__(self, verbose):
40 """ 41 Constructor 42 43 Remember whether we are in verbose mode. 44 """ 45 pisiprogress.AbstractCallback.__init__(self) 46 self.isVerbose = verbose 47 if not verbose: 48 self.term = TerminalController() 49 warnings.filterwarnings("ignore")
50
51 - def message(self, st):
52 """ 53 Output the string to console 54 """ 55 if not self.isVerbose: 56 sys.stdout.write(self.term.CLEAR_LINE) 57 print st
58
59 - def error(self, st):
60 """ 61 Redirect to L{message} 62 """ 63 self.message(st)
64
65 - def verbose(self, st):
66 """ 67 Only redirect to L{message} if we are in verbose mode 68 """ 69 if self.isVerbose: 70 self.message(st)
71
72 - def promptGeneric(self, prompt, default):
73 """ 74 Prepare a prompt and return the user input 75 76 If user input is empty, the provided default is returned. 77 """ 78 if default: 79 prompt += "[" + default + "]" 80 st = raw_input(prompt + ": ") 81 if default: 82 if st == '': 83 return default 84 return st
85
86 - def promptGenericConfirmation(self, prompt):
87 """ 88 Ask user for a single confirmation (OK / Cancel) 89 """ 90 st = raw_input(prompt + " (Y/N): ") 91 return st.strip().upper() == 'Y'
92
93 - def update(self, status):
94 """ 95 If we are not in verbose mode, we display the progress here 96 """ 97 if not self.isVerbose: 98 percent = self.progress.calculateOverallProgress() / 100.0 99 message = status 100 background = "." * CONSOLE_PROGRESSBAR_WIDTH 101 dots = int(len(background) * percent) 102 sys.stdout.write(self.term.CLEAR_LINE + '%3d%% '%(100*percent) + self.term.GREEN + '[' + self.term.BOLD + '='*dots + background[dots:] + self.term.NORMAL + self.term.GREEN + '] ' + self.term.NORMAL + message + self.term.BOL) 103 sys.stdout.flush() 104 if status == 'Finished': 105 print
106 # print ("Progress: %d %% (%s)" %(self.progress.calculateOverallProgress(), status)) 107
108 - def _strFixedLen(self, str, width, spaces = " "):
109 """ 110 Supporting function to print a string with a fixed length (good for tables) 111 112 Cuts the string when too long and fills up with characters when too short. 113 """ 114 return (str + (spaces * width))[:width]
115
116 - def _getDetailValue(self, dict, key, default = '<n.a.>'):
117 """ 118 Gets around the problem if an attribute in one data source is not set in the other one at all 119 120 Returns default value if the attribute is not available in the given dictionery (key error). 121 """ 122 try: 123 return dict[key] 124 except KeyError: 125 return default
126 127
128 - def _printConflictDetails(self, entry1, entry2, nameSource1, nameSource2):
129 """ 130 Supporting function to show differences for two contact entries 131 132 Prints a table with all attributes being different in the two sources for comparing two contact entries. 133 """ 134 diffList = pisi.determineConflictDetails(entry1, entry2) 135 136 print "\n " + "=" * 25 + " DETAILS " + "=" * 25 137 print "* %s, %s" %(entry1.attributes['lastname'],entry1.attributes['firstname']) 138 print self._strFixedLen('Attribute', 15) + " | " + self._strFixedLen(nameSource1, 20) + " | " + self._strFixedLen(nameSource2, 20) 139 print "-" * 60 140 for key in diffList.keys(): 141 print self._strFixedLen(key, 15) + " | " + self._strFixedLen(self._getDetailValue(entry1.attributes, key), 20) + " | " + self._strFixedLen(self._getDetailValue(entry2.attributes, key), 20) 142 print "-" * 60
143
144 - def askConfirmation(self, source, idList):
145 """ 146 Use interaction for choosing which contact entry to keep in case of a conflict 147 148 Iterates through the given list of contact IDs and requests the user for choosing an action for every single entry. 149 150 @return: A dictionary which contains the action for each conflict entry; the key is the id of the contact entry, 151 the value one out of a - keep entry from first source, b - keep value from second source and s - skip this entry (no change on either side) 152 """ 153 if len(idList) > 0: 154 print 155 print "Please resolve the following conflicts manually" 156 ret = {} 157 for entryID in idList: 158 entry1 = source[0].getContact(entryID) 159 entry2 = source[1].getContact(entryID) 160 myinput = '' 161 while myinput != 'a' and myinput != 'b' and myinput != 's': 162 if myinput != 'd': 163 print "* %s, %s" %(entry1.attributes['lastname'],entry1.attributes['firstname']) 164 else: 165 self._printConflictDetails(entry1, entry2, source[0].getName(), source[1].getName() ) 166 print " [a]-Keep <%s> [b]-Keep <%s> [d]etails [s]kip [a|b|d|s]: " %(source[0].getName(), source[1].getName()), 167 myinput = raw_input() 168 ret[entryID] = myinput 169 return ret
170
171 -def testConfiguration():
172 """ 173 Checks, whether configuration can be loaded from PISI core. 174 175 If not possible, an error message is printed and False will be returned. 176 @return: False, if an Error occurs when loading the configration from core; otherwise True 177 """ 178 try: 179 pisi.getConfiguration() 180 return True 181 except ValueError: 182 print ("PISI configuration not found") 183 print ("For running PISI you must have a configuration file located in '/home/root/.pisi/conf'.\n\nWith the package a well-documented sample was placed at '/usr/share/doc/pisi/conf.example'. You may rename this for a starting point - then edit this file in order to configure your PIM synchronization data sources.") 184 return False
185 186
187 -def startCLI():
188 """ 189 Controls the major flow for PISI (CLI) 190 191 Calls one after another the supporting functions in this module. 192 """ 193 if not testConfiguration(): 194 sys.exit(0) 195 196 verbose, modulesToLoad, modulesNamesCombined, soft, mergeMode = parseArguments() 197 cb = CLICallback(verbose) 198 pisiprogress.registerCallback(cb) 199 200 cb.progress.push(0, 8) 201 cb.update('Starting Configuration') 202 cb.verbose('') 203 cb.verbose("*" * 55) 204 cb.verbose( "*" * 22 + " PISI " + "*" * 22) 205 cb.verbose( "*" * 55) 206 cb.verbose( "** PISI is synchronizing information " + "*" * 18) 207 cb.verbose( "** http://freshmeat.net/projects/pisiom " + "*" * 8) 208 cb.verbose( "*" * 55) 209 210 cb.verbose( ("\n" + "*" * 15 + " PHASE 0 - Configuration " + "*" * 15)) 211 cb.verbose( "Verbose mode on") 212 cb.verbose( ("In case of conflicts I use the following strategy: %s" %(MERGEMODE_STRINGS[mergeMode]))) 213 214 config, configfolder = pisi.readConfiguration() 215 source = pisi.importModules(configfolder, config, modulesToLoad, modulesNamesCombined, soft) 216 mode = pisi.determineMode(config, modulesToLoad) 217 cb.progress.drop() 218 219 cb.progress.push(8, 10) 220 cb.update('Pre-Processing sources') 221 cb.verbose("\tSource 1") 222 source[0].preProcess() 223 cb.verbose("\tSource 2") 224 source[1].preProcess() 225 cb.verbose(" Pre-Processing Done") 226 cb.progress.drop() 227 228 cb.progress.push(10, 40) 229 cb.update('Loading from sources') 230 cb.verbose("\n" + "*" * 18 + " PHASE 1 - Loading " + "*" * 18) 231 cb.progress.push(0, 50) 232 source[0].load() 233 cb.progress.drop() 234 cb.progress.push(50, 100) 235 cb.update('Loading') 236 source[1].load() 237 cb.progress.drop() 238 cb.progress.drop() 239 240 cb.progress.push(40, 70) 241 cb.update('Comparing sources') 242 cb.verbose("\n" + "*" * 17 + " PHASE 2 - Comparing " + "*" * 17) 243 if mode == MODE_CALENDAR: # events mode 244 pisi.eventsSync.syncEvents(verbose, modulesToLoad, source) 245 elif mode == MODE_CONTACTS: # contacts mode 246 pisi.contactsSync.syncContacts(verbose, modulesToLoad, source, mergeMode) 247 cb.progress.drop() 248 249 cb.progress.push(70, 95) 250 cb.update('Making changes permanent') 251 cb.verbose ("\n" + "*" * 18 + " PHASE 3 - Saving " + "*" * 18) 252 if soft: 253 print "You chose soft mode for PISI - changes are not applied to data sources." 254 else: 255 pisi.applyChanges(source) 256 cb.verbose( "*" * 24 + " DONE " + "*" * 24) 257 cb.progress.drop() 258 259 cb.progress.push(95, 100) 260 cb.update('Post-Processing sources') 261 cb.verbose("\tSource 1") 262 source[0].postProcess() 263 cb.verbose("\tSource 2") 264 source[1].postProcess() 265 cb.verbose(" Post-Processing Done") 266 cb.progress.drop() 267 268 cb.update('Finished')
269 270
271 -def parseArguments ():
272 """ 273 Parses command line arguments 274 275 All information from the command line arguments are returned by this function. 276 If the number of arguments given is not valid, a help text is printed on the console by calling function L{usage}. 277 """ 278 mergeMode = MERGEMODE_SKIP 279 modulesToLoad = [] 280 modulesNamesCombined = "" 281 soft = False 282 verbose = False 283 for arg in sys.argv[1:]: 284 if arg[:1]!='-': 285 modulesToLoad.append( arg ) 286 modulesNamesCombined += arg 287 elif arg=='-v' or arg=='--verbose': 288 verbose=True 289 elif arg=='-s' or arg=='--soft': 290 soft = True 291 elif arg=='-l' or arg=='--list-configurations': 292 list_configurations() 293 elif arg.startswith('-m'): 294 mergeMode = int(arg[2:]) 295 if len(modulesToLoad)!=2: 296 usage() 297 return verbose, modulesToLoad, modulesNamesCombined, soft, mergeMode
298
299 -def usage ():
300 """ 301 Prints a help text to the console 302 303 The application is shut down afterwards. 304 """ 305 usage = """You start the program by specifying 2 sources to synchronize. 306 Like this: 307 ./pisi [options] $SOURCE1 $SOURCE2 308 Flags: 309 -v --verbose 310 Make program verbose 311 -s --soft 312 Don't actually make any changes on the servers/in the files 313 -l --list-configurations 314 List which configurations there are in the config-file (this don't need 315 the two modules) 316 -mX 317 Define the mode to deal with conflicts: 318 -m0 SKIP entry (default) 319 -m1 FLUSH source 1 in the beginning 320 -m2 FLUSH source 2 in the beginning 321 -m3 Overwrite source 1 entry wise 322 -m4 Overwrite source 2 entry wise 323 -m5 Manually confirm each entry 324 325 Configuration: 326 Read http://wiki.github.com/kichkasch/pisi/an-end-user for help. 327 """ 328 sys.exit(usage)
329
330 -def list_configurations():
331 """ 332 Prints available configurations for data sources 333 334 Parses the configuration file for PISI and prints all available data sources to the console. 335 """ 336 config = pisi.getConfiguration() 337 print 'You have these configurations:' 338 print ' [1] Contacts sources' 339 for con in config.sections(): 340 if config.get(con,'module').startswith('contacts'): 341 print ('\t-%s which uses module <%s>: %s' %(con, config.get(con,'module'), config.get(con,'description'))) 342 print ' [2] Calendar sources' 343 for con in config.sections(): 344 if config.get(con,'module').startswith('calendar'): 345 print ('\t-%s which uses module <%s>: %s' %(con, config.get(con,'module'), config.get(con,'description'))) 346 sys.exit(0)
347