Mercurial > libervia-backend
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 |