#!/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(