comparison sat_frontends/jp/cmd_pubsub.py @ 3573:813595f88612

merge changes from main branch
author Goffi <goffi@goffi.org>
date Thu, 17 Jun 2021 13:05:58 +0200
parents 04283582966f
children 5f65f4e9f8cb
comparison
equal deleted inserted replaced
3541:888109774673 3573:813595f88612
80 self.args.service, 80 self.args.service,
81 self.args.node, 81 self.args.node,
82 self.profile, 82 self.profile,
83 ) 83 )
84 except BridgeException as e: 84 except BridgeException as e:
85 if e.condition == 'item-not-found': 85 if e.condition == "item-not-found":
86 self.disp( 86 self.disp(
87 f"The node {self.args.node} doesn't exist on {self.args.service}", 87 f"The node {self.args.node} doesn't exist on {self.args.service}",
88 error=True 88 error=True,
89 ) 89 )
90 self.host.quit(C.EXIT_NOT_FOUND) 90 self.host.quit(C.EXIT_NOT_FOUND)
91 else: 91 else:
92 self.disp(f"can't get node configuration: {e}", error=True) 92 self.disp(f"can't get node configuration: {e}", error=True)
93 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 93 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
153 self.args.node, 153 self.args.node,
154 options, 154 options,
155 self.profile, 155 self.profile,
156 ) 156 )
157 except Exception as e: 157 except Exception as e:
158 self.disp(msg=_(f"can't create node: {e}"), error=True) 158 self.disp(msg=_("can't create node: {e}").format(e=e), error=True)
159 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 159 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
160 else: 160 else:
161 if self.host.verbosity: 161 if self.host.verbosity:
162 announce = _("node created successfully: ") 162 announce = _("node created successfully: ")
163 else: 163 else:
165 self.disp(announce + node_id) 165 self.disp(announce + node_id)
166 self.host.quit() 166 self.host.quit()
167 167
168 168
169 class NodePurge(base.CommandBase): 169 class NodePurge(base.CommandBase):
170
171 def __init__(self, host): 170 def __init__(self, host):
172 super(NodePurge, self).__init__( 171 super(NodePurge, self).__init__(
173 host, 172 host,
174 "purge", 173 "purge",
175 use_pubsub=True, 174 use_pubsub=True,
187 186
188 async def start(self): 187 async def start(self):
189 if not self.args.force: 188 if not self.args.force:
190 if not self.args.service: 189 if not self.args.service:
191 message = _( 190 message = _(
192 f"Are you sure to purge PEP node [{self.args.node}]? This will " 191 "Are you sure to purge PEP node [{node}]? This will "
193 f"delete ALL items from it!") 192 "delete ALL items from it!"
193 ).format(node=self.args.node)
194 else: 194 else:
195 message = _( 195 message = _(
196 f"Are you sure to delete node [{self.args.node}] on service " 196 "Are you sure to delete node [{node}] on service "
197 f"[{self.args.service}]? This will delete ALL items from it!") 197 "[{service}]? This will delete ALL items from it!"
198 ).format(node=self.args.node, service=self.args.service)
198 await self.host.confirmOrQuit(message, _("node purge cancelled")) 199 await self.host.confirmOrQuit(message, _("node purge cancelled"))
199 200
200 try: 201 try:
201 await self.host.bridge.psNodePurge( 202 await self.host.bridge.psNodePurge(
202 self.args.service, 203 self.args.service,
203 self.args.node, 204 self.args.node,
204 self.profile, 205 self.profile,
205 ) 206 )
206 except Exception as e: 207 except Exception as e:
207 self.disp(msg=_(f"can't purge node: {e}"), error=True) 208 self.disp(msg=_("can't purge node: {e}").format(e=e), error=True)
208 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 209 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
209 else: 210 else:
210 self.disp(_(f"node [{self.args.node}] purged successfully")) 211 self.disp(_("node [{node}] purged successfully").format(node=self.args.node))
211 self.host.quit() 212 self.host.quit()
212 213
213 214
214 class NodeDelete(base.CommandBase): 215 class NodeDelete(base.CommandBase):
215 def __init__(self, host): 216 def __init__(self, host):
231 ) 232 )
232 233
233 async def start(self): 234 async def start(self):
234 if not self.args.force: 235 if not self.args.force:
235 if not self.args.service: 236 if not self.args.service:
236 message = _(f"Are you sure to delete PEP node [{self.args.node}] ?") 237 message = _("Are you sure to delete PEP node [{node}] ?").format(
238 node=self.args.node
239 )
237 else: 240 else:
238 message = _(f"Are you sure to delete node [{self.args.node}] on " 241 message = _(
239 f"service [{self.args.service}]?") 242 "Are you sure to delete node [{node}] on " "service [{service}]?"
243 ).format(node=self.args.node, service=self.args.service)
240 await self.host.confirmOrQuit(message, _("node deletion cancelled")) 244 await self.host.confirmOrQuit(message, _("node deletion cancelled"))
241 245
242 try: 246 try:
243 await self.host.bridge.psNodeDelete( 247 await self.host.bridge.psNodeDelete(
244 self.args.service, 248 self.args.service,
247 ) 251 )
248 except Exception as e: 252 except Exception as e:
249 self.disp(f"can't delete node: {e}", error=True) 253 self.disp(f"can't delete node: {e}", error=True)
250 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 254 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
251 else: 255 else:
252 self.disp(_(f"node [{self.args.node}] deleted successfully")) 256 self.disp(_("node [{node}] deleted successfully").format(node=self.args.node))
253 self.host.quit() 257 self.host.quit()
254 258
255 259
256 class NodeSet(base.CommandBase): 260 class NodeSet(base.CommandBase):
257 def __init__(self, host): 261 def __init__(self, host):
305 self.disp(_("node configuration successful"), 1) 309 self.disp(_("node configuration successful"), 1)
306 self.host.quit() 310 self.host.quit()
307 311
308 312
309 class NodeImport(base.CommandBase): 313 class NodeImport(base.CommandBase):
310
311 def __init__(self, host): 314 def __init__(self, host):
312 super(NodeImport, self).__init__( 315 super(NodeImport, self).__init__(
313 host, 316 host,
314 "import", 317 "import",
315 use_pubsub=True, 318 use_pubsub=True,
324 help=_("do a pubsub admin request, needed to change publisher"), 327 help=_("do a pubsub admin request, needed to change publisher"),
325 ) 328 )
326 self.parser.add_argument( 329 self.parser.add_argument(
327 "import_file", 330 "import_file",
328 type=argparse.FileType(), 331 type=argparse.FileType(),
329 help=_("path to the XML file with data to import. The file must contain " 332 help=_(
330 "whole XML of each item to import."), 333 "path to the XML file with data to import. The file must contain "
331 ) 334 "whole XML of each item to import."
332 335 ),
333 async def start(self): 336 )
334 try: 337
335 element, etree = xml_tools.etreeParse(self, self.args.import_file, 338 async def start(self):
336 reraise=True) 339 try:
340 element, etree = xml_tools.etreeParse(
341 self, self.args.import_file, reraise=True
342 )
337 except Exception as e: 343 except Exception as e:
338 from lxml.etree import XMLSyntaxError 344 from lxml.etree import XMLSyntaxError
345
339 if isinstance(e, XMLSyntaxError) and e.code == 5: 346 if isinstance(e, XMLSyntaxError) and e.code == 5:
340 # we have extra content, this probaby means that item are not wrapped 347 # we have extra content, this probaby means that item are not wrapped
341 # so we wrap them here and try again 348 # so we wrap them here and try again
342 self.args.import_file.seek(0) 349 self.args.import_file.seek(0)
343 xml_buf = "<import>" + self.args.import_file.read() + "</import>" 350 xml_buf = "<import>" + self.args.import_file.read() + "</import>"
344 element, etree = xml_tools.etreeParse(self, xml_buf) 351 element, etree = xml_tools.etreeParse(self, xml_buf)
345 352
346 # we reverse element as we expect to have most recently published element first 353 # we reverse element as we expect to have most recently published element first
347 # TODO: make this more explicit and add an option 354 # TODO: make this more explicit and add an option
348 element[:] = reversed(element) 355 element[:] = reversed(element)
349 356
350 if not all([i.tag == '{http://jabber.org/protocol/pubsub}item' for i in element]): 357 if not all([i.tag == "{http://jabber.org/protocol/pubsub}item" for i in element]):
351 self.disp( 358 self.disp(
352 _("You are not using list of pubsub items, we can't import this file"), 359 _("You are not using list of pubsub items, we can't import this file"),
353 error=True) 360 error=True,
361 )
354 self.host.quit(C.EXIT_DATA_ERROR) 362 self.host.quit(C.EXIT_DATA_ERROR)
355 return 363 return
356 364
357 items = [etree.tostring(i, encoding="unicode") for i in element] 365 items = [etree.tostring(i, encoding="unicode") for i in element]
358 if self.args.admin: 366 if self.args.admin:
359 method = self.host.bridge.psAdminItemsSend 367 method = self.host.bridge.psAdminItemsSend
360 else: 368 else:
361 self.disp(_("Items are imported without using admin mode, publisher can't " 369 self.disp(
362 "be changed")) 370 _(
371 "Items are imported without using admin mode, publisher can't "
372 "be changed"
373 )
374 )
363 method = self.host.bridge.psItemsSend 375 method = self.host.bridge.psItemsSend
364 376
365 try: 377 try:
366 items_ids = await method( 378 items_ids = await method(
367 self.args.service, 379 self.args.service,
373 except Exception as e: 385 except Exception as e:
374 self.disp(f"can't send items: {e}", error=True) 386 self.disp(f"can't send items: {e}", error=True)
375 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 387 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
376 else: 388 else:
377 if items_ids: 389 if items_ids:
378 self.disp(_('items published with id(s) {items_ids}').format( 390 self.disp(
379 items_ids=', '.join(items_ids))) 391 _("items published with id(s) {items_ids}").format(
392 items_ids=", ".join(items_ids)
393 )
394 )
380 else: 395 else:
381 self.disp(_('items published')) 396 self.disp(_("items published"))
382 self.host.quit() 397 self.host.quit()
383 398
384 399
385 class NodeAffiliationsGet(base.CommandBase): 400 class NodeAffiliationsGet(base.CommandBase):
386 def __init__(self, host): 401 def __init__(self, host):
643 658
644 async def psSchemaGetCb(self, schema): 659 async def psSchemaGetCb(self, schema):
645 try: 660 try:
646 from lxml import etree 661 from lxml import etree
647 except ImportError: 662 except ImportError:
648 self.disp('lxml module must be installed to use edit, please install it ' 663 self.disp(
649 'with "pip install lxml"', 664 "lxml module must be installed to use edit, please install it "
665 'with "pip install lxml"',
650 error=True, 666 error=True,
651 ) 667 )
652 self.host.quit(1) 668 self.host.quit(1)
653 content_file_obj, content_file_path = self.getTmpFile() 669 content_file_obj, content_file_path = self.getTmpFile()
654 schema = schema.strip() 670 schema = schema.strip()
657 schema_elt = etree.fromstring(schema, parser) 673 schema_elt = etree.fromstring(schema, parser)
658 content_file_obj.write( 674 content_file_obj.write(
659 etree.tostring(schema_elt, encoding="utf-8", pretty_print=True) 675 etree.tostring(schema_elt, encoding="utf-8", pretty_print=True)
660 ) 676 )
661 content_file_obj.seek(0) 677 content_file_obj.seek(0)
662 await self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj) 678 await self.runEditor(
679 "pubsub_schema_editor_args", content_file_path, content_file_obj
680 )
663 681
664 async def start(self): 682 async def start(self):
665 try: 683 try:
666 schema = await self.host.bridge.psSchemaGet( 684 schema = await self.host.bridge.psSchemaGet(
667 self.args.service, 685 self.args.service,
668 self.args.node, 686 self.args.node,
669 self.profile, 687 self.profile,
670 ) 688 )
671 except BridgeException as e: 689 except BridgeException as e:
672 if e.condition == 'item-not-found' or e.classname=="NotFound": 690 if e.condition == "item-not-found" or e.classname == "NotFound":
673 schema = "" 691 schema = ""
674 else: 692 else:
675 self.disp(f"can't edit schema: {e}", error=True) 693 self.disp(f"can't edit schema: {e}", error=True)
676 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 694 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
677 695
700 self.args.service, 718 self.args.service,
701 self.args.node, 719 self.args.node,
702 self.profile, 720 self.profile,
703 ) 721 )
704 except BridgeException as e: 722 except BridgeException as e:
705 if e.condition == 'item-not-found' or e.classname=="NotFound": 723 if e.condition == "item-not-found" or e.classname == "NotFound":
706 schema = None 724 schema = None
707 else: 725 else:
708 self.disp(f"can't get schema: {e}", error=True) 726 self.disp(f"can't get schema: {e}", error=True)
709 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 727 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
710 728
770 element = xml_tools.getPayload(self, element) 788 element = xml_tools.getPayload(self, element)
771 payload = etree.tostring(element, encoding="unicode") 789 payload = etree.tostring(element, encoding="unicode")
772 extra = {} 790 extra = {}
773 publish_options = NodeCreate.get_config_options(self.args) 791 publish_options = NodeCreate.get_config_options(self.args)
774 if publish_options: 792 if publish_options:
775 extra['publish_options'] = publish_options 793 extra["publish_options"] = publish_options
776 794
777 try: 795 try:
778 published_id = await self.host.bridge.psItemSend( 796 published_id = await self.host.bridge.psItemSend(
779 self.args.service, 797 self.args.service,
780 self.args.node, 798 self.args.node,
782 self.args.item, 800 self.args.item,
783 data_format.serialise(extra), 801 data_format.serialise(extra),
784 self.profile, 802 self.profile,
785 ) 803 )
786 except Exception as e: 804 except Exception as e:
787 self.disp(_(f"can't send item: {e}"), error=True) 805 self.disp(_("can't send item: {e}").format(e=e), error=True)
788 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 806 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
789 else: 807 else:
790 if published_id: 808 if published_id:
791 if self.args.quiet: 809 if self.args.quiet:
792 self.disp(published_id, end='') 810 self.disp(published_id, end="")
793 else: 811 else:
794 self.disp(f"Item published at {published_id}") 812 self.disp(f"Item published at {published_id}")
795 else: 813 else:
796 self.disp("Item published") 814 self.disp("Item published")
797 self.host.quit(C.EXIT_OK) 815 self.host.quit(C.EXIT_OK)
831 self.getPubsubExtra(), 849 self.getPubsubExtra(),
832 self.profile, 850 self.profile,
833 ) 851 )
834 ) 852 )
835 except BridgeException as e: 853 except BridgeException as e:
836 if e.condition == 'item-not-found' or e.classname=="NotFound": 854 if e.condition == "item-not-found" or e.classname == "NotFound":
837 self.disp( 855 self.disp(
838 f"The node {self.args.node} doesn't exist on {self.args.service}", 856 f"The node {self.args.node} doesn't exist on {self.args.service}",
839 error=True 857 error=True,
840 ) 858 )
841 self.host.quit(C.EXIT_NOT_FOUND) 859 self.host.quit(C.EXIT_NOT_FOUND)
842 else: 860 else:
843 self.disp(f"can't get pubsub items: {e}", error=True) 861 self.disp(f"can't get pubsub items: {e}", error=True)
844 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 862 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
845 except Exception as e: 863 except Exception as e:
846 self.disp(f"Internal error: {e}", error=True) 864 self.disp(f"Internal error: {e}", error=True)
847 self.host.quit(C.EXIT_INTERNAL_ERROR) 865 self.host.quit(C.EXIT_INTERNAL_ERROR)
848 else: 866 else:
849 await self.output(ps_result['items']) 867 await self.output(ps_result["items"])
850 self.host.quit(C.EXIT_OK) 868 self.host.quit(C.EXIT_OK)
851 869
852 870
853 class Delete(base.CommandBase): 871 class Delete(base.CommandBase):
854 def __init__(self, host): 872 def __init__(self, host):
884 self.args.item, 902 self.args.item,
885 self.args.notify, 903 self.args.notify,
886 self.profile, 904 self.profile,
887 ) 905 )
888 except Exception as e: 906 except Exception as e:
889 self.disp(_(f"can't delete item: {e}"), error=True) 907 self.disp(_("can't delete item: {e}").format(e=e), error=True)
890 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 908 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
891 else: 909 else:
892 self.disp(_(f"item {self.args.item} has been deleted")) 910 self.disp(_("item {item} has been deleted").format(item=self.args.item))
893 self.host.quit(C.EXIT_OK) 911 self.host.quit(C.EXIT_OK)
894 912
895 913
896 class Edit(base.CommandBase, common.BaseEdit): 914 class Edit(base.CommandBase, common.BaseEdit):
897 def __init__(self, host): 915 def __init__(self, host):
926 944
927 async def getItemData(self, service, node, item): 945 async def getItemData(self, service, node, item):
928 try: 946 try:
929 from lxml import etree 947 from lxml import etree
930 except ImportError: 948 except ImportError:
931 self.disp('lxml module must be installed to use edit, please install it ' 949 self.disp(
932 'with "pip install lxml"', 950 "lxml module must be installed to use edit, please install it "
951 'with "pip install lxml"',
933 error=True, 952 error=True,
934 ) 953 )
935 self.host.quit(1) 954 self.host.quit(1)
936 items = [item] if item else [] 955 items = [item] if item else []
937 ps_result = data_format.deserialise( 956 ps_result = data_format.deserialise(
938 await self.host.bridge.psItemsGet( 957 await self.host.bridge.psItemsGet(
939 service, node, 1, items, "", {}, self.profile 958 service, node, 1, items, "", {}, self.profile
940 ) 959 )
941 ) 960 )
942 item_raw = ps_result['items'][0] 961 item_raw = ps_result["items"][0]
943 parser = etree.XMLParser(remove_blank_text=True, recover=True) 962 parser = etree.XMLParser(remove_blank_text=True, recover=True)
944 item_elt = etree.fromstring(item_raw, parser) 963 item_elt = etree.fromstring(item_raw, parser)
945 item_id = item_elt.get("id") 964 item_id = item_elt.get("id")
946 try: 965 try:
947 payload = item_elt[0] 966 payload = item_elt[0]
949 self.disp(_("Item has not payload"), 1) 968 self.disp(_("Item has not payload"), 1)
950 return "", item_id 969 return "", item_id
951 return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id 970 return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id
952 971
953 async def start(self): 972 async def start(self):
954 (self.pubsub_service, 973 (
955 self.pubsub_node, 974 self.pubsub_service,
956 self.pubsub_item, 975 self.pubsub_node,
957 content_file_path, 976 self.pubsub_item,
958 content_file_obj) = await self.getItemPath() 977 content_file_path,
978 content_file_obj,
979 ) = await self.getItemPath()
959 await self.runEditor("pubsub_editor_args", content_file_path, content_file_obj) 980 await self.runEditor("pubsub_editor_args", content_file_path, content_file_obj)
960 self.host.quit() 981 self.host.quit()
961 982
962 983
963 class Rename(base.CommandBase): 984 class Rename(base.CommandBase):
964
965 def __init__(self, host): 985 def __init__(self, host):
966 base.CommandBase.__init__( 986 base.CommandBase.__init__(
967 self, 987 self,
968 host, 988 host,
969 "rename", 989 "rename",
971 pubsub_flags={C.NODE, C.SINGLE_ITEM}, 991 pubsub_flags={C.NODE, C.SINGLE_ITEM},
972 help=_("rename a pubsub item"), 992 help=_("rename a pubsub item"),
973 ) 993 )
974 994
975 def add_parser_options(self): 995 def add_parser_options(self):
976 self.parser.add_argument( 996 self.parser.add_argument("new_id", help=_("new item id to use"))
977 "new_id",
978 help=_("new item id to use")
979 )
980 997
981 async def start(self): 998 async def start(self):
982 try: 999 try:
983 await self.host.bridge.psItemRename( 1000 await self.host.bridge.psItemRename(
984 self.args.service, 1001 self.args.service,
986 self.args.item, 1003 self.args.item,
987 self.args.new_id, 1004 self.args.new_id,
988 self.profile, 1005 self.profile,
989 ) 1006 )
990 except Exception as e: 1007 except Exception as e:
991 self.disp( 1008 self.disp(f"can't rename item: {e}", error=True)
992 f"can't rename item: {e}", error=True
993 )
994 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1009 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
995 else: 1010 else:
996 self.disp("Item renamed") 1011 self.disp("Item renamed")
997 self.host.quit(C.EXIT_OK) 1012 self.host.quit(C.EXIT_OK)
998 1013
1019 self.args.node, 1034 self.args.node,
1020 {}, 1035 {},
1021 self.profile, 1036 self.profile,
1022 ) 1037 )
1023 except Exception as e: 1038 except Exception as e:
1024 self.disp(_(f"can't subscribe to node: {e}"), error=True) 1039 self.disp(_("can't subscribe to node: {e}").format(e=e), error=True)
1025 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1040 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1026 else: 1041 else:
1027 self.disp(_("subscription done"), 1) 1042 self.disp(_("subscription done"), 1)
1028 if sub_id: 1043 if sub_id:
1029 self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id)) 1044 self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id))
1053 self.args.service, 1068 self.args.service,
1054 self.args.node, 1069 self.args.node,
1055 self.profile, 1070 self.profile,
1056 ) 1071 )
1057 except Exception as e: 1072 except Exception as e:
1058 self.disp(_(f"can't unsubscribe from node: {e}"), error=True) 1073 self.disp(_("can't unsubscribe from node: {e}").format(e=e), error=True)
1059 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1074 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1060 else: 1075 else:
1061 self.disp(_("subscription removed"), 1) 1076 self.disp(_("subscription removed"), 1)
1062 self.host.quit() 1077 self.host.quit()
1063 1078
1082 self.args.service, 1097 self.args.service,
1083 self.args.node, 1098 self.args.node,
1084 self.profile, 1099 self.profile,
1085 ) 1100 )
1086 except Exception as e: 1101 except Exception as e:
1087 self.disp(_(f"can't retrieve subscriptions: {e}"), error=True) 1102 self.disp(_("can't retrieve subscriptions: {e}").format(e=e), error=True)
1088 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1103 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1089 else: 1104 else:
1090 await self.output(subscriptions) 1105 await self.output(subscriptions)
1091 self.host.quit() 1106 self.host.quit()
1092 1107
1111 self.args.service, 1126 self.args.service,
1112 self.args.node, 1127 self.args.node,
1113 self.profile, 1128 self.profile,
1114 ) 1129 )
1115 except Exception as e: 1130 except Exception as e:
1116 self.disp( 1131 self.disp(f"can't get node affiliations: {e}", error=True)
1117 f"can't get node affiliations: {e}", error=True
1118 )
1119 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1132 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1120 else: 1133 else:
1121 await self.output(affiliations) 1134 await self.output(affiliations)
1122 self.host.quit() 1135 self.host.quit()
1123 1136
1167 self.parser.add_argument( 1180 self.parser.add_argument(
1168 "-D", 1181 "-D",
1169 "--max-depth", 1182 "--max-depth",
1170 type=int, 1183 type=int,
1171 default=0, 1184 default=0,
1172 help=_("maximum depth of recursion (will search linked nodes if > 0, " 1185 help=_(
1173 "DEFAULT: 0)"), 1186 "maximum depth of recursion (will search linked nodes if > 0, "
1187 "DEFAULT: 0)"
1188 ),
1174 ) 1189 )
1175 self.parser.add_argument( 1190 self.parser.add_argument(
1176 "-M", 1191 "-M",
1177 "--node-max", 1192 "--node-max",
1178 type=int, 1193 type=int,
1179 default=30, 1194 default=30,
1180 help=_("maximum number of items to get per node ({} to get all items, " 1195 help=_(
1181 "DEFAULT: 30)".format( C.NO_LIMIT)), 1196 "maximum number of items to get per node ({} to get all items, "
1197 "DEFAULT: 30)".format(C.NO_LIMIT)
1198 ),
1182 ) 1199 )
1183 self.parser.add_argument( 1200 self.parser.add_argument(
1184 "-N", 1201 "-N",
1185 "--namespace", 1202 "--namespace",
1186 action="append", 1203 action="append",
1231 "--python", 1248 "--python",
1232 action="append", 1249 action="append",
1233 dest="filters", 1250 dest="filters",
1234 type=filter_python, 1251 type=filter_python,
1235 metavar="PYTHON_CODE", 1252 metavar="PYTHON_CODE",
1236 help=_('Python expression which much return a bool (True to keep item, ' 1253 help=_(
1237 'False to reject it). "item" is raw text item, "item_xml" is ' 1254 "Python expression which much return a bool (True to keep item, "
1238 'lxml\'s etree.Element' 1255 'False to reject it). "item" is raw text item, "item_xml" is '
1256 "lxml's etree.Element"
1239 ), 1257 ),
1240 ) 1258 )
1241 1259
1242 # filters flags 1260 # filters flags
1243 flag_case = partial(self.filter_flag, type_="ignore-case") 1261 flag_case = partial(self.filter_flag, type_="ignore-case")
1360 def parseXml(self, item): 1378 def parseXml(self, item):
1361 try: 1379 try:
1362 return self.etree.fromstring(item) 1380 return self.etree.fromstring(item)
1363 except self.etree.XMLSyntaxError: 1381 except self.etree.XMLSyntaxError:
1364 self.disp( 1382 self.disp(
1365 _("item doesn't looks like XML, you have probably used --only-matching " 1383 _(
1366 "somewhere before and we have no more XML"), 1384 "item doesn't looks like XML, you have probably used --only-matching "
1385 "somewhere before and we have no more XML"
1386 ),
1367 error=True, 1387 error=True,
1368 ) 1388 )
1369 self.host.quit(C.EXIT_BAD_ARG) 1389 self.host.quit(C.EXIT_BAD_ARG)
1370 1390
1371 def filter(self, item): 1391 def filter(self, item):
1395 keep = False 1415 keep = False
1396 if keep and only_matching: 1416 if keep and only_matching:
1397 # doesn't really make sens to keep a fixed string 1417 # doesn't really make sens to keep a fixed string
1398 # so we raise an error 1418 # so we raise an error
1399 self.host.disp( 1419 self.host.disp(
1400 _( 1420 _("--only-matching used with fixed --text string, are you sure?"),
1401 "--only-matching used with fixed --text string, are you sure?"
1402 ),
1403 error=True, 1421 error=True,
1404 ) 1422 )
1405 self.host.quit(C.EXIT_BAD_ARG) 1423 self.host.quit(C.EXIT_BAD_ARG)
1406 elif type_ == "regex": 1424 elif type_ == "regex":
1407 flags = self.RE_FLAGS 1425 flags = self.RE_FLAGS
1418 if item_xml is None: 1436 if item_xml is None:
1419 item_xml = self.parseXml(item) 1437 item_xml = self.parseXml(item)
1420 try: 1438 try:
1421 elts = item_xml.xpath(value, namespaces=self.args.namespace) 1439 elts = item_xml.xpath(value, namespaces=self.args.namespace)
1422 except self.etree.XPathEvalError as e: 1440 except self.etree.XPathEvalError as e:
1423 self.disp( 1441 self.disp(_("can't use xpath: {reason}").format(reason=e), error=True)
1424 _("can't use xpath: {reason}").format(reason=e), error=True
1425 )
1426 self.host.quit(C.EXIT_BAD_ARG) 1442 self.host.quit(C.EXIT_BAD_ARG)
1427 keep = bool(elts) 1443 keep = bool(elts)
1428 if keep and only_matching: 1444 if keep and only_matching:
1429 item_xml = elts[0] 1445 item_xml = elts[0]
1430 try: 1446 try:
1434 item = str(item_xml) 1450 item = str(item_xml)
1435 item_xml = None 1451 item_xml = None
1436 elif type_ == "python": 1452 elif type_ == "python":
1437 if item_xml is None: 1453 if item_xml is None:
1438 item_xml = self.parseXml(item) 1454 item_xml = self.parseXml(item)
1439 cmd_ns = { 1455 cmd_ns = {"etree": self.etree, "item": item, "item_xml": item_xml}
1440 "etree": self.etree,
1441 "item": item,
1442 "item_xml": item_xml
1443 }
1444 try: 1456 try:
1445 keep = eval(value, cmd_ns) 1457 keep = eval(value, cmd_ns)
1446 except SyntaxError as e: 1458 except SyntaxError as e:
1447 self.disp(str(e), error=True) 1459 self.disp(str(e), error=True)
1448 self.host.quit(C.EXIT_BAD_ARG) 1460 self.host.quit(C.EXIT_BAD_ARG)
1449 1461
1450 ## flags 1462 ## flags
1451 1463
1452 elif type_ == "ignore-case": 1464 elif type_ == "ignore-case":
1453 ignore_case = value 1465 ignore_case = value
1454 elif type_ == "invert": 1466 elif type_ == "invert":
1455 invert = value 1467 invert = value
1510 ) 1522 )
1511 if action == "exec": 1523 if action == "exec":
1512 p = await asyncio.create_subprocess_exec(*cmd_args) 1524 p = await asyncio.create_subprocess_exec(*cmd_args)
1513 ret = await p.wait() 1525 ret = await p.wait()
1514 else: 1526 else:
1515 p = await asyncio.create_subprocess_exec(*cmd_args, 1527 p = await asyncio.create_subprocess_exec(*cmd_args, stdin=subprocess.PIPE)
1516 stdin=subprocess.PIPE)
1517 await p.communicate(item.encode(sys.getfilesystemencoding())) 1528 await p.communicate(item.encode(sys.getfilesystemencoding()))
1518 ret = p.returncode 1529 ret = p.returncode
1519 if ret != 0: 1530 if ret != 0:
1520 self.disp( 1531 self.disp(
1521 A.color( 1532 A.color(
1522 C.A_FAILURE, 1533 C.A_FAILURE,
1523 _(f"executed command failed with exit code {ret}"), 1534 _("executed command failed with exit code {ret}").format(ret=ret),
1524 ) 1535 )
1525 ) 1536 )
1526 1537
1527 async def search(self, ps_result, depth): 1538 async def search(self, ps_result, depth):
1528 """callback of getItems 1539 """callback of getItems
1531 do the requested action, and exit the command when everything is done 1542 do the requested action, and exit the command when everything is done
1532 @param items_data(tuple): result of getItems 1543 @param items_data(tuple): result of getItems
1533 @param depth(int): current depth level 1544 @param depth(int): current depth level
1534 0 for first node, 1 for first children, and so on 1545 0 for first node, 1 for first children, and so on
1535 """ 1546 """
1536 for item in ps_result['items']: 1547 for item in ps_result["items"]:
1537 if depth < self.args.max_depth: 1548 if depth < self.args.max_depth:
1538 await self.getSubNodes(item, depth) 1549 await self.getSubNodes(item, depth)
1539 keep, item = self.filter(item) 1550 keep, item = self.filter(item)
1540 if not keep: 1551 if not keep:
1541 continue 1552 continue
1542 await self.doItemAction(item, ps_result) 1553 await self.doItemAction(item, ps_result)
1543 1554
1544 #  we check if we got all getItems results 1555 #  we check if we got all getItems results
1545 self.to_get -= 1 1556 self.to_get -= 1
1546 if self.to_get == 0: 1557 if self.to_get == 0:
1547 # yes, we can quit 1558 # yes, we can quit
1548 self.host.quit() 1559 self.host.quit()
1549 assert self.to_get > 0 1560 assert self.to_get > 0
1560 if self.args.action in self.EXEC_ACTIONS: 1571 if self.args.action in self.EXEC_ACTIONS:
1561 self.parser.error(_("you need to specify a command to execute")) 1572 self.parser.error(_("you need to specify a command to execute"))
1562 if not self.args.node: 1573 if not self.args.node:
1563 # TODO: handle get service affiliations when node is not set 1574 # TODO: handle get service affiliations when node is not set
1564 self.parser.error(_("empty node is not handled yet")) 1575 self.parser.error(_("empty node is not handled yet"))
1565 # to_get is increased on each get and decreased on each answer 1576 # to_get is increased on each get and decreased on each answer
1566 # when it reach 0 again, the command is finished 1577 # when it reach 0 again, the command is finished
1567 self.to_get = 0 1578 self.to_get = 0
1568 self._etree = None 1579 self._etree = None
1569 if self.args.filters is None: 1580 if self.args.filters is None:
1570 self.args.filters = [] 1581 self.args.filters = []
1571 self.args.namespace = dict( 1582 self.args.namespace = dict(
1599 self.parser.add_argument( 1610 self.parser.add_argument(
1600 "-I", 1611 "-I",
1601 "--ignore-errors", 1612 "--ignore-errors",
1602 action="store_true", 1613 action="store_true",
1603 help=_( 1614 help=_(
1604 "if command return a non zero exit code, ignore the item and continue"), 1615 "if command return a non zero exit code, ignore the item and continue"
1616 ),
1605 ) 1617 )
1606 self.parser.add_argument( 1618 self.parser.add_argument(
1607 "-A", 1619 "-A",
1608 "--all", 1620 "--all",
1609 action="store_true", 1621 action="store_true",
1610 help=_("get all items by looping over all pages using RSM") 1622 help=_("get all items by looping over all pages using RSM"),
1611 ) 1623 )
1612 self.parser.add_argument( 1624 self.parser.add_argument(
1613 "command_path", 1625 "command_path",
1614 help=_("path to the command to use. Will be called repetitivly with an " 1626 help=_(
1615 "item as input. Output (full item XML) will be used as new one. " 1627 "path to the command to use. Will be called repetitivly with an "
1616 'Return "DELETE" string to delete the item, and "SKIP" to ignore it'), 1628 "item as input. Output (full item XML) will be used as new one. "
1629 'Return "DELETE" string to delete the item, and "SKIP" to ignore it'
1630 ),
1617 ) 1631 )
1618 1632
1619 async def psItemsSendCb(self, item_ids, metadata): 1633 async def psItemsSendCb(self, item_ids, metadata):
1620 if item_ids: 1634 if item_ids:
1621 self.disp(_('items published with ids {item_ids}').format( 1635 self.disp(
1622 item_ids=', '.join(item_ids))) 1636 _("items published with ids {item_ids}").format(
1623 else: 1637 item_ids=", ".join(item_ids)
1624 self.disp(_('items published')) 1638 )
1639 )
1640 else:
1641 self.disp(_("items published"))
1625 if self.args.all: 1642 if self.args.all:
1626 return await self.handleNextPage(metadata) 1643 return await self.handleNextPage(metadata)
1627 else: 1644 else:
1628 self.host.quit() 1645 self.host.quit()
1629 1646
1632 1649
1633 use to handle --all option 1650 use to handle --all option
1634 @param metadata(dict): metadata as returned by psItemsGet 1651 @param metadata(dict): metadata as returned by psItemsGet
1635 """ 1652 """
1636 try: 1653 try:
1637 last = metadata['rsm']['last'] 1654 last = metadata["rsm"]["last"]
1638 index = int(metadata['rsm']['index']) 1655 index = int(metadata["rsm"]["index"])
1639 count = int(metadata['rsm']['count']) 1656 count = int(metadata["rsm"]["count"])
1640 except KeyError: 1657 except KeyError:
1641 self.disp(_("Can't retrieve all items, RSM metadata not available"), 1658 self.disp(
1642 error=True) 1659 _("Can't retrieve all items, RSM metadata not available"), error=True
1660 )
1643 self.host.quit(C.EXIT_MISSING_FEATURE) 1661 self.host.quit(C.EXIT_MISSING_FEATURE)
1644 except ValueError as e: 1662 except ValueError as e:
1645 self.disp(_("Can't retrieve all items, bad RSM metadata: {msg}") 1663 self.disp(
1646 .format(msg=e), error=True) 1664 _("Can't retrieve all items, bad RSM metadata: {msg}").format(msg=e),
1665 error=True,
1666 )
1647 self.host.quit(C.EXIT_ERROR) 1667 self.host.quit(C.EXIT_ERROR)
1648 1668
1649 if index + self.args.rsm_max >= count: 1669 if index + self.args.rsm_max >= count:
1650 self.disp(_('All items transformed')) 1670 self.disp(_("All items transformed"))
1651 self.host.quit(0) 1671 self.host.quit(0)
1652 1672
1653 self.disp(_('Retrieving next page ({page_idx}/{page_total})').format( 1673 self.disp(
1654 page_idx = int(index/self.args.rsm_max) + 1, 1674 _("Retrieving next page ({page_idx}/{page_total})").format(
1655 page_total = int(count/self.args.rsm_max), 1675 page_idx=int(index / self.args.rsm_max) + 1,
1676 page_total=int(count / self.args.rsm_max),
1656 ) 1677 )
1657 ) 1678 )
1658 1679
1659 extra = self.getPubsubExtra() 1680 extra = self.getPubsubExtra()
1660 extra['rsm_after'] = last 1681 extra["rsm_after"] = last
1661 try: 1682 try:
1662 ps_result = await data_format.deserialise( 1683 ps_result = await data_format.deserialise(
1663 self.host.bridge.psItemsGet( 1684 self.host.bridge.psItemsGet(
1664 self.args.service, 1685 self.args.service,
1665 self.args.node, 1686 self.args.node,
1669 extra, 1690 extra,
1670 self.profile, 1691 self.profile,
1671 ) 1692 )
1672 ) 1693 )
1673 except Exception as e: 1694 except Exception as e:
1674 self.disp( 1695 self.disp(f"can't retrieve items: {e}", error=True)
1675 f"can't retrieve items: {e}", error=True
1676 )
1677 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1696 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1678 else: 1697 else:
1679 await self.psItemsGetCb(ps_result) 1698 await self.psItemsGetCb(ps_result)
1680 1699
1681 async def psItemsGetCb(self, ps_result): 1700 async def psItemsGetCb(self, ps_result):
1682 encoding = 'utf-8' 1701 encoding = "utf-8"
1683 new_items = [] 1702 new_items = []
1684 1703
1685 for item in ps_result['items']: 1704 for item in ps_result["items"]:
1686 if self.check_duplicates: 1705 if self.check_duplicates:
1687 # this is used when we are not ordering by creation 1706 # this is used when we are not ordering by creation
1688 # to avoid infinite loop 1707 # to avoid infinite loop
1689 item_elt, __ = xml_tools.etreeParse(self, item) 1708 item_elt, __ = xml_tools.etreeParse(self, item)
1690 item_id = item_elt.get('id') 1709 item_id = item_elt.get("id")
1691 if item_id in self.items_ids: 1710 if item_id in self.items_ids:
1692 self.disp(_( 1711 self.disp(
1693 "Duplicate found on item {item_id}, we have probably handled " 1712 _(
1694 "all items.").format(item_id=item_id)) 1713 "Duplicate found on item {item_id}, we have probably handled "
1714 "all items."
1715 ).format(item_id=item_id)
1716 )
1695 self.host.quit() 1717 self.host.quit()
1696 self.items_ids.append(item_id) 1718 self.items_ids.append(item_id)
1697 1719
1698 # we launch the command to filter the item 1720 # we launch the command to filter the item
1699 try: 1721 try:
1700 p = await asyncio.create_subprocess_exec( 1722 p = await asyncio.create_subprocess_exec(
1701 self.args.command_path, 1723 self.args.command_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE
1702 stdin=subprocess.PIPE, 1724 )
1703 stdout=subprocess.PIPE)
1704 except OSError as e: 1725 except OSError as e:
1705 exit_code = C.EXIT_CMD_NOT_FOUND if e.errno == 2 else C.EXIT_ERROR 1726 exit_code = C.EXIT_CMD_NOT_FOUND if e.errno == 2 else C.EXIT_ERROR
1706 self.disp(f"Can't execute the command: {e}", error=True) 1727 self.disp(f"Can't execute the command: {e}", error=True)
1707 self.host.quit(exit_code) 1728 self.host.quit(exit_code)
1708 encoding = "utf-8" 1729 encoding = "utf-8"
1709 cmd_std_out, cmd_std_err = await p.communicate(item.encode(encoding)) 1730 cmd_std_out, cmd_std_err = await p.communicate(item.encode(encoding))
1710 ret = p.returncode 1731 ret = p.returncode
1711 if ret != 0: 1732 if ret != 0:
1712 self.disp(f"The command returned a non zero status while parsing the " 1733 self.disp(
1713 f"following item:\n\n{item}", error=True) 1734 f"The command returned a non zero status while parsing the "
1735 f"following item:\n\n{item}",
1736 error=True,
1737 )
1714 if self.args.ignore_errors: 1738 if self.args.ignore_errors:
1715 continue 1739 continue
1716 else: 1740 else:
1717 self.host.quit(C.EXIT_CMD_ERROR) 1741 self.host.quit(C.EXIT_CMD_ERROR)
1718 if cmd_std_err is not None: 1742 if cmd_std_err is not None:
1719 cmd_std_err = cmd_std_err.decode(encoding, errors='ignore') 1743 cmd_std_err = cmd_std_err.decode(encoding, errors="ignore")
1720 self.disp(cmd_std_err, error=True) 1744 self.disp(cmd_std_err, error=True)
1721 cmd_std_out = cmd_std_out.decode(encoding).strip() 1745 cmd_std_out = cmd_std_out.decode(encoding).strip()
1722 if cmd_std_out == "DELETE": 1746 if cmd_std_out == "DELETE":
1723 item_elt, __ = xml_tools.etreeParse(self, item) 1747 item_elt, __ = xml_tools.etreeParse(self, item)
1724 item_id = item_elt.get('id') 1748 item_id = item_elt.get("id")
1725 self.disp(_(f"Deleting item {item_id}")) 1749 self.disp(_("Deleting item {item_id}").format(item_id=item_id))
1726 if self.args.apply: 1750 if self.args.apply:
1727 try: 1751 try:
1728 await self.host.bridge.psItemRetract( 1752 await self.host.bridge.psItemRetract(
1729 self.args.service, 1753 self.args.service,
1730 self.args.node, 1754 self.args.node,
1731 item_id, 1755 item_id,
1732 False, 1756 False,
1733 self.profile, 1757 self.profile,
1734 ) 1758 )
1735 except Exception as e: 1759 except Exception as e:
1736 self.disp( 1760 self.disp(f"can't delete item {item_id}: {e}", error=True)
1737 f"can't delete item {item_id}: {e}", error=True
1738 )
1739 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1761 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1740 continue 1762 continue
1741 elif cmd_std_out == "SKIP": 1763 elif cmd_std_out == "SKIP":
1742 item_elt, __ = xml_tools.etreeParse(self, item) 1764 item_elt, __ = xml_tools.etreeParse(self, item)
1743 item_id = item_elt.get('id') 1765 item_id = item_elt.get("id")
1744 self.disp(_("Skipping item {item_id}").format(item_id=item_id)) 1766 self.disp(_("Skipping item {item_id}").format(item_id=item_id))
1745 continue 1767 continue
1746 element, etree = xml_tools.etreeParse(self, cmd_std_out) 1768 element, etree = xml_tools.etreeParse(self, cmd_std_out)
1747 1769
1748 # at this point command has been run and we have a etree.Element object 1770 # at this point command has been run and we have a etree.Element object
1749 if element.tag not in ("item", "{http://jabber.org/protocol/pubsub}item"): 1771 if element.tag not in ("item", "{http://jabber.org/protocol/pubsub}item"):
1750 self.disp("your script must return a whole item, this is not:\n{xml}" 1772 self.disp(
1751 .format(xml=etree.tostring(element, encoding="unicode")), error=True) 1773 "your script must return a whole item, this is not:\n{xml}".format(
1774 xml=etree.tostring(element, encoding="unicode")
1775 ),
1776 error=True,
1777 )
1752 self.host.quit(C.EXIT_DATA_ERROR) 1778 self.host.quit(C.EXIT_DATA_ERROR)
1753 1779
1754 if not self.args.apply: 1780 if not self.args.apply:
1755 # we have a dry run, we just display filtered items 1781 # we have a dry run, we just display filtered items
1756 serialised = etree.tostring(element, encoding='unicode', 1782 serialised = etree.tostring(
1757 pretty_print=True) 1783 element, encoding="unicode", pretty_print=True
1784 )
1758 self.disp(serialised) 1785 self.disp(serialised)
1759 else: 1786 else:
1760 new_items.append(etree.tostring(element, encoding="unicode")) 1787 new_items.append(etree.tostring(element, encoding="unicode"))
1761 1788
1762 if not self.args.apply: 1789 if not self.args.apply:
1786 1813
1787 async def start(self): 1814 async def start(self):
1788 if self.args.all and self.args.order_by != C.ORDER_BY_CREATION: 1815 if self.args.all and self.args.order_by != C.ORDER_BY_CREATION:
1789 self.check_duplicates = True 1816 self.check_duplicates = True
1790 self.items_ids = [] 1817 self.items_ids = []
1791 self.disp(A.color( 1818 self.disp(
1792 A.FG_RED, A.BOLD, 1819 A.color(
1793 '/!\\ "--all" should be used with "--order-by creation" /!\\\n', 1820 A.FG_RED,
1794 A.RESET, 1821 A.BOLD,
1795 "We'll update items, so order may change during transformation,\n" 1822 '/!\\ "--all" should be used with "--order-by creation" /!\\\n',
1796 "we'll try to mitigate that by stopping on first duplicate,\n" 1823 A.RESET,
1797 "but this method is not safe, and some items may be missed.\n---\n")) 1824 "We'll update items, so order may change during transformation,\n"
1825 "we'll try to mitigate that by stopping on first duplicate,\n"
1826 "but this method is not safe, and some items may be missed.\n---\n",
1827 )
1828 )
1798 else: 1829 else:
1799 self.check_duplicates = False 1830 self.check_duplicates = False
1800 1831
1801 try: 1832 try:
1802 ps_result = data_format.deserialise( 1833 ps_result = data_format.deserialise(
1853 1884
1854 async def start(self): 1885 async def start(self):
1855 if not self.args.service: 1886 if not self.args.service:
1856 try: 1887 try:
1857 jid_ = await self.host.bridge.asyncGetParamA( 1888 jid_ = await self.host.bridge.asyncGetParamA(
1858 "JabberID", 1889 "JabberID", "Connection", profile_key=self.args.profile
1859 "Connection",
1860 profile_key=self.args.profile
1861 ) 1890 )
1862 except Exception as e: 1891 except Exception as e:
1863 self.disp(f"can't retrieve jid: {e}", error=True) 1892 self.disp(f"can't retrieve jid: {e}", error=True)
1864 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1893 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1865 else: 1894 else:
1966 ) 1995 )
1967 except Exception as e: 1996 except Exception as e:
1968 self.disp(f"can't delete hook: {e}", error=True) 1997 self.disp(f"can't delete hook: {e}", error=True)
1969 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 1998 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
1970 else: 1999 else:
1971 self.disp(_(f"{nb_deleted} hook(s) have been deleted")) 2000 self.disp(
2001 _("{nb_deleted} hook(s) have been deleted").format(nb_deleted=nb_deleted)
2002 )
1972 self.host.quit() 2003 self.host.quit()
1973 2004
1974 2005
1975 class HookList(base.CommandBase): 2006 class HookList(base.CommandBase):
1976 def __init__(self, host): 2007 def __init__(self, host):