comparison frontends/src/jp/base.py @ 817:c39117d00f35

jp: refactoring: - imports from sat_frontends.jp instead of local imports - added __init__.py - commands now inherits from a base class: each base.CommandBase instance is a subcommand - new arguments are added in CommandBase.add_parser_options methods, starting point si CommandBase.run or CommandBase.connected if a profile connection is needed - commands are exported using a __commands__ variable at the top of the module - sub-subcommand are easily added by using an other CommandBase instance as parent instead of using a Jp instance. In this case, the parent subcommand must be the one exported, and have a subcommands iterable (see cmd_file or cmd_pipe for examples). - options which are often used (like --profile) are automatically added on demand (use_profile=True, use_progress=True) - commands are automatically loaded when there are in a module named cmd_XXX - restored --connect option - restored progress bar - restored getVersion bridge call on jp --version - fixed file and pipe commands - fixed forgotten translations - fixed non SàT compliant docstrings - better about/version dialog
author Goffi <goffi@goffi.org>
date Mon, 10 Feb 2014 13:44:09 +0100
parents 59c7bc51c323
children 300b4de701a6
comparison
equal deleted inserted replaced
816:4429bd7d5efb 817:c39117d00f35
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from __future__ import with_statement
21 from sat.core.i18n import _ 20 from sat.core.i18n import _
22
23 #consts
24 name = u"jp"
25 about = name+u""" v%s (c) Jérôme Poisson (aka Goffi) 2009, 2010, 2011, 2012, 2013, 2014
26
27 ---
28 """+name+u""" Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (aka Goffi)
29 This program comes with ABSOLUTELY NO WARRANTY;
30 This is free software, and you are welcome to redistribute it
31 under certain conditions.
32 ---
33
34 This software is a command line tool for jabber
35 Get the latest version at http://www.goffi.org
36 """
37 21
38 global pbar_available 22 global pbar_available
39 pbar_available = True #checked before using ProgressBar 23 pbar_available = True #checked before using ProgressBar
40 24
41 ### logging ### 25 ### logging ###
44 logging.basicConfig(level=logging.DEBUG, 28 logging.basicConfig(level=logging.DEBUG,
45 format='%(message)s') 29 format='%(message)s')
46 ### 30 ###
47 31
48 import sys 32 import sys
49 import os 33 import locale
50 from os.path import abspath, basename, dirname 34 import os.path
51 from argparse import ArgumentParser 35 import argparse
36 import gobject
37 from glob import iglob
38 from importlib import import_module
52 from sat.tools.jid import JID 39 from sat.tools.jid import JID
53 import gobject
54 from sat_frontends.bridge.DBus import DBusBridgeFrontend 40 from sat_frontends.bridge.DBus import DBusBridgeFrontend
55 from sat.core.exceptions import BridgeExceptionNoService, BridgeInitError 41 from sat.core import exceptions
56 from sat.tools.utils import clean_ustr 42 import sat_frontends.jp
57 import tarfile
58 import tempfile
59 import shutil
60 try: 43 try:
61 from progressbar import ProgressBar, Percentage, Bar, ETA, FileTransferSpeed 44 import progressbar
62 except ImportError, e: 45 except ImportError:
63 info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar')) 46 info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
64 info (_('Progress bar deactivated\n--\n')) 47 info (_('Progress bar deactivated\n--\n'))
65 pbar_available=False 48 progressbar=None
66 49
67 50 #consts
68 #version = unicode(self.bridge.getVersion()) 51 prog_name = u"jp"
69 version = "undefined" 52 description = """This software is a command line tool for XMPP.
70 parser = ArgumentParser() 53 Get the latest version at http://sat.goffi.org"""
71 parser.add_argument('--version', action='version', version=about % version) 54
72 subparser = parser.add_subparsers(dest='subparser_name') 55 copyleft = u"""Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (aka Goffi)
73 # File managment 56 This program comes with ABSOLUTELY NO WARRANTY;
74 57 This is free software, and you are welcome to redistribute it under certain conditions.
75 58 """
76 59
77 class JP(object): 60
61 def unicode_decoder(arg):
62 # Needed to have unicode strings from arguments
63 return arg.decode(locale.getpreferredencoding())
64
65
66 class Jp(object):
78 """ 67 """
79 This class can be use to establish a connection with the 68 This class can be use to establish a connection with the
80 bridge. Moreover, it should manage a main loop. 69 bridge. Moreover, it should manage a main loop.
81 70
82 To use it, you mainly have to redefine the method run to perform 71 To use it, you mainly have to redefine the method run to perform
83 specify what kind of operation you want to perform. 72 specify what kind of operation you want to perform.
84 73
85 """ 74 """
86 def __init__(self, start_mainloop = False): 75 def __init__(self):
87 try: 76 try:
88 self.bridge=DBusBridgeFrontend() 77 self.bridge = DBusBridgeFrontend()
89 except BridgeExceptionNoService: 78 except exceptions.BridgeExceptionNoService:
90 print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) 79 print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
91 sys.exit(1) 80 sys.exit(1)
92 except BridgeInitError: 81 except excpetions.BridgeInitError:
93 print(_(u"Can't init bridge")) 82 print(_(u"Can't init bridge"))
94 sys.exit(1) 83 sys.exit(1)
95 84
96 self._start_loop = start_mainloop 85 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
97 86 description=description)
98 def run(self): 87
99 raise NotImplementedError 88 self._make_parents()
100 89 self.add_parser_options()
101 def _run(self): 90 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
102 """Call run and lauch a loop if needed""" 91 self._auto_loop = False # when loop is used for internal reasons
103 print "You are connected!" 92 self.need_loop = False # to set by commands when loop is needed
104 self.run() 93 self._progress_id = None # TODO: manage several progress ids
105 if self._start_loop: 94 self.quit_on_progress_end = True # set to False if you manage yourself exiting, or if you want the user to stop by himself
106 print "Exiting loop..." 95
107 self.loop.quit() 96 @property
108 97 def version(self):
109 def _loop_start(self): 98 return self.bridge.getVersion()
99
100 @property
101 def progress_id(self):
102 return self._progress_id
103
104 @progress_id.setter
105 def progress_id(self, value):
106 self._progress_id = value
107
108 def _make_parents(self):
109 self.parents = {}
110
111 profile_parent = self.parents['profile'] = argparse.ArgumentParser(add_help=False)
112 profile_parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
113 profile_parent.add_argument("-c", "--connect", action="store_true", help=_("Connect the profile before doing anything else"))
114
115 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
116 if progressbar:
117 progress_parent.add_argument("-g", "--progress", action="store_true", help=_("Show progress bar"))
118
119 def add_parser_options(self):
120 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': prog_name, 'version': self.version, 'copyleft': copyleft}))
121
122 def import_commands(self):
123 """ Automaticaly import commands to jp
124 looks from modules names cmd_*.py in jp path and import them
125
126 """
127 path = os.path.dirname(sat_frontends.jp.__file__)
128 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, "cmd_*.py"))))
129 for module_name in modules:
130 module = import_module("sat_frontends.jp."+module_name)
131 try:
132 self.import_command_module(module)
133 except ImportError:
134 continue
135
136 def import_command_module(self, module):
137 """ Add commands from a module to jp
138 @param module: module containing commands
139
140 """
141 try:
142 for classname in module.__commands__:
143 cls = getattr(module, classname)
144 except AttributeError:
145 warning(_("Invalid module %s") % module)
146 raise ImportError
147 cls(self)
148
149
150 def run(self, args=None):
151 self.args = self.parser.parse_args(args)
152 self.args.func()
153 if self.need_loop or self._auto_loop:
154 self._start_loop()
155
156 def _start_loop(self):
110 self.loop = gobject.MainLoop() 157 self.loop = gobject.MainLoop()
111 try: 158 try:
112 self.loop.run() 159 self.loop.run()
113 except KeyboardInterrupt: 160 except KeyboardInterrupt:
114 info(_("User interruption: good bye")) 161 info(_("User interruption: good bye"))
115 162
116 def start_mainloop(self): 163 def stop_loop(self):
117 self._start_loop = True 164 try:
118 165 self.loop.quit()
119 def go(self): 166 except AttributeError:
120 self.run() 167 pass
121 if self._start_loop: 168
122 self._loop_start() 169 def quit(self, errcode=0):
123 170 self.stop_loop()
124 171 if errcode:
125 class JPWithProfile(JP): 172 sys.exit(errcode)
126 """Manage a bridge (inherit from :class:`JP`), but it also adds
127 profile managment, ie, connection to the profile.
128
129 Moreover, some useful methods are predefined such as
130 :py:meth:`check_jids`. The connection to XMPP is automatically
131 managed.
132 """
133
134 def __init__(self, profile_name, start_mainloop = False):
135 JP.__init__(self, start_mainloop)
136 self.profile_name = profile_name
137 173
138 def check_jids(self, jids): 174 def check_jids(self, jids):
139 """Check jids validity, transform roster name to corresponding jids 175 """Check jids validity, transform roster name to corresponding jids
140 176
141 :param profile: A profile name 177 @param profile: profile name
142 :param jids: A list of jids 178 @param jids: list of jids
143 :rtype: A list of jids 179 @return: List of jids
180
144 """ 181 """
145 names2jid = {} 182 names2jid = {}
146 nodes2jid = {} 183 nodes2jid = {}
147 184
148 for contact in self.bridge.getContacts(self.profile): 185 for contact in self.bridge.getContacts(self.profile):
149 _jid, attr, groups = contact 186 _jid, attr, groups = contact
150 if attr.has_key("name"): 187 if attr.has_key("name"):
151 names2jid[attr["name"].lower()] = _jid 188 names2jid[attr["name"].lower()] = _jid
152 nodes2jid[JID(_jid).node.lower()] = _jid 189 nodes2jid[JID(_jid).node.lower()] = _jid
153 190
154 def expandJid(jid): 191 def expand_jid(jid):
155 _jid = jid.lower() 192 _jid = jid.lower()
156 if _jid in names2jid: 193 if _jid in names2jid:
157 expanded = names2jid[_jid] 194 expanded = names2jid[_jid]
158 elif _jid in nodes2jid: 195 elif _jid in nodes2jid:
159 expanded = nodes2jid[_jid] 196 expanded = nodes2jid[_jid]
162 return unicode(expanded) 199 return unicode(expanded)
163 200
164 def check(jid): 201 def check(jid):
165 if not jid.is_valid: 202 if not jid.is_valid:
166 error (_("%s is not a valid JID !"), jid) 203 error (_("%s is not a valid JID !"), jid)
167 exit(1) 204 self.quit(1)
168 205
169 dest_jids=[] 206 dest_jids=[]
170 try: 207 try:
171 for i in range(len(jids)): 208 for i in range(len(jids)):
172 dest_jids.append(expandJid(jids[i])) 209 dest_jids.append(expand_jid(jids[i]))
173 check(dest_jids[i]) 210 check(dest_jids[i])
174 except AttributeError: 211 except AttributeError:
175 pass 212 pass
176 213
177 return dest_jids 214 return dest_jids
178 215
179 def check_jabber_connection(self): 216 def connect_profile(self, callback):
180 """Check that jabber status is allright""" 217 """ Check if the profile is connected
181 def cantConnect(arg): 218 @param callback: method to call when profile is connected
182 print arg 219 @exit: - 1 when profile is not connected and --connect is not set
220 - 1 when the profile doesn't exists
221 - 1 when there is a connection error
222 """
223 # FIXME: need better exit codes
224
225 def cant_connect():
183 error(_(u"Can't connect profile")) 226 error(_(u"Can't connect profile"))
184 exit(1) 227 self.quit(1)
185 228
186 self.profile = self.bridge.getProfileName(self.profile_name) 229 self.profile = self.bridge.getProfileName(self.args.profile)
230
187 if not self.profile: 231 if not self.profile:
188 error(_("The profile asked doesn't exist")) 232 error(_("The profile [%s] doesn't exist") % self.args.profile)
189 exit(1) 233 self.quit(1)
190 234
191 if self.bridge.isConnected(self.profile): 235 if self.args.connect: #if connection is asked, we connect the profile
192 print "Already connected" 236 self.bridge.asyncConnect(self.profile, callback, cant_connect)
193 else: 237 self._auto_loop = True
194 self._start_loop = True
195 self.bridge.asyncConnect(self.profile, self._run, cantConnect)
196 return 238 return
197 self.run() 239
198 240 elif not self.bridge.isConnected(self.profile):
199 241 error(_(u"Profile [%(profile)s] is not connected, please connect it before using jp, or use --connect option") % { "profile": self.profile })
200 def _getFullJid(self, param_jid): 242 self.quit(1)
201 """Return the full jid if possible (add last resource when find a bare jid""" 243
244 callback()
245
246 def get_full_jid(self, param_jid):
247 """Return the full jid if possible (add last resource when find a bare jid)"""
202 _jid = JID(param_jid) 248 _jid = JID(param_jid)
203 if not _jid.resource: 249 if not _jid.resource:
204 #if the resource is not given, we try to add the last known resource 250 #if the resource is not given, we try to add the last known resource
205 last_resource = self.bridge.getLastResource(param_jid, self.profile_name) 251 last_resource = self.bridge.getLastResource(param_jid, self.profile)
206 if last_resource: 252 if last_resource:
207 return "%s/%s" % (_jid.bare, last_resource) 253 return "%s/%s" % (_jid.bare, last_resource)
208 return param_jid 254 return param_jid
209 255
210 def go(self): 256 def watch_progress(self):
211 self.check_jabber_connection() 257 self.pbar = None
212 if self._start_loop: 258 gobject.timeout_add(10, self._progress_cb)
213 self._loop_start() 259
214 260 def _progress_cb(self):
215 261 if self.progress_id:
216 262 data = self.bridge.getProgress(self.progress_id, self.profile)
217 class JPAsk(JPWithProfile): 263 if data:
218 def confirm_type(self): 264 if not data['position']:
219 """Must return a string containing the confirm type. For instance, 265 data['position'] = '0'
220 FILE_TRANSFER or PIPE_TRANSFER, etc. 266 if not self.pbar:
221 267 #first answer, we must construct the bar
222 :rtype: str 268 self.pbar = progressbar.ProgressBar(int(data['size']),
223 """ 269 [_("Progress: "),progressbar.Percentage(),
224 raise NotImplemented 270 " ",
225 271 progressbar.Bar(),
226 def dest_jids(self): 272 " ",
227 return None 273 progressbar.FileTransferSpeed(),
228 274 " ",
229 def _askConfirmation(self, confirm_id, confirm_type, data, profile): 275 progressbar.ETA()])
276 self.pbar.start()
277
278 self.pbar.update(int(data['position']))
279
280 elif self.pbar:
281 self.pbar.finish()
282 if self.quit_on_progress_end:
283 self.quit()
284 return False
285
286 return True
287
288
289 class CommandBase(object):
290
291 def __init__(self, host, name, use_profile=True, use_progress=False, help=None, **kwargs):
292 """ Initialise CommandBase
293 @param host: Jp instance
294 @param name: name of the new command
295 @param use_profile: if True, add profile selection/connection commands
296 @param use_progress: if True, add progress bar activation commands
297 @param help: help message to display
298 @param **kwargs: args passed to ArgumentParser
299
300 """
301 try: # If we have subcommands, host is a CommandBase and we need to use host.host
302 self.host = host.host
303 except AttributeError:
304 self.host = host
305
306 parents = kwargs.setdefault('parents', set())
307 if use_profile:
308 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments
309 parents.add(self.host.parents['profile'])
310 if use_progress:
311 parents.add(self.host.parents['progress'])
312
313 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
314 if hasattr(self, "subcommands"):
315 self.subparsers = self.parser.add_subparsers()
316 else:
317 self.parser.set_defaults(func=self.run)
318 self.add_parser_options()
319
320 @property
321 def args(self):
322 return self.host.args
323
324 @property
325 def need_loop(self):
326 return self.host.need_loop
327
328 @need_loop.setter
329 def need_loop(self, value):
330 self.host.need_loop = value
331
332 @property
333 def profile(self):
334 return self.host.profile
335
336 @property
337 def progress_id(self):
338 return self.host.progress_id
339
340 @progress_id.setter
341 def progress_id(self, value):
342 self.host.progress_id = value
343
344 def add_parser_options(self):
345 try:
346 subcommands = self.subcommands
347 except AttributeError:
348 # We don't have subcommands, the class need to implements add_parser_options
349 raise NotImplementedError
350
351 # now we add subcommands to ourself
352 for cls in subcommands:
353 cls(self)
354
355 def run(self):
356 try:
357 if self.args.profile:
358 self.host.connect_profile(self.connected)
359 except AttributeError:
360 # the command doesn't need to connect profile
361 pass
362 try:
363 if self.args.progress:
364 self.host.watch_progress()
365 except AttributeError:
366 # the command doesn't use progress bar
367 pass
368
369 def connected(self):
370 if not self.need_loop:
371 self.host.stop_loop()
372
373
374 class CommandAnswering(CommandBase):
375 #FIXME: temp, will be refactored when progress_bar/confirmations will be refactored
376
377 def _ask_confirmation(self, confirm_id, confirm_type, data, profile):
378 """ Callback used for file transfer, accept files depending on parameters"""
230 if profile != self.profile: 379 if profile != self.profile:
231 debug("Ask confirmation ignored: not our profile") 380 debug("Ask confirmation ignored: not our profile")
232 return 381 return
233 if confirm_type == self.confirm_type(): 382 if confirm_type == self.confirm_type:
234 self._confirm_id = confirm_id 383 if self.dest_jids and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]:
235 if self.dest_jids() and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]:
236 return #file is not sent by a filtered jid 384 return #file is not sent by a filtered jid
237 else: 385 else:
238 self.ask(data) 386 self.ask(data, confirm_id)
239 387
240 def ask(self): 388 def ask(self):
241 """ 389 """
242 The return value is used to answer to the bridge. 390 The return value is used to answer to the bridge.
243 :rtype: (bool, dict) 391 @return: bool or dict
244 """ 392 """
245 raise NotImplementedError 393 raise NotImplementedError
246 394
247 def answer(self, accepted, answer_data): 395 def connected(self):
248 """
249 :param accepted: boolean
250 :param aswer_data: dict of answer datas
251 """
252 self.bridge.confirmationAnswer(self._confirm_id, False, answer_data, self.profile)
253
254 def run(self):
255 """Auto reply to confirmations requests""" 396 """Auto reply to confirmations requests"""
256 #we register incoming confirmation 397 self.need_loop = True
257 self.bridge.register("askConfirmation", self._askConfirmation) 398 super(CommandAnswering, self).connected()
399 # we watch confirmation signals
400 self.host.bridge.register("ask_confirmation", self._ask_confirmation)
258 401
259 #and we ask those we have missed 402 #and we ask those we have missed
260 for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile): 403 for confirm_id, confirm_type, data in self.host.bridge.getWaitingConf(self.profile):
261 self._askConfirmation(confirm_id, confirm_type, data, self.profile) 404 self._ask_confirmation(confirm_id, confirm_type, data, self.profile)
262
263