comparison libervia/cli/base.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 5115976e1e3d
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
19 import asyncio 19 import asyncio
20 from libervia.backend.core.i18n import _ 20 from libervia.backend.core.i18n import _
21 21
22 ### logging ### 22 ### logging ###
23 import logging as log 23 import logging as log
24 log.basicConfig(level=log.WARNING, 24
25 format='[%(name)s] %(message)s') 25 log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s")
26 ### 26 ###
27 27
28 import sys 28 import sys
29 import os 29 import os
30 import os.path 30 import os.path
53 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI 53 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
54 from collections import OrderedDict 54 from collections import OrderedDict
55 from rich import console 55 from rich import console
56 56
57 ## bridge handling 57 ## bridge handling
58 # we get bridge name from conf and initialise the right class accordingly 58 # we get bridge name from conf and initialise the right class accordingly
59 main_config = config.parse_main_conf() 59 main_config = config.parse_main_conf()
60 bridge_name = config.config_get(main_config, '', 'bridge', 'dbus') 60 bridge_name = config.config_get(main_config, "", "bridge", "dbus")
61 LiberviaCLILoop = get_libervia_cli_loop(bridge_name) 61 LiberviaCLILoop = get_libervia_cli_loop(bridge_name)
62 62
63 63
64 try: 64 try:
65 import progressbar 65 import progressbar
66 except ImportError: 66 except ImportError:
67 msg = (_('ProgressBar not available, please download it at ' 67 msg = _(
68 'http://pypi.python.org/pypi/progressbar\n' 68 "ProgressBar not available, please download it at "
69 'Progress bar deactivated\n--\n')) 69 "http://pypi.python.org/pypi/progressbar\n"
70 "Progress bar deactivated\n--\n"
71 )
70 print(msg, file=sys.stderr) 72 print(msg, file=sys.stderr)
71 progressbar=None 73 progressbar = None
72 74
73 #consts 75 # consts
74 DESCRIPTION = """This software is a command line tool for XMPP. 76 DESCRIPTION = (
75 Get the latest version at """ + C.APP_URL 77 """This software is a command line tool for XMPP.
78 Get the latest version at """
79 + C.APP_URL
80 )
76 81
77 COPYLEFT = """Copyright (C) 2009-2024 Jérôme Poisson, Adrien Cossa 82 COPYLEFT = """Copyright (C) 2009-2024 Jérôme Poisson, Adrien Cossa
78 This program comes with ABSOLUTELY NO WARRANTY; 83 This program comes with ABSOLUTELY NO WARRANTY;
79 This is free software, and you are welcome to redistribute it under certain conditions. 84 This is free software, and you are welcome to redistribute it under certain conditions.
80 """ 85 """
81 86
82 PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s 87 PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s
83 88
84 89
85 def date_decoder(arg): 90 def date_decoder(arg):
86 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) 91 return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
87 92
93 98
94 To use it, you mainly have to redefine the method run to perform 99 To use it, you mainly have to redefine the method run to perform
95 specify what kind of operation you want to perform. 100 specify what kind of operation you want to perform.
96 101
97 """ 102 """
103
98 def __init__(self): 104 def __init__(self):
99 """ 105 """
100 106
101 @attribute quit_on_progress_end (bool): set to False if you manage yourself 107 @attribute quit_on_progress_end (bool): set to False if you manage yourself
102 exiting, or if you want the user to stop by himself 108 exiting, or if you want the user to stop by himself
108 by default display a message 114 by default display a message
109 """ 115 """
110 self.console = console.Console(theme=C.THEME_DEFAULT) 116 self.console = console.Console(theme=C.THEME_DEFAULT)
111 self.sat_conf = main_config 117 self.sat_conf = main_config
112 self.set_color_theme() 118 self.set_color_theme()
113 bridge_module = dynamic_import.bridge(bridge_name, 'libervia.frontends.bridge') 119 bridge_module = dynamic_import.bridge(bridge_name, "libervia.frontends.bridge")
114 if bridge_module is None: 120 if bridge_module is None:
115 log.error("Can't import {} bridge".format(bridge_name)) 121 log.error("Can't import {} bridge".format(bridge_name))
116 sys.exit(1) 122 sys.exit(1)
117 123
118 self.bridge = bridge_module.AIOBridge() 124 self.bridge = bridge_module.AIOBridge()
130 except ValueError: 136 except ValueError:
131 vte_version = 0 137 vte_version = 0
132 138
133 color_fg_bg = os.getenv("COLORFGBG") 139 color_fg_bg = os.getenv("COLORFGBG")
134 140
135 if ((sys.stdin.isatty() and sys.stdout.isatty() 141 if (
136 and ( 142 sys.stdin.isatty()
137 # XTerm 143 and sys.stdout.isatty()
138 os.getenv("XTERM_VERSION") 144 and (
139 # Konsole 145 # XTerm
140 or os.getenv("KONSOLE_VERSION") 146 os.getenv("XTERM_VERSION")
141 # All VTE based terminals 147 # Konsole
142 or vte_version >= 3502 148 or os.getenv("KONSOLE_VERSION")
143 ))): 149 # All VTE based terminals
150 or vte_version >= 3502
151 )
152 ):
144 # ANSI escape sequence 153 # ANSI escape sequence
145 stdin_fd = sys.stdin.fileno() 154 stdin_fd = sys.stdin.fileno()
146 old_settings = termios.tcgetattr(stdin_fd) 155 old_settings = termios.tcgetattr(stdin_fd)
147 try: 156 try:
148 tty.setraw(sys.stdin.fileno()) 157 tty.setraw(sys.stdin.fileno())
153 for c in expected: 162 for c in expected:
154 ch = sys.stdin.read(1) 163 ch = sys.stdin.read(1)
155 if ch != c: 164 if ch != c:
156 # background id is not supported, we default to "dark" 165 # background id is not supported, we default to "dark"
157 # TODO: log something? 166 # TODO: log something?
158 return 'dark' 167 return "dark"
159 red, green, blue = [ 168 red, green, blue = [
160 int(c, 16)/65535 for c in sys.stdin.read(14).split('/') 169 int(c, 16) / 65535 for c in sys.stdin.read(14).split("/")
161 ] 170 ]
162 # '\a' is the last character 171 # '\a' is the last character
163 sys.stdin.read(1) 172 sys.stdin.read(1)
164 finally: 173 finally:
165 termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) 174 termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
166 175
167 lum = utils.per_luminance(red, green, blue) 176 lum = utils.per_luminance(red, green, blue)
168 if lum <= 0.5: 177 if lum <= 0.5:
169 return 'dark' 178 return "dark"
170 else: 179 else:
171 return 'light' 180 return "light"
172 elif color_fg_bg: 181 elif color_fg_bg:
173 # no luck with ANSI escape sequence, we try COLORFGBG environment variable 182 # no luck with ANSI escape sequence, we try COLORFGBG environment variable
174 try: 183 try:
175 bg = int(color_fg_bg.split(";")[-1]) 184 bg = int(color_fg_bg.split(";")[-1])
176 except ValueError: 185 except ValueError:
182 else: 191 else:
183 # no autodetection method found 192 # no autodetection method found
184 return "dark" 193 return "dark"
185 194
186 def set_color_theme(self): 195 def set_color_theme(self):
187 background = self.get_config('background', default='auto') 196 background = self.get_config("background", default="auto")
188 if background == 'auto': 197 if background == "auto":
189 background = self.guess_background() 198 background = self.guess_background()
190 if background not in ('dark', 'light'): 199 if background not in ("dark", "light"):
191 raise exceptions.ConfigError(_( 200 raise exceptions.ConfigError(
192 'Invalid value set for "background" ({background}), please check ' 201 _(
193 'your settings in libervia.conf').format( 202 'Invalid value set for "background" ({background}), please check '
194 background=repr(background) 203 "your settings in libervia.conf"
195 )) 204 ).format(background=repr(background))
205 )
196 self.background = background 206 self.background = background
197 if background == 'light': 207 if background == "light":
198 C.A_HEADER = A.FG_MAGENTA 208 C.A_HEADER = A.FG_MAGENTA
199 C.A_SUBHEADER = A.BOLD + A.FG_RED 209 C.A_SUBHEADER = A.BOLD + A.FG_RED
200 C.A_LEVEL_COLORS = (C.A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN) 210 C.A_LEVEL_COLORS = (C.A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN)
201 C.A_SUCCESS = A.FG_GREEN 211 C.A_SUCCESS = A.FG_GREEN
202 C.A_FAILURE = A.BOLD + A.FG_RED 212 C.A_FAILURE = A.BOLD + A.FG_RED
206 C.A_DIRECTORY = A.BOLD + A.FG_MAGENTA 216 C.A_DIRECTORY = A.BOLD + A.FG_MAGENTA
207 C.A_FILE = A.FG_BLACK 217 C.A_FILE = A.FG_BLACK
208 218
209 def _bridge_connected(self): 219 def _bridge_connected(self):
210 self.parser = argparse.ArgumentParser( 220 self.parser = argparse.ArgumentParser(
211 formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) 221 formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION
222 )
212 self._make_parents() 223 self._make_parents()
213 self.add_parser_options() 224 self.add_parser_options()
214 self.subparsers = self.parser.add_subparsers( 225 self.subparsers = self.parser.add_subparsers(
215 title=_('Available commands'), dest='command', required=True) 226 title=_("Available commands"), dest="command", required=True
227 )
216 228
217 # progress attributes 229 # progress attributes
218 self._progress_id = None # TODO: manage several progress ids 230 self._progress_id = None # TODO: manage several progress ids
219 self.quit_on_progress_end = True 231 self.quit_on_progress_end = True
220 232
221 # outputs 233 # outputs
222 self._outputs = {} 234 self._outputs = {}
223 for type_ in C.OUTPUT_TYPES: 235 for type_ in C.OUTPUT_TYPES:
231 return self._progress_id 243 return self._progress_id
232 244
233 async def set_progress_id(self, progress_id): 245 async def set_progress_id(self, progress_id):
234 # because we use async, we need an explicit setter 246 # because we use async, we need an explicit setter
235 self._progress_id = progress_id 247 self._progress_id = progress_id
236 await self.replay_cache('progress_ids_cache') 248 await self.replay_cache("progress_ids_cache")
237 249
238 @property 250 @property
239 def watch_progress(self): 251 def watch_progress(self):
240 try: 252 try:
241 self.pbar 253 self.pbar
270 pass 282 pass
271 else: 283 else:
272 for cache_data in cache: 284 for cache_data in cache:
273 await cache_data[0](*cache_data[1:]) 285 await cache_data[0](*cache_data[1:])
274 286
275 def disp(self, msg, verbosity=0, error=False, end='\n'): 287 def disp(self, msg, verbosity=0, error=False, end="\n"):
276 """Print a message to user 288 """Print a message to user
277 289
278 @param msg(unicode): message to print 290 @param msg(unicode): message to print
279 @param verbosity(int): minimal verbosity to display the message 291 @param verbosity(int): minimal verbosity to display the message
280 @param error(bool): if True, print to stderr instead of stdout 292 @param error(bool): if True, print to stderr instead of stdout
288 300
289 async def output(self, type_, name, extra_outputs, data): 301 async def output(self, type_, name, extra_outputs, data):
290 if name in extra_outputs: 302 if name in extra_outputs:
291 method = extra_outputs[name] 303 method = extra_outputs[name]
292 else: 304 else:
293 method = self._outputs[type_][name]['callback'] 305 method = self._outputs[type_][name]["callback"]
294 306
295 ret = method(data) 307 ret = method(data)
296 if inspect.isawaitable(ret): 308 if inspect.isawaitable(ret):
297 await ret 309 await ret
298 310
315 self.parents = {} 327 self.parents = {}
316 328
317 # we have a special case here as the start-session option is present only if 329 # we have a special case here as the start-session option is present only if
318 # connection is not needed, so we create two similar parents, one with the 330 # connection is not needed, so we create two similar parents, one with the
319 # option, the other one without it 331 # option, the other one without it
320 for parent_name in ('profile', 'profile_session'): 332 for parent_name in ("profile", "profile_session"):
321 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) 333 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
322 parent.add_argument( 334 parent.add_argument(
323 "-p", "--profile", action="store", type=str, default='@DEFAULT@', 335 "-p",
324 help=_("Use PROFILE profile key (default: %(default)s)")) 336 "--profile",
337 action="store",
338 type=str,
339 default="@DEFAULT@",
340 help=_("Use PROFILE profile key (default: %(default)s)"),
341 )
325 parent.add_argument( 342 parent.add_argument(
326 "--pwd", action="store", metavar='PASSWORD', 343 "--pwd",
327 help=_("Password used to connect profile, if necessary")) 344 action="store",
328 345 metavar="PASSWORD",
329 profile_parent, profile_session_parent = (self.parents['profile'], 346 help=_("Password used to connect profile, if necessary"),
330 self.parents['profile_session']) 347 )
348
349 profile_parent, profile_session_parent = (
350 self.parents["profile"],
351 self.parents["profile_session"],
352 )
331 353
332 connect_short, connect_long, connect_action, connect_help = ( 354 connect_short, connect_long, connect_action, connect_help = (
333 "-c", "--connect", "store_true", 355 "-c",
334 _("Connect the profile before doing anything else") 356 "--connect",
357 "store_true",
358 _("Connect the profile before doing anything else"),
335 ) 359 )
336 profile_parent.add_argument( 360 profile_parent.add_argument(
337 connect_short, connect_long, action=connect_action, help=connect_help) 361 connect_short, connect_long, action=connect_action, help=connect_help
338 362 )
339 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() 363
364 profile_session_connect_group = (
365 profile_session_parent.add_mutually_exclusive_group()
366 )
340 profile_session_connect_group.add_argument( 367 profile_session_connect_group.add_argument(
341 connect_short, connect_long, action=connect_action, help=connect_help) 368 connect_short, connect_long, action=connect_action, help=connect_help
369 )
342 profile_session_connect_group.add_argument( 370 profile_session_connect_group.add_argument(
343 "--start-session", action="store_true", 371 "--start-session",
344 help=_("Start a profile session without connecting")) 372 action="store_true",
345 373 help=_("Start a profile session without connecting"),
346 progress_parent = self.parents['progress'] = argparse.ArgumentParser( 374 )
347 add_help=False) 375
376 progress_parent = self.parents["progress"] = argparse.ArgumentParser(
377 add_help=False
378 )
348 if progressbar: 379 if progressbar:
349 progress_parent.add_argument( 380 progress_parent.add_argument(
350 "-P", "--progress", action="store_true", help=_("Show progress bar")) 381 "-P", "--progress", action="store_true", help=_("Show progress bar")
351 382 )
352 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) 383
384 verbose_parent = self.parents["verbose"] = argparse.ArgumentParser(add_help=False)
353 verbose_parent.add_argument( 385 verbose_parent.add_argument(
354 '--verbose', '-v', action='count', default=0, 386 "--verbose",
355 help=_("Add a verbosity level (can be used multiple times)")) 387 "-v",
356 388 action="count",
357 quiet_parent = self.parents['quiet'] = argparse.ArgumentParser(add_help=False) 389 default=0,
390 help=_("Add a verbosity level (can be used multiple times)"),
391 )
392
393 quiet_parent = self.parents["quiet"] = argparse.ArgumentParser(add_help=False)
358 quiet_parent.add_argument( 394 quiet_parent.add_argument(
359 '--quiet', '-q', action='store_true', 395 "--quiet",
360 help=_("be quiet (only output machine readable data)")) 396 "-q",
361 397 action="store_true",
362 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) 398 help=_("be quiet (only output machine readable data)"),
363 draft_group = draft_parent.add_argument_group(_('draft handling')) 399 )
400
401 draft_parent = self.parents["draft"] = argparse.ArgumentParser(add_help=False)
402 draft_group = draft_parent.add_argument_group(_("draft handling"))
364 draft_group.add_argument( 403 draft_group.add_argument(
365 "-D", "--current", action="store_true", help=_("load current draft")) 404 "-D", "--current", action="store_true", help=_("load current draft")
405 )
366 draft_group.add_argument( 406 draft_group.add_argument(
367 "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")) 407 "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")
368 408 )
369 409
370 def make_pubsub_group(self, flags, defaults): 410 def make_pubsub_group(self, flags, defaults):
371 """Generate pubsub options according to flags 411 """Generate pubsub options according to flags
372 412
373 @param flags(iterable[unicode]): see [CommandBase.__init__] 413 @param flags(iterable[unicode]): see [CommandBase.__init__]
376 value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT 416 value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT
377 @return (ArgumentParser): parser to add 417 @return (ArgumentParser): parser to add
378 """ 418 """
379 flags = misc.FlagsHandler(flags) 419 flags = misc.FlagsHandler(flags)
380 parent = argparse.ArgumentParser(add_help=False) 420 parent = argparse.ArgumentParser(add_help=False)
381 pubsub_group = parent.add_argument_group('pubsub') 421 pubsub_group = parent.add_argument_group("pubsub")
382 pubsub_group.add_argument("-u", "--pubsub-url", 422 pubsub_group.add_argument(
383 help=_("Pubsub URL (xmpp or http)")) 423 "-u", "--pubsub-url", help=_("Pubsub URL (xmpp or http)")
424 )
384 425
385 service_help = _("JID of the PubSub service") 426 service_help = _("JID of the PubSub service")
386 if not flags.service: 427 if not flags.service:
387 default = defaults.pop('service', _('PEP service')) 428 default = defaults.pop("service", _("PEP service"))
388 if default is not None: 429 if default is not None:
389 service_help += _(" (DEFAULT: {default})".format(default=default)) 430 service_help += _(" (DEFAULT: {default})".format(default=default))
390 pubsub_group.add_argument("-s", "--service", default='', 431 pubsub_group.add_argument("-s", "--service", default="", help=service_help)
391 help=service_help)
392 432
393 node_help = _("node to request") 433 node_help = _("node to request")
394 if not flags.node: 434 if not flags.node:
395 default = defaults.pop('node', _('standard node')) 435 default = defaults.pop("node", _("standard node"))
396 if default is not None: 436 if default is not None:
397 node_help += _(" (DEFAULT: {default})".format(default=default)) 437 node_help += _(" (DEFAULT: {default})".format(default=default))
398 pubsub_group.add_argument("-n", "--node", default='', help=node_help) 438 pubsub_group.add_argument("-n", "--node", default="", help=node_help)
399 439
400 if flags.single_item: 440 if flags.single_item:
401 item_help = ("item to retrieve") 441 item_help = "item to retrieve"
402 if not flags.item: 442 if not flags.item:
403 default = defaults.pop('item', _('last item')) 443 default = defaults.pop("item", _("last item"))
404 if default is not None: 444 if default is not None:
405 item_help += _(" (DEFAULT: {default})".format(default=default)) 445 item_help += _(" (DEFAULT: {default})".format(default=default))
406 pubsub_group.add_argument("-i", "--item", default='', 446 pubsub_group.add_argument("-i", "--item", default="", help=item_help)
407 help=item_help)
408 pubsub_group.add_argument( 447 pubsub_group.add_argument(
409 "-L", "--last-item", action='store_true', help=_('retrieve last item')) 448 "-L", "--last-item", action="store_true", help=_("retrieve last item")
449 )
410 elif flags.multi_items: 450 elif flags.multi_items:
411 # mutiple items, this activate several features: max-items, RSM, MAM 451 # mutiple items, this activate several features: max-items, RSM, MAM
412 # and Orbder-by 452 # and Orbder-by
413 pubsub_group.add_argument( 453 pubsub_group.add_argument(
414 "-i", "--item", action='append', dest='items', default=[], 454 "-i",
415 help=_("items to retrieve (DEFAULT: all)")) 455 "--item",
456 action="append",
457 dest="items",
458 default=[],
459 help=_("items to retrieve (DEFAULT: all)"),
460 )
416 if not flags.no_max: 461 if not flags.no_max:
417 max_group = pubsub_group.add_mutually_exclusive_group() 462 max_group = pubsub_group.add_mutually_exclusive_group()
418 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args 463 # XXX: defaut value for --max-items or --max is set in parse_pubsub_args
419 max_group.add_argument( 464 max_group.add_argument(
420 "-M", "--max-items", dest="max", type=int, 465 "-M",
421 help=_("maximum number of items to get ({no_limit} to get all items)" 466 "--max-items",
422 .format(no_limit=C.NO_LIMIT))) 467 dest="max",
468 type=int,
469 help=_(
470 "maximum number of items to get ({no_limit} to get all items)".format(
471 no_limit=C.NO_LIMIT
472 )
473 ),
474 )
423 # FIXME: it could be possible to no duplicate max (between pubsub 475 # FIXME: it could be possible to no duplicate max (between pubsub
424 # max-items and RSM max)should not be duplicated, RSM could be 476 # max-items and RSM max)should not be duplicated, RSM could be
425 # used when available and pubsub max otherwise 477 # used when available and pubsub max otherwise
426 max_group.add_argument( 478 max_group.add_argument(
427 "-m", "--max", dest="rsm_max", type=int, 479 "-m",
428 help=_("maximum number of items to get per page (DEFAULT: 10)")) 480 "--max",
481 dest="rsm_max",
482 type=int,
483 help=_("maximum number of items to get per page (DEFAULT: 10)"),
484 )
429 485
430 # RSM 486 # RSM
431 487
432 rsm_page_group = pubsub_group.add_mutually_exclusive_group() 488 rsm_page_group = pubsub_group.add_mutually_exclusive_group()
433 rsm_page_group.add_argument( 489 rsm_page_group.add_argument(
434 "-a", "--after", dest="rsm_after", 490 "-a",
435 help=_("find page after this item"), metavar='ITEM_ID') 491 "--after",
492 dest="rsm_after",
493 help=_("find page after this item"),
494 metavar="ITEM_ID",
495 )
436 rsm_page_group.add_argument( 496 rsm_page_group.add_argument(
437 "-b", "--before", dest="rsm_before", 497 "-b",
438 help=_("find page before this item"), metavar='ITEM_ID') 498 "--before",
499 dest="rsm_before",
500 help=_("find page before this item"),
501 metavar="ITEM_ID",
502 )
439 rsm_page_group.add_argument( 503 rsm_page_group.add_argument(
440 "--index", dest="rsm_index", type=int, 504 "--index",
441 help=_("index of the first item to retrieve")) 505 dest="rsm_index",
442 506 type=int,
507 help=_("index of the first item to retrieve"),
508 )
443 509
444 # MAM 510 # MAM
445 511
446 pubsub_group.add_argument( 512 pubsub_group.add_argument(
447 "-f", "--filter", dest='mam_filters', nargs=2, 513 "-f",
448 action='append', default=[], help=_("MAM filters to use"), 514 "--filter",
449 metavar=("FILTER_NAME", "VALUE") 515 dest="mam_filters",
516 nargs=2,
517 action="append",
518 default=[],
519 help=_("MAM filters to use"),
520 metavar=("FILTER_NAME", "VALUE"),
450 ) 521 )
451 522
452 # Order-By 523 # Order-By
453 524
454 # TODO: order-by should be a list to handle several levels of ordering 525 # TODO: order-by should be a list to handle several levels of ordering
455 # but this is not yet done in SàT (and not really useful with 526 # but this is not yet done in SàT (and not really useful with
456 # current specifications, as only "creation" and "modification" are 527 # current specifications, as only "creation" and "modification" are
457 # available) 528 # available)
458 pubsub_group.add_argument( 529 pubsub_group.add_argument(
459 "-o", "--order-by", choices=[C.ORDER_BY_CREATION, 530 "-o",
460 C.ORDER_BY_MODIFICATION], 531 "--order-by",
461 help=_("how items should be ordered")) 532 choices=[C.ORDER_BY_CREATION, C.ORDER_BY_MODIFICATION],
533 help=_("how items should be ordered"),
534 )
462 535
463 if flags[C.CACHE]: 536 if flags[C.CACHE]:
464 pubsub_group.add_argument( 537 pubsub_group.add_argument(
465 "-C", "--no-cache", dest="use_cache", action='store_false', 538 "-C",
466 help=_("don't use Pubsub cache") 539 "--no-cache",
540 dest="use_cache",
541 action="store_false",
542 help=_("don't use Pubsub cache"),
467 ) 543 )
468 544
469 if not flags.all_used: 545 if not flags.all_used:
470 raise exceptions.InternalError('unknown flags: {flags}'.format( 546 raise exceptions.InternalError(
471 flags=', '.join(flags.unused))) 547 "unknown flags: {flags}".format(flags=", ".join(flags.unused))
548 )
472 if defaults: 549 if defaults:
473 raise exceptions.InternalError(f'unused defaults: {defaults}') 550 raise exceptions.InternalError(f"unused defaults: {defaults}")
474 551
475 return parent 552 return parent
476 553
477 def add_parser_options(self): 554 def add_parser_options(self):
478 self.parser.add_argument( 555 self.parser.add_argument(
479 '--version', 556 "--version",
480 action='version', 557 action="version",
481 version=("{name} {version} {copyleft}".format( 558 version=(
482 name = C.APP_NAME, 559 "{name} {version} {copyleft}".format(
483 version = self.version, 560 name=C.APP_NAME, version=self.version, copyleft=COPYLEFT
484 copyleft = COPYLEFT)) 561 )
562 ),
485 ) 563 )
486 564
487 def register_output(self, type_, name, callback, description="", default=False): 565 def register_output(self, type_, name, callback, description="", default=False):
488 if type_ not in C.OUTPUT_TYPES: 566 if type_ not in C.OUTPUT_TYPES:
489 log.error("Invalid output type {}".format(type_)) 567 log.error("Invalid output type {}".format(type_))
490 return 568 return
491 self._outputs[type_][name] = {'callback': callback, 569 self._outputs[type_][name] = {"callback": callback, "description": description}
492 'description': description
493 }
494 if default: 570 if default:
495 if type_ in self.default_output: 571 if type_ in self.default_output:
496 self.disp( 572 self.disp(
497 _('there is already a default output for {type}, ignoring new one') 573 _(
498 .format(type=type_) 574 "there is already a default output for {type}, ignoring new one"
575 ).format(type=type_)
499 ) 576 )
500 else: 577 else:
501 self.default_output[type_] = name 578 self.default_output[type_] = name
502
503 579
504 def parse_output_options(self): 580 def parse_output_options(self):
505 options = self.command.args.output_opts 581 options = self.command.args.output_opts
506 options_dict = {} 582 options_dict = {}
507 for option in options: 583 for option in options:
508 try: 584 try:
509 key, value = option.split('=', 1) 585 key, value = option.split("=", 1)
510 except ValueError: 586 except ValueError:
511 key, value = option, None 587 key, value = option, None
512 options_dict[key.strip()] = value.strip() if value is not None else None 588 options_dict[key.strip()] = value.strip() if value is not None else None
513 return options_dict 589 return options_dict
514 590
515 def check_output_options(self, accepted_set, options): 591 def check_output_options(self, accepted_set, options):
516 if not accepted_set.issuperset(options): 592 if not accepted_set.issuperset(options):
517 self.disp( 593 self.disp(
518 _("The following output options are invalid: {invalid_options}").format( 594 _("The following output options are invalid: {invalid_options}").format(
519 invalid_options = ', '.join(set(options).difference(accepted_set))), 595 invalid_options=", ".join(set(options).difference(accepted_set))
520 error=True) 596 ),
597 error=True,
598 )
521 self.quit(C.EXIT_BAD_ARG) 599 self.quit(C.EXIT_BAD_ARG)
522 600
523 def import_plugins(self): 601 def import_plugins(self):
524 """Automaticaly import commands and outputs in CLI frontend 602 """Automaticaly import commands and outputs in CLI frontend
525 603
526 looks from modules names cmd_*.py in CLI frontend path and import them 604 looks from modules names cmd_*.py in CLI frontend path and import them
527 """ 605 """
528 path = os.path.dirname(libervia.cli.__file__) 606 path = os.path.dirname(libervia.cli.__file__)
529 # XXX: outputs must be imported before commands as they are used for arguments 607 # XXX: outputs must be imported before commands as they are used for arguments
530 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), 608 for type_, pattern in (
531 (C.PLUGIN_CMD, 'cmd_*.py')): 609 (C.PLUGIN_OUTPUT, "output_*.py"),
610 (C.PLUGIN_CMD, "cmd_*.py"),
611 ):
532 modules = ( 612 modules = (
533 os.path.splitext(module)[0] 613 os.path.splitext(module)[0]
534 for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) 614 for module in map(os.path.basename, iglob(os.path.join(path, pattern)))
615 )
535 for module_name in modules: 616 for module_name in modules:
536 module_path = "libervia.cli." + module_name 617 module_path = "libervia.cli." + module_name
537 try: 618 try:
538 module = import_module(module_path) 619 module = import_module(module_path)
539 self.import_plugin_module(module, type_) 620 self.import_plugin_module(module, type_)
540 except exceptions.CancelError: 621 except exceptions.CancelError:
541 continue 622 continue
542 except exceptions.MissingModule as e: 623 except exceptions.MissingModule as e:
543 self.disp(_("Missing module for plugin {name}: {missing}".format( 624 self.disp(
544 name = module_path, 625 _(
545 missing = e)), error=True) 626 "Missing module for plugin {name}: {missing}".format(
627 name=module_path, missing=e
628 )
629 ),
630 error=True,
631 )
546 except Exception as e: 632 except Exception as e:
547 self.disp( 633 self.disp(
548 _("Can't import {module_path} plugin, ignoring it: {e}") 634 _("Can't import {module_path} plugin, ignoring it: {e}").format(
549 .format(module_path=module_path, e=e), 635 module_path=module_path, e=e
550 error=True) 636 ),
551 637 error=True,
638 )
552 639
553 def import_plugin_module(self, module, type_): 640 def import_plugin_module(self, module, type_):
554 """add commands or outpus from a module to CLI frontend 641 """add commands or outpus from a module to CLI frontend
555 642
556 @param module: module containing commands or outputs 643 @param module: module containing commands or outputs
557 @param type_(str): one of C_PLUGIN_* 644 @param type_(str): one of C_PLUGIN_*
558 """ 645 """
559 try: 646 try:
560 class_names = getattr(module, '__{}__'.format(type_)) 647 class_names = getattr(module, "__{}__".format(type_))
561 except AttributeError: 648 except AttributeError:
562 log.disp( 649 log.disp(
563 _("Invalid plugin module [{type}] {module}") 650 _("Invalid plugin module [{type}] {module}").format(
564 .format(type=type_, module=module), 651 type=type_, module=module
565 error=True) 652 ),
653 error=True,
654 )
566 raise ImportError 655 raise ImportError
567 else: 656 else:
568 for class_name in class_names: 657 for class_name in class_names:
569 cls = getattr(module, class_name) 658 cls = getattr(module, class_name)
570 cls(self) 659 cls(self)
571 660
572 def get_xmpp_uri_from_http(self, http_url): 661 def get_xmpp_uri_from_http(self, http_url):
573 """parse HTML page at http(s) URL, and looks for xmpp: uri""" 662 """parse HTML page at http(s) URL, and looks for xmpp: uri"""
574 if http_url.startswith('https'): 663 if http_url.startswith("https"):
575 scheme = 'https' 664 scheme = "https"
576 elif http_url.startswith('http'): 665 elif http_url.startswith("http"):
577 scheme = 'http' 666 scheme = "http"
578 else: 667 else:
579 raise exceptions.InternalError('An HTTP scheme is expected in this method') 668 raise exceptions.InternalError("An HTTP scheme is expected in this method")
580 self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1) 669 self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1)
581 # HTTP URL, we try to find xmpp: links 670 # HTTP URL, we try to find xmpp: links
582 try: 671 try:
583 from lxml import etree 672 from lxml import etree
584 except ImportError: 673 except ImportError:
585 self.disp( 674 self.disp(
586 "lxml module must be installed to use http(s) scheme, please install it " 675 "lxml module must be installed to use http(s) scheme, please install it "
587 "with \"pip install lxml\"", 676 'with "pip install lxml"',
588 error=True) 677 error=True,
678 )
589 self.quit(1) 679 self.quit(1)
590 import urllib.request, urllib.error, urllib.parse 680 import urllib.request, urllib.error, urllib.parse
681
591 parser = etree.HTMLParser() 682 parser = etree.HTMLParser()
592 try: 683 try:
593 root = etree.parse(urllib.request.urlopen(http_url), parser) 684 root = etree.parse(urllib.request.urlopen(http_url), parser)
594 except etree.XMLSyntaxError as e: 685 except etree.XMLSyntaxError as e:
595 self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) 686 self.disp(_("Can't parse HTML page : {msg}").format(msg=e))
596 links = [] 687 links = []
597 else: 688 else:
598 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") 689 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
599 if not links: 690 if not links:
600 self.disp( 691 self.disp(
601 _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP ' 692 _(
602 'PubSub node/item'), 693 'Could not find alternate "xmpp:" URI, can\'t find associated XMPP '
603 error=True) 694 "PubSub node/item"
695 ),
696 error=True,
697 )
604 self.quit(1) 698 self.quit(1)
605 xmpp_uri = links[0].get('href') 699 xmpp_uri = links[0].get("href")
606 return xmpp_uri 700 return xmpp_uri
607 701
608 def parse_pubsub_args(self): 702 def parse_pubsub_args(self):
609 if self.args.pubsub_url is not None: 703 if self.args.pubsub_url is not None:
610 url = self.args.pubsub_url 704 url = self.args.pubsub_url
611 705
612 if url.startswith('http'): 706 if url.startswith("http"):
613 # http(s) URL, we try to retrieve xmpp one from there 707 # http(s) URL, we try to retrieve xmpp one from there
614 url = self.get_xmpp_uri_from_http(url) 708 url = self.get_xmpp_uri_from_http(url)
615 709
616 try: 710 try:
617 uri_data = uri.parse_xmpp_uri(url) 711 uri_data = uri.parse_xmpp_uri(url)
618 except ValueError: 712 except ValueError:
619 self.parser.error(_('invalid XMPP URL: {url}').format(url=url)) 713 self.parser.error(_("invalid XMPP URL: {url}").format(url=url))
620 else: 714 else:
621 if uri_data['type'] == 'pubsub': 715 if uri_data["type"] == "pubsub":
622 # URL is alright, we only set data not already set by other options 716 # URL is alright, we only set data not already set by other options
623 if not self.args.service: 717 if not self.args.service:
624 self.args.service = uri_data['path'] 718 self.args.service = uri_data["path"]
625 if not self.args.node: 719 if not self.args.node:
626 self.args.node = uri_data['node'] 720 self.args.node = uri_data["node"]
627 uri_item = uri_data.get('item') 721 uri_item = uri_data.get("item")
628 if uri_item: 722 if uri_item:
629 # there is an item in URI 723 # there is an item in URI
630 # we use it only if item is not already set 724 # we use it only if item is not already set
631 # and item_last is not used either 725 # and item_last is not used either
632 try: 726 try:
634 except AttributeError: 728 except AttributeError:
635 try: 729 try:
636 items = self.args.items 730 items = self.args.items
637 except AttributeError: 731 except AttributeError:
638 self.disp( 732 self.disp(
639 _("item specified in URL but not needed in command, " 733 _(
640 "ignoring it"), 734 "item specified in URL but not needed in command, "
641 error=True) 735 "ignoring it"
736 ),
737 error=True,
738 )
642 else: 739 else:
643 if not items: 740 if not items:
644 self.args.items = [uri_item] 741 self.args.items = [uri_item]
645 else: 742 else:
646 if not item: 743 if not item:
650 item_last = False 747 item_last = False
651 if not item_last: 748 if not item_last:
652 self.args.item = uri_item 749 self.args.item = uri_item
653 else: 750 else:
654 self.parser.error( 751 self.parser.error(
655 _('XMPP URL is not a pubsub one: {url}').format(url=url) 752 _("XMPP URL is not a pubsub one: {url}").format(url=url)
656 ) 753 )
657 flags = self.args._cmd._pubsub_flags 754 flags = self.args._cmd._pubsub_flags
658 # we check required arguments here instead of using add_arguments' required option 755 # we check required arguments here instead of using add_arguments' required option
659 # because the required argument can be set in URL 756 # because the required argument can be set in URL
660 if C.SERVICE in flags and not self.args.service: 757 if C.SERVICE in flags and not self.args.service:
667 # FIXME: mutually groups can't be nested in a group and don't support title 764 # FIXME: mutually groups can't be nested in a group and don't support title
668 # so we check conflict here. This may be fixed in Python 3, to be checked 765 # so we check conflict here. This may be fixed in Python 3, to be checked
669 try: 766 try:
670 if self.args.item and self.args.item_last: 767 if self.args.item and self.args.item_last:
671 self.parser.error( 768 self.parser.error(
672 _("--item and --item-last can't be used at the same time")) 769 _("--item and --item-last can't be used at the same time")
770 )
673 except AttributeError: 771 except AttributeError:
674 pass 772 pass
675 773
676 try: 774 try:
677 max_items = self.args.max 775 max_items = self.args.max
681 else: 779 else:
682 # we need to set a default value for max, but we need to know if we want 780 # we need to set a default value for max, but we need to know if we want
683 # to use pubsub's max or RSM's max. The later is used if any RSM or MAM 781 # to use pubsub's max or RSM's max. The later is used if any RSM or MAM
684 # argument is set 782 # argument is set
685 if max_items is None and rsm_max is None: 783 if max_items is None and rsm_max is None:
686 to_check = ('mam_filters', 'rsm_max', 'rsm_after', 'rsm_before', 784 to_check = (
687 'rsm_index') 785 "mam_filters",
786 "rsm_max",
787 "rsm_after",
788 "rsm_before",
789 "rsm_index",
790 )
688 if any((getattr(self.args, name) for name in to_check)): 791 if any((getattr(self.args, name) for name in to_check)):
689 # we use RSM 792 # we use RSM
690 self.args.rsm_max = 10 793 self.args.rsm_max = 10
691 else: 794 else:
692 # we use pubsub without RSM 795 # we use pubsub without RSM
698 try: 801 try:
699 await self.bridge.bridge_connect() 802 await self.bridge.bridge_connect()
700 except Exception as e: 803 except Exception as e:
701 if isinstance(e, exceptions.BridgeExceptionNoService): 804 if isinstance(e, exceptions.BridgeExceptionNoService):
702 print( 805 print(
703 _("Can't connect to Libervia backend, are you sure that it's " 806 _(
704 "launched ?") 807 "Can't connect to Libervia backend, are you sure that it's "
808 "launched ?"
809 )
705 ) 810 )
706 self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False) 811 self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False)
707 elif isinstance(e, exceptions.BridgeInitError): 812 elif isinstance(e, exceptions.BridgeInitError):
708 print(_("Can't init bridge")) 813 print(_("Can't init bridge"))
709 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) 814 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
710 else: 815 else:
711 print( 816 print(_("Error while initialising bridge: {e}").format(e=e))
712 _("Error while initialising bridge: {e}").format(e=e)
713 )
714 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) 817 self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
715 return 818 return
716 # we wait on init_pre_script instead of ready_get, so the CLI frontend can be used 819 # we wait on init_pre_script instead of ready_get, so the CLI frontend can be used
717 # in init script. 820 # in init script.
718 await self.bridge.init_pre_script() 821 await self.bridge.init_pre_script()
741 844
742 @classmethod 845 @classmethod
743 def run(cls): 846 def run(cls):
744 cls()._run() 847 cls()._run()
745 848
746
747 def _read_stdin(self, stdin_fut): 849 def _read_stdin(self, stdin_fut):
748 """Callback called by ainput to read stdin""" 850 """Callback called by ainput to read stdin"""
749 line = sys.stdin.readline() 851 line = sys.stdin.readline()
750 if line: 852 if line:
751 stdin_fut.set_result(line.rstrip(os.linesep)) 853 stdin_fut.set_result(line.rstrip(os.linesep))
752 else: 854 else:
753 stdin_fut.set_exception(EOFError()) 855 stdin_fut.set_exception(EOFError())
754 856
755 async def ainput(self, msg=''): 857 async def ainput(self, msg=""):
756 """Asynchronous version of buildin "input" function""" 858 """Asynchronous version of buildin "input" function"""
757 self.disp(msg, end=' ') 859 self.disp(msg, end=" ")
758 sys.stdout.flush() 860 sys.stdout.flush()
759 loop = asyncio.get_running_loop() 861 loop = asyncio.get_running_loop()
760 stdin_fut = loop.create_future() 862 stdin_fut = loop.create_future()
761 loop.add_reader(sys.stdin, self._read_stdin, stdin_fut) 863 loop.add_reader(sys.stdin, self._read_stdin, stdin_fut)
762 return await stdin_fut 864 return await stdin_fut
764 async def confirm(self, message): 866 async def confirm(self, message):
765 """Request user to confirm action, return answer as boolean""" 867 """Request user to confirm action, return answer as boolean"""
766 res = await self.ainput(f"{message} (y/N)? ") 868 res = await self.ainput(f"{message} (y/N)? ")
767 return res in ("y", "Y") 869 return res in ("y", "Y")
768 870
769 async def confirm_or_quit(self, message, cancel_message=_("action cancelled by user")): 871 async def confirm_or_quit(
872 self, message, cancel_message=_("action cancelled by user")
873 ):
770 """Request user to confirm action, and quit if he doesn't""" 874 """Request user to confirm action, and quit if he doesn't"""
771 confirmed = await self.confirm(message) 875 confirmed = await self.confirm(message)
772 if not confirmed: 876 if not confirmed:
773 self.disp(cancel_message) 877 self.disp(cancel_message)
774 self.quit(C.EXIT_USER_CANCELLED) 878 self.quit(C.EXIT_USER_CANCELLED)
802 906
803 self.loop.quit(exit_code) 907 self.loop.quit(exit_code)
804 if raise_exc: 908 if raise_exc:
805 raise QuitException 909 raise QuitException
806 910
807 async def a_quit(self, exit_code: int=0, raise_exc=True): 911 async def a_quit(self, exit_code: int = 0, raise_exc=True):
808 """Execute async quit callback before actually quitting 912 """Execute async quit callback before actually quitting
809 913
810 This method should be prefered to ``quit``, as it executes async quit callbacks 914 This method should be prefered to ``quit``, as it executes async quit callbacks
811 which may be necessary for proper cleaning of session. 915 which may be necessary for proper cleaning of session.
812 """ 916 """
862 expanded = jid 966 expanded = jid
863 return expanded 967 return expanded
864 968
865 def check(jid): 969 def check(jid):
866 if not jid.is_valid: 970 if not jid.is_valid:
867 log.error (_("%s is not a valid JID !"), jid) 971 log.error(_("%s is not a valid JID !"), jid)
868 self.quit(1) 972 self.quit(1)
869 973
870 dest_jids=[] 974 dest_jids = []
871 try: 975 try:
872 for i in range(len(jids)): 976 for i in range(len(jids)):
873 dest_jids.append(expand_jid(jids[i])) 977 dest_jids.append(expand_jid(jids[i]))
874 check(dest_jids[i]) 978 check(dest_jids[i])
875 except AttributeError: 979 except AttributeError:
876 pass 980 pass
877 981
878 return dest_jids 982 return dest_jids
879 983
880 async def a_pwd_input(self, msg=''): 984 async def a_pwd_input(self, msg=""):
881 """Like ainput but with echo disabled (useful for passwords)""" 985 """Like ainput but with echo disabled (useful for passwords)"""
882 # we disable echo, code adapted from getpass standard module which has been 986 # we disable echo, code adapted from getpass standard module which has been
883 # written by Piers Lauder (original), Guido van Rossum (Windows support and 987 # written by Piers Lauder (original), Guido van Rossum (Windows support and
884 # cleanup) and Gregory P. Smith (tty support & GetPassWarning), a big thanks 988 # cleanup) and Gregory P. Smith (tty support & GetPassWarning), a big thanks
885 # to them (and for all the amazing work on Python). 989 # to them (and for all the amazing work on Python).
886 stdin_fd = sys.stdin.fileno() 990 stdin_fd = sys.stdin.fileno()
887 old = termios.tcgetattr(sys.stdin) 991 old = termios.tcgetattr(sys.stdin)
888 new = old[:] 992 new = old[:]
889 new[3] &= ~termios.ECHO 993 new[3] &= ~termios.ECHO
890 tcsetattr_flags = termios.TCSAFLUSH 994 tcsetattr_flags = termios.TCSAFLUSH
891 if hasattr(termios, 'TCSASOFT'): 995 if hasattr(termios, "TCSASOFT"):
892 tcsetattr_flags |= termios.TCSASOFT 996 tcsetattr_flags |= termios.TCSASOFT
893 try: 997 try:
894 termios.tcsetattr(stdin_fd, tcsetattr_flags, new) 998 termios.tcsetattr(stdin_fd, tcsetattr_flags, new)
895 pwd = await self.ainput(msg=msg) 999 pwd = await self.ainput(msg=msg)
896 finally: 1000 finally:
897 termios.tcsetattr(stdin_fd, tcsetattr_flags, old) 1001 termios.tcsetattr(stdin_fd, tcsetattr_flags, old)
898 sys.stderr.flush() 1002 sys.stderr.flush()
899 self.disp('') 1003 self.disp("")
900 return pwd 1004 return pwd
901 1005
902 async def connect_or_prompt(self, method, err_msg=None): 1006 async def connect_or_prompt(self, method, err_msg=None):
903 """Try to connect/start profile session and prompt for password if needed 1007 """Try to connect/start profile session and prompt for password if needed
904 1008
908 @param err_msg(str): message to show if connection fail 1012 @param err_msg(str): message to show if connection fail
909 """ 1013 """
910 password = self.args.pwd 1014 password = self.args.pwd
911 while True: 1015 while True:
912 try: 1016 try:
913 await method(password or '') 1017 await method(password or "")
914 except Exception as e: 1018 except Exception as e:
915 if ((isinstance(e, BridgeException) 1019 if (
916 and e.classname == 'PasswordError' 1020 isinstance(e, BridgeException)
917 and self.args.pwd is None)): 1021 and e.classname == "PasswordError"
1022 and self.args.pwd is None
1023 ):
918 if password is not None: 1024 if password is not None:
919 self.disp(A.color(C.A_WARNING, _("invalid password"))) 1025 self.disp(A.color(C.A_WARNING, _("invalid password")))
920 password = await self.a_pwd_input( 1026 password = await self.a_pwd_input(_("please enter profile password:"))
921 _("please enter profile password:"))
922 else: 1027 else:
923 self.disp(err_msg.format(profile=self.profile, e=e), error=True) 1028 self.disp(err_msg.format(profile=self.profile, e=e), error=True)
924 self.quit(C.EXIT_ERROR) 1029 self.quit(C.EXIT_ERROR)
925 else: 1030 else:
926 break 1031 break
936 1041
937 self.profile = await self.bridge.profile_name_get(self.args.profile) 1042 self.profile = await self.bridge.profile_name_get(self.args.profile)
938 1043
939 if not self.profile: 1044 if not self.profile:
940 log.error( 1045 log.error(
941 _("The profile [{profile}] doesn't exist") 1046 _("The profile [{profile}] doesn't exist").format(
942 .format(profile=self.args.profile) 1047 profile=self.args.profile
1048 )
943 ) 1049 )
944 self.quit(C.EXIT_ERROR) 1050 self.quit(C.EXIT_ERROR)
945 1051
946 try: 1052 try:
947 start_session = self.args.start_session 1053 start_session = self.args.start_session
949 pass 1055 pass
950 else: 1056 else:
951 if start_session: 1057 if start_session:
952 await self.connect_or_prompt( 1058 await self.connect_or_prompt(
953 lambda pwd: self.bridge.profile_start_session(pwd, self.profile), 1059 lambda pwd: self.bridge.profile_start_session(pwd, self.profile),
954 err_msg="Can't start {profile}'s session: {e}" 1060 err_msg="Can't start {profile}'s session: {e}",
955 ) 1061 )
956 return 1062 return
957 elif not await self.bridge.profile_is_session_started(self.profile): 1063 elif not await self.bridge.profile_is_session_started(self.profile):
958 if not self.args.connect: 1064 if not self.args.connect:
959 self.disp(_( 1065 self.disp(
960 "Session for [{profile}] is not started, please start it " 1066 _(
961 "before using libervia-cli, or use either --start-session or " 1067 "Session for [{profile}] is not started, please start it "
962 "--connect option" 1068 "before using libervia-cli, or use either --start-session or "
963 .format(profile=self.profile) 1069 "--connect option".format(profile=self.profile)
964 ), error=True) 1070 ),
1071 error=True,
1072 )
965 self.quit(1) 1073 self.quit(1)
966 elif not getattr(self.args, "connect", False): 1074 elif not getattr(self.args, "connect", False):
967 return 1075 return
968 1076
969 1077 if not hasattr(self.args, "connect"):
970 if not hasattr(self.args, 'connect'):
971 # a profile can be present without connect option (e.g. on profile 1078 # a profile can be present without connect option (e.g. on profile
972 # creation/deletion) 1079 # creation/deletion)
973 return 1080 return
974 elif self.args.connect is True: # if connection is asked, we connect the profile 1081 elif self.args.connect is True: # if connection is asked, we connect the profile
975 await self.connect_or_prompt( 1082 await self.connect_or_prompt(
976 lambda pwd: self.bridge.connect(self.profile, pwd, {}), 1083 lambda pwd: self.bridge.connect(self.profile, pwd, {}),
977 err_msg = 'Can\'t connect profile "{profile!s}": {e}' 1084 err_msg='Can\'t connect profile "{profile!s}": {e}',
978 ) 1085 )
979 return 1086 return
980 else: 1087 else:
981 if not await self.bridge.is_connected(self.profile): 1088 if not await self.bridge.is_connected(self.profile):
982 log.error( 1089 log.error(
983 _("Profile [{profile}] is not connected, please connect it " 1090 _(
984 "before using libervia-cli, or use --connect option") 1091 "Profile [{profile}] is not connected, please connect it "
985 .format(profile=self.profile) 1092 "before using libervia-cli, or use --connect option"
1093 ).format(profile=self.profile)
986 ) 1094 )
987 self.quit(1) 1095 self.quit(1)
988 1096
989 async def get_full_jid(self, param_jid): 1097 async def get_full_jid(self, param_jid):
990 """Return the full jid if possible (add main resource when find a bare jid)""" 1098 """Return the full jid if possible (add main resource when find a bare jid)"""
991 # TODO: to be removed, bare jid should work with all commands, notably for file 1099 # TODO: to be removed, bare jid should work with all commands, notably for file
992 # as backend now handle jingles message initiation 1100 # as backend now handle jingles message initiation
993 _jid = JID(param_jid) 1101 _jid = JID(param_jid)
994 if not _jid.resource: 1102 if not _jid.resource:
995 #if the resource is not given, we try to add the main resource 1103 # if the resource is not given, we try to add the main resource
996 main_resource = await self.bridge.main_resource_get(param_jid, self.profile) 1104 main_resource = await self.bridge.main_resource_get(param_jid, self.profile)
997 if main_resource: 1105 if main_resource:
998 return f"{_jid.bare}/{main_resource}" 1106 return f"{_jid.bare}/{main_resource}"
999 return param_jid 1107 return param_jid
1000 1108
1015 use_profile: bool = True, 1123 use_profile: bool = True,
1016 use_output: Union[bool, str] = False, 1124 use_output: Union[bool, str] = False,
1017 extra_outputs: Optional[dict] = None, 1125 extra_outputs: Optional[dict] = None,
1018 need_connect: Optional[bool] = None, 1126 need_connect: Optional[bool] = None,
1019 help: Optional[str] = None, 1127 help: Optional[str] = None,
1020 **kwargs 1128 **kwargs,
1021 ): 1129 ):
1022 """Initialise CommandBase 1130 """Initialise CommandBase
1023 1131
1024 @param host: LiberviaCli instance 1132 @param host: LiberviaCli instance
1025 @param name: name of the new command 1133 @param name: name of the new command
1049 C.SERVICE: service is required 1157 C.SERVICE: service is required
1050 C.NODE: node is required 1158 C.NODE: node is required
1051 C.ITEM: item is required 1159 C.ITEM: item is required
1052 C.SINGLE_ITEM: only one item is allowed 1160 C.SINGLE_ITEM: only one item is allowed
1053 """ 1161 """
1054 try: # If we have subcommands, host is a CommandBase and we need to use host.host 1162 try: # If we have subcommands, host is a CommandBase and we need to use host.host
1055 self.host = host.host 1163 self.host = host.host
1056 except AttributeError: 1164 except AttributeError:
1057 self.host = host 1165 self.host = host
1058 1166
1059 # --profile option 1167 # --profile option
1060 parents = kwargs.setdefault('parents', set()) 1168 parents = kwargs.setdefault("parents", set())
1061 if use_profile: 1169 if use_profile:
1062 # self.host.parents['profile'] is an ArgumentParser with profile connection 1170 # self.host.parents['profile'] is an ArgumentParser with profile connection
1063 # arguments 1171 # arguments
1064 if need_connect is None: 1172 if need_connect is None:
1065 need_connect = True 1173 need_connect = True
1066 parents.add( 1174 parents.add(
1067 self.host.parents['profile' if need_connect else 'profile_session']) 1175 self.host.parents["profile" if need_connect else "profile_session"]
1176 )
1068 else: 1177 else:
1069 assert need_connect is None 1178 assert need_connect is None
1070 self.need_connect = need_connect 1179 self.need_connect = need_connect
1071 # from this point, self.need_connect is None if connection is not needed at all 1180 # from this point, self.need_connect is None if connection is not needed at all
1072 # False if session starting is needed, and True if full connection is needed 1181 # False if session starting is needed, and True if full connection is needed
1083 output_parent = argparse.ArgumentParser(add_help=False) 1192 output_parent = argparse.ArgumentParser(add_help=False)
1084 choices = set(self.host.get_output_choices(use_output)) 1193 choices = set(self.host.get_output_choices(use_output))
1085 choices.update(extra_outputs) 1194 choices.update(extra_outputs)
1086 if not choices: 1195 if not choices:
1087 raise exceptions.InternalError( 1196 raise exceptions.InternalError(
1088 "No choice found for {} output type".format(use_output)) 1197 "No choice found for {} output type".format(use_output)
1198 )
1089 try: 1199 try:
1090 default = self.host.default_output[use_output] 1200 default = self.host.default_output[use_output]
1091 except KeyError: 1201 except KeyError:
1092 if 'default' in choices: 1202 if "default" in choices:
1093 default = 'default' 1203 default = "default"
1094 elif 'simple' in choices: 1204 elif "simple" in choices:
1095 default = 'simple' 1205 default = "simple"
1096 else: 1206 else:
1097 default = list(choices)[0] 1207 default = list(choices)[0]
1098 output_parent.add_argument( 1208 output_parent.add_argument(
1099 '--output', '-O', choices=sorted(choices), default=default, 1209 "--output",
1100 help=_("select output format (default: {})".format(default))) 1210 "-O",
1211 choices=sorted(choices),
1212 default=default,
1213 help=_("select output format (default: {})".format(default)),
1214 )
1101 output_parent.add_argument( 1215 output_parent.add_argument(
1102 '--output-option', '--oo', action="append", dest='output_opts', 1216 "--output-option",
1103 default=[], help=_("output specific option")) 1217 "--oo",
1218 action="append",
1219 dest="output_opts",
1220 default=[],
1221 help=_("output specific option"),
1222 )
1104 parents.add(output_parent) 1223 parents.add(output_parent)
1105 else: 1224 else:
1106 assert extra_outputs is None 1225 assert extra_outputs is None
1107 1226
1108 self._use_pubsub = kwargs.pop('use_pubsub', False) 1227 self._use_pubsub = kwargs.pop("use_pubsub", False)
1109 if self._use_pubsub: 1228 if self._use_pubsub:
1110 flags = kwargs.pop('pubsub_flags', []) 1229 flags = kwargs.pop("pubsub_flags", [])
1111 defaults = kwargs.pop('pubsub_defaults', {}) 1230 defaults = kwargs.pop("pubsub_defaults", {})
1112 parents.add(self.host.make_pubsub_group(flags, defaults)) 1231 parents.add(self.host.make_pubsub_group(flags, defaults))
1113 self._pubsub_flags = flags 1232 self._pubsub_flags = flags
1114 1233
1115 # other common options 1234 # other common options
1116 use_opts = {k:v for k,v in kwargs.items() if k.startswith('use_')} 1235 use_opts = {k: v for k, v in kwargs.items() if k.startswith("use_")}
1117 for param, do_use in use_opts.items(): 1236 for param, do_use in use_opts.items():
1118 opt=param[4:] # if param is use_verbose, opt is verbose 1237 opt = param[4:] # if param is use_verbose, opt is verbose
1119 if opt not in self.host.parents: 1238 if opt not in self.host.parents:
1120 raise exceptions.InternalError("Unknown parent option {}".format(opt)) 1239 raise exceptions.InternalError("Unknown parent option {}".format(opt))
1121 del kwargs[param] 1240 del kwargs[param]
1122 if do_use: 1241 if do_use:
1123 parents.add(self.host.parents[opt]) 1242 parents.add(self.host.parents[opt])
1124 1243
1125 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) 1244 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
1126 if hasattr(self, "subcommands"): 1245 if hasattr(self, "subcommands"):
1127 self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True) 1246 self.subparsers = self.parser.add_subparsers(dest="subcommand", required=True)
1128 else: 1247 else:
1129 self.parser.set_defaults(_cmd=self) 1248 self.parser.set_defaults(_cmd=self)
1130 self.add_parser_options() 1249 self.add_parser_options()
1131 1250
1132 @property 1251 @property
1196 async def progress_error_handler(self, uid, message, profile): 1315 async def progress_error_handler(self, uid, message, profile):
1197 if profile != self.profile: 1316 if profile != self.profile:
1198 return 1317 return
1199 if uid == self.progress_id: 1318 if uid == self.progress_id:
1200 if self.args.progress: 1319 if self.args.progress:
1201 self.disp('') # progress is not finished, so we skip a line 1320 self.disp("") # progress is not finished, so we skip a line
1202 if self.host.quit_on_progress_end: 1321 if self.host.quit_on_progress_end:
1203 await self.on_progress_error(message) 1322 await self.on_progress_error(message)
1204 self.host.quit_from_signal(C.EXIT_ERROR) 1323 self.host.quit_from_signal(C.EXIT_ERROR)
1205 1324
1206 async def progress_update(self): 1325 async def progress_update(self):
1209 @return (bool): False to stop being called 1328 @return (bool): False to stop being called
1210 """ 1329 """
1211 data = await self.host.bridge.progress_get(self.progress_id, self.profile) 1330 data = await self.host.bridge.progress_get(self.progress_id, self.profile)
1212 if data: 1331 if data:
1213 try: 1332 try:
1214 size = data['size'] 1333 size = data["size"]
1215 except KeyError: 1334 except KeyError:
1216 self.disp(_("file size is not known, we can't show a progress bar"), 1, 1335 self.disp(
1217 error=True) 1336 _("file size is not known, we can't show a progress bar"),
1337 1,
1338 error=True,
1339 )
1218 return False 1340 return False
1219 if self.host.pbar is None: 1341 if self.host.pbar is None:
1220 #first answer, we must construct the bar 1342 # first answer, we must construct the bar
1221 1343
1222 # if the instance has a pbar_template attribute, it is used has model, 1344 # if the instance has a pbar_template attribute, it is used has model,
1223 # else default one is used 1345 # else default one is used
1224 # template is a list of part, where part can be either a str to show directly 1346 # template is a list of part, where part can be either a str to show directly
1225 # or a list where first argument is a name of a progressbar widget, and others 1347 # or a list where first argument is a name of a progressbar widget, and others
1226 # are used as widget arguments 1348 # are used as widget arguments
1227 try: 1349 try:
1228 template = self.pbar_template 1350 template = self.pbar_template
1229 except AttributeError: 1351 except AttributeError:
1230 template = [ 1352 template = [
1231 _("Progress: "), ["Percentage"], " ", ["Bar"], " ", 1353 _("Progress: "),
1232 ["FileTransferSpeed"], " ", ["ETA"] 1354 ["Percentage"],
1355 " ",
1356 ["Bar"],
1357 " ",
1358 ["FileTransferSpeed"],
1359 " ",
1360 ["ETA"],
1233 ] 1361 ]
1234 1362
1235 widgets = [] 1363 widgets = []
1236 for part in template: 1364 for part in template:
1237 if isinstance(part, str): 1365 if isinstance(part, str):
1238 widgets.append(part) 1366 widgets.append(part)
1239 else: 1367 else:
1240 widget = getattr(progressbar, part.pop(0)) 1368 widget = getattr(progressbar, part.pop(0))
1241 widgets.append(widget(*part)) 1369 widgets.append(widget(*part))
1242 1370
1243 self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets) 1371 self.host.pbar = progressbar.ProgressBar(
1372 max_value=int(size), widgets=widgets
1373 )
1244 self.host.pbar.start() 1374 self.host.pbar.start()
1245 1375
1246 self.host.pbar.update(int(data['position'])) 1376 self.host.pbar.update(int(data["position"]))
1247 1377
1248 elif self.host.pbar is not None: 1378 elif self.host.pbar is not None:
1249 return False 1379 return False
1250 1380
1251 await self.on_progress_update(data) 1381 await self.on_progress_update(data)
1281 1411
1282 @param error_msg(unicode): error message as sent by bridge.progress_error 1412 @param error_msg(unicode): error message as sent by bridge.progress_error
1283 """ 1413 """
1284 self.disp(_("Error while doing operation: {e}").format(e=e), error=True) 1414 self.disp(_("Error while doing operation: {e}").format(e=e), error=True)
1285 1415
1286 def disp(self, msg, verbosity=0, error=False, end='\n'): 1416 def disp(self, msg, verbosity=0, error=False, end="\n"):
1287 return self.host.disp(msg, verbosity, error, end) 1417 return self.host.disp(msg, verbosity, error, end)
1288 1418
1289 def output(self, data): 1419 def output(self, data):
1290 try: 1420 try:
1291 output_type = self._output_type 1421 output_type = self._output_type
1292 except AttributeError: 1422 except AttributeError:
1293 raise exceptions.InternalError( 1423 raise exceptions.InternalError(
1294 _('trying to use output when use_output has not been set')) 1424 _("trying to use output when use_output has not been set")
1425 )
1295 return self.host.output(output_type, self.args.output, self.extra_outputs, data) 1426 return self.host.output(output_type, self.args.output, self.extra_outputs, data)
1296 1427
1297 def get_pubsub_extra(self, extra: Optional[dict] = None) -> str: 1428 def get_pubsub_extra(self, extra: Optional[dict] = None) -> str:
1298 """Helper method to compute extra data from pubsub arguments 1429 """Helper method to compute extra data from pubsub arguments
1299 1430
1305 else: 1436 else:
1306 intersection = {C.KEY_ORDER_BY}.intersection(list(extra.keys())) 1437 intersection = {C.KEY_ORDER_BY}.intersection(list(extra.keys()))
1307 if intersection: 1438 if intersection:
1308 raise exceptions.ConflictError( 1439 raise exceptions.ConflictError(
1309 "given extra dict has conflicting keys with pubsub keys " 1440 "given extra dict has conflicting keys with pubsub keys "
1310 "{intersection}".format(intersection=intersection)) 1441 "{intersection}".format(intersection=intersection)
1442 )
1311 1443
1312 # RSM 1444 # RSM
1313 1445
1314 for attribute in ('max', 'after', 'before', 'index'): 1446 for attribute in ("max", "after", "before", "index"):
1315 key = 'rsm_' + attribute 1447 key = "rsm_" + attribute
1316 if key in extra: 1448 if key in extra:
1317 raise exceptions.ConflictError( 1449 raise exceptions.ConflictError(
1318 "This key already exists in extra: u{key}".format(key=key)) 1450 "This key already exists in extra: u{key}".format(key=key)
1451 )
1319 value = getattr(self.args, key, None) 1452 value = getattr(self.args, key, None)
1320 if value is not None: 1453 if value is not None:
1321 extra[key] = str(value) 1454 extra[key] = str(value)
1322 1455
1323 # MAM 1456 # MAM
1324 1457
1325 if hasattr(self.args, 'mam_filters'): 1458 if hasattr(self.args, "mam_filters"):
1326 for key, value in self.args.mam_filters: 1459 for key, value in self.args.mam_filters:
1327 key = 'filter_' + key 1460 key = "filter_" + key
1328 if key in extra: 1461 if key in extra:
1329 raise exceptions.ConflictError( 1462 raise exceptions.ConflictError(
1330 "This key already exists in extra: u{key}".format(key=key)) 1463 "This key already exists in extra: u{key}".format(key=key)
1464 )
1331 extra[key] = value 1465 extra[key] = value
1332 1466
1333 # Order-By 1467 # Order-By
1334 1468
1335 try: 1469 try:
1388 if show_progress: 1522 if show_progress:
1389 self.host.watch_progress = True 1523 self.host.watch_progress = True
1390 # we need to register the following signal even if we don't display the 1524 # we need to register the following signal even if we don't display the
1391 # progress bar 1525 # progress bar
1392 self.host.bridge.register_signal( 1526 self.host.bridge.register_signal(
1393 "progress_started", self.progress_started_handler) 1527 "progress_started", self.progress_started_handler
1528 )
1394 self.host.bridge.register_signal( 1529 self.host.bridge.register_signal(
1395 "progress_finished", self.progress_finished_handler) 1530 "progress_finished", self.progress_finished_handler
1531 )
1396 self.host.bridge.register_signal( 1532 self.host.bridge.register_signal(
1397 "progress_error", self.progress_error_handler) 1533 "progress_error", self.progress_error_handler
1534 )
1398 1535
1399 if self.need_connect is not None: 1536 if self.need_connect is not None:
1400 await self.host.connect_profile() 1537 await self.host.connect_profile()
1401 await self.start() 1538 await self.start()
1402 1539
1411 class CommandAnswering(CommandBase): 1548 class CommandAnswering(CommandBase):
1412 """Specialised commands which answer to specific actions 1549 """Specialised commands which answer to specific actions
1413 1550
1414 to manage action_types answer, 1551 to manage action_types answer,
1415 """ 1552 """
1416 action_callbacks = {} # XXX: set managed action types in a dict here: 1553
1417 # key is the action_type, value is the callable 1554 action_callbacks = {} # XXX: set managed action types in a dict here:
1418 # which will manage the answer. profile filtering is 1555 # key is the action_type, value is the callable
1419 # already managed when callback is called 1556 # which will manage the answer. profile filtering is
1557 # already managed when callback is called
1420 1558
1421 def __init__(self, *args, **kwargs): 1559 def __init__(self, *args, **kwargs):
1422 super(CommandAnswering, self).__init__(*args, **kwargs) 1560 super(CommandAnswering, self).__init__(*args, **kwargs)
1423 1561
1424 async def on_action_new( 1562 async def on_action_new(
1425 self, 1563 self, action_data_s: str, action_id: str, security_limit: int, profile: str
1426 action_data_s: str,
1427 action_id: str,
1428 security_limit: int,
1429 profile: str
1430 ) -> None: 1564 ) -> None:
1431 if profile != self.profile: 1565 if profile != self.profile:
1432 return 1566 return
1433 action_data = data_format.deserialise(action_data_s) 1567 action_data = data_format.deserialise(action_data_s)
1434 try: 1568 try:
1435 action_type = action_data['type'] 1569 action_type = action_data["type"]
1436 except KeyError: 1570 except KeyError:
1437 try: 1571 try:
1438 xml_ui = action_data["xmlui"] 1572 xml_ui = action_data["xmlui"]
1439 except KeyError: 1573 except KeyError:
1440 pass 1574 pass
1454 @param xml_ui (unicode): dialog XML representation 1588 @param xml_ui (unicode): dialog XML representation
1455 """ 1589 """
1456 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module 1590 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
1457 # should be available in the future 1591 # should be available in the future
1458 # TODO: XMLUI module 1592 # TODO: XMLUI module
1459 ui = ET.fromstring(xml_ui.encode('utf-8')) 1593 ui = ET.fromstring(xml_ui.encode("utf-8"))
1460 dialog = ui.find("dialog") 1594 dialog = ui.find("dialog")
1461 if dialog is not None: 1595 if dialog is not None:
1462 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") 1596 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error")
1463 1597
1464 async def start_answering(self): 1598 async def start_answering(self):
1465 """Auto reply to confirmation requests""" 1599 """Auto reply to confirmation requests"""
1466 self.host.bridge.register_signal("action_new", self.on_action_new) 1600 self.host.bridge.register_signal("action_new", self.on_action_new)
1467 actions = await self.host.bridge.actions_get(self.profile) 1601 actions = await self.host.bridge.actions_get(self.profile)
1468 for action_data_s, action_id, security_limit in actions: 1602 for action_data_s, action_id, security_limit in actions:
1469 await self.on_action_new(action_data_s, action_id, security_limit, self.profile) 1603 await self.on_action_new(
1604 action_data_s, action_id, security_limit, self.profile
1605 )