Mercurial > libervia-backend
comparison frontends/src/jp/cmd_pubsub.py @ 2316:7b448ac50a69
jp (pubsub): new search command:
search is a kind of "grep for Pubsub". It's a powerful command which allows to look for specific data in a pubsub node, recurse sub nodes if requested, and execute an action on the result.
search allows to look for items with following filter:
- simple text search
- regex
- xpath
- python code
filters are read an applied in the order in which they appear on the command line.
Then flags can be used to modify behaviour, currently there are:
- ignore-case to specify if search must be case sensitive or not
- invert to invert result of the search (i.e. don't match instead of match)
- dot-all which is specific for regex, cf. re module
- only-matching which return the matching part instead of the full item
Once a item match filters, an action is applied to it, currenlty there are:
- print, which do a simple output of the full item (default)
- exec, which run a jp command, specifying the service, node and item corresponding to the match
- exteral, which run a external command, sending the full item on stdin
By default search is only done on requested node, but if max-depth is more than 0, sub nodes will be searched too.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 08 Jul 2017 21:54:24 +0200 |
parents | 0b21d87c91cf |
children | f4e05600577b |
comparison
equal
deleted
inserted
replaced
2315:8d9bd5d77336 | 2316:7b448ac50a69 |
---|---|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | 20 |
21 import base | 21 import base |
22 from sat.core.i18n import _ | 22 from sat.core.i18n import _ |
23 from sat.core import exceptions | |
23 from sat_frontends.jp.constants import Const as C | 24 from sat_frontends.jp.constants import Const as C |
24 from sat_frontends.jp import common | 25 from sat_frontends.jp import common |
26 from sat_frontends.jp import arg_tools | |
25 from functools import partial | 27 from functools import partial |
26 from sat.tools.common import uri | 28 from sat.tools.common import uri |
27 from sat_frontends.tools import jid | 29 from sat.tools.common.ansi import ANSI as A |
30 from sat_frontends.tools import jid, strings | |
31 import argparse | |
28 import os.path | 32 import os.path |
33 import re | |
34 import subprocess | |
35 import sys | |
29 | 36 |
30 __commands__ = ["Pubsub"] | 37 __commands__ = ["Pubsub"] |
31 | 38 |
32 PUBSUB_TMP_DIR = u"pubsub" | 39 PUBSUB_TMP_DIR = u"pubsub" |
33 | 40 |
411 self.profile, | 418 self.profile, |
412 callback=self.psAffiliationsGetCb, | 419 callback=self.psAffiliationsGetCb, |
413 errback=self.psAffiliationsGetEb) | 420 errback=self.psAffiliationsGetEb) |
414 | 421 |
415 | 422 |
423 class Search(base.CommandBase): | |
424 """this command to a search without using MAM, i.e. by checking every items if dound by itself, so it may be heavy in resources both for server and client""" | |
425 RE_FLAGS = re.MULTILINE | re.UNICODE | |
426 EXEC_ACTIONS = (u'exec', u'external') | |
427 | |
428 def __init__(self, host): | |
429 base.CommandBase.__init__(self, host, 'search', use_output=C.OUTPUT_XML, use_pubsub=True, use_verbose=True, help=_(u'search items corresponding to filters')) | |
430 self.need_loop=True | |
431 | |
432 @property | |
433 def etree(self): | |
434 """load lxml.etree only if needed""" | |
435 if self._etree is None: | |
436 from lxml import etree | |
437 self._etree = etree | |
438 return self._etree | |
439 | |
440 def filter_opt(self, value, type_): | |
441 value = base.unicode_decoder(value) | |
442 return (type_, value) | |
443 | |
444 def filter_flag(self, value, type_): | |
445 value = C.bool(value) | |
446 return (type_, value) | |
447 | |
448 def add_parser_options(self): | |
449 self.parser.add_argument("-i", "--item", action="append", default=[], dest='items', type=base.unicode_decoder, help=_(u"item id(s)")) | |
450 self.parser.add_argument("-D", "--max-depth", type=int, default=0, help=_(u"maximum depth of recursion (will search linked nodes if > 0, default: 0)")) | |
451 self.parser.add_argument("-m", "--max", type=int, default=30, help=_(u"maximum number of items to get per node ({} to get all items, default: 30)".format(C.NO_LIMIT))) | |
452 self.parser.add_argument("-N", "--namespace", action='append', nargs=2, default=[], | |
453 metavar="NAME NAMESPACE", help=_(u"namespace to use for xpath")) | |
454 | |
455 # filters | |
456 filter_text = partial(self.filter_opt, type_=u'text') | |
457 filter_re = partial(self.filter_opt, type_=u'regex') | |
458 filter_xpath = partial(self.filter_opt, type_=u'xpath') | |
459 filter_python = partial(self.filter_opt, type_=u'python') | |
460 filters = self.parser.add_argument_group(_(u'filters'), _(u'only items corresponding to following filters will be kept')) | |
461 filters.add_argument("-t", "--text", | |
462 action='append', dest='filters', type=filter_text, | |
463 metavar='TEXT', | |
464 help=_(u"full text filter, item must contain this string (XML included)")) | |
465 filters.add_argument("-r", "--regex", | |
466 action='append', dest='filters', type=filter_re, | |
467 metavar='EXPRESSION', | |
468 help=_(u"like --text but using a regular expression")) | |
469 filters.add_argument("-x", "--xpath", | |
470 action='append', dest='filters', type=filter_xpath, | |
471 metavar='XPATH', | |
472 help=_(u"filter items which has elements matching this xpath")) | |
473 filters.add_argument("-P", "--python", | |
474 action='append', dest='filters', type=filter_python, | |
475 metavar='PYTHON_CODE', | |
476 help=_(u'Python expression which much return a bool (True to keep item, False to reject it). "item" is raw text item, "item_xml" is lxml\'s etree.Element')) | |
477 | |
478 # filters flags | |
479 flag_case = partial(self.filter_flag, type_=u'ignore-case') | |
480 flag_invert = partial(self.filter_flag, type_=u'invert') | |
481 flag_dotall = partial(self.filter_flag, type_=u'dotall') | |
482 flag_matching = partial(self.filter_flag, type_=u'only-matching') | |
483 flags = self.parser.add_argument_group(_(u'filters flags'), _(u'filters modifiers (change behaviour of following filters)')) | |
484 flags.add_argument("-C", "--ignore-case", | |
485 action='append', dest='filters', type=flag_case, | |
486 const=('ignore-case', True), nargs='?', | |
487 metavar='BOOLEAN', | |
488 help=_(u"(don't) ignore case in following filters (default: case sensitive)")) | |
489 flags.add_argument("-I", "--invert", | |
490 action='append', dest='filters', type=flag_invert, | |
491 const=('invert', True), nargs='?', | |
492 metavar='BOOLEAN', | |
493 help=_(u"(don't) invert effect of following filters (default: don't invert)")) | |
494 flags.add_argument("-A", "--dot-all", | |
495 action='append', dest='filters', type=flag_dotall, | |
496 const=('dotall', True), nargs='?', | |
497 metavar='BOOLEAN', | |
498 help=_(u"(don't) use DOTALL option for regex (default: don't use)")) | |
499 flags.add_argument("-o", "--only-matching", | |
500 action='append', dest='filters', type=flag_matching, | |
501 const=('only-matching', True), nargs='?', | |
502 metavar='BOOLEAN', | |
503 help=_(u"keep only the matching part of the item")) | |
504 | |
505 # action | |
506 self.parser.add_argument("action", | |
507 default="print", | |
508 nargs='?', | |
509 choices=('print', 'exec', 'external'), | |
510 help=_(u"action to do on found items (default: print)")) | |
511 self.parser.add_argument("command", nargs=argparse.REMAINDER) | |
512 | |
513 def psItemsGetEb(self, failure_, service, node): | |
514 self.disp(u"can't get pubsub items at {service} (node: {node}): {reason}".format( | |
515 service=service, | |
516 node=node, | |
517 reason=failure_), error=True) | |
518 self.to_get -= 1 | |
519 | |
520 def getItems(self, depth, service, node, items): | |
521 search = partial(self.search, depth=depth) | |
522 errback = partial(self.psItemsGetEb, service=service, node=node) | |
523 self.host.bridge.psItemsGet( | |
524 service, | |
525 node, | |
526 self.args.max, | |
527 [], | |
528 "", | |
529 {}, | |
530 self.profile, | |
531 callback=search, | |
532 errback=errback | |
533 ) | |
534 self.to_get += 1 | |
535 | |
536 def _checkPubsubURL(self, match, found_nodes): | |
537 """check that the matched URL is an xmpp: one | |
538 | |
539 @param found_nodes(list[unicode]): found_nodes | |
540 this list will be filled while xmpp: URIs are discovered | |
541 """ | |
542 url = match.group(0) | |
543 if url.startswith(u'xmpp'): | |
544 try: | |
545 url_data = uri.parseXMPPUri(url) | |
546 except ValueError: | |
547 return | |
548 if url_data[u'type'] == u'pubsub': | |
549 found_node = {u'service': url_data[u'path'], | |
550 u'node': url_data[u'node']} | |
551 if u'item' in url_data: | |
552 found_node[u'item'] = url_data[u'item'] | |
553 found_nodes.append(found_node) | |
554 | |
555 def getSubNodes(self, item, depth): | |
556 """look for pubsub URIs in item, and getItems on the linked nodes""" | |
557 found_nodes = [] | |
558 checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes) | |
559 strings.RE_URL.sub(checkURI, item) | |
560 for data in found_nodes: | |
561 self.getItems(depth+1, | |
562 data[u'service'], | |
563 data[u'node'], | |
564 [data[u'item']] if u'item' in data else [] | |
565 ) | |
566 | |
567 def parseXml(self, item): | |
568 try: | |
569 return self.etree.fromstring(item) | |
570 except self.etree.XMLSyntaxError: | |
571 self.disp(_(u"item doesn't looks like XML, you have probably used --only-matching somewhere before and we have no more XML"), error=True) | |
572 self.host.quit(C.EXIT_BAD_ARG) | |
573 | |
574 def filter(self, item): | |
575 """apply filters given on command line | |
576 | |
577 if only-matching is used, item may be modified | |
578 @return (tuple[bool, unicode]): a tuple with: | |
579 - keep: True if item passed the filters | |
580 - item: it is returned in case of modifications | |
581 """ | |
582 ignore_case = False | |
583 invert = False | |
584 dotall = False | |
585 only_matching = False | |
586 item_xml = None | |
587 for type_, value in self.args.filters: | |
588 keep = True | |
589 | |
590 ## filters | |
591 | |
592 if type_ == u'text': | |
593 if ignore_case: | |
594 if value.lower() not in item.lower(): | |
595 keep = False | |
596 else: | |
597 if value not in item: | |
598 keep = False | |
599 if keep and only_matching: | |
600 # doesn't really make sens to keep a fixed string | |
601 # so we raise an error | |
602 self.host.disp(_(u"--only-matching used with fixed --text string, are you sure?"), error=True) | |
603 self.host.quit(C.EXIT_BAD_ARG) | |
604 elif type_ == u'regex': | |
605 flags = self.RE_FLAGS | |
606 if ignore_case: | |
607 flags |= re.IGNORECASE | |
608 if dotall: | |
609 flags |= re.DOTALL | |
610 match = re.search(value, item, flags) | |
611 keep = match != None | |
612 if keep and only_matching: | |
613 item = match.group() | |
614 item_xml = None | |
615 elif type_ == u'xpath': | |
616 if item_xml is None: | |
617 item_xml = self.parseXml(item) | |
618 try: | |
619 elts = item_xml.xpath(value, namespaces=self.args.namespace) | |
620 except self.etree.XPathEvalError as e: | |
621 self.disp(_(u"can't use xpath: {reason}").format(reason=e), error=True) | |
622 self.host.quit(C.EXIT_BAD_ARG) | |
623 keep = bool(elts) | |
624 if keep and only_matching: | |
625 item_xml = elts[0] | |
626 try: | |
627 item = self.etree.tostring(item_xml, encoding='unicode') | |
628 except TypeError: | |
629 # we have a string only, not an element | |
630 item = unicode(item_xml) | |
631 item_xml = None | |
632 elif type_ == u'python': | |
633 if item_xml is None: | |
634 item_xml = self.parseXml(item) | |
635 cmd_ns = {u'item': item, | |
636 u'item_xml': item_xml | |
637 } | |
638 try: | |
639 keep = eval(value, cmd_ns) | |
640 except SyntaxError as e: | |
641 self.disp(unicode(e), error=True) | |
642 self.host.quit(C.EXIT_BAD_ARG) | |
643 | |
644 ## flags | |
645 | |
646 elif type_ == u'ignore-case': | |
647 ignore_case = value | |
648 elif type_ == u'invert': | |
649 invert = value | |
650 # we need to continue, else loop would end here | |
651 continue | |
652 elif type_ == u'dotall': | |
653 dotall = value | |
654 elif type_ == u'only-matching': | |
655 only_matching = value | |
656 else: | |
657 raise exceptions.InternalError(_(u"unknown filter type {type}").format(type=type_)) | |
658 | |
659 if invert: | |
660 keep = not keep | |
661 if not keep: | |
662 return False, item | |
663 | |
664 return True, item | |
665 | |
666 def doItemAction(self, item, metadata): | |
667 """called when item has been kepts and the action need to be done | |
668 | |
669 @param item(unicode): accepted item | |
670 """ | |
671 action = self.args.action | |
672 if action == u'print' or self.host.verbosity > 0: | |
673 try: | |
674 self.output(item) | |
675 except self.etree.XMLSyntaxError: | |
676 # item is not valid XML, but a string | |
677 # can happen when --only-matching is used | |
678 self.disp(item) | |
679 if action in self.EXEC_ACTIONS: | |
680 item_elt = self.parseXml(item) | |
681 if action == u'exec': | |
682 use = {'service': metadata[u'service'], | |
683 'node': metadata[u'node'], | |
684 'item': item_elt.get('id'), | |
685 } | |
686 args = arg_tools.get_use_args(self.host, | |
687 self.args.command, | |
688 use, | |
689 verbose=self.host.verbosity > 1 | |
690 ) | |
691 cmd_args = sys.argv[0:1] + args + self.args.command | |
692 else: | |
693 cmd_args = self.args.command | |
694 | |
695 | |
696 self.disp(u'COMMAND: {command}'.format( | |
697 command = u' '.join([arg_tools.escape(a) for a in cmd_args])), 2) | |
698 if action == u'exec': | |
699 ret = subprocess.call(cmd_args) | |
700 else: | |
701 p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE) | |
702 p.communicate(item) | |
703 ret = p.wait() | |
704 if ret != 0: | |
705 self.disp(A.color(C.A_FAILURE, _(u"executed command failed with exit code {code}").format(code=ret))) | |
706 | |
707 def search(self, items_data, depth): | |
708 """callback of getItems | |
709 | |
710 this method filters items, get sub nodes if needed, | |
711 do the requested action, and exit the command when everything is done | |
712 @param items_data(tuple): result of getItems | |
713 @param depth(int): current depth level | |
714 0 for first node, 1 for first children, and so on | |
715 """ | |
716 items, metadata = items_data | |
717 for item in items: | |
718 if depth < self.args.max_depth: | |
719 self.getSubNodes(item, depth) | |
720 keep, item = self.filter(item) | |
721 if not keep: | |
722 continue | |
723 self.doItemAction(item, metadata) | |
724 | |
725 # we check if we got all getItems results | |
726 self.to_get -= 1 | |
727 if self.to_get == 0: | |
728 # yes, we can quit | |
729 self.host.quit() | |
730 assert self.to_get > 0 | |
731 | |
732 def start(self): | |
733 if self.args.command: | |
734 if self.args.action not in self.EXEC_ACTIONS: | |
735 self.parser.error(_(u"Command can only be used with {actions} actions").format( | |
736 actions=u', '.join(self.EXEC_ACTIONS))) | |
737 else: | |
738 if self.args.action in self.EXEC_ACTIONS: | |
739 self.parser.error(_(u"you need to specify a command to execute")) | |
740 if not self.args.node: | |
741 # TODO: handle get service affiliations when node is not set | |
742 self.parser.error(_(u"empty node is not handled yet")) | |
743 # to_get is increased on each get and decreased on each answer | |
744 # when it reach 0 again, the command is finished | |
745 self.to_get = 0 | |
746 self._etree = None | |
747 if self.args.filters is None: | |
748 self.args.filters = [] | |
749 self.args.namespace = dict(self.args.namespace + [('pubsub', "http://jabber.org/protocol/pubsub")]) | |
750 common.checkURI(self.args) | |
751 self.getItems(0, self.args.service, self.args.node, self.args.items) | |
752 | |
753 | |
416 class Uri(base.CommandBase): | 754 class Uri(base.CommandBase): |
417 | 755 |
418 def __init__(self, host): | 756 def __init__(self, host): |
419 base.CommandBase.__init__(self, host, 'uri', use_profile=False, use_pubsub_node_req=True, help=_(u'build URI')) | 757 base.CommandBase.__init__(self, host, 'uri', use_profile=False, use_pubsub_node_req=True, help=_(u'build URI')) |
420 self.need_loop=True | 758 self.need_loop=True |
490 def __init__(self, host): | 828 def __init__(self, host): |
491 base.CommandBase.__init__(self, host, 'delete', use_pubsub_node_req=True, help=_(u'delete a Pubsub hook')) | 829 base.CommandBase.__init__(self, host, 'delete', use_pubsub_node_req=True, help=_(u'delete a Pubsub hook')) |
492 self.need_loop=True | 830 self.need_loop=True |
493 | 831 |
494 def add_parser_options(self): | 832 def add_parser_options(self): |
495 self.parser.add_argument('-t', '--type', default=u'', choices=('', 'python', 'python_file', 'python_code'), help=_(u"hook type to remove, empty to remove all (DEFAULT: remove all)")) | 833 self.parser.add_argument('-t', '--type', default=u'', choices=('', 'python', 'python_file', 'python_code'), help=_(u"hook type to remove, empty to remove all (default: remove all)")) |
496 self.parser.add_argument('-a', '--arg', dest='hook_arg', type=base.unicode_decoder, default=u'', help=_(u"argument of the hook to remove, empty to remove all (DEFAULT: remove all)")) | 834 self.parser.add_argument('-a', '--arg', dest='hook_arg', type=base.unicode_decoder, default=u'', help=_(u"argument of the hook to remove, empty to remove all (default: remove all)")) |
497 | 835 |
498 def psHookRemoveCb(self, nb_deleted): | 836 def psHookRemoveCb(self, nb_deleted): |
499 self.disp(_(u'{nb_deleted} hook(s) have been deleted').format( | 837 self.disp(_(u'{nb_deleted} hook(s) have been deleted').format( |
500 nb_deleted = nb_deleted)) | 838 nb_deleted = nb_deleted)) |
501 self.host.quit() | 839 self.host.quit() |
545 def __init__(self, host): | 883 def __init__(self, host): |
546 super(Hook, self).__init__(host, 'hook', use_profile=False, help=_('trigger action on Pubsub notifications')) | 884 super(Hook, self).__init__(host, 'hook', use_profile=False, help=_('trigger action on Pubsub notifications')) |
547 | 885 |
548 | 886 |
549 class Pubsub(base.CommandBase): | 887 class Pubsub(base.CommandBase): |
550 subcommands = (Get, Delete, Edit, Node, Affiliations, Hook, Uri) | 888 subcommands = (Get, Delete, Edit, Node, Affiliations, Search, Hook, Uri) |
551 | 889 |
552 def __init__(self, host): | 890 def __init__(self, host): |
553 super(Pubsub, self).__init__(host, 'pubsub', use_profile=False, help=_('PubSub nodes/items management')) | 891 super(Pubsub, self).__init__(host, 'pubsub', use_profile=False, help=_('PubSub nodes/items management')) |