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