#!/usr/bin/env python3
# Libervia CLI
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import argparse
import os.path
import re
import sys
import subprocess
import asyncio
import json
from . import base
from libervia.backend.core.i18n import _
from libervia.backend.core import exceptions
from libervia.cli.constants import Const as C
from libervia.cli import common
from libervia.cli import arg_tools
from libervia.cli import xml_tools
from functools import partial
from libervia.backend.tools.common import data_format
from libervia.backend.tools.common import uri
from libervia.backend.tools.common.ansi import ANSI as A
from libervia.backend.tools.common import date_utils
from libervia.frontends.tools import jid, strings
from libervia.frontends.bridge.bridge_frontend import BridgeException
__commands__ = ["Pubsub"]
PUBSUB_TMP_DIR = "pubsub"
PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema"
ALLOWED_SUBSCRIPTIONS_OWNER = ("subscribed", "pending", "none")
# TODO: need to split this class in several modules, plugin should handle subcommands
class NodeInfo(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"info",
use_output=C.OUTPUT_DICT,
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("retrieve node configuration"),
)
def add_parser_options(self):
self.parser.add_argument(
"-k",
"--key",
action="append",
dest="keys",
help=_("data key to filter"),
)
def remove_prefix(self, key):
return key[7:] if key.startswith("pubsub#") else key
def filter_key(self, key):
return any((key == k or key == "pubsub#" + k) for k in self.args.keys)
async def start(self):
try:
config_dict = await self.host.bridge.ps_node_configuration_get(
self.args.service,
self.args.node,
self.profile,
)
except BridgeException as e:
if e.condition == "item-not-found":
service = self.args.service or "PEP"
self.disp(
f"The node {self.args.node} doesn't exist on {service}",
error=True,
)
self.host.quit(C.EXIT_NOT_FOUND)
else:
self.disp(f"can't get node configuration: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
except Exception as e:
self.disp(f"Internal error: {e}", error=True)
self.host.quit(C.EXIT_INTERNAL_ERROR)
else:
key_filter = (lambda k: True) if not self.args.keys else self.filter_key
config_dict = {
self.remove_prefix(k): v for k, v in config_dict.items() if key_filter(k)
}
await self.output(config_dict)
self.host.quit()
class NodeCreate(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"create",
use_output=C.OUTPUT_DICT,
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("create a node"),
)
@staticmethod
def add_node_config_options(parser):
parser.add_argument(
"-f",
"--field",
action="append",
nargs=2,
dest="fields",
default=[],
metavar=("KEY", "VALUE"),
help=_("configuration field to set"),
)
parser.add_argument(
"-F",
"--full-prefix",
action="store_true",
help=_('don\'t prepend "pubsub#" prefix to field names'),
)
def add_parser_options(self):
self.add_node_config_options(self.parser)
@staticmethod
def get_config_options(args):
if not args.full_prefix:
return {"pubsub#" + k: v for k, v in args.fields}
else:
return dict(args.fields)
async def start(self):
options = self.get_config_options(self.args)
try:
node_id = await self.host.bridge.ps_node_create(
self.args.service,
self.args.node,
options,
self.profile,
)
except Exception as e:
self.disp(msg=_("can't create node: {e}").format(e=e), error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
if self.host.verbosity:
announce = _("node created successfully: ")
else:
announce = ""
self.disp(announce + node_id)
self.host.quit()
class NodePurge(base.CommandBase):
def __init__(self, host):
super(NodePurge, self).__init__(
host,
"purge",
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("purge a node (i.e. remove all items from it)"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f",
"--force",
action="store_true",
help=_("purge node without confirmation"),
)
async def start(self):
if not self.args.force:
if not self.args.service:
message = _(
"Are you sure to purge PEP node [{node}]? This will "
"delete ALL items from it!"
).format(node=self.args.node)
else:
message = _(
"Are you sure to delete node [{node}] on service "
"[{service}]? This will delete ALL items from it!"
).format(node=self.args.node, service=self.args.service)
await self.host.confirm_or_quit(message, _("node purge cancelled"))
try:
await self.host.bridge.ps_node_purge(
self.args.service,
self.args.node,
self.profile,
)
except Exception as e:
self.disp(msg=_("can't purge node: {e}").format(e=e), error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("node [{node}] purged successfully").format(node=self.args.node))
self.host.quit()
class NodeDelete(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"delete",
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("delete a node"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f",
"--force",
action="store_true",
help=_("delete node without confirmation"),
)
async def start(self):
if not self.args.force:
if not self.args.service:
message = _("Are you sure to delete PEP node [{node}] ?").format(
node=self.args.node
)
else:
message = _(
"Are you sure to delete node [{node}] on " "service [{service}]?"
).format(node=self.args.node, service=self.args.service)
await self.host.confirm_or_quit(message, _("node deletion cancelled"))
try:
await self.host.bridge.ps_node_delete(
self.args.service,
self.args.node,
self.profile,
)
except Exception as e:
self.disp(f"can't delete node: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("node [{node}] deleted successfully").format(node=self.args.node))
self.host.quit()
class NodeSet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"set",
use_output=C.OUTPUT_DICT,
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("set node configuration"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f",
"--field",
action="append",
nargs=2,
dest="fields",
required=True,
metavar=("KEY", "VALUE"),
help=_("configuration field to set (required)"),
)
self.parser.add_argument(
"-F",
"--full-prefix",
action="store_true",
help=_('don\'t prepend "pubsub#" prefix to field names'),
)
def get_key_name(self, k):
if self.args.full_prefix or k.startswith("pubsub#"):
return k
else:
return "pubsub#" + k
async def start(self):
try:
await self.host.bridge.ps_node_configuration_set(
self.args.service,
self.args.node,
{self.get_key_name(k): v for k, v in self.args.fields},
self.profile,
)
except Exception as e:
self.disp(f"can't set node configuration: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("node configuration successful"), 1)
self.host.quit()
class NodeImport(base.CommandBase):
def __init__(self, host):
super(NodeImport, self).__init__(
host,
"import",
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("import raw XML to a node"),
)
def add_parser_options(self):
self.parser.add_argument(
"--admin",
action="store_true",
help=_("do a pubsub admin request, needed to change publisher"),
)
self.parser.add_argument(
"import_file",
type=argparse.FileType(),
help=_(
"path to the XML file with data to import. The file must contain "
"whole XML of each item to import."
),
)
async def start(self):
try:
element, etree = xml_tools.etree_parse(
self, self.args.import_file, reraise=True
)
except Exception as e:
from lxml.etree import XMLSyntaxError
if isinstance(e, XMLSyntaxError) and e.code == 5:
# we have extra content, this probaby means that item are not wrapped
# so we wrap them here and try again
self.args.import_file.seek(0)
xml_buf = "" + self.args.import_file.read() + ""
element, etree = xml_tools.etree_parse(self, xml_buf)
# we reverse element as we expect to have most recently published element first
# TODO: make this more explicit and add an option
element[:] = reversed(element)
if not all([i.tag == "{http://jabber.org/protocol/pubsub}item" for i in element]):
self.disp(
_("You are not using list of pubsub items, we can't import this file"),
error=True,
)
self.host.quit(C.EXIT_DATA_ERROR)
return
items = [etree.tostring(i, encoding="unicode") for i in element]
if self.args.admin:
method = self.host.bridge.ps_admin_items_send
else:
self.disp(
_(
"Items are imported without using admin mode, publisher can't "
"be changed"
)
)
method = self.host.bridge.ps_items_send
try:
items_ids = await method(
self.args.service,
self.args.node,
items,
"",
self.profile,
)
except Exception as e:
self.disp(f"can't send items: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
if items_ids:
self.disp(
_("items published with id(s) {items_ids}").format(
items_ids=", ".join(items_ids)
)
)
else:
self.disp(_("items published"))
self.host.quit()
class NodeAffiliationsGet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"get",
use_output=C.OUTPUT_DICT,
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("retrieve node affiliations (for node owner)"),
)
def add_parser_options(self):
pass
async def start(self):
try:
affiliations = await self.host.bridge.ps_node_affiliations_get(
self.args.service,
self.args.node,
self.profile,
)
except Exception as e:
self.disp(f"can't get node affiliations: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
await self.output(affiliations)
self.host.quit()
class NodeAffiliationsSet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"set",
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("set affiliations (for node owner)"),
)
def add_parser_options(self):
# XXX: we use optional argument syntax for a required one because list of list of 2 elements
# (used to construct dicts) don't work with positional arguments
self.parser.add_argument(
"-a",
"--affiliation",
dest="affiliations",
metavar=("JID", "AFFILIATION"),
required=True,
action="append",
nargs=2,
help=_("entity/affiliation couple(s)"),
)
async def start(self):
affiliations = dict(self.args.affiliations)
try:
await self.host.bridge.ps_node_affiliations_set(
self.args.service,
self.args.node,
affiliations,
self.profile,
)
except Exception as e:
self.disp(f"can't set node affiliations: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("affiliations have been set"), 1)
self.host.quit()
class NodeAffiliations(base.CommandBase):
subcommands = (NodeAffiliationsGet, NodeAffiliationsSet)
def __init__(self, host):
super(NodeAffiliations, self).__init__(
host,
"affiliations",
use_profile=False,
help=_("set or retrieve node affiliations"),
)
class NodeSubscriptionsGet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"get",
use_output=C.OUTPUT_DICT,
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("retrieve node subscriptions (for node owner)"),
)
def add_parser_options(self):
self.parser.add_argument(
"--public",
action="store_true",
help=_("get public subscriptions"),
)
async def start(self):
if self.args.public:
method = self.host.bridge.ps_public_node_subscriptions_get
else:
method = self.host.bridge.ps_node_subscriptions_get
try:
subscriptions = await method(
self.args.service,
self.args.node,
self.profile,
)
except Exception as e:
self.disp(f"can't get node subscriptions: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
await self.output(subscriptions)
self.host.quit()
class StoreSubscriptionAction(argparse.Action):
"""Action which handle subscription parameter for owner
list is given by pairs: jid and subscription state
if subscription state is not specified, it default to "subscribed"
"""
def __call__(self, parser, namespace, values, option_string):
dest_dict = getattr(namespace, self.dest)
while values:
jid_s = values.pop(0)
try:
subscription = values.pop(0)
except IndexError:
subscription = "subscribed"
if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER:
parser.error(
_("subscription must be one of {}").format(
", ".join(ALLOWED_SUBSCRIPTIONS_OWNER)
)
)
dest_dict[jid_s] = subscription
class NodeSubscriptionsSet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"set",
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("set/modify subscriptions (for node owner)"),
)
def add_parser_options(self):
# XXX: we use optional argument syntax for a required one because list of list of 2 elements
# (uses to construct dicts) don't work with positional arguments
self.parser.add_argument(
"-S",
"--subscription",
dest="subscriptions",
default={},
nargs="+",
metavar=("JID [SUSBSCRIPTION]"),
required=True,
action=StoreSubscriptionAction,
help=_("entity/subscription couple(s)"),
)
async def start(self):
try:
self.host.bridge.ps_node_subscriptions_set(
self.args.service,
self.args.node,
self.args.subscriptions,
self.profile,
)
except Exception as e:
self.disp(f"can't set node subscriptions: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("subscriptions have been set"), 1)
self.host.quit()
class NodeSubscriptions(base.CommandBase):
subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet)
def __init__(self, host):
super(NodeSubscriptions, self).__init__(
host,
"subscriptions",
use_profile=False,
help=_("get or modify node subscriptions"),
)
class NodeSchemaSet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"set",
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("set/replace a schema"),
)
def add_parser_options(self):
self.parser.add_argument("schema", help=_("schema to set (must be XML)"))
async def start(self):
try:
await self.host.bridge.ps_schema_set(
self.args.service,
self.args.node,
self.args.schema,
self.profile,
)
except Exception as e:
self.disp(f"can't set schema: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("schema has been set"), 1)
self.host.quit()
class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
use_items = False
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"edit",
use_pubsub=True,
pubsub_flags={C.NODE},
use_draft=True,
use_verbose=True,
help=_("edit a schema"),
)
common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR)
def add_parser_options(self):
pass
async def publish(self, schema):
try:
await self.host.bridge.ps_schema_set(
self.args.service,
self.args.node,
schema,
self.profile,
)
except Exception as e:
self.disp(f"can't set schema: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp(_("schema has been set"), 1)
self.host.quit()
async def ps_schema_get_cb(self, schema):
try:
from lxml import etree
except ImportError:
self.disp(
"lxml module must be installed to use edit, please install it "
'with "pip install lxml"',
error=True,
)
self.host.quit(1)
content_file_obj, content_file_path = self.get_tmp_file()
schema = schema.strip()
if schema:
parser = etree.XMLParser(remove_blank_text=True)
schema_elt = etree.fromstring(schema, parser)
content_file_obj.write(
etree.tostring(schema_elt, encoding="utf-8", pretty_print=True)
)
content_file_obj.seek(0)
await self.run_editor(
"pubsub_schema_editor_args", content_file_path, content_file_obj
)
async def start(self):
try:
schema = await self.host.bridge.ps_schema_get(
self.args.service,
self.args.node,
self.profile,
)
except BridgeException as e:
if e.condition == "item-not-found" or e.classname == "NotFound":
schema = ""
else:
self.disp(f"can't edit schema: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
await self.ps_schema_get_cb(schema)
class NodeSchemaGet(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"get",
use_output=C.OUTPUT_XML,
use_pubsub=True,
pubsub_flags={C.NODE},
use_verbose=True,
help=_("get schema"),
)
def add_parser_options(self):
pass
async def start(self):
try:
schema = await self.host.bridge.ps_schema_get(
self.args.service,
self.args.node,
self.profile,
)
except BridgeException as e:
if e.condition == "item-not-found" or e.classname == "NotFound":
schema = None
else:
self.disp(f"can't get schema: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
if schema:
await self.output(schema)
self.host.quit()
else:
self.disp(_("no schema found"), 1)
self.host.quit(C.EXIT_NOT_FOUND)
class NodeSchema(base.CommandBase):
subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet)
def __init__(self, host):
super(NodeSchema, self).__init__(
host, "schema", use_profile=False, help=_("data schema manipulation")
)
class Node(base.CommandBase):
subcommands = (
NodeInfo,
NodeCreate,
NodePurge,
NodeDelete,
NodeSet,
NodeImport,
NodeAffiliations,
NodeSubscriptions,
NodeSchema,
)
def __init__(self, host):
super(Node, self).__init__(
host, "node", use_profile=False, help=_("node handling")
)
class CacheGet(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"get",
use_output=C.OUTPUT_LIST_XML,
use_pubsub=True,
pubsub_flags={C.NODE, C.MULTI_ITEMS, C.CACHE},
help=_("get pubsub item(s) from cache"),
)
def add_parser_options(self):
self.parser.add_argument(
"-S",
"--sub-id",
default="",
help=_("subscription id"),
)
async def start(self):
try:
ps_result = data_format.deserialise(
await self.host.bridge.ps_cache_get(
self.args.service,
self.args.node,
self.args.max,
self.args.items,
self.args.sub_id,
self.get_pubsub_extra(),
self.profile,
)
)
except BridgeException as e:
if e.classname == "NotFound":
self.disp(
f"The node {self.args.node} from {self.args.service} is not in cache "
f"for {self.profile}",
error=True,
)
self.host.quit(C.EXIT_NOT_FOUND)
else:
self.disp(f"can't get pubsub items from cache: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
except Exception as e:
self.disp(f"Internal error: {e}", error=True)
self.host.quit(C.EXIT_INTERNAL_ERROR)
else:
await self.output(ps_result["items"])
self.host.quit(C.EXIT_OK)
class CacheSync(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"sync",
use_pubsub=True,
pubsub_flags={C.NODE},
help=_("(re)synchronise a pubsub node"),
)
def add_parser_options(self):
pass
async def start(self):
try:
await self.host.bridge.ps_cache_sync(
self.args.service,
self.args.node,
self.profile,
)
except BridgeException as e:
if e.condition == "item-not-found" or e.classname == "NotFound":
self.disp(
f"The node {self.args.node} doesn't exist on {self.args.service}",
error=True,
)
self.host.quit(C.EXIT_NOT_FOUND)
else:
self.disp(f"can't synchronise pubsub node: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
except Exception as e:
self.disp(f"Internal error: {e}", error=True)
self.host.quit(C.EXIT_INTERNAL_ERROR)
else:
self.host.quit(C.EXIT_OK)
class CachePurge(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"purge",
use_profile=False,
help=_("purge (delete) items from cache"),
)
def add_parser_options(self):
self.parser.add_argument(
"-s", "--service", action="append", metavar="JID", dest="services",
help="purge items only for these services. If not specified, items from ALL "
"services will be purged. May be used several times."
)
self.parser.add_argument(
"-n", "--node", action="append", dest="nodes",
help="purge items only for these nodes. If not specified, items from ALL "
"nodes will be purged. May be used several times."
)
self.parser.add_argument(
"-p", "--profile", action="append", dest="profiles",
help="purge items only for these profiles. If not specified, items from ALL "
"profiles will be purged. May be used several times."
)
self.parser.add_argument(
"-b", "--updated-before", type=base.date_decoder, metavar="TIME_PATTERN",
help="purge items which have been last updated before given time."
)
self.parser.add_argument(
"-C", "--created-before", type=base.date_decoder, metavar="TIME_PATTERN",
help="purge items which have been last created before given time."
)
self.parser.add_argument(
"-t", "--type", action="append", dest="types",
help="purge items flagged with TYPE. May be used several times."
)
self.parser.add_argument(
"-S", "--subtype", action="append", dest="subtypes",
help="purge items flagged with SUBTYPE. May be used several times."
)
self.parser.add_argument(
"-f", "--force", action="store_true",
help=_("purge items without confirmation")
)
async def start(self):
if not self.args.force:
await self.host.confirm_or_quit(
_(
"Are you sure to purge items from cache? You'll have to bypass cache "
"or resynchronise nodes to access deleted items again."
),
_("Items purgins has been cancelled.")
)
purge_data = {}
for key in (
"services", "nodes", "profiles", "updated_before", "created_before",
"types", "subtypes"
):
value = getattr(self.args, key)
if value is not None:
purge_data[key] = value
try:
await self.host.bridge.ps_cache_purge(
data_format.serialise(
purge_data
)
)
except Exception as e:
self.disp(f"Internal error: {e}", error=True)
self.host.quit(C.EXIT_INTERNAL_ERROR)
else:
self.host.quit(C.EXIT_OK)
class CacheReset(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"reset",
use_profile=False,
help=_("remove everything from cache"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f", "--force", action="store_true",
help=_("reset cache without confirmation")
)
async def start(self):
if not self.args.force:
await self.host.confirm_or_quit(
_(
"Are you sure to reset cache? All nodes and items will be removed "
"from it, then it will be progressively refilled as if it were new. "
"This may be resources intensive."
),
_("Pubsub cache reset has been cancelled.")
)
try:
await self.host.bridge.ps_cache_reset()
except Exception as e:
self.disp(f"Internal error: {e}", error=True)
self.host.quit(C.EXIT_INTERNAL_ERROR)
else:
self.host.quit(C.EXIT_OK)
class CacheSearch(base.CommandBase):
def __init__(self, host):
extra_outputs = {
"default": self.default_output,
"xml": self.xml_output,
"xml-raw": self.xml_raw_output,
}
super().__init__(
host,
"search",
use_profile=False,
use_output=C.OUTPUT_LIST_DICT,
extra_outputs=extra_outputs,
help=_("search for pubsub items in cache"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f", "--fts", help=_("Full-Text Search query"), metavar="FTS_QUERY"
)
self.parser.add_argument(
"-p", "--profile", action="append", dest="profiles", metavar="PROFILE",
help="search items only from these profiles. May be used several times."
)
self.parser.add_argument(
"-s", "--service", action="append", dest="services", metavar="SERVICE",
help="items must be from specified service. May be used several times."
)
self.parser.add_argument(
"-n", "--node", action="append", dest="nodes", metavar="NODE",
help="items must be in the specified node. May be used several times."
)
self.parser.add_argument(
"-t", "--type", action="append", dest="types", metavar="TYPE",
help="items must be of specified type. May be used several times."
)
self.parser.add_argument(
"-S", "--subtype", action="append", dest="subtypes", metavar="SUBTYPE",
help="items must be of specified subtype. May be used several times."
)
self.parser.add_argument(
"-P", "--payload", action="store_true", help=_("include item XML payload")
)
self.parser.add_argument(
"-o", "--order-by", action="append", nargs="+",
metavar=("ORDER", "[FIELD] [DIRECTION]"),
help=_("how items must be ordered. May be used several times.")
)
self.parser.add_argument(
"-l", "--limit", type=int, help=_("maximum number of items to return")
)
self.parser.add_argument(
"-i", "--index", type=int, help=_("return results starting from this index")
)
self.parser.add_argument(
"-F",
"--field",
action="append",
nargs=3,
dest="fields",
default=[],
metavar=("PATH", "OPERATOR", "VALUE"),
help=_("parsed data field filter. May be used several times."),
)
self.parser.add_argument(
"-k",
"--key",
action="append",
dest="keys",
metavar="KEY",
help=_(
"data key(s) to display. May be used several times. DEFAULT: show all "
"keys"
),
)
async def start(self):
query = {}
for arg in ("fts", "profiles", "services", "nodes", "types", "subtypes"):
value = getattr(self.args, arg)
if value:
if arg in ("types", "subtypes"):
# empty string is used to find items without type and/or subtype
value = [v or None for v in value]
query[arg] = value
for arg in ("limit", "index"):
value = getattr(self.args, arg)
if value is not None:
query[arg] = value
if self.args.order_by is not None:
for order_data in self.args.order_by:
order, *args = order_data
if order == "field":
if not args:
self.parser.error(_("field data must be specified in --order-by"))
elif len(args) == 1:
path = args[0]
direction = "asc"
elif len(args) == 2:
path, direction = args
else:
self.parser.error(_(
"You can't specify more that 2 arguments for a field in "
"--order-by"
))
try:
path = json.loads(path)
except json.JSONDecodeError:
pass
order_query = {
"path": path,
}
else:
order_query = {
"order": order
}
if not args:
direction = "asc"
elif len(args) == 1:
direction = args[0]
else:
self.parser.error(_(
"there are too many arguments in --order-by option"
))
if direction.lower() not in ("asc", "desc"):
self.parser.error(_("invalid --order-by direction: {direction!r}"))
order_query["direction"] = direction
query.setdefault("order-by", []).append(order_query)
if self.args.fields:
parsed = []
for field in self.args.fields:
path, operator, value = field
try:
path = json.loads(path)
except json.JSONDecodeError:
# this is not a JSON encoded value, we keep it as a string
pass
if not isinstance(path, list):
path = [path]
# handling of TP(