Mercurial > libervia-backend
comparison sat_frontends/jp/base.py @ 3040:fee60f17ebac
jp: jp asyncio port:
/!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\
This patch implements the port of jp to asyncio, so it is now correctly using the bridge
asynchronously, and it can be used with bridges like `pb`. This also simplify the code,
notably for things which were previously implemented with many callbacks (like pagination
with RSM).
During the process, some behaviours have been modified/fixed, in jp and backends, check
diff for details.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 25 Sep 2019 08:56:41 +0200 |
parents | ab2696e34d29 |
children | 3df611adb598 |
comparison
equal
deleted
inserted
replaced
3039:a1bc34f90fa5 | 3040:fee60f17ebac |
---|---|
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 import asyncio | |
20 from sat.core.i18n import _ | 21 from sat.core.i18n import _ |
21 | 22 |
22 ### logging ### | 23 ### logging ### |
23 import logging as log | 24 import logging as log |
24 log.basicConfig(level=log.DEBUG, | 25 log.basicConfig(level=log.WARNING, |
25 format='%(message)s') | 26 format='[%(name)s] %(message)s') |
26 ### | 27 ### |
27 | 28 |
28 import sys | 29 import sys |
29 import locale | 30 import os |
30 import os.path | 31 import os.path |
31 import argparse | 32 import argparse |
33 import inspect | |
34 from pathlib import Path | |
32 from glob import iglob | 35 from glob import iglob |
33 from importlib import import_module | 36 from importlib import import_module |
34 from sat_frontends.tools.jid import JID | 37 from sat_frontends.tools.jid import JID |
35 from sat.tools import config | 38 from sat.tools import config |
36 from sat.tools.common import dynamic_import | 39 from sat.tools.common import dynamic_import |
39 from sat.core import exceptions | 42 from sat.core import exceptions |
40 import sat_frontends.jp | 43 import sat_frontends.jp |
41 from sat_frontends.jp.constants import Const as C | 44 from sat_frontends.jp.constants import Const as C |
42 from sat_frontends.tools import misc | 45 from sat_frontends.tools import misc |
43 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI | 46 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI |
44 import shlex | |
45 from collections import OrderedDict | 47 from collections import OrderedDict |
46 | 48 |
47 ## bridge handling | 49 ## bridge handling |
48 # we get bridge name from conf and initialise the right class accordingly | 50 # we get bridge name from conf and initialise the right class accordingly |
49 main_config = config.parseMainConf() | 51 main_config = config.parseMainConf() |
50 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') | 52 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') |
53 USER_INTER_MSG = _("User interruption: good bye") | |
54 | |
55 | |
56 class QuitException(BaseException): | |
57 """Quitting is requested | |
58 | |
59 This is used to stop execution when host.quit() is called | |
60 """ | |
51 | 61 |
52 | 62 |
53 # TODO: move loops handling in a separated module | 63 # TODO: move loops handling in a separated module |
54 if 'dbus' in bridge_name: | 64 if 'dbus' in bridge_name: |
55 from gi.repository import GLib | 65 from gi.repository import GLib |
61 self.loop = GLib.MainLoop() | 71 self.loop = GLib.MainLoop() |
62 | 72 |
63 def run(self): | 73 def run(self): |
64 self.loop.run() | 74 self.loop.run() |
65 | 75 |
66 def quit(self): | 76 def quit(self, exit_code): |
67 self.loop.quit() | 77 self.loop.quit() |
78 sys.exit(exit_code) | |
68 | 79 |
69 def call_later(self, delay, callback, *args): | 80 def call_later(self, delay, callback, *args): |
70 """call a callback repeatedly | 81 """call a callback repeatedly |
71 | 82 |
72 @param delay(int): delay between calls in ms | 83 @param delay(int): delay between calls in ms |
76 @param *args: args of the callbac | 87 @param *args: args of the callbac |
77 """ | 88 """ |
78 GLib.timeout_add(delay, callback, *args) | 89 GLib.timeout_add(delay, callback, *args) |
79 | 90 |
80 else: | 91 else: |
81 print("can't start jp: only D-Bus bridge is currently handled") | 92 import signal |
82 sys.exit(C.EXIT_ERROR) | 93 from twisted.internet import asyncioreactor |
83 # FIXME: twisted loop can be used when jp can handle fully async bridges | 94 asyncioreactor.install() |
84 # from twisted.internet import reactor | 95 from twisted.internet import reactor, defer |
85 | 96 |
86 # class JPLoop(object): | 97 class JPLoop(object): |
87 | 98 |
88 # def run(self): | 99 def __init__(self): |
89 # reactor.run() | 100 # exit code must be set when using quit, so if it's not set |
90 | 101 # something got wrong and we must report it |
91 # def quit(self): | 102 self._exit_code = C.EXIT_INTERNAL_ERROR |
92 # reactor.stop() | 103 |
93 | 104 def run(self, jp, *args): |
94 # def _timeout_cb(self, args, callback, delay): | 105 self.jp = jp |
95 # ret = callback(*args) | 106 signal.signal(signal.SIGINT, self._on_sigint) |
96 # if ret: | 107 defer.ensureDeferred(self._start(jp, *args)) |
97 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) | 108 try: |
98 | 109 reactor.run(installSignalHandlers=False) |
99 # def call_later(self, delay, callback, *args): | 110 except SystemExit as e: |
100 # delay = float(delay) / 1000 | 111 self._exit_code = e.code |
101 # reactor.callLater(delay, self._timeout_cb, args, callback, delay) | 112 sys.exit(self._exit_code) |
113 | |
114 async def _start(self, jp, *args): | |
115 fut = asyncio.ensure_future(jp.main(*args)) | |
116 try: | |
117 await defer.Deferred.fromFuture(fut) | |
118 except BaseException: | |
119 import traceback | |
120 traceback.print_exc() | |
121 jp.quit(1) | |
122 | |
123 def quit(self, exit_code): | |
124 self._exit_code = exit_code | |
125 reactor.stop() | |
126 | |
127 def _timeout_cb(self, args, callback, delay): | |
128 try: | |
129 ret = callback(*args) | |
130 # FIXME: temporary hack to avoid traceback when using XMLUI | |
131 # to be removed once create_task is not used anymore in | |
132 # xmlui_manager (i.e. once sat_frontends.tools.xmlui fully supports | |
133 # async syntax) | |
134 except QuitException: | |
135 return | |
136 if ret: | |
137 reactor.callLater(delay, self._timeout_cb, args, callback, delay) | |
138 | |
139 def call_later(self, delay, callback, *args): | |
140 delay = float(delay) / 1000 | |
141 reactor.callLater(delay, self._timeout_cb, args, callback, delay) | |
142 | |
143 def _on_sigint(self, sig_number, stack_frame): | |
144 """Called on keyboard interruption | |
145 | |
146 Print user interruption message, set exit code and stop reactor | |
147 """ | |
148 print("\r" + USER_INTER_MSG) | |
149 self._exit_code = C.EXIT_USER_CANCELLED | |
150 reactor.callFromThread(reactor.stop) | |
151 | |
102 | 152 |
103 if bridge_name == "embedded": | 153 if bridge_name == "embedded": |
104 from sat.core import sat_main | 154 from sat.core import sat_main |
105 sat = sat_main.SAT() | 155 sat = sat_main.SAT() |
106 | 156 |
107 try: | 157 try: |
108 import progressbar | 158 import progressbar |
109 except ImportError: | 159 except ImportError: |
110 msg = (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + | 160 msg = (_('ProgressBar not available, please download it at ' |
111 _('Progress bar deactivated\n--\n')) | 161 'http://pypi.python.org/pypi/progressbar\n' |
112 print(msg.encode('utf-8'), file=sys.stderr) | 162 'Progress bar deactivated\n--\n')) |
163 print(msg, file=sys.stderr) | |
113 progressbar=None | 164 progressbar=None |
114 | 165 |
115 #consts | 166 #consts |
116 PROG_NAME = "jp" | 167 PROG_NAME = "jp" |
117 DESCRIPTION = """This software is a command line tool for XMPP. | 168 DESCRIPTION = """This software is a command line tool for XMPP. |
120 COPYLEFT = """Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa | 171 COPYLEFT = """Copyright (C) 2009-2019 Jérôme Poisson, Adrien Cossa |
121 This program comes with ABSOLUTELY NO WARRANTY; | 172 This program comes with ABSOLUTELY NO WARRANTY; |
122 This is free software, and you are welcome to redistribute it under certain conditions. | 173 This is free software, and you are welcome to redistribute it under certain conditions. |
123 """ | 174 """ |
124 | 175 |
125 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms | 176 PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s |
126 | 177 |
127 | 178 |
128 def date_decoder(arg): | 179 def date_decoder(arg): |
129 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) | 180 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) |
130 | 181 |
139 | 190 |
140 """ | 191 """ |
141 def __init__(self): | 192 def __init__(self): |
142 """ | 193 """ |
143 | 194 |
144 @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, | 195 @attribute quit_on_progress_end (bool): set to False if you manage yourself |
145 or if you want the user to stop by himself | 196 exiting, or if you want the user to stop by himself |
146 @attribute progress_success(callable): method to call when progress just started | 197 @attribute progress_success(callable): method to call when progress just started |
147 by default display a message | 198 by default display a message |
148 @attribute progress_success(callable): method to call when progress is successfully finished | 199 @attribute progress_success(callable): method to call when progress is |
149 by default display a message | 200 successfully finished by default display a message |
150 @attribute progress_failure(callable): method to call when progress failed | 201 @attribute progress_failure(callable): method to call when progress failed |
151 by default display a message | 202 by default display a message |
152 """ | 203 """ |
153 # FIXME: need_loop should be removed, everything must be async in bridge so | |
154 # loop will always be needed | |
155 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') | 204 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') |
156 if bridge_module is None: | 205 if bridge_module is None: |
157 log.error("Can't import {} bridge".format(bridge_name)) | 206 log.error("Can't import {} bridge".format(bridge_name)) |
158 sys.exit(1) | 207 sys.exit(1) |
159 | 208 |
160 self.bridge = bridge_module.Bridge() | 209 self.bridge = bridge_module.AIOBridge() |
161 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) | 210 self._onQuitCallbacks = [] |
162 | 211 |
163 def _bridgeCb(self): | 212 def _bridgeConnected(self): |
164 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, | 213 self.parser = argparse.ArgumentParser( |
165 description=DESCRIPTION) | 214 formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) |
166 self._make_parents() | 215 self._make_parents() |
167 self.add_parser_options() | 216 self.add_parser_options() |
168 self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name') | 217 self.subparsers = self.parser.add_subparsers( |
169 self._auto_loop = False # when loop is used for internal reasons | 218 title=_('Available commands'), dest='command', required=True) |
170 self._need_loop = False | |
171 | 219 |
172 # progress attributes | 220 # progress attributes |
173 self._progress_id = None # TODO: manage several progress ids | 221 self._progress_id = None # TODO: manage several progress ids |
174 self.quit_on_progress_end = True | 222 self.quit_on_progress_end = True |
175 | 223 |
179 self._outputs[type_] = OrderedDict() | 227 self._outputs[type_] = OrderedDict() |
180 self.default_output = {} | 228 self.default_output = {} |
181 | 229 |
182 self.own_jid = None # must be filled at runtime if needed | 230 self.own_jid = None # must be filled at runtime if needed |
183 | 231 |
184 def _bridgeEb(self, failure): | |
185 if isinstance(failure, exceptions.BridgeExceptionNoService): | |
186 print((_("Can't connect to SàT backend, are you sure it's launched ?"))) | |
187 elif isinstance(failure, exceptions.BridgeInitError): | |
188 print((_("Can't init bridge"))) | |
189 else: | |
190 print((_("Error while initialising bridge: {}".format(failure)))) | |
191 sys.exit(C.EXIT_BRIDGE_ERROR) | |
192 | |
193 @property | |
194 def version(self): | |
195 return self.bridge.getVersion() | |
196 | |
197 @property | 232 @property |
198 def progress_id(self): | 233 def progress_id(self): |
199 return self._progress_id | 234 return self._progress_id |
200 | 235 |
201 @progress_id.setter | 236 async def set_progress_id(self, progress_id): |
202 def progress_id(self, value): | 237 # because we use async, we need an explicit setter |
203 self._progress_id = value | 238 self._progress_id = progress_id |
204 self.replayCache('progress_ids_cache') | 239 await self.replayCache('progress_ids_cache') |
205 | 240 |
206 @property | 241 @property |
207 def watch_progress(self): | 242 def watch_progress(self): |
208 try: | 243 try: |
209 self.pbar | 244 self.pbar |
222 try: | 257 try: |
223 return self.args.verbose | 258 return self.args.verbose |
224 except AttributeError: | 259 except AttributeError: |
225 return 0 | 260 return 0 |
226 | 261 |
227 def replayCache(self, cache_attribute): | 262 async def replayCache(self, cache_attribute): |
228 """Replay cached signals | 263 """Replay cached signals |
229 | 264 |
230 @param cache_attribute(str): name of the attribute containing the cache | 265 @param cache_attribute(str): name of the attribute containing the cache |
231 if the attribute doesn't exist, there is no cache and the call is ignored | 266 if the attribute doesn't exist, there is no cache and the call is ignored |
232 else the cache must be a list of tuples containing the replay callback as first item, | 267 else the cache must be a list of tuples containing the replay callback as |
233 then the arguments to use | 268 first item, then the arguments to use |
234 """ | 269 """ |
235 try: | 270 try: |
236 cache = getattr(self, cache_attribute) | 271 cache = getattr(self, cache_attribute) |
237 except AttributeError: | 272 except AttributeError: |
238 pass | 273 pass |
239 else: | 274 else: |
240 for cache_data in cache: | 275 for cache_data in cache: |
241 cache_data[0](*cache_data[1:]) | 276 await cache_data[0](*cache_data[1:]) |
242 | 277 |
243 def disp(self, msg, verbosity=0, error=False, no_lf=False): | 278 def disp(self, msg, verbosity=0, error=False, no_lf=False): |
244 """Print a message to user | 279 """Print a message to user |
245 | 280 |
246 @param msg(unicode): message to print | 281 @param msg(unicode): message to print |
258 if no_lf: | 293 if no_lf: |
259 print(msg, end=' ') | 294 print(msg, end=' ') |
260 else: | 295 else: |
261 print(msg) | 296 print(msg) |
262 | 297 |
263 def output(self, type_, name, extra_outputs, data): | 298 async def output(self, type_, name, extra_outputs, data): |
264 if name in extra_outputs: | 299 if name in extra_outputs: |
265 extra_outputs[name](data) | 300 method = extra_outputs[name] |
266 else: | 301 else: |
267 self._outputs[type_][name]['callback'](data) | 302 method = self._outputs[type_][name]['callback'] |
303 | |
304 ret = method(data) | |
305 if inspect.isawaitable(ret): | |
306 await ret | |
268 | 307 |
269 def addOnQuitCallback(self, callback, *args, **kwargs): | 308 def addOnQuitCallback(self, callback, *args, **kwargs): |
270 """Add a callback which will be called on quit command | 309 """Add a callback which will be called on quit command |
271 | 310 |
272 @param callback(callback): method to call | 311 @param callback(callback): method to call |
273 """ | 312 """ |
274 try: | 313 self._onQuitCallbacks.append((callback, args, kwargs)) |
275 callbacks_list = self._onQuitCallbacks | |
276 except AttributeError: | |
277 callbacks_list = self._onQuitCallbacks = [] | |
278 finally: | |
279 callbacks_list.append((callback, args, kwargs)) | |
280 | 314 |
281 def getOutputChoices(self, output_type): | 315 def getOutputChoices(self, output_type): |
282 """Return valid output filters for output_type | 316 """Return valid output filters for output_type |
283 | 317 |
284 @param output_type: True for default, | 318 @param output_type: True for default, |
287 return list(self._outputs[output_type].keys()) | 321 return list(self._outputs[output_type].keys()) |
288 | 322 |
289 def _make_parents(self): | 323 def _make_parents(self): |
290 self.parents = {} | 324 self.parents = {} |
291 | 325 |
292 # we have a special case here as the start-session option is present only if connection is not needed, | 326 # we have a special case here as the start-session option is present only if |
293 # so we create two similar parents, one with the option, the other one without it | 327 # connection is not needed, so we create two similar parents, one with the |
328 # option, the other one without it | |
294 for parent_name in ('profile', 'profile_session'): | 329 for parent_name in ('profile', 'profile_session'): |
295 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) | 330 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) |
296 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) | 331 parent.add_argument( |
297 parent.add_argument("--pwd", action="store", default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) | 332 "-p", "--profile", action="store", type=str, default='@DEFAULT@', |
298 | 333 help=_("Use PROFILE profile key (default: %(default)s)")) |
299 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] | 334 parent.add_argument( |
300 | 335 "--pwd", action="store", default='', metavar='PASSWORD', |
301 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _("Connect the profile before doing anything else") | 336 help=_("Password used to connect profile, if necessary")) |
302 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) | 337 |
338 profile_parent, profile_session_parent = (self.parents['profile'], | |
339 self.parents['profile_session']) | |
340 | |
341 connect_short, connect_long, connect_action, connect_help = ( | |
342 "-c", "--connect", "store_true", | |
343 _("Connect the profile before doing anything else") | |
344 ) | |
345 profile_parent.add_argument( | |
346 connect_short, connect_long, action=connect_action, help=connect_help) | |
303 | 347 |
304 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() | 348 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() |
305 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) | 349 profile_session_connect_group.add_argument( |
306 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) | 350 connect_short, connect_long, action=connect_action, help=connect_help) |
307 | 351 profile_session_connect_group.add_argument( |
308 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) | 352 "--start-session", action="store_true", |
353 help=_("Start a profile session without connecting")) | |
354 | |
355 progress_parent = self.parents['progress'] = argparse.ArgumentParser( | |
356 add_help=False) | |
309 if progressbar: | 357 if progressbar: |
310 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) | 358 progress_parent.add_argument( |
359 "-P", "--progress", action="store_true", help=_("Show progress bar")) | |
311 | 360 |
312 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) | 361 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) |
313 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_("Add a verbosity level (can be used multiple times)")) | 362 verbose_parent.add_argument( |
363 '--verbose', '-v', action='count', default=0, | |
364 help=_("Add a verbosity level (can be used multiple times)")) | |
314 | 365 |
315 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) | 366 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) |
316 draft_group = draft_parent.add_argument_group(_('draft handling')) | 367 draft_group = draft_parent.add_argument_group(_('draft handling')) |
317 draft_group.add_argument("-D", "--current", action="store_true", help=_("load current draft")) | 368 draft_group.add_argument( |
318 draft_group.add_argument("-F", "--draft-path", help=_("path to a draft file to retrieve")) | 369 "-D", "--current", action="store_true", help=_("load current draft")) |
370 draft_group.add_argument( | |
371 "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")) | |
319 | 372 |
320 | 373 |
321 def make_pubsub_group(self, flags, defaults): | 374 def make_pubsub_group(self, flags, defaults): |
322 """generate pubsub options according to flags | 375 """generate pubsub options according to flags |
323 | 376 |
354 default = defaults.pop('item', _('last item')) | 407 default = defaults.pop('item', _('last item')) |
355 if default is not None: | 408 if default is not None: |
356 item_help += _(" (DEFAULT: {default})".format(default=default)) | 409 item_help += _(" (DEFAULT: {default})".format(default=default)) |
357 pubsub_group.add_argument("-i", "--item", default='', | 410 pubsub_group.add_argument("-i", "--item", default='', |
358 help=item_help) | 411 help=item_help) |
359 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_('retrieve last item')) | 412 pubsub_group.add_argument( |
413 "-L", "--last-item", action='store_true', help=_('retrieve last item')) | |
360 elif flags.multi_items: | 414 elif flags.multi_items: |
361 # mutiple items, this activate several features: max-items, RSM, MAM | 415 # mutiple items, this activate several features: max-items, RSM, MAM |
362 # and Orbder-by | 416 # and Orbder-by |
363 pubsub_group.add_argument("-i", "--item", action='append', dest='items', default=[], help=_("items to retrieve (DEFAULT: all)")) | 417 pubsub_group.add_argument( |
418 "-i", "--item", action='append', dest='items', default=[], | |
419 help=_("items to retrieve (DEFAULT: all)")) | |
364 if not flags.no_max: | 420 if not flags.no_max: |
365 max_group = pubsub_group.add_mutually_exclusive_group() | 421 max_group = pubsub_group.add_mutually_exclusive_group() |
366 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args | 422 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args |
367 max_group.add_argument( | 423 max_group.add_argument( |
368 "-M", "--max-items", dest="max", type=int, | 424 "-M", "--max-items", dest="max", type=int, |
407 "-o", "--order-by", choices=[C.ORDER_BY_CREATION, | 463 "-o", "--order-by", choices=[C.ORDER_BY_CREATION, |
408 C.ORDER_BY_MODIFICATION], | 464 C.ORDER_BY_MODIFICATION], |
409 help=_("how items should be ordered")) | 465 help=_("how items should be ordered")) |
410 | 466 |
411 if not flags.all_used: | 467 if not flags.all_used: |
412 raise exceptions.InternalError('unknown flags: {flags}'.format(flags=', '.join(flags.unused))) | 468 raise exceptions.InternalError('unknown flags: {flags}'.format( |
469 flags=', '.join(flags.unused))) | |
413 if defaults: | 470 if defaults: |
414 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) | 471 raise exceptions.InternalError(f'unused defaults: {defaults}') |
415 | 472 |
416 return parent | 473 return parent |
417 | 474 |
418 def add_parser_options(self): | 475 def add_parser_options(self): |
419 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) | 476 self.parser.add_argument( |
477 '--version', | |
478 action='version', | |
479 version=("{name} {version} {copyleft}".format( | |
480 name = PROG_NAME, | |
481 version = self.version, | |
482 copyleft = COPYLEFT)) | |
483 ) | |
420 | 484 |
421 def register_output(self, type_, name, callback, description="", default=False): | 485 def register_output(self, type_, name, callback, description="", default=False): |
422 if type_ not in C.OUTPUT_TYPES: | 486 if type_ not in C.OUTPUT_TYPES: |
423 log.error("Invalid output type {}".format(type_)) | 487 log.error("Invalid output type {}".format(type_)) |
424 return | 488 return |
425 self._outputs[type_][name] = {'callback': callback, | 489 self._outputs[type_][name] = {'callback': callback, |
426 'description': description | 490 'description': description |
427 } | 491 } |
428 if default: | 492 if default: |
429 if type_ in self.default_output: | 493 if type_ in self.default_output: |
430 self.disp(_('there is already a default output for {}, ignoring new one').format(type_)) | 494 self.disp( |
495 _(f'there is already a default output for {type_}, ignoring new one') | |
496 ) | |
431 else: | 497 else: |
432 self.default_output[type_] = name | 498 self.default_output[type_] = name |
433 | 499 |
434 | 500 |
435 def parse_output_options(self): | 501 def parse_output_options(self): |
443 options_dict[key.strip()] = value.strip() if value is not None else None | 509 options_dict[key.strip()] = value.strip() if value is not None else None |
444 return options_dict | 510 return options_dict |
445 | 511 |
446 def check_output_options(self, accepted_set, options): | 512 def check_output_options(self, accepted_set, options): |
447 if not accepted_set.issuperset(options): | 513 if not accepted_set.issuperset(options): |
448 self.disp("The following output options are invalid: {invalid_options}".format( | 514 self.disp( |
515 _("The following output options are invalid: {invalid_options}").format( | |
449 invalid_options = ', '.join(set(options).difference(accepted_set))), | 516 invalid_options = ', '.join(set(options).difference(accepted_set))), |
450 error=True) | 517 error=True) |
451 self.quit(C.EXIT_BAD_ARG) | 518 self.quit(C.EXIT_BAD_ARG) |
452 | 519 |
453 def import_plugins(self): | 520 def import_plugins(self): |
455 | 522 |
456 looks from modules names cmd_*.py in jp path and import them | 523 looks from modules names cmd_*.py in jp path and import them |
457 """ | 524 """ |
458 path = os.path.dirname(sat_frontends.jp.__file__) | 525 path = os.path.dirname(sat_frontends.jp.__file__) |
459 # XXX: outputs must be imported before commands as they are used for arguments | 526 # XXX: outputs must be imported before commands as they are used for arguments |
460 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): | 527 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), |
461 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) | 528 (C.PLUGIN_CMD, 'cmd_*.py')): |
529 modules = ( | |
530 os.path.splitext(module)[0] | |
531 for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) | |
462 for module_name in modules: | 532 for module_name in modules: |
463 module_path = "sat_frontends.jp." + module_name | 533 module_path = "sat_frontends.jp." + module_name |
464 try: | 534 try: |
465 module = import_module(module_path) | 535 module = import_module(module_path) |
466 self.import_plugin_module(module, type_) | 536 self.import_plugin_module(module, type_) |
467 except ImportError as e: | 537 except ImportError as e: |
468 self.disp(_("Can't import {module_path} plugin, ignoring it: {msg}".format( | 538 self.disp( |
469 module_path = module_path, | 539 _(f"Can't import {module_path} plugin, ignoring it: {e}"), |
470 msg = e)), error=True) | 540 error=True) |
471 except exceptions.CancelError: | 541 except exceptions.CancelError: |
472 continue | 542 continue |
473 except exceptions.MissingModule as e: | 543 except exceptions.MissingModule as e: |
474 self.disp(_("Missing module for plugin {name}: {missing}".format( | 544 self.disp(_("Missing module for plugin {name}: {missing}".format( |
475 name = module_path, | 545 name = module_path, |
483 @param type_(str): one of C_PLUGIN_* | 553 @param type_(str): one of C_PLUGIN_* |
484 """ | 554 """ |
485 try: | 555 try: |
486 class_names = getattr(module, '__{}__'.format(type_)) | 556 class_names = getattr(module, '__{}__'.format(type_)) |
487 except AttributeError: | 557 except AttributeError: |
488 log.disp(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) | 558 log.disp(_(f"Invalid plugin module [{type_}] {module}"), error=True) |
489 raise ImportError | 559 raise ImportError |
490 else: | 560 else: |
491 for class_name in class_names: | 561 for class_name in class_names: |
492 cls = getattr(module, class_name) | 562 cls = getattr(module, class_name) |
493 cls(self) | 563 cls(self) |
498 scheme = 'https' | 568 scheme = 'https' |
499 elif http_url.startswith('http'): | 569 elif http_url.startswith('http'): |
500 scheme = 'http' | 570 scheme = 'http' |
501 else: | 571 else: |
502 raise exceptions.InternalError('An HTTP scheme is expected in this method') | 572 raise exceptions.InternalError('An HTTP scheme is expected in this method') |
503 self.disp("{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) | 573 self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1) |
504 # HTTP URL, we try to find xmpp: links | 574 # HTTP URL, we try to find xmpp: links |
505 try: | 575 try: |
506 from lxml import etree | 576 from lxml import etree |
507 except ImportError: | 577 except ImportError: |
508 self.disp("lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) | 578 self.disp( |
579 "lxml module must be installed to use http(s) scheme, please install it " | |
580 "with \"pip install lxml\"", | |
581 error=True) | |
509 self.quit(1) | 582 self.quit(1) |
510 import urllib.request, urllib.error, urllib.parse | 583 import urllib.request, urllib.error, urllib.parse |
511 parser = etree.HTMLParser() | 584 parser = etree.HTMLParser() |
512 try: | 585 try: |
513 root = etree.parse(urllib.request.urlopen(http_url), parser) | 586 root = etree.parse(urllib.request.urlopen(http_url), parser) |
515 self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) | 588 self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) |
516 links = [] | 589 links = [] |
517 else: | 590 else: |
518 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") | 591 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") |
519 if not links: | 592 if not links: |
520 self.disp('Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) | 593 self.disp( |
594 _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP ' | |
595 'PubSub node/item'), | |
596 error=True) | |
521 self.quit(1) | 597 self.quit(1) |
522 xmpp_uri = links[0].get('href') | 598 xmpp_uri = links[0].get('href') |
523 return xmpp_uri | 599 return xmpp_uri |
524 | 600 |
525 def parse_pubsub_args(self): | 601 def parse_pubsub_args(self): |
550 item = self.args.item | 626 item = self.args.item |
551 except AttributeError: | 627 except AttributeError: |
552 try: | 628 try: |
553 items = self.args.items | 629 items = self.args.items |
554 except AttributeError: | 630 except AttributeError: |
555 self.disp(_("item specified in URL but not needed in command, ignoring it"), error=True) | 631 self.disp( |
632 _("item specified in URL but not needed in command, " | |
633 "ignoring it"), | |
634 error=True) | |
556 else: | 635 else: |
557 if not items: | 636 if not items: |
558 self.args.items = [uri_item] | 637 self.args.items = [uri_item] |
559 else: | 638 else: |
560 if not item: | 639 if not item: |
563 except AttributeError: | 642 except AttributeError: |
564 item_last = False | 643 item_last = False |
565 if not item_last: | 644 if not item_last: |
566 self.args.item = uri_item | 645 self.args.item = uri_item |
567 else: | 646 else: |
568 self.parser.error(_('XMPP URL is not a pubsub one: {url}').format(url=url)) | 647 self.parser.error(_(f'XMPP URL is not a pubsub one: {url}')) |
569 flags = self.args._cmd._pubsub_flags | 648 flags = self.args._cmd._pubsub_flags |
570 # we check required arguments here instead of using add_arguments' required option | 649 # we check required arguments here instead of using add_arguments' required option |
571 # because the required argument can be set in URL | 650 # because the required argument can be set in URL |
572 if C.SERVICE in flags and not self.args.service: | 651 if C.SERVICE in flags and not self.args.service: |
573 self.parser.error(_("argument -s/--service is required")) | 652 self.parser.error(_("argument -s/--service is required")) |
578 | 657 |
579 # FIXME: mutually groups can't be nested in a group and don't support title | 658 # FIXME: mutually groups can't be nested in a group and don't support title |
580 # so we check conflict here. This may be fixed in Python 3, to be checked | 659 # so we check conflict here. This may be fixed in Python 3, to be checked |
581 try: | 660 try: |
582 if self.args.item and self.args.item_last: | 661 if self.args.item and self.args.item_last: |
583 self.parser.error(_("--item and --item-last can't be used at the same time")) | 662 self.parser.error( |
663 _("--item and --item-last can't be used at the same time")) | |
584 except AttributeError: | 664 except AttributeError: |
585 pass | 665 pass |
586 | 666 |
587 try: | 667 try: |
588 max_items = self.args.max | 668 max_items = self.args.max |
603 # we use pubsub without RSM | 683 # we use pubsub without RSM |
604 self.args.max = 10 | 684 self.args.max = 10 |
605 if self.args.max is None: | 685 if self.args.max is None: |
606 self.args.max = C.NO_LIMIT | 686 self.args.max = C.NO_LIMIT |
607 | 687 |
688 async def main(self, args, namespace): | |
689 try: | |
690 await self.bridge.bridgeConnect() | |
691 except Exception as e: | |
692 if isinstance(e, exceptions.BridgeExceptionNoService): | |
693 print((_("Can't connect to SàT backend, are you sure it's launched ?"))) | |
694 elif isinstance(e, exceptions.BridgeInitError): | |
695 print((_("Can't init bridge"))) | |
696 else: | |
697 print((_(f"Error while initialising bridge: {e}"))) | |
698 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) | |
699 return | |
700 self.version = await self.bridge.getVersion() | |
701 self._bridgeConnected() | |
702 self.import_plugins() | |
703 try: | |
704 self.args = self.parser.parse_args(args, namespace=None) | |
705 if self.args._cmd._use_pubsub: | |
706 self.parse_pubsub_args() | |
707 await self.args._cmd.run() | |
708 except SystemExit as e: | |
709 self.quit(e.code, raise_exc=False) | |
710 return | |
711 except QuitException: | |
712 return | |
713 | |
608 def run(self, args=None, namespace=None): | 714 def run(self, args=None, namespace=None): |
609 self.args = self.parser.parse_args(args, namespace=None) | |
610 if self.args._cmd._use_pubsub: | |
611 self.parse_pubsub_args() | |
612 try: | |
613 self.args._cmd.run() | |
614 if self._need_loop or self._auto_loop: | |
615 self._start_loop() | |
616 except KeyboardInterrupt: | |
617 self.disp(_("User interruption: good bye")) | |
618 | |
619 def _start_loop(self): | |
620 self.loop = JPLoop() | 715 self.loop = JPLoop() |
621 self.loop.run() | 716 self.loop.run(self, args, namespace) |
622 | 717 |
623 def stop_loop(self): | 718 def _read_stdin(self, stdin_fut): |
624 try: | 719 """Callback called by ainput to read stdin""" |
625 self.loop.quit() | 720 line = sys.stdin.readline() |
626 except AttributeError: | 721 if line: |
627 pass | 722 stdin_fut.set_result(line.rstrip(os.linesep)) |
628 | 723 else: |
629 def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")): | 724 stdin_fut.set_exception(EOFError()) |
725 | |
726 async def ainput(self, msg=''): | |
727 """Asynchronous version of buildin "input" function""" | |
728 self.disp(msg, no_lf=True) | |
729 sys.stdout.flush() | |
730 loop = asyncio.get_running_loop() | |
731 stdin_fut = loop.create_future() | |
732 loop.add_reader(sys.stdin, self._read_stdin, stdin_fut) | |
733 return await stdin_fut | |
734 | |
735 async def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")): | |
630 """Request user to confirm action, and quit if he doesn't""" | 736 """Request user to confirm action, and quit if he doesn't""" |
631 | 737 |
632 res = input("{} (y/N)? ".format(message)) | 738 res = await self.ainput(f"{message} (y/N)? ") |
633 if res not in ("y", "Y"): | 739 if res not in ("y", "Y"): |
634 self.disp(cancel_message) | 740 self.disp(cancel_message) |
635 self.quit(C.EXIT_USER_CANCELLED) | 741 self.quit(C.EXIT_USER_CANCELLED) |
636 | 742 |
637 def quitFromSignal(self, errcode=0): | 743 def quitFromSignal(self, exit_code=0): |
638 """Same as self.quit, but from a signal handler | 744 r"""Same as self.quit, but from a signal handler |
639 | 745 |
640 /!\: return must be used after calling this method ! | 746 /!\: return must be used after calling this method ! |
641 """ | 747 """ |
642 assert self._need_loop | |
643 # XXX: python-dbus will show a traceback if we exit in a signal handler | 748 # XXX: python-dbus will show a traceback if we exit in a signal handler |
644 # so we use this little timeout trick to avoid it | 749 # so we use this little timeout trick to avoid it |
645 self.loop.call_later(0, self.quit, errcode) | 750 self.loop.call_later(0, self.quit, exit_code) |
646 | 751 |
647 def quit(self, errcode=0): | 752 def quit(self, exit_code=0, raise_exc=True): |
753 """Terminate the execution with specified exit_code | |
754 | |
755 This will stop the loop. | |
756 @param exit_code(int): code to return when quitting the program | |
757 @param raise_exp(boolean): if True raise a QuitException to stop code execution | |
758 The default value should be used most of time. | |
759 """ | |
648 # first the onQuitCallbacks | 760 # first the onQuitCallbacks |
649 try: | 761 try: |
650 callbacks_list = self._onQuitCallbacks | 762 callbacks_list = self._onQuitCallbacks |
651 except AttributeError: | 763 except AttributeError: |
652 pass | 764 pass |
653 else: | 765 else: |
654 for callback, args, kwargs in callbacks_list: | 766 for callback, args, kwargs in callbacks_list: |
655 callback(*args, **kwargs) | 767 callback(*args, **kwargs) |
656 | 768 |
657 self.stop_loop() | 769 self.loop.quit(exit_code) |
658 sys.exit(errcode) | 770 if raise_exc: |
659 | 771 raise QuitException |
660 def check_jids(self, jids): | 772 |
773 async def check_jids(self, jids): | |
661 """Check jids validity, transform roster name to corresponding jids | 774 """Check jids validity, transform roster name to corresponding jids |
662 | 775 |
663 @param profile: profile name | 776 @param profile: profile name |
664 @param jids: list of jids | 777 @param jids: list of jids |
665 @return: List of jids | 778 @return: List of jids |
666 | 779 |
667 """ | 780 """ |
668 names2jid = {} | 781 names2jid = {} |
669 nodes2jid = {} | 782 nodes2jid = {} |
670 | 783 |
671 for contact in self.bridge.getContacts(self.profile): | 784 for contact in await self.bridge.getContacts(self.profile): |
672 jid_s, attr, groups = contact | 785 jid_s, attr, groups = contact |
673 _jid = JID(jid_s) | 786 _jid = JID(jid_s) |
674 try: | 787 try: |
675 names2jid[attr["name"].lower()] = jid_s | 788 names2jid[attr["name"].lower()] = jid_s |
676 except KeyError: | 789 except KeyError: |
702 except AttributeError: | 815 except AttributeError: |
703 pass | 816 pass |
704 | 817 |
705 return dest_jids | 818 return dest_jids |
706 | 819 |
707 def connect_profile(self, callback): | 820 async def connect_profile(self): |
708 """ Check if the profile is connected and do it if requested | 821 """Check if the profile is connected and do it if requested |
709 | 822 |
710 @param callback: method to call when profile is connected | |
711 @exit: - 1 when profile is not connected and --connect is not set | 823 @exit: - 1 when profile is not connected and --connect is not set |
712 - 1 when the profile doesn't exists | 824 - 1 when the profile doesn't exists |
713 - 1 when there is a connection error | 825 - 1 when there is a connection error |
714 """ | 826 """ |
715 # FIXME: need better exit codes | 827 # FIXME: need better exit codes |
716 | 828 |
717 def cant_connect(failure): | 829 self.profile = await self.bridge.profileNameGet(self.args.profile) |
718 log.error(_("Can't connect profile: {reason}").format(reason=failure)) | |
719 self.quit(1) | |
720 | |
721 def cant_start_session(failure): | |
722 log.error(_("Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) | |
723 self.quit(1) | |
724 | |
725 self.profile = self.bridge.profileNameGet(self.args.profile) | |
726 | 830 |
727 if not self.profile: | 831 if not self.profile: |
728 log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) | 832 log.error(_(f"The profile [{self.args.profile}] doesn't exist")) |
729 self.quit(1) | 833 self.quit(C.EXIT_ERROR) |
730 | 834 |
731 try: | 835 try: |
732 start_session = self.args.start_session | 836 start_session = self.args.start_session |
733 except AttributeError: | 837 except AttributeError: |
734 pass | 838 pass |
735 else: | 839 else: |
736 if start_session: | 840 if start_session: |
737 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session) | 841 try: |
738 self._auto_loop = True | 842 await self.bridge.profileStartSession(self.args.pwd, self.profile) |
843 except Exception as e: | |
844 self.disp(_(f"Can't start {self.profile}'s session: {e}"), err=True) | |
845 self.quit(1) | |
739 return | 846 return |
740 elif not self.bridge.profileIsSessionStarted(self.profile): | 847 elif not await self.bridge.profileIsSessionStarted(self.profile): |
741 if not self.args.connect: | 848 if not self.args.connect: |
742 log.error(_("Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile)) | 849 self.disp(_( |
850 f"Session for [{self.profile}] is not started, please start it " | |
851 f"before using jp, or use either --start-session or --connect " | |
852 f"option"), error=True) | |
743 self.quit(1) | 853 self.quit(1) |
744 elif not getattr(self.args, "connect", False): | 854 elif not getattr(self.args, "connect", False): |
745 callback() | |
746 return | 855 return |
747 | 856 |
748 | 857 |
749 if not hasattr(self.args, 'connect'): | 858 if not hasattr(self.args, 'connect'): |
750 # a profile can be present without connect option (e.g. on profile creation/deletion) | 859 # a profile can be present without connect option (e.g. on profile |
860 # creation/deletion) | |
751 return | 861 return |
752 elif self.args.connect is True: # if connection is asked, we connect the profile | 862 elif self.args.connect is True: # if connection is asked, we connect the profile |
753 self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect) | 863 try: |
754 self._auto_loop = True | 864 await self.bridge.connect(self.profile, self.args.pwd, {}) |
865 except Exception as e: | |
866 self.disp(_(f"Can't connect profile: {e}"), error=True) | |
867 self.quit(1) | |
755 return | 868 return |
756 else: | 869 else: |
757 if not self.bridge.isConnected(self.profile): | 870 if not await self.bridge.isConnected(self.profile): |
758 log.error(_("Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) | 871 log.error( |
872 _(f"Profile [{self.profile}] is not connected, please connect it " | |
873 f"before using jp, or use --connect option")) | |
759 self.quit(1) | 874 self.quit(1) |
760 | 875 |
761 callback() | 876 async def get_full_jid(self, param_jid): |
762 | |
763 def get_full_jid(self, param_jid): | |
764 """Return the full jid if possible (add main resource when find a bare jid)""" | 877 """Return the full jid if possible (add main resource when find a bare jid)""" |
765 _jid = JID(param_jid) | 878 _jid = JID(param_jid) |
766 if not _jid.resource: | 879 if not _jid.resource: |
767 #if the resource is not given, we try to add the main resource | 880 #if the resource is not given, we try to add the main resource |
768 main_resource = self.bridge.getMainResource(param_jid, self.profile) | 881 main_resource = await self.bridge.getMainResource(param_jid, self.profile) |
769 if main_resource: | 882 if main_resource: |
770 return "%s/%s" % (_jid.bare, main_resource) | 883 return f"{_jid.bare}/{main_resource}" |
771 return param_jid | 884 return param_jid |
772 | 885 |
773 | 886 |
774 class CommandBase(object): | 887 class CommandBase(object): |
775 | 888 |
781 @param name(unicode): name of the new command | 894 @param name(unicode): name of the new command |
782 @param use_profile(bool): if True, add profile selection/connection commands | 895 @param use_profile(bool): if True, add profile selection/connection commands |
783 @param use_output(bool, unicode): if not False, add --output option | 896 @param use_output(bool, unicode): if not False, add --output option |
784 @param extra_outputs(dict): list of command specific outputs: | 897 @param extra_outputs(dict): list of command specific outputs: |
785 key is output name ("default" to use as main output) | 898 key is output name ("default" to use as main output) |
786 value is a callable which will format the output (data will be used as only argument) | 899 value is a callable which will format the output (data will be used as only |
900 argument) | |
787 if a key already exists with normal outputs, the extra one will be used | 901 if a key already exists with normal outputs, the extra one will be used |
788 @param need_connect(bool, None): True if profile connection is needed | 902 @param need_connect(bool, None): True if profile connection is needed |
789 False else (profile session must still be started) | 903 False else (profile session must still be started) |
790 None to set auto value (i.e. True if use_profile is set) | 904 None to set auto value (i.e. True if use_profile is set) |
791 Can't be set if use_profile is False | 905 Can't be set if use_profile is False |
797 - use_verbose(bool): if True, add verbosity option | 911 - use_verbose(bool): if True, add verbosity option |
798 - use_pubsub(bool): if True, add pubsub options | 912 - use_pubsub(bool): if True, add pubsub options |
799 mandatory arguments are controlled by pubsub_req | 913 mandatory arguments are controlled by pubsub_req |
800 - use_draft(bool): if True, add draft handling options | 914 - use_draft(bool): if True, add draft handling options |
801 ** other arguments ** | 915 ** other arguments ** |
802 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be: | 916 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, |
917 can be: | |
803 C.SERVICE: service is required | 918 C.SERVICE: service is required |
804 C.NODE: node is required | 919 C.NODE: node is required |
805 C.ITEM: item is required | 920 C.ITEM: item is required |
806 C.SINGLE_ITEM: only one item is allowed | 921 C.SINGLE_ITEM: only one item is allowed |
807 @attribute need_loop(bool): to set by commands when loop is needed | 922 """ |
808 """ | |
809 self.need_loop = False # to be set by commands when loop is needed | |
810 try: # If we have subcommands, host is a CommandBase and we need to use host.host | 923 try: # If we have subcommands, host is a CommandBase and we need to use host.host |
811 self.host = host.host | 924 self.host = host.host |
812 except AttributeError: | 925 except AttributeError: |
813 self.host = host | 926 self.host = host |
814 | 927 |
815 # --profile option | 928 # --profile option |
816 parents = kwargs.setdefault('parents', set()) | 929 parents = kwargs.setdefault('parents', set()) |
817 if use_profile: | 930 if use_profile: |
818 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments | 931 # self.host.parents['profile'] is an ArgumentParser with profile connection |
932 # arguments | |
819 if need_connect is None: | 933 if need_connect is None: |
820 need_connect = True | 934 need_connect = True |
821 parents.add(self.host.parents['profile' if need_connect else 'profile_session']) | 935 parents.add( |
936 self.host.parents['profile' if need_connect else 'profile_session']) | |
822 else: | 937 else: |
823 assert need_connect is None | 938 assert need_connect is None |
824 self.need_connect = need_connect | 939 self.need_connect = need_connect |
825 # from this point, self.need_connect is None if connection is not needed at all | 940 # from this point, self.need_connect is None if connection is not needed at all |
826 # False if session starting is needed, and True if full connection is needed | 941 # False if session starting is needed, and True if full connection is needed |
836 self._output_type = use_output | 951 self._output_type = use_output |
837 output_parent = argparse.ArgumentParser(add_help=False) | 952 output_parent = argparse.ArgumentParser(add_help=False) |
838 choices = set(self.host.getOutputChoices(use_output)) | 953 choices = set(self.host.getOutputChoices(use_output)) |
839 choices.update(extra_outputs) | 954 choices.update(extra_outputs) |
840 if not choices: | 955 if not choices: |
841 raise exceptions.InternalError("No choice found for {} output type".format(use_output)) | 956 raise exceptions.InternalError( |
957 "No choice found for {} output type".format(use_output)) | |
842 try: | 958 try: |
843 default = self.host.default_output[use_output] | 959 default = self.host.default_output[use_output] |
844 except KeyError: | 960 except KeyError: |
845 if 'default' in choices: | 961 if 'default' in choices: |
846 default = 'default' | 962 default = 'default' |
847 elif 'simple' in choices: | 963 elif 'simple' in choices: |
848 default = 'simple' | 964 default = 'simple' |
849 else: | 965 else: |
850 default = list(choices)[0] | 966 default = list(choices)[0] |
851 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_("select output format (default: {})".format(default))) | 967 output_parent.add_argument( |
852 output_parent.add_argument('--output-option', '--oo', action="append", dest='output_opts', default=[], help=_("output specific option")) | 968 '--output', '-O', choices=sorted(choices), default=default, |
969 help=_("select output format (default: {})".format(default))) | |
970 output_parent.add_argument( | |
971 '--output-option', '--oo', action="append", dest='output_opts', | |
972 default=[], help=_("output specific option")) | |
853 parents.add(output_parent) | 973 parents.add(output_parent) |
854 else: | 974 else: |
855 assert extra_outputs is None | 975 assert extra_outputs is None |
856 | 976 |
857 self._use_pubsub = kwargs.pop('use_pubsub', False) | 977 self._use_pubsub = kwargs.pop('use_pubsub', False) |
871 if do_use: | 991 if do_use: |
872 parents.add(self.host.parents[opt]) | 992 parents.add(self.host.parents[opt]) |
873 | 993 |
874 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) | 994 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) |
875 if hasattr(self, "subcommands"): | 995 if hasattr(self, "subcommands"): |
876 self.subparsers = self.parser.add_subparsers() | 996 self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True) |
877 else: | 997 else: |
878 self.parser.set_defaults(_cmd=self) | 998 self.parser.set_defaults(_cmd=self) |
879 self.add_parser_options() | 999 self.add_parser_options() |
880 | 1000 |
881 @property | 1001 @property |
892 | 1012 |
893 @property | 1013 @property |
894 def progress_id(self): | 1014 def progress_id(self): |
895 return self.host.progress_id | 1015 return self.host.progress_id |
896 | 1016 |
897 @progress_id.setter | 1017 async def set_progress_id(self, progress_id): |
898 def progress_id(self, value): | 1018 return await self.host.set_progress_id(progress_id) |
899 self.host.progress_id = value | 1019 |
900 | 1020 async def progressStartedHandler(self, uid, metadata, profile): |
901 def progressStartedHandler(self, uid, metadata, profile): | |
902 if profile != self.profile: | 1021 if profile != self.profile: |
903 return | 1022 return |
904 if self.progress_id is None: | 1023 if self.progress_id is None: |
905 # the progress started message can be received before the id | 1024 # the progress started message can be received before the id |
906 # so we keep progressStarted signals in cache to replay they | 1025 # so we keep progressStarted signals in cache to replay they |
907 # when the progress_id is received | 1026 # when the progress_id is received |
908 cache_data = (self.progressStartedHandler, uid, metadata, profile) | 1027 cache_data = (self.progressStartedHandler, uid, metadata, profile) |
909 try: | 1028 try: |
910 self.host.progress_ids_cache.append(cache_data) | 1029 cache = self.host.progress_ids_cache |
911 except AttributeError: | 1030 except AttributeError: |
912 self.host.progress_ids_cache = [cache_data] | 1031 cache = self.host.progress_ids_cache = [] |
1032 cache.append(cache_data) | |
913 else: | 1033 else: |
914 if self.host.watch_progress and uid == self.progress_id: | 1034 if self.host.watch_progress and uid == self.progress_id: |
915 self.onProgressStarted(metadata) | 1035 await self.onProgressStarted(metadata) |
916 self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate) | 1036 while True: |
917 | 1037 await asyncio.sleep(PROGRESS_DELAY) |
918 def progressFinishedHandler(self, uid, metadata, profile): | 1038 cont = await self.progressUpdate() |
1039 if not cont: | |
1040 break | |
1041 | |
1042 async def progressFinishedHandler(self, uid, metadata, profile): | |
919 if profile != self.profile: | 1043 if profile != self.profile: |
920 return | 1044 return |
921 if uid == self.progress_id: | 1045 if uid == self.progress_id: |
922 try: | 1046 try: |
923 self.host.pbar.finish() | 1047 self.host.pbar.finish() |
924 except AttributeError: | 1048 except AttributeError: |
925 pass | 1049 pass |
926 self.onProgressFinished(metadata) | 1050 await self.onProgressFinished(metadata) |
927 if self.host.quit_on_progress_end: | 1051 if self.host.quit_on_progress_end: |
928 self.host.quitFromSignal() | 1052 self.host.quitFromSignal() |
929 | 1053 |
930 def progressErrorHandler(self, uid, message, profile): | 1054 async def progressErrorHandler(self, uid, message, profile): |
931 if profile != self.profile: | 1055 if profile != self.profile: |
932 return | 1056 return |
933 if uid == self.progress_id: | 1057 if uid == self.progress_id: |
934 if self.args.progress: | 1058 if self.args.progress: |
935 self.disp('') # progress is not finished, so we skip a line | 1059 self.disp('') # progress is not finished, so we skip a line |
936 if self.host.quit_on_progress_end: | 1060 if self.host.quit_on_progress_end: |
937 self.onProgressError(message) | 1061 await self.onProgressError(message) |
938 self.host.quitFromSignal(1) | 1062 self.host.quitFromSignal(C.EXIT_ERROR) |
939 | 1063 |
940 def progressUpdate(self): | 1064 async def progressUpdate(self): |
941 """This method is continualy called to update the progress bar""" | 1065 """This method is continualy called to update the progress bar |
942 data = self.host.bridge.progressGet(self.progress_id, self.profile) | 1066 |
1067 @return (bool): False to stop being called | |
1068 """ | |
1069 data = await self.host.bridge.progressGet(self.progress_id, self.profile) | |
943 if data: | 1070 if data: |
944 try: | 1071 try: |
945 size = data['size'] | 1072 size = data['size'] |
946 except KeyError: | 1073 except KeyError: |
947 self.disp(_("file size is not known, we can't show a progress bar"), 1, error=True) | 1074 self.disp(_("file size is not known, we can't show a progress bar"), 1, |
1075 error=True) | |
948 return False | 1076 return False |
949 if self.host.pbar is None: | 1077 if self.host.pbar is None: |
950 #first answer, we must construct the bar | 1078 #first answer, we must construct the bar |
951 self.host.pbar = progressbar.ProgressBar(max_value=int(size), | 1079 |
952 widgets=[_("Progress: "),progressbar.Percentage(), | 1080 # if the instance has a pbar_template attribute, it is used has model, |
953 " ", | 1081 # else default one is used |
954 progressbar.Bar(), | 1082 # template is a list of part, where part can be either a str to show directly |
955 " ", | 1083 # or a list where first argument is a name of a progressbar widget, and others |
956 progressbar.FileTransferSpeed(), | 1084 # are used as widget arguments |
957 " ", | 1085 try: |
958 progressbar.ETA()]) | 1086 template = self.pbar_template |
1087 except AttributeError: | |
1088 template = [ | |
1089 _("Progress: "), ["Percentage"], " ", ["Bar"], " ", | |
1090 ["FileTransferSpeed"], " ", ["ETA"] | |
1091 ] | |
1092 | |
1093 widgets = [] | |
1094 for part in template: | |
1095 if isinstance(part, str): | |
1096 widgets.append(part) | |
1097 else: | |
1098 widget = getattr(progressbar, part.pop(0)) | |
1099 widgets.append(widget(*part)) | |
1100 | |
1101 self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets) | |
959 self.host.pbar.start() | 1102 self.host.pbar.start() |
960 | 1103 |
961 self.host.pbar.update(int(data['position'])) | 1104 self.host.pbar.update(int(data['position'])) |
962 | 1105 |
963 elif self.host.pbar is not None: | 1106 elif self.host.pbar is not None: |
964 return False | 1107 return False |
965 | 1108 |
966 self.onProgressUpdate(data) | 1109 await self.onProgressUpdate(data) |
967 | 1110 |
968 return True | 1111 return True |
969 | 1112 |
970 def onProgressStarted(self, metadata): | 1113 async def onProgressStarted(self, metadata): |
971 """Called when progress has just started | 1114 """Called when progress has just started |
972 | 1115 |
973 can be overidden by a command | 1116 can be overidden by a command |
974 @param metadata(dict): metadata as sent by bridge.progressStarted | 1117 @param metadata(dict): metadata as sent by bridge.progressStarted |
975 """ | 1118 """ |
976 self.disp(_("Operation started"), 2) | 1119 self.disp(_("Operation started"), 2) |
977 | 1120 |
978 def onProgressUpdate(self, metadata): | 1121 async def onProgressUpdate(self, metadata): |
979 """Method called on each progress updata | 1122 """Method called on each progress updata |
980 | 1123 |
981 can be overidden by a command to handle progress metadata | 1124 can be overidden by a command to handle progress metadata |
982 @para metadata(dict): metadata as returned by bridge.progressGet | 1125 @para metadata(dict): metadata as returned by bridge.progressGet |
983 """ | 1126 """ |
984 pass | 1127 pass |
985 | 1128 |
986 def onProgressFinished(self, metadata): | 1129 async def onProgressFinished(self, metadata): |
987 """Called when progress has just finished | 1130 """Called when progress has just finished |
988 | 1131 |
989 can be overidden by a command | 1132 can be overidden by a command |
990 @param metadata(dict): metadata as sent by bridge.progressFinished | 1133 @param metadata(dict): metadata as sent by bridge.progressFinished |
991 """ | 1134 """ |
992 self.disp(_("Operation successfully finished"), 2) | 1135 self.disp(_("Operation successfully finished"), 2) |
993 | 1136 |
994 def onProgressError(self, error_msg): | 1137 async def onProgressError(self, e): |
995 """Called when a progress failed | 1138 """Called when a progress failed |
996 | 1139 |
997 @param error_msg(unicode): error message as sent by bridge.progressError | 1140 @param error_msg(unicode): error message as sent by bridge.progressError |
998 """ | 1141 """ |
999 self.disp(_("Error while doing operation: {}").format(error_msg), error=True) | 1142 self.disp(_(f"Error while doing operation: {e}"), error=True) |
1000 | 1143 |
1001 def disp(self, msg, verbosity=0, error=False, no_lf=False): | 1144 def disp(self, msg, verbosity=0, error=False, no_lf=False): |
1002 return self.host.disp(msg, verbosity, error, no_lf) | 1145 return self.host.disp(msg, verbosity, error, no_lf) |
1003 | 1146 |
1004 def output(self, data): | 1147 def output(self, data): |
1005 try: | 1148 try: |
1006 output_type = self._output_type | 1149 output_type = self._output_type |
1007 except AttributeError: | 1150 except AttributeError: |
1008 raise exceptions.InternalError(_('trying to use output when use_output has not been set')) | 1151 raise exceptions.InternalError( |
1152 _('trying to use output when use_output has not been set')) | |
1009 return self.host.output(output_type, self.args.output, self.extra_outputs, data) | 1153 return self.host.output(output_type, self.args.output, self.extra_outputs, data) |
1010 | |
1011 def exitCb(self, msg=None): | |
1012 """generic callback for success | |
1013 | |
1014 optionally print a message, and quit | |
1015 msg(None, unicode): if not None, print this message | |
1016 """ | |
1017 if msg is not None: | |
1018 self.disp(msg) | |
1019 self.host.quit(C.EXIT_OK) | |
1020 | |
1021 def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR): | |
1022 """generic callback for errbacks | |
1023 | |
1024 display failure_ then quit with generic error | |
1025 @param failure_: arguments returned by errback | |
1026 @param msg(unicode, None): message template | |
1027 use {} if you want to display failure message | |
1028 @param exit_code(int): shell exit code | |
1029 """ | |
1030 if msg is None: | |
1031 msg = _("error: {}") | |
1032 self.disp(msg.format(failure_), error=True) | |
1033 self.host.quit(exit_code) | |
1034 | 1154 |
1035 def getPubsubExtra(self, extra=None): | 1155 def getPubsubExtra(self, extra=None): |
1036 """Helper method to compute extra data from pubsub arguments | 1156 """Helper method to compute extra data from pubsub arguments |
1037 | 1157 |
1038 @param extra(None, dict): base extra dict, or None to generate a new one | 1158 @param extra(None, dict): base extra dict, or None to generate a new one |
1088 | 1208 |
1089 # now we add subcommands to ourself | 1209 # now we add subcommands to ourself |
1090 for cls in subcommands: | 1210 for cls in subcommands: |
1091 cls(self) | 1211 cls(self) |
1092 | 1212 |
1093 def run(self): | 1213 async def run(self): |
1094 """this method is called when a command is actually run | 1214 """this method is called when a command is actually run |
1095 | 1215 |
1096 It set stuff like progression callbacks and profile connection | 1216 It set stuff like progression callbacks and profile connection |
1097 You should not overide this method: you should call self.start instead | 1217 You should not overide this method: you should call self.start instead |
1098 """ | 1218 """ |
1099 # we keep a reference to run command, it may be useful e.g. for outputs | 1219 # we keep a reference to run command, it may be useful e.g. for outputs |
1100 self.host.command = self | 1220 self.host.command = self |
1101 # host._need_loop is set here from our current value and not before | |
1102 # as the need_loop decision must be taken only by then running command | |
1103 self.host._need_loop = self.need_loop | |
1104 | 1221 |
1105 try: | 1222 try: |
1106 show_progress = self.args.progress | 1223 show_progress = self.args.progress |
1107 except AttributeError: | 1224 except AttributeError: |
1108 # the command doesn't use progress bar | 1225 # the command doesn't use progress bar |
1109 pass | 1226 pass |
1110 else: | 1227 else: |
1111 if show_progress: | 1228 if show_progress: |
1112 self.host.watch_progress = True | 1229 self.host.watch_progress = True |
1113 # we need to register the following signal even if we don't display the progress bar | 1230 # we need to register the following signal even if we don't display the |
1114 self.host.bridge.register_signal("progressStarted", self.progressStartedHandler) | 1231 # progress bar |
1115 self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler) | 1232 self.host.bridge.register_signal( |
1116 self.host.bridge.register_signal("progressError", self.progressErrorHandler) | 1233 "progressStarted", self.progressStartedHandler) |
1234 self.host.bridge.register_signal( | |
1235 "progressFinished", self.progressFinishedHandler) | |
1236 self.host.bridge.register_signal( | |
1237 "progressError", self.progressErrorHandler) | |
1117 | 1238 |
1118 if self.need_connect is not None: | 1239 if self.need_connect is not None: |
1119 self.host.connect_profile(self.connected) | 1240 await self.host.connect_profile() |
1120 else: | 1241 await self.start() |
1121 self.start() | 1242 |
1122 | 1243 async def start(self): |
1123 def connected(self): | 1244 """This is the starting point of the command, this method must be overriden |
1124 """this method is called when profile is connected (or session is started) | |
1125 | |
1126 this method is only called when use_profile is True | |
1127 most of time you should override self.start instead of this method, but if loop | |
1128 if not always needed depending on your arguments, you may override this method, | |
1129 but don't forget to call the parent one (i.e. this one) after self.need_loop is set | |
1130 """ | |
1131 if not self.need_loop: | |
1132 self.host.stop_loop() | |
1133 self.start() | |
1134 | |
1135 def start(self): | |
1136 """This is the starting point of the command, this method should be overriden | |
1137 | 1245 |
1138 at this point, profile are connected if needed | 1246 at this point, profile are connected if needed |
1139 """ | 1247 """ |
1140 pass | 1248 raise NotImplementedError |
1141 | 1249 |
1142 | 1250 |
1143 class CommandAnswering(CommandBase): | 1251 class CommandAnswering(CommandBase): |
1144 """Specialised commands which answer to specific actions | 1252 """Specialised commands which answer to specific actions |
1145 | 1253 |
1150 # which will manage the answer. profile filtering is | 1258 # which will manage the answer. profile filtering is |
1151 # already managed when callback is called | 1259 # already managed when callback is called |
1152 | 1260 |
1153 def __init__(self, *args, **kwargs): | 1261 def __init__(self, *args, **kwargs): |
1154 super(CommandAnswering, self).__init__(*args, **kwargs) | 1262 super(CommandAnswering, self).__init__(*args, **kwargs) |
1155 self.need_loop = True | 1263 |
1156 | 1264 async def onActionNew(self, action_data, action_id, security_limit, profile): |
1157 def onActionNew(self, action_data, action_id, security_limit, profile): | |
1158 if profile != self.profile: | 1265 if profile != self.profile: |
1159 return | 1266 return |
1160 try: | 1267 try: |
1161 action_type = action_data['meta_type'] | 1268 action_type = action_data['meta_type'] |
1162 except KeyError: | 1269 except KeyError: |
1170 try: | 1277 try: |
1171 callback = self.action_callbacks[action_type] | 1278 callback = self.action_callbacks[action_type] |
1172 except KeyError: | 1279 except KeyError: |
1173 pass | 1280 pass |
1174 else: | 1281 else: |
1175 callback(action_data, action_id, security_limit, profile) | 1282 await callback(action_data, action_id, security_limit, profile) |
1176 | 1283 |
1177 def onXMLUI(self, xml_ui): | 1284 def onXMLUI(self, xml_ui): |
1178 """Display a dialog received from the backend. | 1285 """Display a dialog received from the backend. |
1179 | 1286 |
1180 @param xml_ui (unicode): dialog XML representation | 1287 @param xml_ui (unicode): dialog XML representation |
1185 ui = ET.fromstring(xml_ui.encode('utf-8')) | 1292 ui = ET.fromstring(xml_ui.encode('utf-8')) |
1186 dialog = ui.find("dialog") | 1293 dialog = ui.find("dialog") |
1187 if dialog is not None: | 1294 if dialog is not None: |
1188 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") | 1295 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") |
1189 | 1296 |
1190 def connected(self): | 1297 async def start_answering(self): |
1191 """Auto reply to confirmations requests""" | 1298 """Auto reply to confirmation requests""" |
1192 self.need_loop = True | |
1193 super(CommandAnswering, self).connected() | |
1194 self.host.bridge.register_signal("actionNew", self.onActionNew) | 1299 self.host.bridge.register_signal("actionNew", self.onActionNew) |
1195 actions = self.host.bridge.actionsGet(self.profile) | 1300 actions = await self.host.bridge.actionsGet(self.profile) |
1196 for action_data, action_id, security_limit in actions: | 1301 for action_data, action_id, security_limit in actions: |
1197 self.onActionNew(action_data, action_id, security_limit, self.profile) | 1302 await self.onActionNew(action_data, action_id, security_limit, self.profile) |