comparison frontends/src/jp/base.py @ 2532:772447ec070f

jp: pubsub options refactoring: There is now only "use_pubsub", and specification are set using "pubsub_flags" argument when instantiating CommandBase. Options are more Python Zen compliant by using explicit arguments for item, draft, url instead of trying to guess with magic keyword and type detection. Pubsub node and item are now always using respecively "-n" and "-i" even when required, this way shell history can be used to change command more easily, and it's globally less confusing for user. if --pubsub-url is used, elements can be overwritten with individual option (e.g. change item id with --item). New "use_draft" argument in CommandBase, to re-use current draft or open a file path as draft. Item can now be specified when using a draft. If it already exists, its content will be added to current draft (with a separator), to avoid loosing data. common.BaseEdit.getItemPath could be simplified thanks to those changes. Pubsub URI handling has been moved to base.py.
author Goffi <goffi@goffi.org>
date Wed, 21 Mar 2018 19:13:22 +0100
parents b4bf282d6354
children 0a22dc80d671
comparison
equal deleted inserted replaced
2531:1dfc5516dead 2532:772447ec070f
32 from glob import iglob 32 from glob import iglob
33 from importlib import import_module 33 from importlib import import_module
34 from sat_frontends.tools.jid import JID 34 from sat_frontends.tools.jid import JID
35 from sat.tools import config 35 from sat.tools import config
36 from sat.tools.common import dynamic_import 36 from sat.tools.common import dynamic_import
37 from sat.tools.common import uri
37 from sat.core import exceptions 38 from sat.core import exceptions
38 import sat_frontends.jp 39 import sat_frontends.jp
39 from sat_frontends.jp.constants import Const as C 40 from sat_frontends.jp.constants import Const as C
41 from sat_frontends.tools import misc
40 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI 42 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
41 import shlex 43 import shlex
42 from collections import OrderedDict 44 from collections import OrderedDict
43 45
44 ## bridge handling 46 ## bridge handling
314 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) 316 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar"))
315 317
316 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) 318 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
317 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) 319 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)"))
318 320
319 for parent_name in ('pubsub', 'pubsub_node_req'): 321 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
320 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) 322 draft_group = draft_parent.add_argument_group(_('draft handling'))
321 parent.add_argument("-s", "--service", type=unicode_decoder, default=u'', 323 draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft"))
322 help=_(u"JID of the PubSub service (default: PEP service)")) 324 draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve"))
323 if parent_name == 'pubsub_node_req': 325
324 parent.add_argument("node", type=unicode_decoder, help=_(u"node to request")) 326
325 else: 327 def make_pubsub_group(self, flags):
326 parent.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=_(u"node to request")) 328 """generate pubsub options according to flags
329
330 @param flags(iterable[unicode]): see [CommandBase.__init__]
331 @return (ArgumentParser): parser to add
332 """
333 flags = misc.FlagsHandler(flags)
334 parent = argparse.ArgumentParser(add_help=False)
335 pubsub_group = parent.add_argument_group('pubsub')
336 pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder,
337 help=_(u"Pubsub URL (xmpp or http)"))
338
339 service_help = _(u"JID of the PubSub service")
340 if not flags.service:
341 service_help += _(u" (default: PEP service)")
342 pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'',
343 help=service_help)
344
345 node_help = _(u"node to request")
346 if not flags.node:
347 node_help += _(u" (DEFAULT: standard node)")
348 pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help)
349
350 if flags.single_item:
351 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=_(u"item to retrieve"))
352 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item'))
353 elif flags.multi_items:
354 # mutiple items
355 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)"))
356
357 if flags:
358 raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags)))
359
360 return parent
327 361
328 def add_parser_options(self): 362 def add_parser_options(self):
329 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) 363 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
330 364
331 def register_output(self, type_, name, callback, description="", default=False): 365 def register_output(self, type_, name, callback, description="", default=False):
400 else: 434 else:
401 for class_name in class_names: 435 for class_name in class_names:
402 cls = getattr(module, class_name) 436 cls = getattr(module, class_name)
403 cls(self) 437 cls(self)
404 438
439 def get_xmpp_uri_from_http(self, http_url):
440 """parse HTML page at http(s) URL, and looks for xmpp: uri"""
441 if http_url.startswith('https'):
442 scheme = u'https'
443 elif http_url.startswith('http'):
444 scheme = u'http'
445 else:
446 raise exceptions.InternalError(u'An HTTP scheme is expected in this method')
447 self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
448 # HTTP URL, we try to find xmpp: links
449 try:
450 from lxml import etree
451 except ImportError:
452 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
453 self.host.quit(1)
454 import urllib2
455 parser = etree.HTMLParser()
456 try:
457 root = etree.parse(urllib2.urlopen(http_url), parser)
458 except etree.XMLSyntaxError as e:
459 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
460 links = []
461 else:
462 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
463 if not links:
464 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
465 self.host.quit(1)
466 xmpp_uri = links[0].get('href')
467 return xmpp_uri
468
469 def parse_pubsub_args(self):
470 if self.args.pubsub_url is not None:
471 url = self.args.pubsub_url
472
473 if url.startswith('http'):
474 # http(s) URL, we try to retrieve xmpp one from there
475 url = self.get_xmpp_uri_from_http(url)
476
477 try:
478 uri_data = uri.parseXMPPUri(url)
479 except ValueError:
480 self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url))
481 else:
482 if uri_data[u'type'] == 'pubsub':
483 # URL is alright, we only set data not already set by other options
484 if not self.args.service:
485 self.args.service = uri_data[u'path']
486 if not self.args.node:
487 self.args.node = uri_data[u'node']
488 uri_item = uri_data.get(u'item')
489 if uri_item:
490 try:
491 item, item_magic = self.args.item, self.args.item_magic
492 except AttributeError:
493 if not self.args.items:
494 self.args.items = [uri_item]
495 else:
496 if not item and not item_magic:
497 self.args.item = uri_item
498 else:
499 self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url))
500 flags = self.args._cmd._pubsub_flags
501 # we check required arguments here instead of using add_arguments' required option
502 # because the required argument can be set in URL
503 if C.SERVICE in flags and not self.args.service:
504 self.parser.error(_(u"argument -s/--service is required"))
505 if C.NODE in flags and not self.args.node:
506 self.parser.error(_(u"argument -n/--node is required"))
507
508 # FIXME: mutually groups can't be nested in a group and don't support title
509 # so we check conflict here. This may be fixed in Python 3, to be checked
510 try:
511 if self.args.item and self.args.item_last:
512 self.parser.error(_(u"--item and --item-last can't be used at the same time"))
513 except AttributeError:
514 pass
515
405 def run(self, args=None, namespace=None): 516 def run(self, args=None, namespace=None):
406 self.args = self.parser.parse_args(args, namespace=None) 517 self.args = self.parser.parse_args(args, namespace=None)
407 try: 518 if self.args._cmd._use_pubsub:
408 self.args.func() 519 self.parse_pubsub_args()
520 try:
521 self.args._cmd.run()
409 if self._need_loop or self._auto_loop: 522 if self._need_loop or self._auto_loop:
410 self._start_loop() 523 self._start_loop()
411 except KeyboardInterrupt: 524 except KeyboardInterrupt:
412 log.info(_("User interruption: good bye")) 525 log.info(_("User interruption: good bye"))
413 526
580 @param **kwargs: args passed to ArgumentParser 693 @param **kwargs: args passed to ArgumentParser
581 use_* are handled directly, they can be: 694 use_* are handled directly, they can be:
582 - use_progress(bool): if True, add progress bar activation option 695 - use_progress(bool): if True, add progress bar activation option
583 progress* signals will be handled 696 progress* signals will be handled
584 - use_verbose(bool): if True, add verbosity option 697 - use_verbose(bool): if True, add verbosity option
698 - use_pubsub(bool): if True, add pubsub options
699 mandatory arguments are controlled by pubsub_req
700 - use_draft(bool): if True, add draft handling options
701 ** other arguments **
702 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be:
703 C.SERVICE: service is required
704 C.NODE: node is required
705 C.SINGLE_ITEM: only one item is allowed
585 @attribute need_loop(bool): to set by commands when loop is needed 706 @attribute need_loop(bool): to set by commands when loop is needed
586 """ 707 """
587 self.need_loop = False # to be set by commands when loop is needed 708 self.need_loop = False # to be set by commands when loop is needed
588 try: # If we have subcommands, host is a CommandBase and we need to use host.host 709 try: # If we have subcommands, host is a CommandBase and we need to use host.host
589 self.host = host.host 710 self.host = host.host
630 output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option")) 751 output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option"))
631 parents.add(output_parent) 752 parents.add(output_parent)
632 else: 753 else:
633 assert extra_outputs is None 754 assert extra_outputs is None
634 755
635 if 'use_pubsub' in kwargs and 'use_pubsub_node_req' in kwargs: 756 self._use_pubsub = kwargs.pop('use_pubsub', False)
636 raise exceptions.InternalError(u"use_pubsub and use_pubsub_node_req can't be used at the same time." 757 if self._use_pubsub:
637 u"Use the later one when node is required (else an empty string is used as default)") 758 flags = kwargs.pop('pubsub_flags', None)
759 parents.add(self.host.make_pubsub_group(flags))
760 self._pubsub_flags = flags
638 761
639 # other common options 762 # other common options
640 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')} 763 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
641 for param, do_use in use_opts.iteritems(): 764 for param, do_use in use_opts.iteritems():
642 opt=param[4:] # if param is use_verbose, opt is verbose 765 opt=param[4:] # if param is use_verbose, opt is verbose
648 771
649 self.parser = host.subparsers.add_parser(name, help=help, **kwargs) 772 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
650 if hasattr(self, "subcommands"): 773 if hasattr(self, "subcommands"):
651 self.subparsers = self.parser.add_subparsers() 774 self.subparsers = self.parser.add_subparsers()
652 else: 775 else:
653 self.parser.set_defaults(func=self.run) 776 self.parser.set_defaults(_cmd=self)
654 self.add_parser_options() 777 self.add_parser_options()
655 778
656 @property 779 @property
657 def args(self): 780 def args(self):
658 return self.host.args 781 return self.host.args