diff 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
line wrap: on
line diff
--- a/frontends/src/jp/base.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/base.py	Wed Mar 21 19:13:22 2018 +0100
@@ -34,9 +34,11 @@
 from sat_frontends.tools.jid import JID
 from sat.tools import config
 from sat.tools.common import dynamic_import
+from sat.tools.common import uri
 from sat.core import exceptions
 import sat_frontends.jp
 from sat_frontends.jp.constants import Const as C
+from sat_frontends.tools import misc
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
 import shlex
 from collections import OrderedDict
@@ -316,14 +318,46 @@
         verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
         verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)"))
 
-        for parent_name in ('pubsub', 'pubsub_node_req'):
-            parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
-            parent.add_argument("-s", "--service", type=unicode_decoder, default=u'',
-                                help=_(u"JID of the PubSub service (default: PEP service)"))
-            if parent_name == 'pubsub_node_req':
-                parent.add_argument("node", type=unicode_decoder, help=_(u"node to request"))
-            else:
-                parent.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=_(u"node to request"))
+        draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
+        draft_group = draft_parent.add_argument_group(_('draft handling'))
+        draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft"))
+        draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve"))
+
+
+    def make_pubsub_group(self, flags):
+        """generate pubsub options according to flags
+
+        @param flags(iterable[unicode]): see [CommandBase.__init__]
+        @return (ArgumentParser): parser to add
+        """
+        flags = misc.FlagsHandler(flags)
+        parent = argparse.ArgumentParser(add_help=False)
+        pubsub_group = parent.add_argument_group('pubsub')
+        pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder,
+                                  help=_(u"Pubsub URL (xmpp or http)"))
+
+        service_help = _(u"JID of the PubSub service")
+        if not flags.service:
+            service_help += _(u" (default: PEP service)")
+        pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'',
+                                  help=service_help)
+
+        node_help = _(u"node to request")
+        if not flags.node:
+            node_help += _(u" (DEFAULT: standard node)")
+        pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help)
+
+        if flags.single_item:
+            pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=_(u"item to retrieve"))
+            pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item'))
+        elif flags.multi_items:
+            # mutiple items
+            pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)"))
+
+        if flags:
+            raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags)))
+
+        return parent
 
     def add_parser_options(self):
         self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
@@ -402,10 +436,89 @@
                 cls = getattr(module, class_name)
                 cls(self)
 
+    def get_xmpp_uri_from_http(self, http_url):
+        """parse HTML page at http(s) URL, and looks for xmpp: uri"""
+        if http_url.startswith('https'):
+            scheme = u'https'
+        elif http_url.startswith('http'):
+            scheme = u'http'
+        else:
+            raise exceptions.InternalError(u'An HTTP scheme is expected in this method')
+        self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
+        # HTTP URL, we try to find xmpp: links
+        try:
+            from lxml import etree
+        except ImportError:
+            self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
+            self.host.quit(1)
+        import urllib2
+        parser = etree.HTMLParser()
+        try:
+            root = etree.parse(urllib2.urlopen(http_url), parser)
+        except etree.XMLSyntaxError as e:
+            self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
+            links = []
+        else:
+            links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
+        if not links:
+            self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
+            self.host.quit(1)
+        xmpp_uri = links[0].get('href')
+        return xmpp_uri
+
+    def parse_pubsub_args(self):
+        if self.args.pubsub_url is not None:
+            url = self.args.pubsub_url
+
+            if url.startswith('http'):
+                # http(s) URL, we try to retrieve xmpp one from there
+                url = self.get_xmpp_uri_from_http(url)
+
+            try:
+                uri_data = uri.parseXMPPUri(url)
+            except ValueError:
+                self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url))
+            else:
+                if uri_data[u'type'] == 'pubsub':
+                    # URL is alright, we only set data not already set by other options
+                    if not self.args.service:
+                        self.args.service = uri_data[u'path']
+                    if not self.args.node:
+                        self.args.node = uri_data[u'node']
+                    uri_item = uri_data.get(u'item')
+                    if uri_item:
+                        try:
+                            item, item_magic = self.args.item, self.args.item_magic
+                        except AttributeError:
+                            if not self.args.items:
+                                self.args.items = [uri_item]
+                        else:
+                            if not item and not item_magic:
+                                self.args.item = uri_item
+                else:
+                    self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url))
+        flags = self.args._cmd._pubsub_flags
+        # we check required arguments here instead of using add_arguments' required option
+        # because the required argument can be set in URL
+        if C.SERVICE in flags and not self.args.service:
+            self.parser.error(_(u"argument -s/--service is required"))
+        if C.NODE in flags and not self.args.node:
+            self.parser.error(_(u"argument -n/--node is required"))
+
+        # FIXME: mutually groups can't be nested in a group and don't support title
+        #        so we check conflict here. This may be fixed in Python 3, to be checked
+        try:
+            if self.args.item and self.args.item_last:
+                self.parser.error(_(u"--item and --item-last can't be used at the same time"))
+        except AttributeError:
+            pass
+
     def run(self, args=None, namespace=None):
         self.args = self.parser.parse_args(args, namespace=None)
+        if self.args._cmd._use_pubsub:
+            self.parse_pubsub_args()
         try:
-            self.args.func()
+            self.args._cmd.run()
             if self._need_loop or self._auto_loop:
                 self._start_loop()
         except KeyboardInterrupt:
@@ -582,6 +695,14 @@
             - use_progress(bool): if True, add progress bar activation option
                 progress* signals will be handled
             - use_verbose(bool): if True, add verbosity option
+            - use_pubsub(bool): if True, add pubsub options
+                mandatory arguments are controlled by pubsub_req
+            - use_draft(bool): if True, add draft handling options
+            ** other arguments **
+            - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be:
+                C.SERVICE: service is required
+                C.NODE: node is required
+                C.SINGLE_ITEM: only one item is allowed
         @attribute need_loop(bool): to set by commands when loop is needed
         """
         self.need_loop = False # to be set by commands when loop is needed
@@ -632,9 +753,11 @@
         else:
             assert extra_outputs is None
 
-        if 'use_pubsub' in kwargs and 'use_pubsub_node_req' in kwargs:
-            raise exceptions.InternalError(u"use_pubsub and use_pubsub_node_req can't be used at the same time."
-                                           u"Use the later one when node is required (else an empty string is used as default)")
+        self._use_pubsub = kwargs.pop('use_pubsub', False)
+        if self._use_pubsub:
+            flags = kwargs.pop('pubsub_flags', None)
+            parents.add(self.host.make_pubsub_group(flags))
+            self._pubsub_flags = flags
 
         # other common options
         use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
@@ -650,7 +773,7 @@
         if hasattr(self, "subcommands"):
             self.subparsers = self.parser.add_subparsers()
         else:
-            self.parser.set_defaults(func=self.run)
+            self.parser.set_defaults(_cmd=self)
         self.add_parser_options()
 
     @property