# HG changeset patch # User Goffi # Date 1569394601 -7200 # Node ID fee60f17ebaceaa2f5fe653ee1f40086d9f8bf5f # Parent a1bc34f90fa524a4e87c7fe94f2e951397ad017c jp: jp asyncio port: /!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\ This patch implements the port of jp to asyncio, so it is now correctly using the bridge asynchronously, and it can be used with bridges like `pb`. This also simplify the code, notably for things which were previously implemented with many callbacks (like pagination with RSM). During the process, some behaviours have been modified/fixed, in jp and backends, check diff for details. diff -r a1bc34f90fa5 -r fee60f17ebac sat/core/constants.py --- a/sat/core/constants.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/core/constants.py Wed Sep 25 08:56:41 2019 +0200 @@ -334,6 +334,7 @@ ## Progress error special values ## PROGRESS_ERROR_DECLINED = "declined" #  session has been declined by peer user + PROGRESS_ERROR_FAILED = "failed" #  something went wrong with the session ## Files ## FILE_TYPE_DIRECTORY = "directory" diff -r a1bc34f90fa5 -r fee60f17ebac sat/core/sat_main.py --- a/sat/core/sat_main.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/core/sat_main.py Wed Sep 25 08:56:41 2019 +0200 @@ -260,7 +260,7 @@ ) self._unimport_plugin(plugin_path) continue - except Exception as e: + except Exception: import traceback log.error( diff -r a1bc34f90fa5 -r fee60f17ebac sat/core/xmpp.py --- a/sat/core/xmpp.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/core/xmpp.py Wed Sep 25 08:56:41 2019 +0200 @@ -86,11 +86,11 @@ self.actions = {} # used to keep track of actions for retrieval (key = action_id) self.encryption = encryption.EncryptionHandler(self) - def __unicode__(self): - return "Client instance for profile {profile}".format(profile=self.profile) + def __str__(self): + return f"Client for profile {self.profile}" - def __str__(self): - return self.__unicode__.encode('utf-8') + def __repr__(self): + return f"{super().__repr__()} - profile: {self.profile!r}" ## initialisation ## diff -r a1bc34f90fa5 -r fee60f17ebac sat/memory/crypto.py --- a/sat/memory/crypto.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/memory/crypto.py Wed Sep 25 08:56:41 2019 +0200 @@ -45,7 +45,7 @@ @param key (unicode): the encryption key @param text (unicode): the text to encrypt @param leave_empty (bool): if True, empty text will be returned "as is" - @return: Deferred: base-64 encoded str + @return (D(str)): base-64 encoded encrypted message """ if leave_empty and text == "": return succeed(text) @@ -59,6 +59,7 @@ cipher = AES.new(key, AES.MODE_CFB, iv) d = deferToThread(cipher.encrypt, BlockCipher.pad(text.encode("utf-8"))) d.addCallback(lambda ciphertext: b64encode(iv + ciphertext)) + d.addCallback(lambda bytes_cypher: bytes_cypher.decode('utf-8')) return d @classmethod @@ -137,7 +138,7 @@ @return: Deferred: base-64 encoded str """ if leave_empty and password == "": - return succeed(b"") + return succeed("") salt = ( b64decode(salt)[: PasswordHasher.SALT_LEN] if salt @@ -145,11 +146,12 @@ ) d = deferToThread(PBKDF2, password, salt) d.addCallback(lambda hashed: b64encode(salt + hashed)) + d.addCallback(lambda hashed_bytes: hashed_bytes.decode('utf-8')) return d @classmethod def compare_hash(cls, hashed_attempt, hashed): - assert isinstance(hashed, bytes) + assert isinstance(hashed, str) return hashed_attempt == hashed @classmethod @@ -160,7 +162,9 @@ @param hashed (str): the hash of the password @return: Deferred: boolean """ + assert isinstance(attempt, str) + assert isinstance(hashed, str) leave_empty = hashed == "" d = PasswordHasher.hash(attempt, hashed, leave_empty) - d.addCallback(cls.compare_hash, hashed=hashed.encode('utf-8')) + d.addCallback(cls.compare_hash, hashed=hashed) return d diff -r a1bc34f90fa5 -r fee60f17ebac sat/memory/disco.py --- a/sat/memory/disco.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/memory/disco.py Wed Sep 25 08:56:41 2019 +0200 @@ -434,7 +434,7 @@ extensions[form_type or ""] = fields defer.returnValue(( - disco_infos.features, + [str(f) for f in disco_infos.features], [(cat, type_, name or "") for (cat, type_), name in list(disco_infos.identities.items())], extensions)) diff -r a1bc34f90fa5 -r fee60f17ebac sat/memory/memory.py --- a/sat/memory/memory.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/memory/memory.py Wed Sep 25 08:56:41 2019 +0200 @@ -560,9 +560,11 @@ def initPersonalKey(__): # be sure to call this after checking that the profile doesn't exist yet + + # generated once for all and saved in a PersistentDict personal_key = BlockCipher.getRandomKey( base64=True - ) # generated once for all and saved in a PersistentDict + ).decode('utf-8') self.auth_sessions.newSession( {C.MEMORY_CRYPTO_KEY: personal_key}, profile=name ) # will be encrypted by setParam @@ -1260,8 +1262,8 @@ parent = current["parent"] if not parent: break - files_data = yield self.getFile( - self, client, peer_jid=None, file_id=parent, perms_to_check=None + files_data = yield self.getFiles( + client, peer_jid=None, file_id=parent, perms_to_check=None ) try: current = files_data[0] diff -r a1bc34f90fa5 -r fee60f17ebac sat/memory/params.py --- a/sat/memory/params.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/memory/params.py Wed Sep 25 08:56:41 2019 +0200 @@ -529,17 +529,18 @@ return d.addCallback(gotPlainPassword) - def __type_to_string(self, result): - """ convert result to string, according to its type """ + def _type_to_str(self, result): + """Convert result to string, according to its type """ if isinstance(result, bool): - return "true" if result else "false" - elif isinstance(result, int): + return C.boolConst(result) + elif isinstance(result, (list, set, tuple)): + return ', '.join(self._type_to_str(r) for r in result) + else: return str(result) - return result def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): """ Same as getParamA but for bridge: convert non string value to string """ - return self.__type_to_string( + return self._type_to_str( self.getParamA(name, category, attr, profile_key=profile_key) ) @@ -599,15 +600,10 @@ return self._getAttr(node[1], attr, value) def asyncGetStringParamA( - self, - name, - category, - attr="value", - security_limit=C.NO_SECURITY_LIMIT, - profile_key=C.PROF_KEY_NONE, - ): + self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE): d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) - d.addCallback(self.__type_to_string) + d.addCallback(self._type_to_str) return d def asyncGetParamA( @@ -957,14 +953,8 @@ categories.append(cat.getAttribute("name")) return categories - def setParam( - self, - name, - value, - category, - security_limit=C.NO_SECURITY_LIMIT, - profile_key=C.PROF_KEY_NONE, - ): + def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE): """Set a parameter, return None if the parameter is not in param xml. Parameter of type 'password' that are not the SàT profile password are @@ -994,13 +984,11 @@ return defer.succeed(None) if not self.checkSecurityLimit(node[1], security_limit): - log.warning( - _( - "Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!" - % {"param": name, "cat": category} - ) - ) - return defer.succeed(None) + msg = _( + f"{profile!r} is trying to set parameter {name!r} in category " + f"{category!r} without authorization!!!") + log.warning(msg) + raise exceptions.PermissionError(msg) type_ = node[1].getAttribute("type") if type_ == "int": @@ -1010,12 +998,10 @@ try: int(value) except ValueError: - log.debug( - _( - "Trying to set parameter '%(param)s' in category '%(cat)s' with an non-integer value" - % {"param": name, "cat": category} - ) - ) + log.warning(_( + f"Trying to set parameter {name!r} in category {category!r} with" + f"an non-integer value" + )) return defer.succeed(None) if node[1].hasAttribute("constraint"): constraint = node[1].getAttribute("constraint") diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_adhoc_dbus.py --- a/sat/plugins/plugin_adhoc_dbus.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_adhoc_dbus.py Wed Sep 25 08:56:41 2019 +0200 @@ -236,7 +236,7 @@ flags=flags, ) - defer.returnValue((bus_name, methods)) + defer.returnValue((str(bus_name), methods)) def _addCommand(self, client, adhoc_name, bus_name, methods, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_exp_events.py --- a/sat/plugins/plugin_exp_events.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_exp_events.py Wed Sep 25 08:56:41 2019 +0200 @@ -381,7 +381,7 @@ if register: yield self.register( - client, service, None, {}, node, event_id, event_elt, creator=True) + client, None, {}, service, node, event_id, item_elt, creator=True) defer.returnValue(node) def _eventModify(self, service, node, id_, timestamp_update, data_update, @@ -447,24 +447,29 @@ events.append((timestamp, data)) defer.returnValue(events) - def _eventInviteeGet(self, service, node, profile_key): + def _eventInviteeGet(self, service, node, invitee_jid_s, profile_key): service = jid.JID(service) if service else None node = node if node else NS_EVENT client = self.host.getClient(profile_key) - return self.eventInviteeGet(client, service, node) + invitee_jid = jid.JID(invitee_jid_s) if invitee_jid_s else None + return self.eventInviteeGet(client, service, node, invitee_jid) @defer.inlineCallbacks - def eventInviteeGet(self, client, service, node): + def eventInviteeGet(self, client, service, node, invitee_jid=None): """Retrieve attendance from event node @param service(unicode, None): PubSub service - @param node(unicode): PubSub node of the event + @param node(unicode): PubSub node of the event's invitees + @param invitee_jid(jid.JId, None): jid of the invitee to retrieve (None to + retrieve profile's invitation). The bare jid correspond to the PubSub item id. @return (dict): a dict with current attendance status, an empty dict is returned if nothing has been answered yed """ + if invitee_jid is None: + invitee_jid = client.jid try: items, metadata = yield self._p.getItems( - client, service, node, item_ids=[client.jid.userhost()] + client, service, node, item_ids=[invitee_jid.userhost()] ) event_elt = next(items[0].elements(NS_EVENT, "invitee")) except (exceptions.NotFound, IndexError): diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_exp_pubsub_schema.py --- a/sat/plugins/plugin_exp_pubsub_schema.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_exp_pubsub_schema.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,11 +18,11 @@ # along with this program. If not, see . from collections import Iterable -import copy import itertools from zope.interface import implementer from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.xish import domish from twisted.internet import defer from wokkel import disco, iwokkel from wokkel import data_form @@ -174,7 +174,16 @@ ) elif isinstance(schema, data_form.Form): if copy_form: - schema = copy.deepcopy(schema) + # XXX: we don't use deepcopy as it will do an infinite loop if a + # domish.Element is present in the form fields (happens for + # XEP-0315 data forms XML Element) + schema = data_form.Form( + formType = schema.formType, + title = schema.title, + instructions = schema.instructions[:], + formNamespace = schema.formNamespace, + fields = schema.fieldList, + ) defer.returnValue(schema) try: @@ -394,6 +403,18 @@ if not values_list: # if values don't map to allowed values, we use default ones values_list = field.values + elif field.ext_type == 'xml': + # FIXME: XML elements are not handled correctly, we need to know if we + # actual XML/XHTML, or text to escape + for idx, value in enumerate(values_list[:]): + if not isinstance(value, domish.Element): + if field.value and field.value.uri == C.NS_XHTML: + div_elt = domish.Element((C.NS_XHTML, 'div')) + div_elt.addContent(str(value)) + values_list[idx] = div_elt + else: + raise NotImplementedError + field.values = values_list yield self._p.sendItem( @@ -546,9 +567,7 @@ form = data_form.findForm(item_elt, form_ns) if form is None: log.warning( - _( - "Can't parse previous item, update ignored: data form not found" - ).format(reason=e) + _("Can't parse previous item, update ignored: data form not found") ) else: for name, field in form.fields.items(): diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_import.py --- a/sat/plugins/plugin_import.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_import.py Wed Sep 25 08:56:41 2019 +0200 @@ -64,7 +64,7 @@ @param name(unicode): import handler name """ assert name == name.lower().strip() - log.info(_("initializing {name} import handler").format(name=name)) + log.info(_(f"initializing {name} import handler")) import_handler.name = name import_handler.register = partial(self.register, import_handler) import_handler.unregister = partial(self.unregister, import_handler) @@ -139,16 +139,8 @@ else: return importer.short_desc, importer.long_desc - def _doImport( - self, - import_handler, - name, - location, - options, - pubsub_service="", - pubsub_node="", - profile=C.PROF_KEY_NONE, - ): + def _doImport(self, import_handler, name, location, options, pubsub_service="", + pubsub_node="", profile=C.PROF_KEY_NONE): client = self.host.getClient(profile) options = {key: str(value) for key, value in options.items()} for option in import_handler.BOOL_OPTIONS: @@ -159,9 +151,11 @@ for option in import_handler.JSON_OPTIONS: try: options[option] = json.loads(options[option]) + except KeyError: + pass except ValueError: raise exceptions.DataError( - _("invalid json option: {name}").format(name=option) + _(f"invalid json option: {option}") ) pubsub_service = jid.JID(pubsub_service) if pubsub_service else None return self.doImport( @@ -175,16 +169,8 @@ ) @defer.inlineCallbacks - def doImport( - self, - client, - import_handler, - name, - location, - options=None, - pubsub_service=None, - pubsub_node=None, - ): + def doImport(self, client, import_handler, name, location, options=None, + pubsub_service=None, pubsub_node=None,): """Import data @param import_handler(object): instance of the import handler @@ -193,7 +179,8 @@ can be an url, a file path, or anything which make sense check importer description for more details @param options(dict, None): extra options. - @param pubsub_service(jid.JID, None): jid of the PubSub service where data must be imported + @param pubsub_service(jid.JID, None): jid of the PubSub service where data must be + imported. None to use profile's server @param pubsub_node(unicode, None): PubSub node to use None to use importer's default node diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_merge_req_mercurial.py --- a/sat/plugins/plugin_merge_req_mercurial.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_merge_req_mercurial.py Wed Sep 25 08:56:41 2019 +0200 @@ -53,6 +53,7 @@ @param path(unicode): path to the repository @param command(unicode): hg command to run + @return D(bytes): stdout of the command """ assert "path" not in kwargs kwargs["path"] = path @@ -86,8 +87,10 @@ return d def export(self, repository): - return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()', - '--encoding=utf-8') + d = MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()', + '--encoding=utf-8') + d.addCallback(lambda data: data.decode('utf-8')) + return d def import_(self, repository, data, data_type, item_id, service, node, extra): parsed_data = self.parse(data) @@ -97,7 +100,7 @@ except Exception: parsed_name = '' name = 'mr_{item_id}_{parsed_name}'.format(item_id=CLEAN_RE.sub('', item_id), - parsed_name=parsed_name) + parsed_name=parsed_name) return MercurialProtocol.run(repository, 'qimport', '-g', '--name', name, '--encoding=utf-8', '-', stdin=data) diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_misc_email_invitation.py --- a/sat/plugins/plugin_misc_email_invitation.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_misc_email_invitation.py Wed Sep 25 08:56:41 2019 +0200 @@ -254,7 +254,7 @@ try: yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password) - except error.StanzaError as e: + except error.StanzaError: idx += 1 else: break @@ -321,7 +321,7 @@ format_args['url'] = invite_url yield sat_email.sendEmail( - self.host, + self.host.memory.config, [email] + emails_extra, (kwargs.pop('message_subject', None) or DEFAULT_SUBJECT).format( **format_args), @@ -401,7 +401,7 @@ C.PROF_KEY_NONE: don't filter invitations @return list(unicode): invitations uids """ - invitations = yield list(self.invitations.items()) + invitations = yield self.invitations.items() if profile != C.PROF_KEY_NONE: invitations = {id_:data for id_, data in invitations.items() if data.get('profile') == profile} diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_misc_merge_requests.py --- a/sat/plugins/plugin_misc_merge_requests.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_misc_merge_requests.py Wed Sep 25 08:56:41 2019 +0200 @@ -106,8 +106,8 @@ @param name(unicode): name of the handler @param handler(object): instance of the handler. It must have the following methods, which may all return a Deferred: - - check(repository): True if repository can be handled - - export(repository): return export data, i.e. the patches + - check(repository)->bool: True if repository can be handled + - export(repository)->str: return export data, i.e. the patches - parse(export_data): parse report data and return a list of dict (1 per patch) with: - title: title of the commit message (first line) diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_misc_text_syntaxes.py --- a/sat/plugins/plugin_misc_text_syntaxes.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_misc_text_syntaxes.py Wed Sep 25 08:56:41 2019 +0200 @@ -348,9 +348,8 @@ element.text = '' return html.tostring(xhtml_elt, encoding=str, method="xml") - def convert( - self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True, profile=None - ): + def convert(self, text, syntax_from, syntax_to=_SYNTAX_XHTML, safe=True, + profile=None): """Convert a text between two syntaxes @param text: text to convert diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_misc_xmllog.py --- a/sat/plugins/plugin_misc_xmllog.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_misc_xmllog.py Wed Sep 25 08:56:41 2019 +0200 @@ -20,10 +20,10 @@ from sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger +from twisted.words.xish import domish +from functools import partial log = getLogger(__name__) -from twisted.words.xish import domish -from functools import partial PLUGIN_INFO = { C.PI_NAME: "Raw XML log Plugin", @@ -74,9 +74,9 @@ def onSend(self, obj, client): if isinstance(obj, str): - log = str(obj) + xml_log = obj elif isinstance(obj, domish.Element): - log = obj.toXml() + xml_log = obj.toXml() else: log.error(_("INTERNAL ERROR: Unmanaged XML type")) - self.host.bridge.xmlLog("OUT", log, client.profile) + self.host.bridge.xmlLog("OUT", xml_log, client.profile) diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_sec_otr.py --- a/sat/plugins/plugin_sec_otr.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_sec_otr.py Wed Sep 25 08:56:41 2019 +0200 @@ -108,7 +108,7 @@ message data when an encrypted message is going to be sent """ assert isinstance(self.peer, jid.JID) - msg = msg_str + msg = msg_str.decode('utf-8') client = self.user.client log.debug("injecting encrypted message to {to}".format(to=self.peer)) if appdata is None: @@ -530,7 +530,7 @@ ) # FIXME: temporary and unsecure, must be changed when frontends # are refactored otrctx = client._otr_context_manager.getContextForUser(to_jid) - query = otrctx.sendMessage(0, "?OTRv?") + query = otrctx.sendMessage(0, b"?OTRv?") otrctx.inject(query) def _otrSessionEnd(self, menu_data, profile): diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0048.py --- a/sat/plugins/plugin_xep_0048.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0048.py Wed Sep 25 08:56:41 2019 +0200 @@ -241,12 +241,12 @@ except KeyError: pass - for url in urls_data: + for url, url_data in urls_data.items(): url_elt = storage_elt.addElement("url") url_elt[XEP_0048.URL_KEY] = url for attr in XEP_0048.URL_ATTRS: try: - url_elt[attr] = url[attr] + url_elt[attr] = url_data[attr] except KeyError: pass @@ -440,7 +440,10 @@ return ret data = bookmarks_ori[type_] for bookmark in data: - ret[_storage_location][bookmark.full()] = data[bookmark].copy() + if type_ == XEP_0048.MUC_TYPE: + ret[_storage_location][bookmark.full()] = data[bookmark].copy() + else: + ret[_storage_location][bookmark] = data[bookmark].copy() return ret for _storage_location in ("local", "private", "pubsub"): diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0054.py --- a/sat/plugins/plugin_xep_0054.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0054.py Wed Sep 25 08:56:41 2019 +0200 @@ -44,7 +44,7 @@ raise exceptions.MissingModule( "Missing module pillow, please download/install it from https://python-pillow.github.io" ) -from io import StringIO +import io try: from twisted.words.protocols.xmlstream import XMPPHandler @@ -52,7 +52,8 @@ from wokkel.subprotocols import XMPPHandler AVATAR_PATH = "avatars" -AVATAR_DIM = (64, 64) #  FIXME: dim are not adapted to modern resolutions ! +# AVATAR_DIM = (64, 64) #  FIXME: dim are not adapted to modern resolutions ! +AVATAR_DIM = (128, 128) IQ_GET = '/iq[@type="get"]' NS_VCARD = "vcard-temp" @@ -318,7 +319,7 @@ avatar_hash = yield threads.deferToThread( self.savePhoto, client, elem, entity_jid ) - except (exceptions.DataError, exceptions.NotFound) as e: + except (exceptions.DataError, exceptions.NotFound): avatar_hash = "" vcard_dict["avatar"] = avatar_hash except Exception as e: @@ -515,12 +516,13 @@ left += offset right -= offset img = img.crop((left, upper, right, lower)) - img_buf = StringIO() + img_buf = io.BytesIO() img.save(img_buf, "PNG") photo_elt = vcard_elt.addElement("PHOTO") photo_elt.addElement("TYPE", content="image/png") - photo_elt.addElement("BINVAL", content=b64encode(img_buf.getvalue())) + image_b64 = b64encode(img_buf.getvalue()).decode('utf-8') + photo_elt.addElement("BINVAL", content=image_b64) image_hash = sha1(img_buf.getvalue()).hexdigest() with client.cache.cacheData( PLUGIN_INFO["import_name"], image_hash, "image/png", MAX_AGE diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0065.py --- a/sat/plugins/plugin_xep_0065.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0065.py Wed Sep 25 08:56:41 2019 +0200 @@ -175,17 +175,8 @@ class Candidate(object): - def __init__( - self, - host, - port, - type_, - priority, - jid_, - id_=None, - priority_local=False, - factory=None, - ): + def __init__(self, host, port, type_, priority, jid_, id_=None, priority_local=False, + factory=None,): """ @param host(unicode): host IP or domain @param port(int): port @@ -227,15 +218,6 @@ return self._priority def __str__(self): - # similar to __unicode__ but we don't show jid and we encode id - return "Candidate ({0.priority}): host={0.host} port={0.port} type={0.type}{id}".format( - self, - id=" id={}".format(self.id if self.id is not None else "").encode( - "utf-8", "ignore" - ), - ) - - def __unicode__(self): return "Candidate ({0.priority}): host={0.host} port={0.port} jid={0.jid} type={0.type}{id}".format( self, id=" id={}".format(self.id if self.id is not None else "") ) diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0166.py --- a/sat/plugins/plugin_xep_0166.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0166.py Wed Sep 25 08:56:41 2019 +0200 @@ -113,9 +113,11 @@ try: del client.jingle_sessions[sid] except KeyError: - log.debug("Jingle session id [{}] is unknown, nothing to delete".format(sid)) + log.debug( + f"Jingle session id {sid!r} is unknown, nothing to delete " + f"[{client.profile}]") else: - log.debug("Jingle session id [{}] deleted".format(sid)) + log.debug(f"Jingle session id {sid!r} deleted [{client.profile}]") ## helpers methods to build stanzas ## @@ -146,7 +148,7 @@ condition=error_condition ) ) - client.send(iq_elt) + return client.send(iq_elt) def _terminateEb(self, failure_): log.warning(_("Error while terminating session: {msg}").format(msg=failure_)) @@ -188,23 +190,22 @@ ) self._delSession(client, sid) - def _jingleErrorCb(self, fail, sid, request, client): + def _jingleErrorCb(self, failure_, session, request, client): """Called when something is going wrong while parsing jingle request The error condition depend of the exceptions raised: exceptions.DataError raise a bad-request condition @param fail(failure.Failure): the exceptions raised - @param sid(unicode): jingle session id + @param session(dict): data of the session @param request(domsih.Element): jingle request @param client: %(doc_client)s """ - log.warning("Error while processing jingle request") - if isinstance(fail, exceptions.DataError): - self.sendError(client, "bad-request", sid, request) + log.warning(f"Error while processing jingle request [{client.profile}]") + if isinstance(failure_, exceptions.DataError): + return self.sendError(client, "bad-request", session['id'], request) else: - log.error("Unmanaged jingle exception") - self._delSession(client, sid) - raise fail + log.error(f"Unmanaged jingle exception: {failure_}") + return self.terminate(client, self.REASON_FAILED_APPLICATION, session) ## methods used by other plugins ## @@ -706,19 +707,10 @@ """ return elt - def _callPlugins( - self, - client, - action, - session, - app_method_name="jingleHandler", - transp_method_name="jingleHandler", - app_default_cb=None, - transp_default_cb=None, - delete=True, - elements=True, - force_element=None, - ): + def _callPlugins(self, client, action, session, app_method_name="jingleHandler", + transp_method_name="jingleHandler", app_default_cb=None, + transp_default_cb=None, delete=True, elements=True, + force_element=None): """Call application and transport plugin methods for all contents @param action(unicode): jingle action name @@ -819,7 +811,7 @@ confirm_dlist = defer.gatherResults(confirm_defers) confirm_dlist.addCallback(self._confirmationCb, session, jingle_elt, client) - confirm_dlist.addErrback(self._jingleErrorCb, session["id"], request, client) + confirm_dlist.addErrback(self._jingleErrorCb, session, request, client) def _confirmationCb(self, confirm_results, session, jingle_elt, client): """Method called when confirmation from user has been received @@ -952,7 +944,7 @@ negociate_defers = [] negociate_defers = self._callPlugins(client, XEP_0166.A_SESSION_ACCEPT, session) - negociate_dlist = defer.DeferredList(negociate_defers) + negociate_dlist = defer.gatherResults(negociate_defers) # after negociations we start the transfer negociate_dlist.addCallback( diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0234.py --- a/sat/plugins/plugin_xep_0234.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0234.py Wed Sep 25 08:56:41 2019 +0200 @@ -55,7 +55,7 @@ C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), } -EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash"} +EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"} Range = namedtuple("Range", ("offset", "length")) @@ -106,7 +106,7 @@ # generic methods - def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, + def buildFileElement(self, name=None, file_hash=None, hash_algo=None, size=None, mime_type=None, desc=None, modified=None, transfer_range=None, path=None, namespace=None, file_elt=None, **kwargs): """Generate a element with available metadata @@ -177,14 +177,8 @@ file_data.update(kwargs) return self.buildFileElement(**file_data) - def parseFileElement( - self, - file_elt, - file_data=None, - given=False, - parent_elt=None, - keep_empty_range=False, - ): + def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None, + keep_empty_range=False,): """Parse a element and file dictionary accordingly @param file_data(dict, None): dict where the data will be set @@ -194,10 +188,12 @@ @param given(bool): if True, prefix hash key with "given_" @param parent_elt(domish.Element, None): parent of the file element if set, file_elt must not be set - @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None) - empty range are useful to know if a peer_jid can handle range + @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset + and length are None). + Empty range is useful to know if a peer_jid can handle range @return (dict): file_data - @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements + @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new + elements @raise exceptions.NotFound: there is not element in parent_elt @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT """ @@ -230,7 +226,7 @@ # we don't want to go to parent dir when joining to a path name = "--" file_data["name"] = name - elif name is not None and "/" in name or "\\" in name: + elif name is not None and ("/" in name or "\\" in name): file_data["name"] = regex.pathEscape(name) try: @@ -335,15 +331,8 @@ defer.returnValue(progress_id) def _fileJingleRequest( - self, - peer_jid, - filepath, - name="", - file_hash="", - hash_algo="", - extra=None, - profile=C.PROF_KEY_NONE, - ): + self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, + profile=C.PROF_KEY_NONE): client = self.host.getClient(profile) return self.fileJingleRequest( client, @@ -357,15 +346,8 @@ @defer.inlineCallbacks def fileJingleRequest( - self, - client, - peer_jid, - filepath, - name=None, - file_hash=None, - hash_algo=None, - extra=None, - ): + self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, + extra=None): """Request a file using jingle file transfer @param peer_jid(jid.JID): destinee jid @@ -568,6 +550,7 @@ d.addCallback(gotConfirmation) return d + @defer.inlineCallbacks def jingleHandler(self, client, action, session, content_name, desc_elt): content_data = session["contents"][content_name] application_data = content_data["application_data"] @@ -579,7 +562,7 @@ next(file_elt.elements(NS_JINGLE_FT, "range")) except StopIteration: # initiator doesn't manage , but we do so we advertise it - #  FIXME: to be checked + # FIXME: to be checked log.debug("adding element") file_elt.addElement("range") elif action == self._j.A_SESSION_ACCEPT: @@ -596,21 +579,32 @@ size = int(str(size_elt)) except (StopIteration, ValueError): size = None - # XXX: hash security is not critical here, so we just take the higher mandatory one + # XXX: hash security is not critical here, so we just take the higher + # mandatory one hasher = file_data["hash_hasher"] = self._hash.getHasher() - content_data["stream_object"] = stream.FileStreamObject( - self.host, - client, - file_path, - mode="wb", - uid=self.getProgressId(session, content_name), - size=size, - data_cb=lambda data: hasher.update(data), - ) + progress_id = self.getProgressId(session, content_name) + try: + content_data["stream_object"] = stream.FileStreamObject( + self.host, + client, + file_path, + mode="wb", + uid=progress_id, + size=size, + data_cb=lambda data: hasher.update(data), + ) + except Exception as e: + self.host.bridge.progressError( + progress_id, C.PROGRESS_ERROR_FAILED, client.profile + ) + yield self._j.terminate( + client, self._j.REASON_FAILED_APPLICATION, session) + raise e else: # we are sending the file size = file_data["size"] - # XXX: hash security is not critical here, so we just take the higher mandatory one + # XXX: hash security is not critical here, so we just take the higher + # mandatory one hasher = file_data["hash_hasher"] = self._hash.getHasher() content_data["stream_object"] = stream.FileStreamObject( self.host, @@ -625,7 +619,7 @@ finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) else: log.warning("FIXME: unmanaged action {}".format(action)) - return desc_elt + defer.returnValue(desc_elt) def jingleSessionInfo(self, client, action, session, content_name, jingle_elt): """Called on session-info action @@ -679,6 +673,16 @@ self.host.bridge.progressError( progress_id, C.PROGRESS_ERROR_DECLINED, client.profile ) + elif not jingle_elt.success: + progress_id = self.getProgressId(session, content_name) + first_child = jingle_elt.firstChildElement() + if first_child is not None: + reason = first_child.name + else: + reason = C.PROGRESS_ERROR_FAILED + self.host.bridge.progressError( + progress_id, reason, client.profile + ) def _sendCheckSum(self, client, session, content_name, content_data): """Send the session-info with the hash checksum""" @@ -721,10 +725,10 @@ return True return False hasher = file_data["hash_hasher"] - hash_ = hasher.hexdigest().encode('utf-8') + hash_ = hasher.hexdigest() if hash_ == given_hash: - log.info("Hash checked, file was successfully transfered: {}".format(hash_)) + log.info(f"Hash checked, file was successfully transfered: {hash_}") progress_metadata = { "hash": hash_, "hash_algo": file_data["hash_algo"], diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0300.py --- a/sat/plugins/plugin_xep_0300.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0300.py Wed Sep 25 08:56:41 2019 +0200 @@ -196,13 +196,13 @@ try: idx = algos.index(algo) except ValueError: - log.warning("Proposed {} algorithm is not managed".format(algo)) + log.warning(f"Proposed {algo} algorithm is not managed") algo = None continue if best_algo is None or algos.index(best_algo) < idx: best_algo = algo - best_value = base64.b64decode(str(hash_elt)) + best_value = base64.b64decode(str(hash_elt)).decode('utf-8') if not hash_elt: raise exceptions.NotFound diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0329.py --- a/sat/plugins/plugin_xep_0329.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0329.py Wed Sep 25 08:56:41 2019 +0200 @@ -595,10 +595,14 @@ ): return for file_data in files_data: - file_elt = self._jf.buildFileElementFromDict( - file_data, modified=file_data.get("modified", file_data["created"]) - ) - query_elt.addChild(file_elt) + if file_data['type'] == C.FILE_TYPE_DIRECTORY: + directory_elt = query_elt.addElement("directory") + directory_elt['name'] = file_data['name'] + else: + file_elt = self._jf.buildFileElementFromDict( + file_data, modified=file_data.get("modified", file_data["created"]) + ) + query_elt.addChild(file_elt) client.send(iq_result_elt) def onComponentRequest(self, iq_elt, client): @@ -624,7 +628,7 @@ file_data = {"name": elt["name"], "type": C.FILE_TYPE_DIRECTORY} else: log.warning( - _("unexpected element, ignoring: {elt}").format(elt=elt.toXml()) + _(f"unexpected element, ignoring: {elt.toXml()}") ) continue files.append(file_data) @@ -642,7 +646,7 @@ def _listFiles(self, target_jid, path, extra, profile): client = self.host.getClient(profile) - target_jid = client.jid.userhostJID() if not target_jid else jid.JID(target_jid) + target_jid = client.jid if not target_jid else jid.JID(target_jid) d = self.listFiles(client, target_jid, path or None) d.addCallback(self._serializeData) return d diff -r a1bc34f90fa5 -r fee60f17ebac sat/plugins/plugin_xep_0363.py --- a/sat/plugins/plugin_xep_0363.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/plugins/plugin_xep_0363.py Wed Sep 25 08:56:41 2019 +0200 @@ -36,7 +36,6 @@ from twisted.web import iweb from twisted.python import failure from collections import namedtuple -from zope.interface import implementer from OpenSSL import SSL import os.path import mimetypes @@ -204,7 +203,7 @@ @param ignore_tls_errors(bool): ignore TLS certificate is True @return (tuple """ - log.debug("Got upload slot: {}".format(slot)) + log.debug(f"Got upload slot: {slot}") sat_file = self.host.plugins["FILE"].File( self.host, client, path, size=size, auto_end_signals=False ) @@ -222,7 +221,7 @@ headers[name] = value d = agent.request( - "PUT", + b"PUT", slot.put.encode("utf-8"), http_headers.Headers(headers), file_producer, diff -r a1bc34f90fa5 -r fee60f17ebac sat/tools/common/async_process.py --- a/sat/tools/common/async_process.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat/tools/common/async_process.py Wed Sep 25 08:56:41 2019 +0200 @@ -76,8 +76,7 @@ def processEnded(self, reason): data = b''.join(self.data) if (reason.value.exitCode == 0): - log.debug(_('{name!r} command succeed').format( - name=self.command_name.decode('utf-8', 'ignore'))) + log.debug(_(f'{self.command_name!r} command succeed')) # we don't use "replace" on purpose, we want an exception if decoding # is not working properly self._deferred.callback(data) @@ -106,7 +105,7 @@ - stdin(unicode, None): data to push to standard input - verbose(bool): if True stdout and stderr will be logged other keyword arguments will be used in reactor.spawnProcess - @return ((D)): stdout in case of success + @return ((D)bytes): stdout in case of success @raise RuntimeError: command returned a non zero status stdin and stdout will be given as arguments @@ -115,8 +114,6 @@ if stdin is not None: stdin = stdin.encode('utf-8') verbose = kwargs.pop('verbose', False) - if 'path' in kwargs: - kwargs['path'] = kwargs['path'].encode('utf-8') args = [a.encode('utf-8') for a in args] kwargs = {k:v.encode('utf-8') for k,v in list(kwargs.items())} d = defer.Deferred() diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/base.py --- a/sat_frontends/jp/base.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/base.py Wed Sep 25 08:56:41 2019 +0200 @@ -17,18 +17,21 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio from sat.core.i18n import _ ### logging ### import logging as log -log.basicConfig(level=log.DEBUG, - format='%(message)s') +log.basicConfig(level=log.WARNING, + format='[%(name)s] %(message)s') ### import sys -import locale +import os import os.path import argparse +import inspect +from pathlib import Path from glob import iglob from importlib import import_module from sat_frontends.tools.jid import JID @@ -41,13 +44,20 @@ from sat_frontends.jp.constants import Const as C from sat_frontends.tools import misc import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -import shlex from collections import OrderedDict ## bridge handling # we get bridge name from conf and initialise the right class accordingly main_config = config.parseMainConf() bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') +USER_INTER_MSG = _("User interruption: good bye") + + +class QuitException(BaseException): + """Quitting is requested + + This is used to stop execution when host.quit() is called + """ # TODO: move loops handling in a separated module @@ -63,8 +73,9 @@ def run(self): self.loop.run() - def quit(self): + def quit(self, exit_code): self.loop.quit() + sys.exit(exit_code) def call_later(self, delay, callback, *args): """call a callback repeatedly @@ -78,27 +89,66 @@ GLib.timeout_add(delay, callback, *args) else: - print("can't start jp: only D-Bus bridge is currently handled") - sys.exit(C.EXIT_ERROR) - # FIXME: twisted loop can be used when jp can handle fully async bridges - # from twisted.internet import reactor + import signal + from twisted.internet import asyncioreactor + asyncioreactor.install() + from twisted.internet import reactor, defer + + class JPLoop(object): - # class JPLoop(object): + def __init__(self): + # exit code must be set when using quit, so if it's not set + # something got wrong and we must report it + self._exit_code = C.EXIT_INTERNAL_ERROR - # def run(self): - # reactor.run() + def run(self, jp, *args): + self.jp = jp + signal.signal(signal.SIGINT, self._on_sigint) + defer.ensureDeferred(self._start(jp, *args)) + try: + reactor.run(installSignalHandlers=False) + except SystemExit as e: + self._exit_code = e.code + sys.exit(self._exit_code) - # def quit(self): - # reactor.stop() + async def _start(self, jp, *args): + fut = asyncio.ensure_future(jp.main(*args)) + try: + await defer.Deferred.fromFuture(fut) + except BaseException: + import traceback + traceback.print_exc() + jp.quit(1) + + def quit(self, exit_code): + self._exit_code = exit_code + reactor.stop() - # def _timeout_cb(self, args, callback, delay): - # ret = callback(*args) - # if ret: - # reactor.callLater(delay, self._timeout_cb, args, callback, delay) + def _timeout_cb(self, args, callback, delay): + try: + ret = callback(*args) + # FIXME: temporary hack to avoid traceback when using XMLUI + # to be removed once create_task is not used anymore in + # xmlui_manager (i.e. once sat_frontends.tools.xmlui fully supports + # async syntax) + except QuitException: + return + if ret: + reactor.callLater(delay, self._timeout_cb, args, callback, delay) - # def call_later(self, delay, callback, *args): - # delay = float(delay) / 1000 - # reactor.callLater(delay, self._timeout_cb, args, callback, delay) + def call_later(self, delay, callback, *args): + delay = float(delay) / 1000 + reactor.callLater(delay, self._timeout_cb, args, callback, delay) + + def _on_sigint(self, sig_number, stack_frame): + """Called on keyboard interruption + + Print user interruption message, set exit code and stop reactor + """ + print("\r" + USER_INTER_MSG) + self._exit_code = C.EXIT_USER_CANCELLED + reactor.callFromThread(reactor.stop) + if bridge_name == "embedded": from sat.core import sat_main @@ -107,9 +157,10 @@ try: import progressbar except ImportError: - msg = (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + - _('Progress bar deactivated\n--\n')) - print(msg.encode('utf-8'), file=sys.stderr) + msg = (_('ProgressBar not available, please download it at ' + 'http://pypi.python.org/pypi/progressbar\n' + 'Progress bar deactivated\n--\n')) + print(msg, file=sys.stderr) progressbar=None #consts @@ -122,7 +173,7 @@ This is free software, and you are welcome to redistribute it under certain conditions. """ -PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms +PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s def date_decoder(arg): @@ -141,33 +192,30 @@ def __init__(self): """ - @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, - or if you want the user to stop by himself + @attribute quit_on_progress_end (bool): set to False if you manage yourself + exiting, or if you want the user to stop by himself @attribute progress_success(callable): method to call when progress just started by default display a message - @attribute progress_success(callable): method to call when progress is successfully finished - by default display a message + @attribute progress_success(callable): method to call when progress is + successfully finished by default display a message @attribute progress_failure(callable): method to call when progress failed by default display a message """ - # FIXME: need_loop should be removed, everything must be async in bridge so - # loop will always be needed bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') if bridge_module is None: log.error("Can't import {} bridge".format(bridge_name)) sys.exit(1) - self.bridge = bridge_module.Bridge() - self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) + self.bridge = bridge_module.AIOBridge() + self._onQuitCallbacks = [] - def _bridgeCb(self): - self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=DESCRIPTION) + def _bridgeConnected(self): + self.parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) self._make_parents() self.add_parser_options() - self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name') - self._auto_loop = False # when loop is used for internal reasons - self._need_loop = False + self.subparsers = self.parser.add_subparsers( + title=_('Available commands'), dest='command', required=True) # progress attributes self._progress_id = None # TODO: manage several progress ids @@ -181,27 +229,14 @@ self.own_jid = None # must be filled at runtime if needed - def _bridgeEb(self, failure): - if isinstance(failure, exceptions.BridgeExceptionNoService): - print((_("Can't connect to SàT backend, are you sure it's launched ?"))) - elif isinstance(failure, exceptions.BridgeInitError): - print((_("Can't init bridge"))) - else: - print((_("Error while initialising bridge: {}".format(failure)))) - sys.exit(C.EXIT_BRIDGE_ERROR) - - @property - def version(self): - return self.bridge.getVersion() - @property def progress_id(self): return self._progress_id - @progress_id.setter - def progress_id(self, value): - self._progress_id = value - self.replayCache('progress_ids_cache') + async def set_progress_id(self, progress_id): + # because we use async, we need an explicit setter + self._progress_id = progress_id + await self.replayCache('progress_ids_cache') @property def watch_progress(self): @@ -224,13 +259,13 @@ except AttributeError: return 0 - def replayCache(self, cache_attribute): + async def replayCache(self, cache_attribute): """Replay cached signals @param cache_attribute(str): name of the attribute containing the cache if the attribute doesn't exist, there is no cache and the call is ignored - else the cache must be a list of tuples containing the replay callback as first item, - then the arguments to use + else the cache must be a list of tuples containing the replay callback as + first item, then the arguments to use """ try: cache = getattr(self, cache_attribute) @@ -238,7 +273,7 @@ pass else: for cache_data in cache: - cache_data[0](*cache_data[1:]) + await cache_data[0](*cache_data[1:]) def disp(self, msg, verbosity=0, error=False, no_lf=False): """Print a message to user @@ -260,23 +295,22 @@ else: print(msg) - def output(self, type_, name, extra_outputs, data): + async def output(self, type_, name, extra_outputs, data): if name in extra_outputs: - extra_outputs[name](data) + method = extra_outputs[name] else: - self._outputs[type_][name]['callback'](data) + method = self._outputs[type_][name]['callback'] + + ret = method(data) + if inspect.isawaitable(ret): + await ret def addOnQuitCallback(self, callback, *args, **kwargs): """Add a callback which will be called on quit command @param callback(callback): method to call """ - try: - callbacks_list = self._onQuitCallbacks - except AttributeError: - callbacks_list = self._onQuitCallbacks = [] - finally: - callbacks_list.append((callback, args, kwargs)) + self._onQuitCallbacks.append((callback, args, kwargs)) def getOutputChoices(self, output_type): """Return valid output filters for output_type @@ -289,33 +323,52 @@ def _make_parents(self): self.parents = {} - # we have a special case here as the start-session option is present only if connection is not needed, - # so we create two similar parents, one with the option, the other one without it + # we have a special case here as the start-session option is present only if + # connection is not needed, so we create two similar parents, one with the + # option, the other one without it for parent_name in ('profile', 'profile_session'): parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) - parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) - parent.add_argument("--pwd", action="store", default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) + parent.add_argument( + "-p", "--profile", action="store", type=str, default='@DEFAULT@', + help=_("Use PROFILE profile key (default: %(default)s)")) + parent.add_argument( + "--pwd", action="store", default='', metavar='PASSWORD', + help=_("Password used to connect profile, if necessary")) - profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] + profile_parent, profile_session_parent = (self.parents['profile'], + self.parents['profile_session']) - connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _("Connect the profile before doing anything else") - profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) + connect_short, connect_long, connect_action, connect_help = ( + "-c", "--connect", "store_true", + _("Connect the profile before doing anything else") + ) + profile_parent.add_argument( + connect_short, connect_long, action=connect_action, help=connect_help) profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() - profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) - profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) + profile_session_connect_group.add_argument( + connect_short, connect_long, action=connect_action, help=connect_help) + profile_session_connect_group.add_argument( + "--start-session", action="store_true", + help=_("Start a profile session without connecting")) - progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) + progress_parent = self.parents['progress'] = argparse.ArgumentParser( + add_help=False) if progressbar: - progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) + progress_parent.add_argument( + "-P", "--progress", action="store_true", help=_("Show progress bar")) verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) - verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_("Add a verbosity level (can be used multiple times)")) + verbose_parent.add_argument( + '--verbose', '-v', action='count', default=0, + help=_("Add a verbosity level (can be used multiple times)")) draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) draft_group = draft_parent.add_argument_group(_('draft handling')) - draft_group.add_argument("-D", "--current", action="store_true", help=_("load current draft")) - draft_group.add_argument("-F", "--draft-path", help=_("path to a draft file to retrieve")) + draft_group.add_argument( + "-D", "--current", action="store_true", help=_("load current draft")) + draft_group.add_argument( + "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")) def make_pubsub_group(self, flags, defaults): @@ -356,11 +409,14 @@ item_help += _(" (DEFAULT: {default})".format(default=default)) pubsub_group.add_argument("-i", "--item", default='', help=item_help) - pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_('retrieve last item')) + pubsub_group.add_argument( + "-L", "--last-item", action='store_true', help=_('retrieve last item')) elif flags.multi_items: # mutiple items, this activate several features: max-items, RSM, MAM # and Orbder-by - pubsub_group.add_argument("-i", "--item", action='append', dest='items', default=[], help=_("items to retrieve (DEFAULT: all)")) + pubsub_group.add_argument( + "-i", "--item", action='append', dest='items', default=[], + help=_("items to retrieve (DEFAULT: all)")) if not flags.no_max: max_group = pubsub_group.add_mutually_exclusive_group() # XXX: defaut value for --max-items or --max is set in parse_pubsub_args @@ -409,14 +465,22 @@ help=_("how items should be ordered")) if not flags.all_used: - raise exceptions.InternalError('unknown flags: {flags}'.format(flags=', '.join(flags.unused))) + raise exceptions.InternalError('unknown flags: {flags}'.format( + flags=', '.join(flags.unused))) if defaults: - raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) + raise exceptions.InternalError(f'unused defaults: {defaults}') return parent def add_parser_options(self): - self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) + self.parser.add_argument( + '--version', + action='version', + version=("{name} {version} {copyleft}".format( + name = PROG_NAME, + version = self.version, + copyleft = COPYLEFT)) + ) def register_output(self, type_, name, callback, description="", default=False): if type_ not in C.OUTPUT_TYPES: @@ -427,7 +491,9 @@ } if default: if type_ in self.default_output: - self.disp(_('there is already a default output for {}, ignoring new one').format(type_)) + self.disp( + _(f'there is already a default output for {type_}, ignoring new one') + ) else: self.default_output[type_] = name @@ -445,7 +511,8 @@ def check_output_options(self, accepted_set, options): if not accepted_set.issuperset(options): - self.disp("The following output options are invalid: {invalid_options}".format( + self.disp( + _("The following output options are invalid: {invalid_options}").format( invalid_options = ', '.join(set(options).difference(accepted_set))), error=True) self.quit(C.EXIT_BAD_ARG) @@ -457,17 +524,20 @@ """ path = os.path.dirname(sat_frontends.jp.__file__) # XXX: outputs must be imported before commands as they are used for arguments - for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): - modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) + for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), + (C.PLUGIN_CMD, 'cmd_*.py')): + modules = ( + os.path.splitext(module)[0] + for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) for module_name in modules: module_path = "sat_frontends.jp." + module_name try: module = import_module(module_path) self.import_plugin_module(module, type_) except ImportError as e: - self.disp(_("Can't import {module_path} plugin, ignoring it: {msg}".format( - module_path = module_path, - msg = e)), error=True) + self.disp( + _(f"Can't import {module_path} plugin, ignoring it: {e}"), + error=True) except exceptions.CancelError: continue except exceptions.MissingModule as e: @@ -485,7 +555,7 @@ try: class_names = getattr(module, '__{}__'.format(type_)) except AttributeError: - log.disp(_("Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) + log.disp(_(f"Invalid plugin module [{type_}] {module}"), error=True) raise ImportError else: for class_name in class_names: @@ -500,12 +570,15 @@ scheme = 'http' else: raise exceptions.InternalError('An HTTP scheme is expected in this method') - self.disp("{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) + self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1) # HTTP URL, we try to find xmpp: links try: from lxml import etree except ImportError: - self.disp("lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) + self.disp( + "lxml module must be installed to use http(s) scheme, please install it " + "with \"pip install lxml\"", + error=True) self.quit(1) import urllib.request, urllib.error, urllib.parse parser = etree.HTMLParser() @@ -517,7 +590,10 @@ else: links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") if not links: - self.disp('Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) + self.disp( + _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP ' + 'PubSub node/item'), + error=True) self.quit(1) xmpp_uri = links[0].get('href') return xmpp_uri @@ -552,7 +628,10 @@ try: items = self.args.items except AttributeError: - self.disp(_("item specified in URL but not needed in command, ignoring it"), error=True) + self.disp( + _("item specified in URL but not needed in command, " + "ignoring it"), + error=True) else: if not items: self.args.items = [uri_item] @@ -565,7 +644,7 @@ if not item_last: self.args.item = uri_item else: - self.parser.error(_('XMPP URL is not a pubsub one: {url}').format(url=url)) + self.parser.error(_(f'XMPP URL is not a pubsub one: {url}')) flags = self.args._cmd._pubsub_flags # we check required arguments here instead of using add_arguments' required option # because the required argument can be set in URL @@ -580,7 +659,8 @@ # so we check conflict here. This may be fixed in Python 3, to be checked try: if self.args.item and self.args.item_last: - self.parser.error(_("--item and --item-last can't be used at the same time")) + self.parser.error( + _("--item and --item-last can't be used at the same time")) except AttributeError: pass @@ -605,46 +685,78 @@ if self.args.max is None: self.args.max = C.NO_LIMIT - def run(self, args=None, namespace=None): - self.args = self.parser.parse_args(args, namespace=None) - if self.args._cmd._use_pubsub: - self.parse_pubsub_args() + async def main(self, args, namespace): try: - self.args._cmd.run() - if self._need_loop or self._auto_loop: - self._start_loop() - except KeyboardInterrupt: - self.disp(_("User interruption: good bye")) + await self.bridge.bridgeConnect() + except Exception as e: + if isinstance(e, exceptions.BridgeExceptionNoService): + print((_("Can't connect to SàT backend, are you sure it's launched ?"))) + elif isinstance(e, exceptions.BridgeInitError): + print((_("Can't init bridge"))) + else: + print((_(f"Error while initialising bridge: {e}"))) + self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) + return + self.version = await self.bridge.getVersion() + self._bridgeConnected() + self.import_plugins() + try: + self.args = self.parser.parse_args(args, namespace=None) + if self.args._cmd._use_pubsub: + self.parse_pubsub_args() + await self.args._cmd.run() + except SystemExit as e: + self.quit(e.code, raise_exc=False) + return + except QuitException: + return - def _start_loop(self): + def run(self, args=None, namespace=None): self.loop = JPLoop() - self.loop.run() + self.loop.run(self, args, namespace) - def stop_loop(self): - try: - self.loop.quit() - except AttributeError: - pass + def _read_stdin(self, stdin_fut): + """Callback called by ainput to read stdin""" + line = sys.stdin.readline() + if line: + stdin_fut.set_result(line.rstrip(os.linesep)) + else: + stdin_fut.set_exception(EOFError()) - def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")): + async def ainput(self, msg=''): + """Asynchronous version of buildin "input" function""" + self.disp(msg, no_lf=True) + sys.stdout.flush() + loop = asyncio.get_running_loop() + stdin_fut = loop.create_future() + loop.add_reader(sys.stdin, self._read_stdin, stdin_fut) + return await stdin_fut + + async def confirmOrQuit(self, message, cancel_message=_("action cancelled by user")): """Request user to confirm action, and quit if he doesn't""" - res = input("{} (y/N)? ".format(message)) + res = await self.ainput(f"{message} (y/N)? ") if res not in ("y", "Y"): self.disp(cancel_message) self.quit(C.EXIT_USER_CANCELLED) - def quitFromSignal(self, errcode=0): - """Same as self.quit, but from a signal handler + def quitFromSignal(self, exit_code=0): + r"""Same as self.quit, but from a signal handler /!\: return must be used after calling this method ! """ - assert self._need_loop # XXX: python-dbus will show a traceback if we exit in a signal handler # so we use this little timeout trick to avoid it - self.loop.call_later(0, self.quit, errcode) + self.loop.call_later(0, self.quit, exit_code) + + def quit(self, exit_code=0, raise_exc=True): + """Terminate the execution with specified exit_code - def quit(self, errcode=0): + This will stop the loop. + @param exit_code(int): code to return when quitting the program + @param raise_exp(boolean): if True raise a QuitException to stop code execution + The default value should be used most of time. + """ # first the onQuitCallbacks try: callbacks_list = self._onQuitCallbacks @@ -654,10 +766,11 @@ for callback, args, kwargs in callbacks_list: callback(*args, **kwargs) - self.stop_loop() - sys.exit(errcode) + self.loop.quit(exit_code) + if raise_exc: + raise QuitException - def check_jids(self, jids): + async def check_jids(self, jids): """Check jids validity, transform roster name to corresponding jids @param profile: profile name @@ -668,7 +781,7 @@ names2jid = {} nodes2jid = {} - for contact in self.bridge.getContacts(self.profile): + for contact in await self.bridge.getContacts(self.profile): jid_s, attr, groups = contact _jid = JID(jid_s) try: @@ -704,29 +817,20 @@ return dest_jids - def connect_profile(self, callback): - """ Check if the profile is connected and do it if requested + async def connect_profile(self): + """Check if the profile is connected and do it if requested - @param callback: method to call when profile is connected @exit: - 1 when profile is not connected and --connect is not set - 1 when the profile doesn't exists - 1 when there is a connection error """ # FIXME: need better exit codes - def cant_connect(failure): - log.error(_("Can't connect profile: {reason}").format(reason=failure)) - self.quit(1) - - def cant_start_session(failure): - log.error(_("Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) - self.quit(1) - - self.profile = self.bridge.profileNameGet(self.args.profile) + self.profile = await self.bridge.profileNameGet(self.args.profile) if not self.profile: - log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) - self.quit(1) + log.error(_(f"The profile [{self.args.profile}] doesn't exist")) + self.quit(C.EXIT_ERROR) try: start_session = self.args.start_session @@ -734,40 +838,49 @@ pass else: if start_session: - self.bridge.profileStartSession(self.args.pwd, self.profile, lambda __: callback(), cant_start_session) - self._auto_loop = True + try: + await self.bridge.profileStartSession(self.args.pwd, self.profile) + except Exception as e: + self.disp(_(f"Can't start {self.profile}'s session: {e}"), err=True) + self.quit(1) return - elif not self.bridge.profileIsSessionStarted(self.profile): + elif not await self.bridge.profileIsSessionStarted(self.profile): if not self.args.connect: - log.error(_("Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile)) + self.disp(_( + f"Session for [{self.profile}] is not started, please start it " + f"before using jp, or use either --start-session or --connect " + f"option"), error=True) self.quit(1) elif not getattr(self.args, "connect", False): - callback() return if not hasattr(self.args, 'connect'): - # a profile can be present without connect option (e.g. on profile creation/deletion) + # a profile can be present without connect option (e.g. on profile + # creation/deletion) return elif self.args.connect is True: # if connection is asked, we connect the profile - self.bridge.connect(self.profile, self.args.pwd, {}, lambda __: callback(), cant_connect) - self._auto_loop = True + try: + await self.bridge.connect(self.profile, self.args.pwd, {}) + except Exception as e: + self.disp(_(f"Can't connect profile: {e}"), error=True) + self.quit(1) return else: - if not self.bridge.isConnected(self.profile): - log.error(_("Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) + if not await self.bridge.isConnected(self.profile): + log.error( + _(f"Profile [{self.profile}] is not connected, please connect it " + f"before using jp, or use --connect option")) self.quit(1) - callback() - - def get_full_jid(self, param_jid): + async def get_full_jid(self, param_jid): """Return the full jid if possible (add main resource when find a bare jid)""" _jid = JID(param_jid) if not _jid.resource: #if the resource is not given, we try to add the main resource - main_resource = self.bridge.getMainResource(param_jid, self.profile) + main_resource = await self.bridge.getMainResource(param_jid, self.profile) if main_resource: - return "%s/%s" % (_jid.bare, main_resource) + return f"{_jid.bare}/{main_resource}" return param_jid @@ -783,7 +896,8 @@ @param use_output(bool, unicode): if not False, add --output option @param extra_outputs(dict): list of command specific outputs: key is output name ("default" to use as main output) - value is a callable which will format the output (data will be used as only argument) + value is a callable which will format the output (data will be used as only + argument) if a key already exists with normal outputs, the extra one will be used @param need_connect(bool, None): True if profile connection is needed False else (profile session must still be started) @@ -799,14 +913,13 @@ mandatory arguments are controlled by pubsub_req - use_draft(bool): if True, add draft handling options ** other arguments ** - - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be: + - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, + can be: C.SERVICE: service is required C.NODE: node is required C.ITEM: item is required C.SINGLE_ITEM: only one item is allowed - @attribute need_loop(bool): to set by commands when loop is needed """ - self.need_loop = False # to be set by commands when loop is needed try: # If we have subcommands, host is a CommandBase and we need to use host.host self.host = host.host except AttributeError: @@ -815,10 +928,12 @@ # --profile option parents = kwargs.setdefault('parents', set()) if use_profile: - #self.host.parents['profile'] is an ArgumentParser with profile connection arguments + # self.host.parents['profile'] is an ArgumentParser with profile connection + # arguments if need_connect is None: need_connect = True - parents.add(self.host.parents['profile' if need_connect else 'profile_session']) + parents.add( + self.host.parents['profile' if need_connect else 'profile_session']) else: assert need_connect is None self.need_connect = need_connect @@ -838,7 +953,8 @@ choices = set(self.host.getOutputChoices(use_output)) choices.update(extra_outputs) if not choices: - raise exceptions.InternalError("No choice found for {} output type".format(use_output)) + raise exceptions.InternalError( + "No choice found for {} output type".format(use_output)) try: default = self.host.default_output[use_output] except KeyError: @@ -848,8 +964,12 @@ default = 'simple' else: default = list(choices)[0] - output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_("select output format (default: {})".format(default))) - output_parent.add_argument('--output-option', '--oo', action="append", dest='output_opts', default=[], help=_("output specific option")) + output_parent.add_argument( + '--output', '-O', choices=sorted(choices), default=default, + help=_("select output format (default: {})".format(default))) + output_parent.add_argument( + '--output-option', '--oo', action="append", dest='output_opts', + default=[], help=_("output specific option")) parents.add(output_parent) else: assert extra_outputs is None @@ -873,7 +993,7 @@ self.parser = host.subparsers.add_parser(name, help=help, **kwargs) if hasattr(self, "subcommands"): - self.subparsers = self.parser.add_subparsers() + self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True) else: self.parser.set_defaults(_cmd=self) self.add_parser_options() @@ -894,11 +1014,10 @@ def progress_id(self): return self.host.progress_id - @progress_id.setter - def progress_id(self, value): - self.host.progress_id = value + async def set_progress_id(self, progress_id): + return await self.host.set_progress_id(progress_id) - def progressStartedHandler(self, uid, metadata, profile): + async def progressStartedHandler(self, uid, metadata, profile): if profile != self.profile: return if self.progress_id is None: @@ -907,15 +1026,20 @@ # when the progress_id is received cache_data = (self.progressStartedHandler, uid, metadata, profile) try: - self.host.progress_ids_cache.append(cache_data) + cache = self.host.progress_ids_cache except AttributeError: - self.host.progress_ids_cache = [cache_data] + cache = self.host.progress_ids_cache = [] + cache.append(cache_data) else: if self.host.watch_progress and uid == self.progress_id: - self.onProgressStarted(metadata) - self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate) + await self.onProgressStarted(metadata) + while True: + await asyncio.sleep(PROGRESS_DELAY) + cont = await self.progressUpdate() + if not cont: + break - def progressFinishedHandler(self, uid, metadata, profile): + async def progressFinishedHandler(self, uid, metadata, profile): if profile != self.profile: return if uid == self.progress_id: @@ -923,39 +1047,58 @@ self.host.pbar.finish() except AttributeError: pass - self.onProgressFinished(metadata) + await self.onProgressFinished(metadata) if self.host.quit_on_progress_end: self.host.quitFromSignal() - def progressErrorHandler(self, uid, message, profile): + async def progressErrorHandler(self, uid, message, profile): if profile != self.profile: return if uid == self.progress_id: if self.args.progress: self.disp('') # progress is not finished, so we skip a line if self.host.quit_on_progress_end: - self.onProgressError(message) - self.host.quitFromSignal(1) + await self.onProgressError(message) + self.host.quitFromSignal(C.EXIT_ERROR) - def progressUpdate(self): - """This method is continualy called to update the progress bar""" - data = self.host.bridge.progressGet(self.progress_id, self.profile) + async def progressUpdate(self): + """This method is continualy called to update the progress bar + + @return (bool): False to stop being called + """ + data = await self.host.bridge.progressGet(self.progress_id, self.profile) if data: try: size = data['size'] except KeyError: - self.disp(_("file size is not known, we can't show a progress bar"), 1, error=True) + self.disp(_("file size is not known, we can't show a progress bar"), 1, + error=True) return False if self.host.pbar is None: #first answer, we must construct the bar - self.host.pbar = progressbar.ProgressBar(max_value=int(size), - widgets=[_("Progress: "),progressbar.Percentage(), - " ", - progressbar.Bar(), - " ", - progressbar.FileTransferSpeed(), - " ", - progressbar.ETA()]) + + # if the instance has a pbar_template attribute, it is used has model, + # else default one is used + # template is a list of part, where part can be either a str to show directly + # or a list where first argument is a name of a progressbar widget, and others + # are used as widget arguments + try: + template = self.pbar_template + except AttributeError: + template = [ + _("Progress: "), ["Percentage"], " ", ["Bar"], " ", + ["FileTransferSpeed"], " ", ["ETA"] + ] + + widgets = [] + for part in template: + if isinstance(part, str): + widgets.append(part) + else: + widget = getattr(progressbar, part.pop(0)) + widgets.append(widget(*part)) + + self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets) self.host.pbar.start() self.host.pbar.update(int(data['position'])) @@ -963,11 +1106,11 @@ elif self.host.pbar is not None: return False - self.onProgressUpdate(data) + await self.onProgressUpdate(data) return True - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): """Called when progress has just started can be overidden by a command @@ -975,7 +1118,7 @@ """ self.disp(_("Operation started"), 2) - def onProgressUpdate(self, metadata): + async def onProgressUpdate(self, metadata): """Method called on each progress updata can be overidden by a command to handle progress metadata @@ -983,7 +1126,7 @@ """ pass - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): """Called when progress has just finished can be overidden by a command @@ -991,12 +1134,12 @@ """ self.disp(_("Operation successfully finished"), 2) - def onProgressError(self, error_msg): + async def onProgressError(self, e): """Called when a progress failed @param error_msg(unicode): error message as sent by bridge.progressError """ - self.disp(_("Error while doing operation: {}").format(error_msg), error=True) + self.disp(_(f"Error while doing operation: {e}"), error=True) def disp(self, msg, verbosity=0, error=False, no_lf=False): return self.host.disp(msg, verbosity, error, no_lf) @@ -1005,33 +1148,10 @@ try: output_type = self._output_type except AttributeError: - raise exceptions.InternalError(_('trying to use output when use_output has not been set')) + raise exceptions.InternalError( + _('trying to use output when use_output has not been set')) return self.host.output(output_type, self.args.output, self.extra_outputs, data) - def exitCb(self, msg=None): - """generic callback for success - - optionally print a message, and quit - msg(None, unicode): if not None, print this message - """ - if msg is not None: - self.disp(msg) - self.host.quit(C.EXIT_OK) - - def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR): - """generic callback for errbacks - - display failure_ then quit with generic error - @param failure_: arguments returned by errback - @param msg(unicode, None): message template - use {} if you want to display failure message - @param exit_code(int): shell exit code - """ - if msg is None: - msg = _("error: {}") - self.disp(msg.format(failure_), error=True) - self.host.quit(exit_code) - def getPubsubExtra(self, extra=None): """Helper method to compute extra data from pubsub arguments @@ -1090,7 +1210,7 @@ for cls in subcommands: cls(self) - def run(self): + async def run(self): """this method is called when a command is actually run It set stuff like progression callbacks and profile connection @@ -1098,9 +1218,6 @@ """ # we keep a reference to run command, it may be useful e.g. for outputs self.host.command = self - # host._need_loop is set here from our current value and not before - # as the need_loop decision must be taken only by then running command - self.host._need_loop = self.need_loop try: show_progress = self.args.progress @@ -1110,34 +1227,25 @@ else: if show_progress: self.host.watch_progress = True - # we need to register the following signal even if we don't display the progress bar - self.host.bridge.register_signal("progressStarted", self.progressStartedHandler) - self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler) - self.host.bridge.register_signal("progressError", self.progressErrorHandler) + # we need to register the following signal even if we don't display the + # progress bar + self.host.bridge.register_signal( + "progressStarted", self.progressStartedHandler) + self.host.bridge.register_signal( + "progressFinished", self.progressFinishedHandler) + self.host.bridge.register_signal( + "progressError", self.progressErrorHandler) if self.need_connect is not None: - self.host.connect_profile(self.connected) - else: - self.start() - - def connected(self): - """this method is called when profile is connected (or session is started) + await self.host.connect_profile() + await self.start() - this method is only called when use_profile is True - most of time you should override self.start instead of this method, but if loop - if not always needed depending on your arguments, you may override this method, - but don't forget to call the parent one (i.e. this one) after self.need_loop is set - """ - if not self.need_loop: - self.host.stop_loop() - self.start() - - def start(self): - """This is the starting point of the command, this method should be overriden + async def start(self): + """This is the starting point of the command, this method must be overriden at this point, profile are connected if needed """ - pass + raise NotImplementedError class CommandAnswering(CommandBase): @@ -1152,9 +1260,8 @@ def __init__(self, *args, **kwargs): super(CommandAnswering, self).__init__(*args, **kwargs) - self.need_loop = True - def onActionNew(self, action_data, action_id, security_limit, profile): + async def onActionNew(self, action_data, action_id, security_limit, profile): if profile != self.profile: return try: @@ -1172,7 +1279,7 @@ except KeyError: pass else: - callback(action_data, action_id, security_limit, profile) + await callback(action_data, action_id, security_limit, profile) def onXMLUI(self, xml_ui): """Display a dialog received from the backend. @@ -1187,11 +1294,9 @@ if dialog is not None: self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") - def connected(self): - """Auto reply to confirmations requests""" - self.need_loop = True - super(CommandAnswering, self).connected() + async def start_answering(self): + """Auto reply to confirmation requests""" self.host.bridge.register_signal("actionNew", self.onActionNew) - actions = self.host.bridge.actionsGet(self.profile) + actions = await self.host.bridge.actionsGet(self.profile) for action_data, action_id, security_limit in actions: - self.onActionNew(action_data, action_id, security_limit, self.profile) + await self.onActionNew(action_data, action_id, security_limit, self.profile) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_account.py --- a/sat_frontends/jp/cmd_account.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_account.py Wed Sep 25 08:56:41 2019 +0200 @@ -39,7 +39,6 @@ use_verbose=True, help=_("create a XMPP account"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -72,80 +71,81 @@ "--port", type=int, default=0, - help=_("server port (IP address or domain, default: use localhost)"), - ) - - def _setParamCb(self): - self.host.bridge.setParam( - "Password", - self.args.password, - "Connection", - profile_key=self.args.profile, - callback=self.host.quit, - errback=self.errback, - ) - - def _session_started(self, __): - self.host.bridge.setParam( - "JabberID", - self.args.jid, - "Connection", - profile_key=self.args.profile, - callback=self._setParamCb, - errback=self.errback, + help=_(f"server port (default: {C.XMPP_C2S_PORT})"), ) - def _profileCreateCb(self): - self.disp(_("profile created"), 1) - self.host.bridge.profileStartSession( - self.args.password, - self.args.profile, - callback=self._session_started, - errback=self.errback, - ) + async def start(self): + try: + await self.host.bridge.inBandAccountNew( + self.args.jid, + self.args.password, + self.args.email, + self.args.host, + self.args.port, + ) + except Exception as e: + self.disp( + f"can't create account on {self.args.host or 'localhost'!r} with jid " + f"{self.args.jid!r} using In-Band Registration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def _profileCreateEb(self, failure_): - self.disp( - _( - "Can't create profile {profile} to associate with jid {jid}: {msg}" - ).format(profile=self.args.profile, jid=self.args.jid, msg=failure_), - error=True, - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) + self.disp(_("XMPP account created"), 1) + + if self.args.profile is None: + self.host.quit() - def accountNewCb(self): - self.disp(_("XMPP account created"), 1) - if self.args.profile is not None: - self.disp(_("creating profile"), 2) - self.host.bridge.profileCreate( + + self.disp(_("creating profile"), 2) + try: + await self.host.bridge.profileCreate( self.args.profile, self.args.password, "", - callback=self._profileCreateCb, - errback=self._profileCreateEb, + ) + except Exception as e: + self.disp( + _(f"Can't create profile {self.args.profile} to associate with jid " + f"{self.args.jid}: {e}"), + error=True, + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.disp(_("profile created"), 1) + try: + await self.host.bridge.profileStartSession( + self.args.password, + self.args.profile, ) - else: - self.host.quit() + except Exception as e: + self.disp(f"can't start profile session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def accountNewEb(self, failure_): + try: + await self.host.bridge.setParam( + "JabberID", + self.args.jid, + "Connection", + profile_key=self.args.profile, + ) + except Exception as e: + self.disp(f"can't set JabberID parameter: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.setParam( + "Password", + self.args.password, + "Connection", + profile_key=self.args.profile, + ) + except Exception as e: + self.disp(f"can't set Password parameter: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + self.disp( - _("Can't create new account on server {host} with jid {jid}: {msg}").format( - host=self.args.host or "localhost", jid=self.args.jid, msg=failure_ - ), - error=True, - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.inBandAccountNew( - self.args.jid, - self.args.password, - self.args.email, - self.args.host, - self.args.port, - callback=self.accountNewCb, - errback=self.accountNewEb, - ) + f"profile {self.args.profile} successfully created and associated to the new " + f"account", 1) + self.host.quit() class AccountModify(base.CommandBase): @@ -153,20 +153,23 @@ super(AccountModify, self).__init__( host, "modify", help=_("change password for XMPP account") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( "password", help=_("new XMPP password") ) - def start(self): - self.host.bridge.inBandPasswordChange( - self.args.password, - self.args.profile, - callback=self.host.quit, - errback=self.errback, - ) + async def start(self): + try: + await self.host.bridge.inBandPasswordChange( + self.args.password, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't change XMPP password: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class AccountDelete(base.CommandBase): @@ -174,7 +177,6 @@ super(AccountDelete, self).__init__( host, "delete", help=_("delete a XMPP account") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -184,32 +186,33 @@ help=_("delete account without confirmation"), ) - def _got_jid(self, jid_str): + async def start(self): + try: + jid_str = await self.host.bridge.asyncGetParamA( + "JabberID", + "Connection", + profile_key=self.profile, + ) + except Exception as e: + self.disp(f"can't get JID of the profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + jid_ = jid.JID(jid_str) if not self.args.force: message = ( - "You are about to delete the XMPP account with jid {jid_}\n" - 'This is the XMPP account of profile "{profile}"\n' - "Are you sure that you want to delete this account ?".format( - jid_=jid_, profile=self.profile - ) + f"You are about to delete the XMPP account with jid {jid_!r}\n" + f"This is the XMPP account of profile {self.profile!r}\n" + f"Are you sure that you want to delete this account?" ) - res = input("{} (y/N)? ".format(message)) - if res not in ("y", "Y"): - self.disp(_("Account deletion cancelled")) - self.host.quit(2) - self.host.bridge.inBandUnregister( - jid_.domain, self.args.profile, callback=self.host.quit, errback=self.errback - ) + await self.host.confirmOrQuit(message, _("Account deletion cancelled")) - def start(self): - self.host.bridge.asyncGetParamA( - "JabberID", - "Connection", - profile_key=self.profile, - callback=self._got_jid, - errback=self.errback, - ) + try: + await self.host.bridge.inBandUnregister(jid_.domain, self.args.profile) + except Exception as e: + self.disp(f"can't delete XMPP account with jid {jid_!r}: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() class Account(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_adhoc.py --- a/sat_frontends/jp/cmd_adhoc.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_adhoc.py Wed Sep 25 08:56:41 2019 +0200 @@ -19,7 +19,6 @@ from . import base from sat.core.i18n import _ -from functools import partial from sat_frontends.jp.constants import Const as C from sat_frontends.jp import xmlui_manager @@ -67,7 +66,7 @@ "-l", "--loop", action="store_true", help=_("loop on the commands") ) - def start(self): + async def start(self): name = self.args.software.lower() flags = [] magics = {jid for jid in self.args.jids if jid.count("@") > 1} @@ -75,29 +74,33 @@ jids = set(self.args.jids).difference(magics) if self.args.loop: flags.append(FLAG_LOOP) - bus_name, methods = self.host.bridge.adHocDBusAddAuto( - name, - jids, - self.args.groups, - magics, - self.args.forbidden_jids, - self.args.forbidden_groups, - flags, - self.profile, - ) - if not bus_name: - self.disp(_("No bus name found"), 1) - return - self.disp(_("Bus name found: [%s]" % bus_name), 1) - for method in methods: - path, iface, command = method - self.disp( - _( - "Command found: (path:%(path)s, iface: %(iface)s) [%(command)s]" - % {"path": path, "iface": iface, "command": command} - ), - 1, + try: + bus_name, methods = await self.host.bridge.adHocDBusAddAuto( + name, + list(jids), + self.args.groups, + magics, + self.args.forbidden_jids, + self.args.forbidden_groups, + flags, + self.profile, ) + except Exception as e: + self.disp(f"can't create remote control: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if not bus_name: + self.disp(_("No bus name found"), 1) + self.host.quit(C.EXIT_NOT_FOUND) + else: + self.disp(_("Bus name found: [%s]" % bus_name), 1) + for method in methods: + path, iface, command = method + self.disp( + _(f"Command found: (path:{path}, iface: {iface}) [{command}]"), + 1, + ) + self.host.quit() class Run(base.CommandBase): @@ -107,7 +110,6 @@ super(Run, self).__init__( host, "run", use_verbose=True, help=_("run an Ad-Hoc command") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -140,28 +142,24 @@ help=_("node of the command (default: list commands)"), ) - def adHocRunCb(self, xmlui_raw): - xmlui = xmlui_manager.create(self.host, xmlui_raw) - workflow = self.args.workflow - xmlui.show(workflow) - if not workflow: - if xmlui.type == "form": - xmlui.submitForm() - else: - self.host.quit() - - def start(self): - self.host.bridge.adHocRun( - self.args.jid, - self.args.node, - self.profile, - callback=self.adHocRunCb, - errback=partial( - self.errback, - msg=_("can't get ad-hoc commands list: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + xmlui_raw = await self.host.bridge.adHocRun( + self.args.jid, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get ad-hoc commands list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + xmlui = xmlui_manager.create(self.host, xmlui_raw) + workflow = self.args.workflow + await xmlui.show(workflow) + if not workflow: + if xmlui.type == "form": + await xmlui.submitForm() + self.host.quit() class List(base.CommandBase): @@ -171,7 +169,6 @@ super(List, self).__init__( host, "list", use_verbose=True, help=_("list Ad-Hoc commands of a service") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -181,23 +178,19 @@ help=_("jid of the service (default: profile's server"), ) - def adHocListCb(self, xmlui_raw): - xmlui = xmlui_manager.create(self.host, xmlui_raw) - xmlui.readonly = True - xmlui.show() - self.host.quit() - - def start(self): - self.host.bridge.adHocList( - self.args.jid, - self.profile, - callback=self.adHocListCb, - errback=partial( - self.errback, - msg=_("can't get ad-hoc commands list: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + xmlui_raw = await self.host.bridge.adHocList( + self.args.jid, + self.profile, + ) + except Exception as e: + self.disp(f"can't get ad-hoc commands list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + xmlui = xmlui_manager.create(self.host, xmlui_raw) + await xmlui.show(read_only=True) + self.host.quit() class AdHoc(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_avatar.py --- a/sat_frontends/jp/cmd_avatar.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_avatar.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,59 +18,24 @@ # along with this program. If not, see . -from . import base import os import os.path +import asyncio +from . import base from sat.core.i18n import _ from sat_frontends.jp.constants import Const as C from sat.tools import config -import subprocess __commands__ = ["Avatar"] DISPLAY_CMD = ["xv", "display", "gwenview", "showtell"] -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__( - host, "set", use_verbose=True, help=_("set avatar of the profile") - ) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument( - "image_path", type=str, help=_("path to the image to upload") - ) - - def start(self): - """Send files to jabber contact""" - path = self.args.image_path - if not os.path.exists(path): - self.disp(_("file [{}] doesn't exist !").format(path), error=True) - self.host.quit(1) - path = os.path.abspath(path) - self.host.bridge.avatarSet( - path, self.profile, callback=self._avatarCb, errback=self._avatarEb - ) - - def _avatarCb(self): - self.disp(_("avatar has been set"), 1) - self.host.quit() - - def _avatarEb(self, failure_): - self.disp( - _("error while uploading avatar: {msg}").format(msg=failure_), error=True - ) - self.host.quit(C.EXIT_ERROR) - - class Get(base.CommandBase): def __init__(self, host): super(Get, self).__init__( host, "get", use_verbose=True, help=_("retrieve avatar of an entity") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument("jid", help=_("entity")) @@ -78,54 +43,81 @@ "-s", "--show", action="store_true", help=_("show avatar") ) - def showImage(self, path): + async def showImage(self, path): sat_conf = config.parseMainConf() cmd = config.getConfig(sat_conf, "jp", "image_cmd") cmds = [cmd] + DISPLAY_CMD if cmd else DISPLAY_CMD for cmd in cmds: try: - ret = subprocess.call([cmd] + [path]) + process = await asyncio.create_subprocess_exec(cmd, path) + ret = await process.wait() except OSError: - pass - else: - if ret in (0, 2): - # we can get exit code 2 with display when stopping it with C-c - break + continue + + if ret in (0, 2): + # we can get exit code 2 with display when stopping it with C-c + break else: # didn't worked with commands, we try our luck with webbrowser - # in some cases, webbrowser can actually open the associated display program + # in some cases, webbrowser can actually open the associated display program. + # Note that this may be possibly blocking, depending on the platform and + # available browser import webbrowser webbrowser.open(path) - def _avatarGetCb(self, avatar_path): + async def start(self): + try: + avatar_path = await self.host.bridge.avatarGet( + self.args.jid, + False, + False, + self.profile, + ) + except Exception as e: + self.disp(f"can't retrieve avatar: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + if not avatar_path: self.disp(_("No avatar found."), 1) self.host.quit(C.EXIT_NOT_FOUND) self.disp(avatar_path) if self.args.show: - self.showImage(avatar_path) + await self.showImage(avatar_path) self.host.quit() - def _avatarGetEb(self, failure_): - self.disp(_("error while getting avatar: {msg}").format(msg=failure_), error=True) - self.host.quit(C.EXIT_ERROR) + +class Set(base.CommandBase): + def __init__(self, host): + super(Set, self).__init__( + host, "set", use_verbose=True, help=_("set avatar of the profile") + ) + + def add_parser_options(self): + self.parser.add_argument( + "image_path", type=str, help=_("path to the image to upload") + ) - def start(self): - self.host.bridge.avatarGet( - self.args.jid, - False, - False, - self.profile, - callback=self._avatarGetCb, - errback=self._avatarGetEb, - ) + async def start(self): + path = self.args.image_path + if not os.path.exists(path): + self.disp(_(f"file {path!r} doesn't exist!"), error=True) + self.host.quit(C.EXIT_BAD_ARG) + path = os.path.abspath(path) + try: + await self.host.bridge.avatarSet(path, self.profile) + except Exception as e: + self.disp(f"can't set avatar: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("avatar has been set"), 1) + self.host.quit() class Avatar(base.CommandBase): - subcommands = (Set, Get) + subcommands = (Get, Set) def __init__(self, host): super(Avatar, self).__init__( diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_blog.py --- a/sat_frontends/jp/cmd_blog.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_blog.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,6 +18,16 @@ # along with this program. If not, see . +import json +import sys +import os.path +import os +import time +import tempfile +import subprocess +import asyncio +from asyncio.subprocess import DEVNULL +from pathlib import Path from . import base from sat.core.i18n import _ from sat_frontends.jp.constants import Const as C @@ -27,15 +37,6 @@ from sat.tools.common import uri from sat.tools import config from configparser import NoSectionError, NoOptionError -from functools import partial -import json -import sys -import os.path -import os -import time -import tempfile -import subprocess -import codecs from sat.tools.common import data_format __commands__ = ["Blog"] @@ -64,7 +65,7 @@ ) URL_REDIRECT_PREFIX = "url_redirect_" -INOTIFY_INSTALL = '"pip install inotify"' +AIONOTIFY_INSTALL = '"pip install aionotify"' MB_KEYS = ( "id", "url", @@ -86,7 +87,7 @@ OUTPUT_OPT_NO_HEADER = "no-header" -def guessSyntaxFromPath(host, sat_conf, path): +async def guessSyntaxFromPath(host, sat_conf, path): """Return syntax guessed according to filename extension @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration @@ -101,19 +102,35 @@ return k # if not found, we use current syntax - return host.bridge.getParamA("Syntax", "Composition", "value", host.profile) + return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile) class BlogPublishCommon(object): """handle common option for publising commands (Set and Edit)""" - @property - def current_syntax(self): - if self._current_syntax is None: - self._current_syntax = self.host.bridge.getParamA( + async def get_current_syntax(self): + """Retrieve current_syntax + + Use default syntax if --syntax has not been used, else check given syntax. + Will set self.default_syntax_used to True if default syntax has been used + """ + if self.args.syntax is None: + self.default_syntax_used = True + return await self.host.bridge.getParamA( "Syntax", "Composition", "value", self.profile ) - return self._current_syntax + else: + self.default_syntax_used = False + try: + syntax = await self.host.bridge.syntaxGet(self.current_syntax) + + self.current_syntax = self.args.syntax = syntax + except Exception as e: + if e.classname == "NotFound": + self.parser.error(_(f"unknown syntax requested ({self.args.syntax})")) + else: + raise e + return self.args.syntax def add_parser_options(self): self.parser.add_argument( @@ -143,14 +160,14 @@ help=_("syntax to use (default: get profile's default syntax)"), ) - def setMbDataContent(self, content, mb_data): - if self.args.syntax is None: + async def setMbDataContent(self, content, mb_data): + if self.default_syntax_used: # default syntax has been used mb_data["content_rich"] = content elif self.current_syntax == SYNTAX_XHTML: mb_data["content_xhtml"] = content else: - mb_data["content_xhtml"] = self.host.bridge.syntaxConvert( + mb_data["content_xhtml"] = await self.host.bridge.syntaxConvert( content, self.current_syntax, SYNTAX_XHTML, False, self.profile ) @@ -178,37 +195,35 @@ help=_("publish a new blog item or update an existing one"), ) BlogPublishCommon.__init__(self) - self.need_loop = True def add_parser_options(self): BlogPublishCommon.add_parser_options(self) - def mbSendCb(self): - self.disp("Item published") - self.host.quit(C.EXIT_OK) - - def start(self): - self._current_syntax = self.args.syntax + async def start(self): + self.current_syntax = await self.get_current_syntax() self.pubsub_item = self.args.item mb_data = {} self.setMbDataFromArgs(mb_data) if self.pubsub_item: mb_data["id"] = self.pubsub_item - content = codecs.getreader("utf-8")(sys.stdin).read() - self.setMbDataContent(content, mb_data) + content = sys.stdin.read() + await self.setMbDataContent(content, mb_data) - self.host.bridge.mbSend( - self.args.service, - self.args.node, - data_format.serialise(mb_data), - self.profile, - callback=self.exitCb, - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.mbSend( + self.args.service, + self.args.node, + data_format.serialise(mb_data), + self.profile, + ) + except Exception as e: + self.disp( + f"can't send item: {e}", error=True + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp("Item published") + self.host.quit(C.EXIT_OK) class Get(base.CommandBase): @@ -227,7 +242,6 @@ extra_outputs=extra_outputs, help=_("get blog item(s)"), ) - self.need_loop = True def add_parser_options(self): #  TODO: a key(s) argument to select keys to display @@ -401,28 +415,25 @@ print(("\n" + sep + "\n")) - def mbGetCb(self, mb_result): - items, metadata = mb_result - items = [data_format.deserialise(i) for i in items] - mb_result = items, metadata - self.output(mb_result) - self.host.quit(C.EXIT_OK) - - def mbGetEb(self, failure_): - self.disp("can't get blog items: {reason}".format(reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.mbGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - self.getPubsubExtra(), - self.profile, - callback=self.mbGetCb, - errback=self.mbGetEb, - ) + async def start(self): + try: + mb_result = await self.host.bridge.mbGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + self.getPubsubExtra(), + self.profile + ) + except Exception as e: + self.disp(f"can't get blog items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + items, metadata = mb_result + items = [data_format.deserialise(i) for i in items] + mb_result = items, metadata + await self.output(mb_result) + self.host.quit(C.EXIT_OK) class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): @@ -452,25 +463,27 @@ def buildMetadataFile(self, content_file_path, mb_data=None): """Build a metadata file using json - The file is named after content_file_path, with extension replaced by _metadata.json - @param content_file_path(str): path to the temporary file which will contain the body + The file is named after content_file_path, with extension replaced by + _metadata.json + @param content_file_path(str): path to the temporary file which will contain the + body @param mb_data(dict, None): microblog metadata (for existing items) - @return (tuple[dict, str]): merged metadata put originaly in metadata file + @return (tuple[dict, Path]): merged metadata put originaly in metadata file and path to temporary metadata file """ # we first construct metadata from edited item ones and CLI argumments # or re-use the existing one if it exists - meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF - if os.path.exists(meta_file_path): + meta_file_path = content_file_path.with_name( + content_file_path.stem + common.METADATA_SUFF) + if meta_file_path.exists(): self.disp("Metadata file already exists, we re-use it") try: - with open(meta_file_path, "rb") as f: + with meta_file_path.open("rb") as f: mb_data = json.load(f) except (OSError, IOError, ValueError) as e: self.disp( - "Can't read existing metadata file at {path}, aborting: {reason}".format( - path=meta_file_path, reason=e - ), + f"Can't read existing metadata file at {meta_file_path}, " + f"aborting: {e}", error=True, ) self.host.quit(1) @@ -491,7 +504,8 @@ with os.fdopen( os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b" ) as f: - # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters + # we need to use an intermediate unicode buffer to write to the file + # unicode without escaping characters unicode_dump = json.dumps( mb_data, ensure_ascii=False, @@ -503,19 +517,20 @@ return mb_data, meta_file_path - def edit(self, content_file_path, content_file_obj, mb_data=None): + async def edit(self, content_file_path, content_file_obj, mb_data=None): """Edit the file contening the content using editor, and publish it""" # we first create metadata file meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data) + coroutines = [] + # do we need a preview ? if self.args.preview: self.disp("Preview requested, launching it", 1) # we redirect outputs to /dev/null to avoid console pollution in editor # if user wants to see messages, (s)he can call "blog preview" directly - DEVNULL = open(os.devnull, "wb") - subprocess.Popen( - [ + coroutines.append( + asyncio.create_subprocess_exec( sys.argv[0], "blog", "preview", @@ -523,30 +538,34 @@ "true", "-p", self.profile, - content_file_path, - ], - stdout=DEVNULL, - stderr=subprocess.STDOUT, + str(content_file_path), + stdout=DEVNULL, + stderr=DEVNULL, + ) ) # we launch editor - self.runEditor( - "blog_editor_args", - content_file_path, - content_file_obj, - meta_file_path=meta_file_path, - meta_ori=meta_ori, + coroutines.append( + self.runEditor( + "blog_editor_args", + content_file_path, + content_file_obj, + meta_file_path=meta_file_path, + meta_ori=meta_ori, + ) ) - def publish(self, content, mb_data): - self.setMbDataContent(content, mb_data) + await asyncio.gather(*coroutines) + + async def publish(self, content, mb_data): + await self.setMbDataContent(content, mb_data) if self.pubsub_item: mb_data["id"] = self.pubsub_item mb_data = data_format.serialise(mb_data) - self.host.bridge.mbSend( + await self.host.bridge.mbSend( self.pubsub_service, self.pubsub_node, mb_data, self.profile ) self.disp("Blog item published") @@ -555,22 +574,27 @@ # we get current syntax to determine file extension return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""]) - def getItemData(self, service, node, item): + async def getItemData(self, service, node, item): items = [item] if item else [] - mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0] - mb_data = data_format.deserialise(mb_data) + + mb_data = await self.host.bridge.mbGet( + service, node, 1, items, {}, self.profile) + mb_data = data_format.deserialise(mb_data[0][0]) + try: content = mb_data["content_xhtml"] except KeyError: content = mb_data["content"] if content: - content = self.host.bridge.syntaxConvert( + content = await self.host.bridge.syntaxConvert( content, "text", SYNTAX_XHTML, False, self.profile ) + if content and self.current_syntax != SYNTAX_XHTML: - content = self.host.bridge.syntaxConvert( + content = await self.host.bridge.syntaxConvert( content, SYNTAX_XHTML, self.current_syntax, False, self.profile ) + if content and self.current_syntax == SYNTAX_XHTML: content = content.strip() if not content.startswith('
'): @@ -586,37 +610,16 @@ return content, mb_data, mb_data["id"] - def start(self): + async def start(self): # if there are user defined extension, we use them SYNTAX_EXT.update(config.getConfig(self.sat_conf, "jp", CONF_SYNTAX_EXT, {})) - self._current_syntax = self.args.syntax - if self._current_syntax is not None: - try: - self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet( - self.current_syntax - ) - except Exception as e: - if "NotFound" in str( - e - ): #  FIXME: there is not good way to check bridge errors - self.parser.error( - _("unknown syntax requested ({syntax})").format( - syntax=self.args.syntax - ) - ) - else: - raise e + self.current_syntax = await self.get_current_syntax() - ( - self.pubsub_service, - self.pubsub_node, - self.pubsub_item, - content_file_path, - content_file_obj, - mb_data, - ) = self.getItemPath() + (self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, + content_file_obj, mb_data,) = await self.getItemPath() - self.edit(content_file_path, content_file_obj, mb_data=mb_data) + await self.edit(content_file_path, content_file_obj, mb_data=mb_data) + self.host.quit() class Preview(base.CommandBase, common.BaseEdit): @@ -643,14 +646,14 @@ help=_("path to the content file"), ) - def showPreview(self): + async def showPreview(self): # we implement showPreview here so we don't have to import webbrowser and urllib # when preview is not used - url = "file:{}".format(self.urllib.quote(self.preview_file_path)) + url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) self.webbrowser.open_new_tab(url) - def _launchPreviewExt(self, cmd_line, opt_name): - url = "file:{}".format(self.urllib.quote(self.preview_file_path)) + async def _launchPreviewExt(self, cmd_line, opt_name): + url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) args = common.parse_args( self.host, cmd_line, url=url, preview_file=self.preview_file_path ) @@ -662,34 +665,34 @@ self.host.quit(1) subprocess.Popen(args) - def openPreviewExt(self): - self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd") + async def openPreviewExt(self): + await self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd") - def updatePreviewExt(self): - self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd") + async def updatePreviewExt(self): + await self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd") - def updateContent(self): - with open(self.content_file_path, "rb") as f: + async def updateContent(self): + with self.content_file_path.open("rb") as f: content = f.read().decode("utf-8-sig") if content and self.syntax != SYNTAX_XHTML: # we use safe=True because we want to have a preview as close as possible # to what the people will see - content = self.host.bridge.syntaxConvert( + content = await self.host.bridge.syntaxConvert( content, self.syntax, SYNTAX_XHTML, True, self.profile ) xhtml = ( - '' - '' - "" - "{}" - "" - ).format(content) + f'' + f'' + f'' + f'{content}' + f'' + ) with open(self.preview_file_path, "wb") as f: f.write(xhtml.encode("utf-8")) - def start(self): + async def start(self): import webbrowser import urllib.request, urllib.parse, urllib.error @@ -697,33 +700,24 @@ if self.args.inotify != "false": try: - import inotify.adapters - import inotify.constants - from inotify.calls import InotifyError + import aionotify + except ImportError: if self.args.inotify == "auto": - inotify = None + aionotify = None self.disp( - "inotify module not found, deactivating feature. You can install" - " it with {install}".format(install=INOTIFY_INSTALL) + f"aionotify module not found, deactivating feature. You can " + f"install it with {AIONOTIFY_INSTALL}" ) else: self.disp( - "inotify not found, can't activate the feature! Please install " - "it with {install}".format(install=INOTIFY_INSTALL), + f"aioinotify not found, can't activate the feature! Please " + f"install it with {AIONOTIFY_INSTALL}", error=True, ) self.host.quit(1) - else: - # we deactivate logging in inotify, which is quite annoying - try: - inotify.adapters._LOGGER.setLevel(40) - except AttributeError: - self.disp( - "Logger doesn't exists, inotify may have chanded", error=True - ) else: - inotify = None + aionotify = None sat_conf = config.parseMainConf() SYNTAX_EXT.update(config.getConfig(sat_conf, "jp", CONF_SYNTAX_EXT, {})) @@ -750,76 +744,89 @@ if self.args.file == "current": self.content_file_path = self.getCurrentFile(self.profile) else: - self.content_file_path = os.path.abspath(self.args.file) + try: + self.content_file_path = Path(self.args.file).resolve(strict=True) + except FileNotFoundError: + self.disp(_(f'File "{self.args.file}" doesn\'t exist!')) + self.host.quit(C.EXIT_NOT_FOUND) - self.syntax = guessSyntaxFromPath(self.host, sat_conf, self.content_file_path) + self.syntax = await guessSyntaxFromPath( + self.host, sat_conf, self.content_file_path) # at this point the syntax is converted, we can display the preview preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False) self.preview_file_path = preview_file.name preview_file.close() - self.updateContent() + await self.updateContent() - if inotify is None: - # XXX: we don't delete file automatically because browser need it + if aionotify is None: + # XXX: we don't delete file automatically because browser needs it # (and webbrowser.open can return before it is read) self.disp( - "temporary file created at {}\nthis file will NOT BE DELETED " - "AUTOMATICALLY, please delete it yourself when you have finished".format( - self.preview_file_path - ) + f"temporary file created at {self.preview_file_path}\nthis file will NOT " + f"BE DELETED AUTOMATICALLY, please delete it yourself when you have " + f"finished" ) - open_cb() + await open_cb() else: - open_cb() - i = inotify.adapters.Inotify( - block_duration_s=60 - ) # no need for 1 s duraction, inotify drive actions here + await open_cb() + watcher = aionotify.Watcher() + watcher_kwargs = { + # Watcher don't accept Path so we convert to string + "path": str(self.content_file_path), + "alias": 'content_file', + "flags": aionotify.Flags.CLOSE_WRITE + | aionotify.Flags.DELETE_SELF + | aionotify.Flags.MOVE_SELF, + } + watcher.watch(**watcher_kwargs) - def add_watch(): - i.add_watch( - self.content_file_path.encode('utf-8'), - mask=inotify.constants.IN_CLOSE_WRITE - | inotify.constants.IN_DELETE_SELF - | inotify.constants.IN_MOVE_SELF, - ) - - add_watch() + loop = asyncio.get_event_loop() + await watcher.setup(loop) try: - for event in i.event_gen(): - if event is not None: - self.disp("Content updated", 1) - if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]): + while True: + event = await watcher.get_event() + self.disp("Content updated", 1) + if event.flags & (aionotify.Flags.DELETE_SELF + | aionotify.Flags.MOVE_SELF): + self.disp( + "DELETE/MOVE event catched, changing the watch", + 2, + ) + try: + watcher.unwatch('content_file') + except IOError as e: self.disp( - "{} event catched, changing the watch".format( - ", ".join(event[1]) - ), + f"Can't remove the watch: {e}", 2, ) - i.remove_watch(self.content_file_path) - try: - add_watch() - except InotifyError: - # if the new file is not here yet we can have an error - # as a workaround, we do a little rest - time.sleep(1) - add_watch() - self.updateContent() - update_cb() - except InotifyError: - self.disp( - "Can't catch inotify events, as the file been deleted?", error=True - ) + watcher = aionotify.Watcher() + watcher.watch(**watcher_kwargs) + try: + await watcher.setup(loop) + except OSError: + # if the new file is not here yet we can have an error + # as a workaround, we do a little rest and try again + await asyncio.sleep(1) + await watcher.setup(loop) + await self.updateContent() + await update_cb() + except FileNotFoundError: + self.disp("The file seems to have been deleted.", error=True) + self.host.quit(C.EXIT_NOT_FOUND) finally: os.unlink(self.preview_file_path) try: - i.remove_watch(self.content_file_path) - except InotifyError: - pass + watcher.unwatch('content_file') + except IOError as e: + self.disp( + f"Can't remove the watch: {e}", + 2, + ) -class Import(base.CommandAnswering): +class Import(base.CommandBase): def __init__(self, host): super(Import, self).__init__( host, @@ -828,7 +835,6 @@ use_progress=True, help=_("import an external blog"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -871,10 +877,10 @@ ), ) - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): self.disp(_("Blog upload started"), 2) - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): self.disp(_("Blog uploaded successfully"), 2) redirections = { k[len(URL_REDIRECT_PREFIX) :]: v @@ -897,42 +903,35 @@ ) self.disp( _( - "\nTo redirect old URLs to new ones, put the following lines in your" - " sat.conf file, in [libervia] section:\n\n{conf}".format(conf=conf) + f"\nTo redirect old URLs to new ones, put the following lines in your" + f" sat.conf file, in [libervia] section:\n\n{conf}" ) ) - def onProgressError(self, error_msg): - self.disp(_("Error while uploading blog: {}").format(error_msg), error=True) + async def onProgressError(self, error_msg): + self.disp(_(f"Error while uploading blog: {error_msg}"), error=True) - def error(self, failure): - self.disp( - _("Error while trying to upload a blog: {reason}").format(reason=failure), - error=True, - ) - self.host.quit(1) - - def start(self): + async def start(self): if self.args.location is None: for name in ("option", "service", "no_images_upload"): if getattr(self.args, name): self.parser.error( _( - "{name} argument can't be used without location argument" - ).format(name=name) + f"{name} argument can't be used without location argument" + ) ) if self.args.importer is None: self.disp( "\n".join( [ - "{}: {}".format(name, desc) - for name, desc in self.host.bridge.blogImportList() + f"{name}: {desc}" + for name, desc in await self.host.bridge.blogImportList() ] ) ) else: try: - short_desc, long_desc = self.host.bridge.blogImportDesc( + short_desc, long_desc = await self.host.bridge.blogImportDesc( self.args.importer ) except Exception as e: @@ -942,13 +941,7 @@ self.disp(msg) self.host.quit(1) else: - self.disp( - "{name}: {short_desc}\n\n{long_desc}".format( - name=self.args.importer, - short_desc=short_desc, - long_desc=long_desc, - ) - ) + self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}") self.host.quit() else: # we have a location, an import is requested @@ -967,20 +960,23 @@ elif self.args.upload_ignore_host: options["upload_ignore_host"] = self.args.upload_ignore_host - def gotId(id_): - self.progress_id = id_ + try: + progress_id = await self.host.bridge.blogImport( + self.args.importer, + self.args.location, + options, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp( + _(f"Error while trying to import a blog: {e}"), + error=True, + ) + self.host.quit(1) - self.host.bridge.blogImport( - self.args.importer, - self.args.location, - options, - self.args.service, - self.args.node, - self.profile, - callback=gotId, - errback=self.error, - ) - + await self.set_progress_id(progress_id) class Blog(base.CommandBase): subcommands = (Set, Get, Edit, Preview, Import) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_bookmarks.py --- a/sat_frontends/jp/cmd_bookmarks.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_bookmarks.py Wed Sep 25 08:56:41 2019 +0200 @@ -19,6 +19,7 @@ from . import base from sat.core.i18n import _ +from sat_frontends.jp.constants import Const as C __commands__ = ["Bookmarks"] @@ -32,23 +33,25 @@ self.parser.add_argument('-l', '--location', type=str, choices=(location_default,) + STORAGE_LOCATIONS, default=location_default, help=_("storage location (default: %(default)s)")) self.parser.add_argument('-t', '--type', type=str, choices=TYPES, default=TYPES[0], help=_("bookmarks type (default: %(default)s)")) - def _errback(self, failure): - print((("Something went wrong: [%s]") % failure)) - self.host.quit(1) - class BookmarksList(BookmarksCommon): def __init__(self, host): super(BookmarksList, self).__init__(host, 'list', help=_('list bookmarks')) - def start(self): - data = self.host.bridge.bookmarksList(self.args.type, self.args.location, self.host.profile) + async def start(self): + try: + data = await self.host.bridge.bookmarksList( + self.args.type, self.args.location, self.host.profile) + except Exception as e: + self.disp(f"can't get bookmarks list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + mess = [] for location in STORAGE_LOCATIONS: if not data[location]: continue loc_mess = [] - loc_mess.append("%s:" % location) + loc_mess.append(f"{location}:") book_mess = [] for book_link, book_data in list(data[location].items()): name = book_data.get('name') @@ -62,41 +65,56 @@ mess.append('\n'.join(loc_mess)) print('\n\n'.join(mess)) + self.host.quit() class BookmarksRemove(BookmarksCommon): def __init__(self, host): super(BookmarksRemove, self).__init__(host, 'remove', help=_('remove a bookmark')) - self.need_loop = True def add_parser_options(self): super(BookmarksRemove, self).add_parser_options() - self.parser.add_argument('bookmark', help=_('jid (for muc bookmark) or url of to remove')) + self.parser.add_argument( + 'bookmark', help=_('jid (for muc bookmark) or url of to remove')) + self.parser.add_argument( + "-f", "--force", action="store_true", + help=_("delete bookmark without confirmation"),) + + async def start(self): + if not self.args.force: + await self.host.confirmOrQuit(_("Are you sure to delete this bookmark?")) - def start(self): - self.host.bridge.bookmarksRemove(self.args.type, self.args.bookmark, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback) + try: + await self.host.bridge.bookmarksRemove( + self.args.type, self.args.bookmark, self.args.location, self.host.profile) + except Exception as e: + self.disp(_(f"can't delete bookmark: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_('bookmark deleted')) + self.host.quit() class BookmarksAdd(BookmarksCommon): def __init__(self, host): super(BookmarksAdd, self).__init__(host, 'add', help=_('add a bookmark')) - self.need_loop = True def add_parser_options(self): super(BookmarksAdd, self).add_parser_options(location_default='auto') - self.parser.add_argument('bookmark', help=_('jid (for muc bookmark) or url of to remove')) + self.parser.add_argument( + 'bookmark', help=_('jid (for muc bookmark) or url of to remove')) self.parser.add_argument('-n', '--name', help=_("bookmark name")) muc_group = self.parser.add_argument_group(_('MUC specific options')) muc_group.add_argument('-N', '--nick', help=_('nickname')) - muc_group.add_argument('-a', '--autojoin', action='store_true', help=_('join room on profile connection')) + muc_group.add_argument( + '-a', '--autojoin', action='store_true', + help=_('join room on profile connection')) - def start(self): + async def start(self): if self.args.type == 'url' and (self.args.autojoin or self.args.nick is not None): - # XXX: Argparse doesn't seem to manage this case, any better way ? - print(_("You can't use --autojoin or --nick with --type url")) - self.host.quit(1) + self.parser.error(_("You can't use --autojoin or --nick with --type url")) data = {} if self.args.autojoin: data['autojoin'] = 'true' @@ -104,7 +122,16 @@ data['nick'] = self.args.nick if self.args.name is not None: data['name'] = self.args.name - self.host.bridge.bookmarksAdd(self.args.type, self.args.bookmark, data, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback) + try: + await self.host.bridge.bookmarksAdd( + self.args.type, self.args.bookmark, data, self.args.location, + self.host.profile) + except Exception as e: + self.disp(f"can't add bookmark: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_('bookmark successfully added')) + self.host.quit() class Bookmarks(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_debug.py --- a/sat_frontends/jp/cmd_debug.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_debug.py Wed Sep 25 08:56:41 2019 +0200 @@ -48,7 +48,6 @@ def __init__(self, host): base.CommandBase.__init__(self, host, "method", help=_("call a bridge method")) BridgeCommon.__init__(self) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -58,34 +57,31 @@ "arg", nargs="*", help=_("argument of the method") ) - def method_cb(self, ret=None): - if ret is not None: - self.disp(str(ret)) - self.host.quit() - - def method_eb(self, failure): - self.disp( - _("Error while executing {}: {}".format(self.args.method, failure)), - error=True, - ) - self.host.quit(C.EXIT_ERROR) - - def start(self): + async def start(self): method = getattr(self.host.bridge, self.args.method) + import inspect + argspec = inspect.getargspec(method) + + kwargs = {} + if 'profile_key' in argspec.args: + kwargs['profile_key'] = self.profile + elif 'profile' in argspec.args: + kwargs['profile'] = self.profile + args = self.evalArgs() + try: - method( + ret = await method( *args, - profile=self.profile, - callback=self.method_cb, - errback=self.method_eb + **kwargs, ) - except TypeError: - # maybe the method doesn't need a profile ? - try: - method(*args, callback=self.method_cb, errback=self.method_eb) - except TypeError: - self.method_eb(_("bad arguments")) + except Exception as e: + self.disp(_(f"Error while executing {self.args.method}: {e}"), error=True) + self.host.quit(C.EXIT_ERROR) + else: + if ret is not None: + self.disp(str(ret)) + self.host.quit() class Signal(base.CommandBase, BridgeCommon): @@ -103,12 +99,18 @@ "arg", nargs="*", help=_("argument of the signal") ) - def start(self): + async def start(self): args = self.evalArgs() json_args = json.dumps(args) # XXX: we use self.args.profile and not self.profile # because we want the raw profile_key (so plugin handle C.PROF_KEY_NONE) - self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile) + try: + await self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile) + except Exception as e: + self.disp(_(f"Can't send fake signal: {e}"), error=True) + self.host.quit(C.EXIT_ERROR) + else: + self.host.quit() class Bridge(base.CommandBase): @@ -130,7 +132,6 @@ use_output=C.OUTPUT_XML, help=_("monitor XML stream"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -141,7 +142,7 @@ help=_("stream direction filter"), ) - def printXML(self, direction, xml_data, profile): + async def printXML(self, direction, xml_data, profile): if self.args.direction == "in" and direction != "IN": return if self.args.direction == "out" and direction != "OUT": @@ -155,7 +156,7 @@ whiteping = False if verbosity: - profile_disp = " ({})".format(profile) if verbosity > 1 else "" + profile_disp = f" ({profile})" if verbosity > 1 else "" if direction == "IN": self.disp( A.color( @@ -172,7 +173,7 @@ self.disp("[WHITESPACE PING]") else: try: - self.output(xml_data) + await self.output(xml_data) except Exception: #  initial stream is not valid XML, # in this case we print directly to data @@ -182,7 +183,7 @@ self.disp(xml_data) self.disp("") - def start(self): + async def start(self): self.host.bridge.register_signal("xmlLog", self.printXML, "plugin") diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_encryption.py --- a/sat_frontends/jp/cmd_encryption.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_encryption.py Wed Sep 25 08:56:41 2019 +0200 @@ -20,7 +20,6 @@ from sat_frontends.jp import base from sat_frontends.jp.constants import Const as C from sat.core.i18n import _ -from functools import partial from sat.tools.common import data_format from sat_frontends.jp import xmlui_manager @@ -37,34 +36,27 @@ extra_outputs=extra_outputs, use_profile=False, help=_("show available encryption algorithms")) - self.need_loop = True def add_parser_options(self): pass - def encryptionPluginsGetCb(self, plugins): - self.output(plugins) - self.host.quit() - def default_output(self, plugins): if not plugins: self.disp(_("No encryption plugin registered!")) - self.host.quit(C.EXIT_NOT_FOUND) else: self.disp(_("Following encryption algorithms are available: {algos}").format( algos=', '.join([p['name'] for p in plugins]))) + + async def start(self): + try: + plugins = await self.host.bridge.encryptionPluginsGet() + except Exception as e: + self.disp(f"can't retrieve plugins: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(plugins) self.host.quit() - def start(self): - self.host.bridge.encryptionPluginsGet( - callback=self.encryptionPluginsGetCb, - errback=partial( - self.errback, - msg=_("can't retrieve plugins: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - class EncryptionGet(base.CommandBase): @@ -73,7 +65,6 @@ host, "get", use_output=C.OUTPUT_DICT, help=_("get encryption session data")) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -81,28 +72,23 @@ help=_("jid of the entity to check") ) - def messageEncryptionGetCb(self, serialised): + async def start(self): + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + try: + serialised = await self.host.bridge.messageEncryptionGet(jid, self.profile) + except Exception as e: + self.disp(f"can't get session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + session_data = data_format.deserialise(serialised) if session_data is None: self.disp( "No encryption session found, the messages are sent in plain text.") self.host.quit(C.EXIT_NOT_FOUND) - self.output(session_data) + await self.output(session_data) self.host.quit() - def start(self): - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - self.host.bridge.messageEncryptionGet( - jid, self.profile, - callback=self.messageEncryptionGetCb, - errback=partial( - self.errback, - msg=_("can't get session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - class EncryptionStart(base.CommandBase): @@ -110,7 +96,6 @@ super(EncryptionStart, self).__init__( host, "start", help=_("start encrypted session with an entity")) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -128,30 +113,30 @@ help=_("jid of the entity to stop encrypted session with") ) - def encryptionNamespaceGetCb(self, namespace): - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - self.host.bridge.messageEncryptionStart( - jid, namespace, not self.args.encrypt_noreplace, - self.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_("Can't start encryption session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) + async def start(self): + if self.args.name is not None: + try: + namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + elif self.args.namespace is not None: + namespace = self.args.namespace + else: + namespace = "" - def start(self): - if self.args.name is not None: - self.host.bridge.encryptionNamespaceGet(self.args.name, - callback=self.encryptionNamespaceGetCb, - errback=partial(self.errback, - msg=_("Can't get encryption namespace: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) - elif self.args.namespace is not None: - self.encryptionNamespaceGetCb(self.args.namespace) - else: - self.encryptionNamespaceGetCb("") + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + + try: + await self.host.bridge.messageEncryptionStart( + jid, namespace, not self.args.encrypt_noreplace, + self.profile) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() class EncryptionStop(base.CommandBase): @@ -160,7 +145,6 @@ super(EncryptionStop, self).__init__( host, "stop", help=_("stop encrypted session with an entity")) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -168,18 +152,16 @@ help=_("jid of the entity to stop encrypted session with") ) - def start(self): - jids = self.host.check_jids([self.args.jid]) + async def start(self): + jids = await self.host.check_jids([self.args.jid]) jid = jids[0] - self.host.bridge.messageEncryptionStop( - jid, self.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't end encrypted session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.messageEncryptionStop(jid, self.profile) + except Exception as e: + self.disp(f"can't end encrypted session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() class TrustUI(base.CommandBase): @@ -188,7 +170,6 @@ super(TrustUI, self).__init__( host, "ui", help=_("get UI to manage trust")) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -202,37 +183,33 @@ "-N", "--namespace", help=_("algorithm namespace (DEFAULT: current algorithm)")) - def encryptionTrustUIGetCb(self, xmlui_raw): - xmlui = xmlui_manager.create(self.host, xmlui_raw) - xmlui.show() - xmlui.submitForm() + async def start(self): + if self.args.name is not None: + try: + namespace = await self.host.bridge.encryptionNamespaceGet(self.args.name) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + elif self.args.namespace is not None: + namespace = self.args.namespace + else: + namespace = "" - def encryptionNamespaceGetCb(self, namespace): - jids = self.host.check_jids([self.args.jid]) + jids = await self.host.check_jids([self.args.jid]) jid = jids[0] - self.host.bridge.encryptionTrustUIGet( - jid, namespace, self.profile, - callback=self.encryptionTrustUIGetCb, - errback=partial( - self.errback, - msg=_("can't end encrypted session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - def start(self): - if self.args.name is not None: - self.host.bridge.encryptionNamespaceGet(self.args.name, - callback=self.encryptionNamespaceGetCb, - errback=partial(self.errback, - msg=_("Can't get encryption namespace: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) - elif self.args.namespace is not None: - self.encryptionNamespaceGetCb(self.args.namespace) - else: - self.encryptionNamespaceGetCb("") + try: + xmlui_raw = await self.host.bridge.encryptionTrustUIGet( + jid, namespace, self.profile) + except Exception as e: + self.disp(f"can't get encryption session trust UI: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + xmlui = xmlui_manager.create(self.host, xmlui_raw) + await xmlui.show() + if xmlui.type != C.XMLUI_DIALOG: + await xmlui.submitForm() + self.host.quit() class EncryptionTrust(base.CommandBase): subcommands = (TrustUI,) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_event.py --- a/sat_frontends/jp/cmd_event.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_event.py Wed Sep 25 08:56:41 2019 +0200 @@ -23,7 +23,6 @@ from sat.tools.common.ansi import ANSI as A from sat_frontends.jp.constants import Const as C from sat_frontends.jp import common -from functools import partial from dateutil import parser as du_parser import calendar import time @@ -47,30 +46,26 @@ use_verbose=True, help=_("get event data"), ) - self.need_loop = True def add_parser_options(self): pass - def eventInviteeGetCb(self, result): - event_date, event_data = result - event_data["date"] = event_date - self.output(event_data) - self.host.quit() - - def start(self): - self.host.bridge.eventGet( - self.args.service, - self.args.node, - self.args.item, - self.profile, - callback=self.eventInviteeGetCb, - errback=partial( - self.errback, - msg=_("can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + event_tuple = await self.host.bridge.eventGet( + self.args.service, + self.args.node, + self.args.item, + self.profile, + ) + except Exception as e: + self.disp(f"can't get event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + event_date, event_data = event_tuple + event_data["date"] = event_date + await self.output(event_data) + self.host.quit() class EventBase(object): @@ -126,29 +121,25 @@ help=_("create or replace event"), ) EventBase.__init__(self) - self.need_loop = True - def eventCreateCb(self, node): - self.disp(_("Event created successfuly on node {node}").format(node=node)) - self.host.quit() - - def start(self): + async def start(self): fields = self.parseFields() date = self.parseDate() - self.host.bridge.eventCreate( - date, - fields, - self.args.service, - self.args.node, - self.args.id, - self.profile, - callback=self.eventCreateCb, - errback=partial( - self.errback, - msg=_("can't create event: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + node = await self.host.bridge.eventCreate( + date, + fields, + self.args.service, + self.args.node, + self.args.id, + self.profile, + ) + except Exception as e: + self.disp(f"can't create event: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_(f"Event created successfuly on node {node}")) + self.host.quit() class Modify(EventBase, base.CommandBase): @@ -161,25 +152,24 @@ help=_("modify an existing event"), ) EventBase.__init__(self) - self.need_loop = True - def start(self): + async def start(self): fields = self.parseFields() date = 0 if not self.args.date else self.parseDate() - self.host.bridge.eventModify( - self.args.service, - self.args.node, - self.args.id, - date, - fields, - self.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't update event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + self.host.bridge.eventModify( + self.args.service, + self.args.node, + self.args.id, + date, + fields, + self.profile, + ) + except Exception as e: + self.disp(f"can't update event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class InviteeGet(base.CommandBase): @@ -190,31 +180,30 @@ "get", use_output=C.OUTPUT_DICT, use_pubsub=True, - pubsub_flags={C.NODE, C.ITEM, C.SINGLE_ITEM}, + pubsub_flags={C.NODE}, use_verbose=True, help=_("get event attendance"), ) - self.need_loop = True def add_parser_options(self): - pass - - def eventInviteeGetCb(self, event_data): - self.output(event_data) - self.host.quit() + self.parser.add_argument( + "-j", "--jid", default="", help=_("bare jid of the invitee") + ) - def start(self): - self.host.bridge.eventInviteeGet( - self.args.service, - self.args.node, - self.profile, - callback=self.eventInviteeGetCb, - errback=partial( - self.errback, - msg=_("can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + event_data = await self.host.bridge.eventInviteeGet( + self.args.service, + self.args.node, + self.args.jid, + self.profile, + ) + except Exception as e: + self.disp(f"can't get event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(event_data) + self.host.quit() class InviteeSet(base.CommandBase): @@ -227,7 +216,6 @@ pubsub_flags={C.NODE}, help=_("set event attendance"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -240,20 +228,20 @@ help=_("configuration field to set"), ) - def start(self): + async def start(self): fields = dict(self.args.fields) if self.args.fields else {} - self.host.bridge.eventInviteeSet( - self.args.service, - self.args.node, - fields, - self.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't set event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + self.host.bridge.eventInviteeSet( + self.args.service, + self.args.node, + fields, + self.profile, + ) + except Exception as e: + self.disp(f"can't set event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class InviteesList(base.CommandBase): @@ -270,7 +258,6 @@ use_verbose=True, help=_("get event attendance"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -399,72 +386,68 @@ ) ) - def eventInviteesListCb(self, event_data, prefilled_data): - """fill nicknames and keep only requested people - - @param event_data(dict): R.S.V.P. answers - @param prefilled_data(dict): prefilled data with all people - only filled if --missing is used - """ - if self.args.no_rsvp: - for jid_ in event_data: - # if there is a jid in event_data - # it must be there in prefilled_data too - # so no need to check for KeyError - del prefilled_data[jid_] - else: - # we replace empty dicts for existing people with R.S.V.P. data - prefilled_data.update(event_data) - - # we get nicknames for everybody, make it easier for organisers - for jid_, data in prefilled_data.items(): - id_data = self.host.bridge.identityGet(jid_, self.profile) - data["nick"] = id_data.get("nick", "") - - self.output(prefilled_data) - self.host.quit() - - def getList(self, prefilled_data={}): - self.host.bridge.eventInviteesList( - self.args.service, - self.args.node, - self.profile, - callback=partial(self.eventInviteesListCb, prefilled_data=prefilled_data), - errback=partial( - self.errback, - msg=_("can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - - def psNodeAffiliationsGetCb(self, affiliations): - # we fill all affiliations with empty data - # answered one will be filled in eventInviteesListCb - # we only consider people with "publisher" affiliation as invited, creators are not, and members can just observe - prefilled = { - jid_: {} - for jid_, affiliation in affiliations.items() - if affiliation in ("publisher",) - } - self.getList(prefilled) - - def start(self): + async def start(self): if self.args.no_rsvp and not self.args.missing: self.parser.error(_("you need to use --missing if you use --no-rsvp")) - if self.args.missing: - self.host.bridge.psNodeAffiliationsGet( + if not self.args.missing: + prefilled = {} + else: + # we get prefilled data with all people + try: + affiliations = await self.host.bridge.psNodeAffiliationsGet( + 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: + # we fill all affiliations with empty data, answered one will be filled + # below. We only consider people with "publisher" affiliation as invited, + # creators are not, and members can just observe + prefilled = { + jid_: {} + for jid_, affiliation in affiliations.items() + if affiliation in ("publisher",) + } + + try: + event_data = await self.host.bridge.eventInviteesList( self.args.service, self.args.node, self.profile, - callback=self.psNodeAffiliationsGetCb, - errback=partial( - self.errback, - msg=_("can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), ) + except Exception as e: + self.disp(f"can't get event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + # we fill nicknames and keep only requested people + + if self.args.no_rsvp: + for jid_ in event_data: + # if there is a jid in event_data it must be there in prefilled too + # otherwie somebody is not on the invitees list + try: + del prefilled[jid_] + except KeyError: + self.disp(A.color( + C.A_WARNING, + f"We got a RSVP from somebody who was not in invitees " + f"list: {jid_}" + ), + error=True) else: - self.getList() + # we replace empty dicts for existing people with R.S.V.P. data + prefilled.update(event_data) + + # we get nicknames for everybody, make it easier for organisers + for jid_, data in prefilled.items(): + id_data = await self.host.bridge.identityGet(jid_, self.profile) + data["nick"] = id_data.get("nick", "") + + await self.output(prefilled) + self.host.quit() class InviteeInvite(base.CommandBase): @@ -477,7 +460,6 @@ pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_("invite someone to the event through email"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -524,30 +506,30 @@ help="body of the invitation email (default: generic body)", ) - def start(self): + async def start(self): email = self.args.email[0] if self.args.email else None emails_extra = self.args.email[1:] - self.host.bridge.eventInviteByEmail( - self.args.service, - self.args.node, - self.args.item, - email, - emails_extra, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url_template, - self.args.subject, - self.args.body, - self.args.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't create invitation: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.eventInviteByEmail( + self.args.service, + self.args.node, + self.args.item, + email, + emails_extra, + self.args.name, + self.args.host_name, + self.args.lang, + self.args.url_template, + self.args.subject, + self.args.body, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't create invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class Invitee(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_file.py --- a/sat_frontends/jp/cmd_file.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_file.py Wed Sep 25 08:56:41 2019 +0200 @@ -31,7 +31,6 @@ from sat.tools.common.ansi import ANSI as A import tempfile import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -from functools import partial import json __commands__ = ["File"] @@ -46,7 +45,6 @@ use_verbose=True, help=_("send a file to a contact"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -75,23 +73,19 @@ help=("name to use (DEFAULT: use source file name)"), ) - def start(self): - """Send files to jabber contact""" - self.send_files() - - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): self.disp(_("File copy started"), 2) - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): self.disp(_("File sent successfully"), 2) - def onProgressError(self, error_msg): + async def onProgressError(self, error_msg): if error_msg == C.PROGRESS_ERROR_DECLINED: self.disp(_("The file has been refused by your contact")) else: self.disp(_("Error while sending file: {}").format(error_msg), error=True) - def gotId(self, data, file_): + async def gotId(self, data, file_): """Called when a progress id has been received @param pid(unicode): progress id @@ -100,7 +94,7 @@ # FIXME: this show progress only for last progress_id self.disp(_("File request sent to {jid}".format(jid=self.full_dest_jid)), 1) try: - self.progress_id = data["progress"] + await self.set_progress_id(data["progress"]) except KeyError: # TODO: if 'xmlui' key is present, manage xmlui message display self.disp( @@ -108,27 +102,18 @@ ) self.host.quit(2) - def error(self, failure): - self.disp( - _("Error while trying to send a file: {reason}").format(reason=failure), - error=True, - ) - self.host.quit(1) - - def send_files(self): + async def start(self): for file_ in self.args.files: if not os.path.exists(file_): - self.disp(_("file [{}] doesn't exist !").format(file_), error=True) - self.host.quit(1) + self.disp(_(f"file {file_!r} doesn't exist!"), error=True) + self.host.quit(C.EXIT_BAD_ARG) if not self.args.bz2 and os.path.isdir(file_): self.disp( - _( - "[{}] is a dir ! Please send files inside or use compression" - ).format(file_) + _(f"{file_!r} is a dir! Please send files inside or use compression") ) - self.host.quit(1) + self.host.quit(C.EXIT_BAD_ARG) - self.full_dest_jid = self.host.get_full_jid(self.args.jid) + self.full_dest_jid = await self.host.get_full_jid(self.args.jid) extra = {} if self.args.path: extra["path"] = self.args.path @@ -152,29 +137,37 @@ bz2.close() self.disp(_("Done !"), 1) - self.host.bridge.fileSend( - self.full_dest_jid, - buf.name, - self.args.name or archive_name, - "", - extra, - self.profile, - callback=lambda pid, file_=buf.name: self.gotId(pid, file_), - errback=self.error, - ) + try: + send_data = await self.host.bridge.fileSend( + self.full_dest_jid, + buf.name, + self.args.name or archive_name, + "", + extra, + self.profile, + ) + except Exception as e: + self.disp(f"can't send file: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.gotId(send_data, file_) else: for file_ in self.args.files: path = os.path.abspath(file_) - self.host.bridge.fileSend( - self.full_dest_jid, - path, - self.args.name, - "", - extra, - self.profile, - callback=lambda pid, file_=file_: self.gotId(pid, file_), - errback=self.error, - ) + try: + send_data = await self.host.bridge.fileSend( + self.full_dest_jid, + path, + self.args.name, + "", + extra, + self.profile, + ) + except Exception as e: + self.disp(f"can't send file {file_!r}: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.gotId(send_data, file_) class Request(base.CommandBase): @@ -186,7 +179,6 @@ use_verbose=True, help=_("request a file from a contact"), ) - self.need_loop = True @property def filename(self): @@ -200,7 +192,8 @@ "-D", "--dest", help=_( - "destination path where the file will be saved (default: [current_dir]/[name|hash])" + "destination path where the file will be saved (default: " + "[current_dir]/[name|hash])" ), ) self.parser.add_argument( @@ -238,36 +231,21 @@ help=_("overwrite existing file without confirmation"), ) - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): self.disp(_("File copy started"), 2) - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): self.disp(_("File received successfully"), 2) - def onProgressError(self, error_msg): + async def onProgressError(self, error_msg): if error_msg == C.PROGRESS_ERROR_DECLINED: self.disp(_("The file request has been refused")) else: self.disp(_("Error while requesting file: {}").format(error_msg), error=True) - def gotId(self, progress_id): - """Called when a progress id has been received - - @param progress_id(unicode): progress id - """ - self.progress_id = progress_id - - def error(self, failure): - self.disp( - _("Error while trying to send a file: {reason}").format(reason=failure), - error=True, - ) - self.host.quit(1) - - def start(self): + async def start(self): if not self.args.name and not self.args.hash: self.parser.error(_("at least one of --name or --hash must be provided")) - #  extra = dict(self.args.extra) if self.args.dest: path = os.path.abspath(os.path.expanduser(self.args.dest)) if os.path.isdir(path): @@ -276,35 +254,30 @@ path = os.path.abspath(self.filename) if os.path.exists(path) and not self.args.force: - message = _("File {path} already exists! Do you want to overwrite?").format( - path=path - ) - confirm = input("{} (y/N) ".format(message).encode("utf-8")) - if confirm not in ("y", "Y"): - self.disp(_("file request cancelled")) - self.host.quit(2) + message = _(f"File {path} already exists! Do you want to overwrite?") + await self.host.confirmOrQuit(message, _("file request cancelled")) - self.full_dest_jid = self.host.get_full_jid(self.args.jid) + self.full_dest_jid = await self.host.get_full_jid(self.args.jid) extra = {} if self.args.path: extra["path"] = self.args.path if self.args.namespace: extra["namespace"] = self.args.namespace - self.host.bridge.fileJingleRequest( - self.full_dest_jid, - path, - self.args.name, - self.args.hash, - self.args.hash_algo if self.args.hash else "", - extra, - self.profile, - callback=self.gotId, - errback=partial( - self.errback, - msg=_("can't request file: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + progress_id = await self.host.bridge.fileJingleRequest( + self.full_dest_jid, + path, + self.args.name, + self.args.hash, + self.args.hash_algo if self.args.hash else "", + extra, + self.profile, + ) + except Exception as e: + self.disp(msg=_(f"can't request file: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.set_progress_id(progress_id) class Receive(base.CommandAnswering): @@ -322,89 +295,6 @@ C.META_TYPE_OVERWRITE: self.onOverwriteAction, } - def onProgressStarted(self, metadata): - self.disp(_("File copy started"), 2) - - def onProgressFinished(self, metadata): - self.disp(_("File received successfully"), 2) - if metadata.get("hash_verified", False): - try: - self.disp( - _("hash checked: {algo}:{checksum}").format( - algo=metadata["hash_algo"], checksum=metadata["hash"] - ), - 1, - ) - except KeyError: - self.disp(_("hash is checked but hash value is missing", 1), error=True) - else: - self.disp(_("hash can't be verified"), 1) - - def onProgressError(self, error_msg): - self.disp(_("Error while receiving file: {}").format(error_msg), error=True) - - def getXmluiId(self, action_data): - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the futur - # TODO: XMLUI module - try: - xml_ui = action_data["xmlui"] - except KeyError: - self.disp(_("Action has no XMLUI"), 1) - else: - ui = ET.fromstring(xml_ui.encode("utf-8")) - xmlui_id = ui.get("submit") - if not xmlui_id: - self.disp(_("Invalid XMLUI received"), error=True) - return xmlui_id - - def onFileAction(self, action_data, action_id, security_limit, profile): - xmlui_id = self.getXmluiId(action_data) - if xmlui_id is None: - return self.host.quitFromSignal(1) - try: - from_jid = jid.JID(action_data["meta_from_jid"]) - except KeyError: - self.disp(_("Ignoring action without from_jid data"), 1) - return - try: - progress_id = action_data["meta_progress_id"] - except KeyError: - self.disp(_("ignoring action without progress id"), 1) - return - - if not self.bare_jids or from_jid.bare in self.bare_jids: - if self._overwrite_refused: - self.disp(_("File refused because overwrite is needed"), error=True) - self.host.bridge.launchAction( - xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile - ) - return self.host.quitFromSignal(2) - self.progress_id = progress_id - xmlui_data = {"path": self.path} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - - def onOverwriteAction(self, action_data, action_id, security_limit, profile): - xmlui_id = self.getXmluiId(action_data) - if xmlui_id is None: - return self.host.quitFromSignal(1) - try: - progress_id = action_data["meta_progress_id"] - except KeyError: - self.disp(_("ignoring action without progress id"), 1) - return - self.disp(_("Overwriting needed"), 1) - - if progress_id == self.progress_id: - if self.args.force: - self.disp(_("Overwrite accepted"), 2) - else: - self.disp(_("Refused to overwrite"), 2) - self._overwrite_refused = True - - xmlui_data = {"answer": C.boolConst(self.args.force)} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - def add_parser_options(self): self.parser.add_argument( "jids", @@ -432,15 +322,95 @@ help=_("destination path (default: working directory)"), ) - def start(self): + async def onProgressStarted(self, metadata): + self.disp(_("File copy started"), 2) + + async def onProgressFinished(self, metadata): + self.disp(_("File received successfully"), 2) + if metadata.get("hash_verified", False): + try: + self.disp(_( + f"hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1) + except KeyError: + self.disp(_("hash is checked but hash value is missing", 1), error=True) + else: + self.disp(_("hash can't be verified"), 1) + + async def onProgressError(self, e): + self.disp(_(f"Error while receiving file: {e}"), error=True) + + def getXmluiId(self, action_data): + # FIXME: we temporarily use ElementTree, but a real XMLUI managing module + # should be available in the futur + # TODO: XMLUI module + try: + xml_ui = action_data["xmlui"] + except KeyError: + self.disp(_("Action has no XMLUI"), 1) + else: + ui = ET.fromstring(xml_ui.encode("utf-8")) + xmlui_id = ui.get("submit") + if not xmlui_id: + self.disp(_("Invalid XMLUI received"), error=True) + return xmlui_id + + async def onFileAction(self, action_data, action_id, security_limit, profile): + xmlui_id = self.getXmluiId(action_data) + if xmlui_id is None: + return self.host.quitFromSignal(1) + try: + from_jid = jid.JID(action_data["meta_from_jid"]) + except KeyError: + self.disp(_("Ignoring action without from_jid data"), 1) + return + try: + progress_id = action_data["meta_progress_id"] + except KeyError: + self.disp(_("ignoring action without progress id"), 1) + return + + if not self.bare_jids or from_jid.bare in self.bare_jids: + if self._overwrite_refused: + self.disp(_("File refused because overwrite is needed"), error=True) + await self.host.bridge.launchAction( + xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile + ) + return self.host.quitFromSignal(2) + await self.set_progress_id(progress_id) + xmlui_data = {"path": self.path} + await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) + + async def onOverwriteAction(self, action_data, action_id, security_limit, profile): + xmlui_id = self.getXmluiId(action_data) + if xmlui_id is None: + return self.host.quitFromSignal(1) + try: + progress_id = action_data["meta_progress_id"] + except KeyError: + self.disp(_("ignoring action without progress id"), 1) + return + self.disp(_("Overwriting needed"), 1) + + if progress_id == self.progress_id: + if self.args.force: + self.disp(_("Overwrite accepted"), 2) + else: + self.disp(_("Refused to overwrite"), 2) + self._overwrite_refused = True + + xmlui_data = {"answer": C.boolConst(self.args.force)} + await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) + + async def start(self): self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] self.path = os.path.abspath(self.args.path) if not os.path.isdir(self.path): self.disp(_("Given path is not a directory !", error=True)) - self.host.quit(2) + self.host.quit(C.EXIT_BAD_ARG) if self.args.multiple: self.host.quit_on_progress_end = False self.disp(_("waiting for incoming file request"), 2) + await self.start_answering() class Upload(base.CommandBase): @@ -448,7 +418,6 @@ super(Upload, self).__init__( host, "upload", use_progress=True, use_verbose=True, help=_("upload a file") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument("file", type=str, help=_("file to upload")) @@ -463,10 +432,10 @@ help=_("ignore invalide TLS certificate"), ) - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): self.disp(_("File upload started"), 2) - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): self.disp(_("File uploaded successfully"), 2) try: url = metadata["url"] @@ -477,55 +446,54 @@ # XXX: url is display alone on a line to make parsing easier self.disp(url) - def onProgressError(self, error_msg): + async def onProgressError(self, error_msg): self.disp(_("Error while uploading file: {}").format(error_msg), error=True) - def gotId(self, data, file_): + async def gotId(self, data, file_): """Called when a progress id has been received @param pid(unicode): progress id @param file_(str): file path """ try: - self.progress_id = data["progress"] + await self.set_progress_id(data["progress"]) except KeyError: # TODO: if 'xmlui' key is present, manage xmlui message display self.disp(_("Can't upload file"), error=True) - self.host.quit(2) + self.host.quit(C.EXIT_ERROR) - def error(self, failure): - self.disp( - _("Error while trying to upload a file: {reason}").format(reason=failure), - error=True, - ) - self.host.quit(1) - - def start(self): + async def start(self): file_ = self.args.file if not os.path.exists(file_): - self.disp(_("file [{}] doesn't exist !").format(file_), error=True) - self.host.quit(1) + self.disp(_(f"file {file_!r} doesn't exist !"), error=True) + self.host.quit(C.EXIT_BAD_ARG) if os.path.isdir(file_): - self.disp(_("[{}] is a dir! Can't upload a dir").format(file_)) - self.host.quit(1) + self.disp(_(f"{file_!r} is a dir! Can't upload a dir")) + self.host.quit(C.EXIT_BAD_ARG) - self.full_dest_jid = ( - self.host.get_full_jid(self.args.jid) if self.args.jid is not None else "" - ) + if self.args.jid is None: + self.full_dest_jid = None + else: + self.full_dest_jid = await self.host.get_full_jid(self.args.jid) + options = {} if self.args.ignore_tls_errors: options["ignore_tls_errors"] = C.BOOL_TRUE path = os.path.abspath(file_) - self.host.bridge.fileUpload( - path, - "", - self.full_dest_jid, - options, - self.profile, - callback=lambda pid, file_=file_: self.gotId(pid, file_), - errback=self.error, - ) + try: + upload_data = await self.host.bridge.fileUpload( + path, + "", + self.full_dest_jid, + options, + self.profile, + ) + except Exception as e: + self.disp(f"can't while trying to upload a file: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.gotId(upload_data, file_) class ShareList(base.CommandBase): @@ -539,7 +507,6 @@ help=_("retrieve files shared by an entity"), use_verbose=True, ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -555,12 +522,6 @@ help=_("jid of sharing entity (nothing to check our own jid)"), ) - def file_gen(self, files_data): - for file_data in files_data: - yield file_data["name"] - yield file_data.get("size", "") - yield file_data.get("hash", "") - def _name_filter(self, name, row): if row.type == C.FILE_TYPE_DIRECTORY: return A.color(C.A_DIRECTORY, name) @@ -588,38 +549,37 @@ files_data.sort(key=lambda d: d["name"].lower()) show_header = False if self.verbosity == 0: - headers = ("name", "type") + keys = headers = ("name", "type") elif self.verbosity == 1: - headers = ("name", "type", "size") + keys = headers = ("name", "type", "size") elif self.verbosity > 1: show_header = True + keys = ("name", "type", "size", "file_hash") headers = ("name", "type", "size", "hash") table = common.Table.fromDict( self.host, files_data, - headers, + keys=keys, + headers=headers, filters={"name": self._name_filter, "size": self._size_filter}, - defaults={"size": "", "hash": ""}, + defaults={"size": "", "file_hash": ""}, ) table.display_blank(show_header=show_header, hide_cols=["type"]) - def _FISListCb(self, files_data): - self.output(files_data) - self.host.quit() + async def start(self): + try: + files_data = await self.host.bridge.FISList( + self.args.jid, + self.args.path, + {}, + self.profile, + ) + except Exception as e: + self.disp(f"can't retrieve shared files: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def start(self): - self.host.bridge.FISList( - self.args.jid, - self.args.path, - {}, - self.profile, - callback=self._FISListCb, - errback=partial( - self.errback, - msg=_("can't retrieve shared files: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + await self.output(files_data) + self.host.quit() class SharePath(base.CommandBase): @@ -627,7 +587,6 @@ super(SharePath, self).__init__( host, "path", help=_("share a file or directory"), use_verbose=True ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -640,6 +599,7 @@ perm_group.add_argument( "-j", "--jid", + metavar="JID", action="append", dest="jids", default=[], @@ -649,7 +609,8 @@ "--public", action="store_true", help=_( - "share publicly the file(s) (/!\\ *everybody* will be able to access them)" + r"share publicly the file(s) (/!\ *everybody* will be able to access " + r"them)" ), ) self.parser.add_argument( @@ -657,13 +618,7 @@ help=_("path to a file or directory to share"), ) - def _FISSharePathCb(self, name): - self.disp( - _('{path} shared under the name "{name}"').format(path=self.path, name=name) - ) - self.host.quit() - - def start(self): + async def start(self): self.path = os.path.abspath(self.args.path) if self.args.public: access = {"read": {"type": "public"}} @@ -673,18 +628,19 @@ access = {"read": {"type": "whitelist", "jids": jids}} else: access = {} - self.host.bridge.FISSharePath( - self.args.name, - self.path, - json.dumps(access, ensure_ascii=False), - self.profile, - callback=self._FISSharePathCb, - errback=partial( - self.errback, - msg=_("can't share path: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + name = await self.host.bridge.FISSharePath( + self.args.name, + self.path, + json.dumps(access, ensure_ascii=False), + self.profile, + ) + except Exception as e: + self.disp(f"can't share path: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_(f'{self.path} shared under the name "{name}"')) + self.host.quit() class ShareInvite(base.CommandBase): @@ -692,7 +648,6 @@ super(ShareInvite, self).__init__( host, "invite", help=_("send invitation for a shared repository") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -733,13 +688,7 @@ help=_("jid of the person to invite"), ) - def _FISInviteCb(self): - self.disp( - _('invitation sent to {entity}').format(entity=self.args.jid) - ) - self.host.quit() - - def start(self): + async def start(self): self.path = os.path.normpath(self.args.path) if self.args.path else "" extra = {} if self.args.thumbnail is not None: @@ -747,22 +696,25 @@ self.parser.error(_("only http(s) links are allowed with --thumbnail")) else: extra['thumb_url'] = self.args.thumbnail - self.host.bridge.FISInvite( - self.args.jid, - self.args.service, - self.args.type, - self.args.namespace, - self.path, - self.args.name, - data_format.serialise(extra), - self.profile, - callback=self._FISInviteCb, - errback=partial( - self.errback, - msg=_("can't send invitation: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.FISInvite( + self.args.jid, + self.args.service, + self.args.type, + self.args.namespace, + self.path, + self.args.name, + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't send invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp( + _(f'invitation sent to {self.args.jid}') + ) + self.host.quit() class Share(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_forums.py --- a/sat_frontends/jp/cmd_forums.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_forums.py Wed Sep 25 08:56:41 2019 +0200 @@ -23,7 +23,6 @@ from sat_frontends.jp.constants import Const as C from sat_frontends.jp import common from sat.tools.common.ansi import ANSI as A -from functools import partial import codecs import json @@ -46,7 +45,6 @@ help=_("edit forums"), ) common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -60,26 +58,37 @@ """return suffix used for content file""" return "json" - def forumsSetCb(self): - self.disp(_("forums have been edited"), 1) - self.host.quit() + async def publish(self, forums_raw): + try: + await self.host.bridge.forumsSet( + forums_raw, + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + self.disp(f"can't set forums: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("forums have been edited"), 1) + self.host.quit() - def publish(self, forums_raw): - self.host.bridge.forumsSet( - forums_raw, - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsSetCb, - errback=partial( - self.errback, - msg=_("can't set forums: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + forums_json = await self.host.bridge.forumsGet( + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + if e.classname == "NotFound": + forums_json = "" + else: + self.disp(f"can't get node configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def forumsGetCb(self, forums_json): content_file_obj, content_file_path = self.getTmpFile() forums_json = forums_json.strip() if forums_json: @@ -89,28 +98,7 @@ f = codecs.getwriter("utf-8")(content_file_obj) json.dump(forums, f, ensure_ascii=False, indent=4) content_file_obj.seek(0) - self.runEditor("forums_editor_args", content_file_path, content_file_obj) - - def forumsGetEb(self, failure_): - # FIXME: error handling with bridge is broken, need to be properly fixed - if failure_.condition == "item-not-found": - self.forumsGetCb("") - else: - self.errback( - failure_, - msg=_("can't get forums structure: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ) - - def start(self): - self.host.bridge.forumsGet( - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsGetCb, - errback=self.forumsGetEb, - ) + await self.runEditor("forums_editor_args", content_file_path, content_file_obj) class Get(base.CommandBase): @@ -126,7 +114,6 @@ use_verbose=True, help=_("get forums structure"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -165,27 +152,24 @@ A.color(level * 4 * " ", head_color, key, A.RESET, ": ", value) ) - def forumsGetCb(self, forums_raw): - if not forums_raw: - self.disp(_("no schema found"), 1) - self.host.quit(1) - forums = json.loads(forums_raw) - self.output(forums) - self.host.quit() - - def start(self): - self.host.bridge.forumsGet( - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsGetCb, - errback=partial( - self.errback, - msg=_("can't get forums: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + forums_raw = await self.host.bridge.forumsGet( + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + self.disp(f"can't get forums: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if not forums_raw: + self.disp(_("no schema found"), 1) + self.host.quit(1) + forums = json.loads(forums_raw) + await self.output(forums) + self.host.quit() class Forums(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_identity.py --- a/sat_frontends/jp/cmd_identity.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_identity.py Wed Sep 25 08:56:41 2019 +0200 @@ -21,12 +21,9 @@ from . import base from sat.core.i18n import _ from sat_frontends.jp.constants import Const as C -from functools import partial __commands__ = ["Identity"] -# TODO: move date parsing to base, it may be useful for other commands - class Get(base.CommandBase): def __init__(self, host): @@ -38,29 +35,25 @@ use_verbose=True, help=_("get identity data"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( "jid", help=_("entity to check") ) - def identityGetCb(self, data): - self.output(data) - self.host.quit() - - def start(self): - jid_ = self.host.check_jids([self.args.jid])[0] - self.host.bridge.identityGet( - jid_, - self.profile, - callback=self.identityGetCb, - errback=partial( - self.errback, - msg=_("can't get identity data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + jid_ = (await self.host.check_jids([self.args.jid]))[0] + try: + data = await self.host.bridge.identityGet( + jid_, + self.profile, + ) + except Exception as e: + self.disp(f"can't get identity data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(data) + self.host.quit() class Set(base.CommandBase): @@ -78,20 +71,19 @@ required=True, help=_("identity field(s) to set"), ) - self.need_loop = True - def start(self): + async def start(self): fields = dict(self.args.fields) - self.host.bridge.identitySet( - fields, - self.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't set identity data data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + self.host.bridge.identitySet( + fields, + self.profile, + ) + except Exception as e: + self.disp(f"can't set identity data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class Identity(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_info.py --- a/sat_frontends/jp/cmd_info.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_info.py Wed Sep 25 08:56:41 2019 +0200 @@ -31,53 +31,19 @@ def __init__(self, host): extra_outputs = {'default': self.default_output} - super(Disco, self).__init__(host, 'disco', use_output='complex', extra_outputs=extra_outputs, help=_('service discovery')) - self.need_loop=True + super(Disco, self).__init__( + host, 'disco', use_output='complex', extra_outputs=extra_outputs, + help=_('service discovery')) def add_parser_options(self): self.parser.add_argument("jid", help=_("entity to discover")) - self.parser.add_argument("-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both', help=_("type of data to discover")) + self.parser.add_argument( + "-t", "--type", type=str, choices=('infos', 'items', 'both'), default='both', + help=_("type of data to discover")) self.parser.add_argument("-n", "--node", default='', help=_("node to use")) - self.parser.add_argument("-C", "--no-cache", dest='use_cache', action="store_false", help=_("ignore cache")) - - def start(self): - self.get_infos = self.args.type in ('infos', 'both') - self.get_items = self.args.type in ('items', 'both') - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - if not self.get_infos: - self.gotInfos(None, jid) - else: - self.host.bridge.discoInfos(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda infos: self.gotInfos(infos, jid), errback=self.error) - - def error(self, failure): - print((_("Error while doing discovery [%s]") % failure)) - self.host.quit(1) - - def gotInfos(self, infos, jid): - if not self.get_items: - self.gotItems(infos, None) - else: - self.host.bridge.discoItems(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda items: self.gotItems(infos, items), errback=self.error) - - def gotItems(self, infos, items): - data = {} - - if self.get_infos: - features, identities, extensions = infos - features.sort() - identities.sort(key=lambda identity: identity[2]) - data.update({ - 'features': features, - 'identities': identities, - 'extensions': extensions}) - - if self.get_items: - items.sort(key=lambda item: item[2]) - data['items'] = items - - self.output(data) - self.host.quit() + self.parser.add_argument( + "-C", "--no-cache", dest='use_cache', action="store_false", + help=_("ignore cache")) def default_output(self, data): features = data.get('features', []) @@ -115,15 +81,18 @@ for value in values: field_lines.append(A.color('\t - ', A.BOLD, value)) fields.append('\n'.join(field_lines)) - extensions_tpl.append('{type_}\n{fields}'.format(type_=type_, - fields='\n\n'.join(fields))) + extensions_tpl.append('{type_}\n{fields}'.format( + type_=type_, + fields='\n\n'.join(fields))) - items_table = common.Table(self.host, - items, - headers=(_('entity'), - _('node'), - _('name')), - use_buffer=True) + items_table = common.Table( + self.host, + items, + headers=( + _('entity'), + _('node'), + _('name')), + use_buffer=True) template = [] if features: @@ -135,76 +104,131 @@ if items: template.append(A.color(C.A_HEADER, _("Items")) + "\n\n{items}") - print("\n\n".join(template).format(features = '\n'.join(features), - identities = identities_table.display().string, - extensions = '\n'.join(extensions_tpl), - items = items_table.display().string, - )) + print("\n\n".join(template).format( + features = '\n'.join(features), + identities = identities_table.display().string, + extensions = '\n'.join(extensions_tpl), + items = items_table.display().string, + )) + + async def start(self): + infos_requested = self.args.type in ('infos', 'both') + items_requested = self.args.type in ('items', 'both') + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + + # infos + if not infos_requested: + infos = None + else: + try: + infos = await self.host.bridge.discoInfos( + jid, + node=self.args.node, + use_cache=self.args.use_cache, + profile_key=self.host.profile + ) + except Exception as e: + self.disp(_(f"error while doing discovery: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + # items + if not items_requested: + items = None + else: + try: + items = await self.host.bridge.discoItems( + jid, + node=self.args.node, + use_cache=self.args.use_cache, + profile_key=self.host.profile + ) + except Exception as e: + self.disp(_(f"error while doing discovery: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + # output + data = {} + + if infos_requested: + features, identities, extensions = infos + features.sort() + identities.sort(key=lambda identity: identity[2]) + data.update({ + 'features': features, + 'identities': identities, + 'extensions': extensions}) + + if items_requested: + items.sort(key=lambda item: item[2]) + data['items'] = items + + await self.output(data) + self.host.quit() class Version(base.CommandBase): def __init__(self, host): super(Version, self).__init__(host, 'version', help=_('software version')) - self.need_loop=True def add_parser_options(self): self.parser.add_argument("jid", type=str, help=_("Entity to request")) - def start(self): - jids = self.host.check_jids([self.args.jid]) + async def start(self): + jids = await self.host.check_jids([self.args.jid]) jid = jids[0] - self.host.bridge.getSoftwareVersion(jid, self.host.profile, callback=self.gotVersion, errback=self.error) - - def error(self, failure): - print((_("Error while trying to get version [%s]") % failure)) - self.host.quit(1) + try: + data = await self.host.bridge.getSoftwareVersion(jid, self.host.profile) + except Exception as e: + self.disp(_(f"error while trying to get version: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + infos = [] + name, version, os = data + if name: + infos.append(_(f"Software name: {name}")) + if version: + infos.append(_(f"Software version: {version}")) + if os: + infos.append(_(f"Operating System: {os}")) - def gotVersion(self, data): - infos = [] - name, version, os = data - if name: - infos.append(_("Software name: %s") % name) - if version: - infos.append(_("Software version: %s") % version) - if os: - infos.append(_("Operating System: %s") % os) - - print("\n".join(infos)) - self.host.quit() + print("\n".join(infos)) + self.host.quit() class Session(base.CommandBase): def __init__(self, host): extra_outputs = {'default': self.default_output} - super(Session, self).__init__(host, 'session', use_output='dict', extra_outputs=extra_outputs, help=_('running session')) - self.need_loop=True + super(Session, self).__init__( + host, 'session', use_output='dict', extra_outputs=extra_outputs, + help=_('running session')) - def default_output(self, data): + def add_parser_options(self): + pass + + async def default_output(self, data): started = data['started'] data['started'] = '{short} (UTC, {relative})'.format( short=date_utils.date_fmt(started), relative=date_utils.date_fmt(started, 'relative')) - self.host.output(C.OUTPUT_DICT, 'simple', {}, data) - - def add_parser_options(self): - pass - - def start(self): - self.host.bridge.sessionInfosGet(self.host.profile, callback=self._sessionInfoGetCb, errback=self._sessionInfoGetEb) + await self.host.output(C.OUTPUT_DICT, 'simple', {}, data) - def _sessionInfoGetCb(self, data): - self.output(data) - self.host.quit() - - def _sessionInfoGetEb(self, error_data): - self.disp(_('Error getting session infos: {}').format(error_data), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) + async def start(self): + try: + data = await self.host.bridge.sessionInfosGet(self.host.profile) + except Exception as e: + self.disp(_(f'Error getting session infos: {e}'), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(data) + self.host.quit() class Info(base.CommandBase): subcommands = (Disco, Version, Session) def __init__(self, host): - super(Info, self).__init__(host, 'info', use_profile=False, help=_('Get various pieces of information on entities')) + super(Info, self).__init__( + host, 'info', use_profile=False, + help=_('Get various pieces of information on entities')) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_input.py --- a/sat_frontends/jp/cmd_input.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_input.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,14 +18,16 @@ # along with this program. If not, see . +import subprocess +import argparse +import sys +import shlex +import asyncio from . import base from sat.core.i18n import _ from sat.core import exceptions from sat_frontends.jp.constants import Const as C from sat.tools.common.ansi import ANSI as A -import subprocess -import argparse -import sys __commands__ = ["Input"] OPT_STDIN = "stdin" @@ -105,10 +107,10 @@ help=_("don't actually run commands but echo what would be launched"), ) self.parser.add_argument( - "--log", type=argparse.FileType("wb"), help=_("log stdout to FILE") + "--log", type=argparse.FileType("w"), help=_("log stdout to FILE") ) self.parser.add_argument( - "--log-err", type=argparse.FileType("wb"), help=_("log stderr to FILE") + "--log-err", type=argparse.FileType("w"), help=_("log stderr to FILE") ) self.parser.add_argument("command", nargs=argparse.REMAINDER) @@ -166,15 +168,15 @@ for v in value: if arg_type == OPT_STDIN: - self._stdin.append(v.encode("utf-8")) + self._stdin.append(v) elif arg_type == OPT_SHORT: self._opts.append("-{}".format(arg_name)) - self._opts.append(v.encode("utf-8")) + self._opts.append(v) elif arg_type == OPT_LONG: self._opts.append("--{}".format(arg_name)) - self._opts.append(v.encode("utf-8")) + self._opts.append(v) elif arg_type == OPT_POS: - self._pos.append(v.encode("utf-8")) + self._pos.append(v) elif arg_type == OPT_IGNORE: pass else: @@ -184,7 +186,7 @@ ).format(type_=arg_type, name=arg_name) ) - def runCommand(self): + async def runCommand(self): """run requested command with parsed arguments""" if self.args_idx != len(self.args.arguments): self.disp( @@ -200,7 +202,10 @@ if self.args.debug: self.disp( A.color( - C.A_SUBHEADER, _("values: "), A.RESET, ", ".join(self._values_ori) + C.A_SUBHEADER, + _("values: "), + A.RESET, + ", ".join([shlex.quote(a) for a in self._values_ori]) ), 2, ) @@ -209,34 +214,39 @@ self.disp(A.color(C.A_SUBHEADER, "--- STDIN ---")) self.disp(stdin) self.disp(A.color(C.A_SUBHEADER, "-------------")) + self.disp( "{indent}{prog} {static} {options} {positionals}".format( indent=4 * " ", prog=sys.argv[0], static=" ".join(self.args.command), - options=" ".join([o for o in self._opts]), - positionals=" ".join([p for p in self._pos]), + options=" ".join(shlex.quote(o) for o in self._opts), + positionals=" ".join(shlex.quote(p) for p in self._pos), ) ) self.disp("\n") else: self.disp(" (" + ", ".join(self._values_ori) + ")", 2, no_lf=True) args = [sys.argv[0]] + self.args.command + self._opts + self._pos - p = subprocess.Popen( - args, + p = await asyncio.create_subprocess_exec( + *args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - (stdout, stderr) = p.communicate(stdin) + stdout, stderr = await p.communicate(stdin.encode('utf-8')) log = self.args.log log_err = self.args.log_err log_tpl = "{command}\n{buff}\n\n" if log: - log.write(log_tpl.format(command=" ".join(args), buff=stdout)) + log.write(log_tpl.format( + command=" ".join(shlex.quote(a) for a in args), + buff=stdout.decode('utf-8', 'replace'))) if log_err: - log_err.write(log_tpl.format(command=" ".join(args), buff=stderr)) - ret = p.wait() + log_err.write(log_tpl.format( + command=" ".join(shlex.quote(a) for a in args), + buff=stderr.decode('utf-8', 'replace'))) + ret = p.returncode if ret == 0: self.disp(A.color(C.A_SUCCESS, _("OK"))) else: @@ -307,21 +317,25 @@ super(Csv, self).filter(filter_type, filter_arg, value) - def start(self): + async def start(self): import csv + if self.args.encoding: + sys.stdin.reconfigure(encoding=self.args.encoding, errors="replace") reader = csv.reader(sys.stdin) for idx, row in enumerate(reader): try: if idx < self.args.row: continue for value in row: - self.addValue(value.decode(self.args.encoding)) - self.runCommand() + self.addValue(value) + await self.runCommand() except exceptions.CancelError: #  this row has been cancelled, we skip it continue + self.host.quit() + class Input(base.CommandBase): subcommands = (Csv,) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_invitation.py --- a/sat_frontends/jp/cmd_invitation.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_invitation.py Wed Sep 25 08:56:41 2019 +0200 @@ -23,7 +23,6 @@ from sat_frontends.jp.constants import Const as C from sat.tools.common.ansi import ANSI as A from sat.tools.common import data_format -from functools import partial __commands__ = ["Invitation"] @@ -38,7 +37,6 @@ use_output=C.OUTPUT_DICT, help=_("create and send an invitation"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -115,17 +113,7 @@ help="profile doing the invitation (default: don't associate profile)", ) - def invitationCreateCb(self, invitation_data): - self.output(invitation_data) - self.host.quit(C.EXIT_OK) - - def invitationCreateEb(self, failure_): - self.disp( - "can't create invitation: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): + async def start(self): extra = dict(self.args.extra) email = self.args.email[0] if self.args.email else None emails_extra = self.args.email[1:] @@ -139,22 +127,27 @@ _("you need to specify an email address to send email invitation") ) - self.host.bridge.invitationCreate( - email, - emails_extra, - self.args.jid, - self.args.password, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url, - self.args.subject, - self.args.body, - extra, - self.args.profile, - callback=self.invitationCreateCb, - errback=self.invitationCreateEb, - ) + try: + invitation_data = await self.host.bridge.invitationCreate( + email, + emails_extra, + self.args.jid, + self.args.password, + self.args.name, + self.args.host_name, + self.args.lang, + self.args.url, + self.args.subject, + self.args.body, + extra, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't create invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(invitation_data) + self.host.quit(C.EXIT_OK) class Get(base.CommandBase): @@ -167,7 +160,6 @@ use_output=C.OUTPUT_DICT, help=_("get invitation data"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -180,52 +172,45 @@ help=_("start profile session and retrieve jid"), ) - def output_data(self, data, jid_=None): + async def output_data(self, data, jid_=None): if jid_ is not None: data["jid"] = jid_ - self.output(data) + await self.output(data) self.host.quit() - def invitationGetCb(self, invitation_data): - if self.args.with_jid: + async def start(self): + try: + invitation_data = await self.host.bridge.invitationGet( + self.args.id, + ) + except Exception as e: + self.disp(msg=_(f"can't get invitation data: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if not self.args.with_jid: + await self.output_data(invitation_data) + else: profile = invitation_data["guest_profile"] + try: + await self.host.bridge.profileStartSession( + invitation_data["password"], + profile, + ) + except Exception as e: + self.disp(msg=_(f"can't start session: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def session_started(__): - self.host.bridge.asyncGetParamA( + try: + jid_ = await self.host.bridge.asyncGetParamA( "JabberID", "Connection", profile_key=profile, - callback=lambda jid_: self.output_data(invitation_data, jid_), - errback=partial( - self.errback, - msg=_("can't retrieve jid: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), ) + except Exception as e: + self.disp(msg=_(f"can't retrieve jid: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - self.host.bridge.profileStartSession( - invitation_data["password"], - profile, - callback=session_started, - errback=partial( - self.errback, - msg=_("can't start session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - else: - self.output_data(invitation_data) - - def start(self): - self.host.bridge.invitationGet( - self.args.id, - callback=self.invitationGetCb, - errback=partial( - self.errback, - msg=_("can't get invitation data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + await self.output_data(invitation_data, jid_) class Modify(base.CommandBase): @@ -233,7 +218,6 @@ base.CommandBase.__init__( self, host, "modify", use_profile=False, help=_("modify existing invitation") ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -283,17 +267,7 @@ "id", help=_("invitation UUID") ) - def invitationModifyCb(self): - self.disp(_("invitations have been modified correctly")) - self.host.quit(C.EXIT_OK) - - def invitationModifyEb(self, failure_): - self.disp( - "can't create invitation: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): + async def start(self): extra = dict(self.args.extra) for arg_name in ("name", "host_name", "email", "language", "profile"): value = getattr(self.args, arg_name) @@ -301,18 +275,23 @@ continue if arg_name in extra: self.parser.error( - _( - "you can't set {arg_name} in both optional argument and extra" - ).format(arg_name=arg_name) + _(f"you can't set {arg_name} in both optional argument and extra") ) extra[arg_name] = value - self.host.bridge.invitationModify( - self.args.id, - extra, - self.args.replace, - callback=self.invitationModifyCb, - errback=self.invitationModifyEb, - ) + try: + await self.host.bridge.invitationModify( + self.args.id, + extra, + self.args.replace, + ) + except Exception as e: + self.disp( + f"can't modify invitation: {e}", error=True + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("invitations have been modified successfuly")) + self.host.quit(C.EXIT_OK) class List(base.CommandBase): @@ -327,7 +306,6 @@ extra_outputs=extra_outputs, help=_("list invitations data"), ) - self.need_loop = True def default_output(self, data): for idx, datum in enumerate(data.items()): @@ -347,20 +325,17 @@ help=_("return only invitations linked to this profile"), ) - def invitationListCb(self, data): - self.output(data) - self.host.quit() - - def start(self): - self.host.bridge.invitationList( - self.args.profile, - callback=self.invitationListCb, - errback=partial( - self.errback, - msg=_("can't list invitations: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + data = await self.host.bridge.invitationList( + self.args.profile, + ) + except Exception as e: + self.disp(f"return only invitations linked to this profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(data) + self.host.quit() class Invitation(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_merge_request.py --- a/sat_frontends/jp/cmd_merge_request.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_merge_request.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,19 +18,19 @@ # along with this program. If not, see . +import os.path from . import base from sat.core.i18n import _ from sat.tools.common import data_format from sat_frontends.jp.constants import Const as C from sat_frontends.jp import xmlui_manager from sat_frontends.jp import common -from functools import partial -import os.path __commands__ = ["MergeRequest"] class Set(base.CommandBase): + def __init__(self, host): base.CommandBase.__init__( self, @@ -40,7 +40,6 @@ pubsub_defaults={"service": _("auto"), "node": _("auto")}, help=_("publish or update a merge request"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -70,47 +69,43 @@ help=_("labels to categorize your request"), ) - def mergeRequestSetCb(self, published_id): - if published_id: - self.disp("Merge request published at {pub_id}".format(pub_id=published_id)) - else: - self.disp("Merge request published") - self.host.quit(C.EXIT_OK) + async def start(self): + self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) + await common.fill_well_known_uri(self, self.repository, "merge requests") + if not self.args.force: + message = _( + f"You are going to publish your changes to service " + f"[{self.args.service}], are you sure ?" + ) + await self.host.confirmOrQuit( + message, _("merge request publication cancelled")) - def sendRequest(self): extra = {"update": True} if self.args.item else {} values = {} if self.args.labels is not None: values["labels"] = self.args.labels - self.host.bridge.mergeRequestSet( - self.args.service, - self.args.node, - self.repository, - "auto", - values, - "", - self.args.item, - data_format.serialise(extra), - self.profile, - callback=self.mergeRequestSetCb, - errback=partial( - self.errback, - msg=_("can't create merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + published_id = await self.host.bridge.mergeRequestSet( + self.args.service, + self.args.node, + self.repository, + "auto", + values, + "", + self.args.item, + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't create merge requests: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def askConfirmation(self): - if not self.args.force: - message = _( - "You are going to publish your changes to service [{service}], are you sure ?" - ).format(service=self.args.service) - self.host.confirmOrQuit(message, _("merge request publication cancelled")) - self.sendRequest() + if published_id: + self.disp(_(f"Merge request published at {published_id}")) + else: + self.disp(_("Merge request published")) - def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - common.URIFinder(self, self.repository, "merge requests", self.askConfirmation) + self.host.quit(C.EXIT_OK) class Get(base.CommandBase): @@ -125,45 +120,38 @@ pubsub_defaults={"service": _("auto"), "node": _("auto")}, help=_("get a merge request"), ) - self.need_loop = True def add_parser_options(self): pass - def mergeRequestGetCb(self, requests_data): + async def start(self): + await common.fill_well_known_uri( + self, os.getcwd(), "merge requests", meta_map={}) + extra = {} + try: + requests_data = await self.host.bridge.mergeRequestsGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + extra, + self.profile, + ) + except Exception as e: + self.disp(f"can't get merge request: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + if self.verbosity >= 1: whitelist = None else: whitelist = {"id", "title", "body"} for request_xmlui in requests_data[0]: xmlui = xmlui_manager.create(self.host, request_xmlui, whitelist=whitelist) - xmlui.show(values_only=True) + await xmlui.show(values_only=True) self.disp("") self.host.quit(C.EXIT_OK) - def getRequests(self): - extra = {} - self.host.bridge.mergeRequestsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - extra, - self.profile, - callback=self.mergeRequestGetCb, - errback=partial( - self.errback, - msg=_("can't get merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - - def start(self): - common.URIFinder( - self, os.getcwd(), "merge requests", self.getRequests, meta_map={} - ) - class Import(base.CommandBase): def __init__(self, host): @@ -176,7 +164,6 @@ pubsub_defaults={"service": _("auto"), "node": _("auto")}, help=_("import a merge request"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -187,31 +174,25 @@ help=_("path of the repository (DEFAULT: current directory)"), ) - def mergeRequestImportCb(self): - self.host.quit(C.EXIT_OK) - - def importRequest(self): + async def start(self): + self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) + await common.fill_well_known_uri( + self, self.repository, "merge requests", meta_map={}) extra = {} - self.host.bridge.mergeRequestsImport( - self.repository, - self.args.item, - self.args.service, - self.args.node, - extra, - self.profile, - callback=self.mergeRequestImportCb, - errback=partial( - self.errback, - msg=_("can't import merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - - def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - common.URIFinder( - self, self.repository, "merge requests", self.importRequest, meta_map={} - ) + try: + await self.host.bridge.mergeRequestsImport( + self.repository, + self.args.item, + self.args.service, + self.args.node, + extra, + self.profile, + ) + except Exception as e: + self.disp(f"can't import merge request: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class MergeRequest(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_message.py --- a/sat_frontends/jp/cmd_message.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_message.py Wed Sep 25 08:56:41 2019 +0200 @@ -25,7 +25,6 @@ from sat.tools.utils import clean_ustr from sat.tools.common import data_format from sat.tools.common.ansi import ANSI as A -from functools import partial __commands__ = ["Message"] @@ -33,7 +32,6 @@ class Send(base.CommandBase): def __init__(self, host): super(Send, self).__init__(host, "send", help=_("send a message to a contact")) - self.need_loop=True def add_parser_options(self): self.parser.add_argument( @@ -84,25 +82,15 @@ "jid", help=_("the destination jid") ) - def multi_send_cb(self): - self.sent += 1 - if self.sent == self.to_send: - self.host.quit(self.errcode) - - def multi_send_eb(self, failure_, msg): - self.disp(_("Can't send message [{msg}]: {reason}").format( - msg=msg, reason=failure_)) - self.errcode = C.EXIT_BRIDGE_ERRBACK - self.multi_send_cb() - - def sendStdin(self, dest_jid): + async def sendStdin(self, dest_jid): """Send incomming data on stdin to jabber contact @param dest_jid: destination jid """ header = "\n" if self.args.new_line else "" + # FIXME: stdin is not read asynchronously at the moment stdin_lines = [ - stream.decode("utf-8", "ignore") for stream in sys.stdin.readlines() + stream for stream in sys.stdin.readlines() ] extra = {} if self.args.subject is None: @@ -113,99 +101,98 @@ if self.args.xhtml or self.args.rich: key = "xhtml" if self.args.xhtml else "rich" if self.args.lang: - key = "{}_{}".format(key, self.args.lang) + key = f"{key}_{self.args.lang}" extra[key] = clean_ustr("".join(stdin_lines)) stdin_lines = [] - if self.args.separate: # we send stdin in several messages - self.to_send = 0 - self.sent = 0 - self.errcode = 0 + to_send = [] + + error = False + if self.args.separate: + # we send stdin in several messages if header: - self.to_send += 1 - self.host.bridge.messageSend( + # first we sent the header + try: + await self.host.bridge.messageSend( + dest_jid, + {self.args.lang: header}, + subject, + self.args.type, + profile_key=self.profile, + ) + except Exception as e: + self.disp(f"can't send header: {e}", error=True) + error = True + + to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))} + for l in stdin_lines) + else: + # we sent all in a single message + if not (self.args.xhtml or self.args.rich): + msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))} + else: + msg = {} + to_send.append(msg) + + for msg in to_send: + try: + await self.host.bridge.messageSend( dest_jid, - {self.args.lang: header}, - subject, - self.args.type, - profile_key=self.profile, - callback=lambda: None, - errback=lambda ignore: ignore, - ) - - self.to_send += len(stdin_lines) - for line in stdin_lines: - self.host.bridge.messageSend( - dest_jid, - {self.args.lang: line.replace("\n", "")}, + msg, subject, self.args.type, extra, - profile_key=self.host.profile, - callback=self.multi_send_cb, - errback=partial(self.multi_send_eb, msg=line), - ) + profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't send message {msg!r}: {e}", error=True) + error = True - else: - msg = ( - {self.args.lang: header + clean_ustr("".join(stdin_lines))} - if not (self.args.xhtml or self.args.rich) - else {} - ) - self.host.bridge.messageSend( - dest_jid, - msg, - subject, - self.args.type, - extra, - profile_key=self.host.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_("Can't send message: {}"))) + if error: + # at least one message sending failed + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def encryptionNamespaceGetCb(self, namespace, jid_): - self.host.bridge.messageEncryptionStart( - jid_, namespace, not self.args.encrypt_noreplace, - self.profile, - callback=lambda: self.sendStdin(jid_), - errback=partial(self.errback, - msg=_("Can't start encryption session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) + self.host.quit() - - def start(self): + async def start(self): if self.args.xhtml and self.args.separate: self.disp( "argument -s/--separate is not compatible yet with argument -x/--xhtml", error=True, ) - self.host.quit(2) + self.host.quit(C.EXIT_BAD_ARG) - jids = self.host.check_jids([self.args.jid]) + jids = await self.host.check_jids([self.args.jid]) jid_ = jids[0] if self.args.encrypt_noreplace and self.args.encrypt is None: self.parser.error("You need to use --encrypt if you use --encrypt-noreplace") if self.args.encrypt is not None: - self.host.bridge.encryptionNamespaceGet(self.args.encrypt, - callback=partial(self.encryptionNamespaceGetCb, jid_=jid_), - errback=partial(self.errback, - msg=_("Can't get encryption namespace: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) - else: - self.sendStdin(jid_) + try: + namespace = await self.host.bridge.encryptionNamespaceGet( + self.args.encrypt) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.messageEncryptionStart( + jid_, namespace, not self.args.encrypt_noreplace, self.profile + ) + except Exception as e: + self.disp(f"can't start encryption session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + await self.sendStdin(jid_) class MAM(base.CommandBase): def __init__(self, host): super(MAM, self).__init__( - host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, help=_("query archives using MAM")) - self.need_loop=True + host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, + help=_("query archives using MAM")) def add_parser_options(self): self.parser.add_argument( @@ -235,9 +222,37 @@ "--index", dest="rsm_index", type=int, help=_("index of the page to retrieve")) - def _sessionInfosGetCb(self, session_info, data, metadata): + async def start(self): + extra = {} + if self.args.mam_start is not None: + extra["mam_start"] = float(self.args.mam_start) + if self.args.mam_end is not None: + extra["mam_end"] = float(self.args.mam_end) + if self.args.mam_with is not None: + extra["mam_with"] = self.args.mam_with + for suff in ('max', 'after', 'before', 'index'): + key = 'rsm_' + suff + value = getattr(self.args,key) + if value is not None: + extra[key] = str(value) + try: + data, metadata, profile = await self.host.bridge.MAMGet( + self.args.service, data_format.serialise(extra), self.profile) + except Exception as e: + self.disp(f"can't retrieve MAM archives: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + session_info = await self.host.bridge.sessionInfosGet(self.profile) + except Exception as e: + self.disp(f"can't get session infos: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + # we need to fill own_jid for message output self.host.own_jid = jid.JID(session_info["jid"]) - self.output(data) + + await self.output(data) + # FIXME: metadata are not displayed correctly and don't play nice with output # they should be added to output data somehow if self.verbosity: @@ -250,29 +265,6 @@ self.host.quit() - def _MAMGetCb(self, result): - data, metadata, profile = result - self.host.bridge.sessionInfosGet(self.profile, - callback=partial(self._sessionInfosGetCb, data=data, metadata=metadata), - errback=self.errback) - - def start(self): - extra = {} - if self.args.mam_start is not None: - extra["mam_start"] = float(self.args.mam_start) - if self.args.mam_end is not None: - extra["mam_end"] = float(self.args.mam_end) - if self.args.mam_with is not None: - extra["mam_with"] = self.args.mam_with - for suff in ('max', 'after', 'before', 'index'): - key = 'rsm_' + suff - value = getattr(self.args,key) - if value is not None: - extra[key] = str(value) - self.host.bridge.MAMGet( - self.args.service, data_format.serialise(extra), self.profile, - callback=self._MAMGetCb, errback=self.errback) - class Message(base.CommandBase): subcommands = (Send, MAM) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_param.py --- a/sat_frontends/jp/cmd_param.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_param.py Wed Sep 25 08:56:41 2019 +0200 @@ -21,87 +21,118 @@ from . import base from sat.core.i18n import _ +from .constants import Const as C __commands__ = ["Param"] class Get(base.CommandBase): def __init__(self, host): - super(Get, self).__init__(host, 'get', need_connect=False, help=_('Get a parameter value')) + super(Get, self).__init__( + host, 'get', need_connect=False, help=_('get a parameter value')) def add_parser_options(self): - self.parser.add_argument("category", nargs='?', help=_("Category of the parameter")) - self.parser.add_argument("name", nargs='?', help=_("Name of the parameter")) - self.parser.add_argument("-a", "--attribute", type=str, default="value", help=_("Name of the attribute to get")) - self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit")) + self.parser.add_argument( + "category", nargs='?', help=_("category of the parameter")) + self.parser.add_argument("name", nargs='?', help=_("name of the parameter")) + self.parser.add_argument( + "-a", "--attribute", type=str, default="value", + help=_("name of the attribute to get")) + self.parser.add_argument( + "--security-limit", type=int, default=-1, help=_("security limit")) - def start(self): + async def start(self): if self.args.category is None: - categories = self.host.bridge.getParamsCategories() + categories = await self.host.bridge.getParamsCategories() print("\n".join(categories)) elif self.args.name is None: try: - values_dict = self.host.bridge.asyncGetParamsValuesFromCategory(self.args.category, self.args.security_limit, self.profile) + values_dict = await self.host.bridge.asyncGetParamsValuesFromCategory( + self.args.category, self.args.security_limit, self.profile) except Exception as e: - print("Can't find requested parameters: {}".format(e)) - self.host.quit(1) - for name, value in values_dict.items(): - print("{}\t{}".format(name, value)) + self.disp(_(f"can't find requested parameters: {e}"), error=True) + self.host.quit(C.EXIT_NOT_FOUND) + else: + for name, value in values_dict.items(): + print(f"{name}\t{value}") else: try: - value = self.host.bridge.asyncGetParamA(self.args.name, self.args.category, self.args.attribute, - self.args.security_limit, self.profile) + value = await self.host.bridge.asyncGetParamA( + self.args.name, self.args.category, self.args.attribute, + self.args.security_limit, self.profile) except Exception as e: - print("Can't find requested parameter: {}".format(e)) - self.host.quit(1) - print(value) + self.disp(_(f"can't find requested parameter: {e}"), error=True) + self.host.quit(C.EXIT_NOT_FOUND) + else: + print(value) + self.host.quit() class Set(base.CommandBase): def __init__(self, host): - super(Set, self).__init__(host, 'set', need_connect=False, help=_('Set a parameter value')) + super(Set, self).__init__(host, 'set', need_connect=False, help=_('set a parameter value')) def add_parser_options(self): - self.parser.add_argument("category", help=_("Category of the parameter")) - self.parser.add_argument("name", help=_("Name of the parameter")) - self.parser.add_argument("value", help=_("Name of the parameter")) - self.parser.add_argument("--security-limit", type=int, default=-1, help=_("Security limit")) + self.parser.add_argument("category", help=_("category of the parameter")) + self.parser.add_argument("name", help=_("name of the parameter")) + self.parser.add_argument("value", help=_("name of the parameter")) + self.parser.add_argument("--security-limit", type=int, default=-1, help=_("security limit")) - def start(self): + async def start(self): try: - self.host.bridge.setParam(self.args.name, self.args.value, self.args.category, self.args.security_limit, self.profile) + await self.host.bridge.setParam( + self.args.name, self.args.value, self.args.category, + self.args.security_limit, self.profile) except Exception as e: - print("Can set requested parameter: {}".format(e)) + self.disp(_(f"can't set requested parameter: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class SaveTemplate(base.CommandBase): + # FIXME: this should probably be removed, it's not used and not useful for end-user + def __init__(self, host): - super(SaveTemplate, self).__init__(host, 'save', use_profile=False, help=_('Save parameters template to xml file')) + super(SaveTemplate, self).__init__( + host, 'save', use_profile=False, + help=_('save parameters template to xml file')) def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("Output file")) + self.parser.add_argument("filename", type=str, help=_("output file")) - def start(self): - """Save parameters template to xml file""" - if self.host.bridge.saveParamsTemplate(self.args.filename): - print(_("Parameters saved to file %s") % self.args.filename) + async def start(self): + """Save parameters template to XML file""" + try: + await self.host.bridge.saveParamsTemplate(self.args.filename) + except Exception as e: + self.disp(_(f"can't save parameters to file: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - print(_("Can't save parameters to file %s") % self.args.filename) + self.disp(_(f"parameters saved to file {self.args.filename}")) + self.host.quit() class LoadTemplate(base.CommandBase): + # FIXME: this should probably be removed, it's not used and not useful for end-user def __init__(self, host): - super(LoadTemplate, self).__init__(host, 'load', use_profile=False, help=_('Load parameters template from xml file')) + super(LoadTemplate, self).__init__( + host, 'load', use_profile=False, + help=_('load parameters template from xml file')) def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("Input file")) + self.parser.add_argument("filename", type=str, help=_("input file")) - def start(self): + async def start(self): """Load parameters template from xml file""" - if self.host.bridge.loadParamsTemplate(self.args.filename): - print(_("Parameters loaded from file %s") % self.args.filename) + try: + self.host.bridge.loadParamsTemplate(self.args.filename) + except Exception as e: + self.disp(_(f"can't load parameters from file: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - print(_("Can't load parameters from file %s") % self.args.filename) + self.disp(_(f"parameters loaded from file {self.args.filename}")) + self.host.quit() class Param(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_ping.py --- a/sat_frontends/jp/cmd_ping.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_ping.py Wed Sep 25 08:56:41 2019 +0200 @@ -19,6 +19,7 @@ from . import base from sat.core.i18n import _ +from sat_frontends.jp.constants import Const as C __commands__ = ["Ping"] @@ -27,7 +28,6 @@ def __init__(self, host): super(Ping, self).__init__(host, 'ping', help=_('ping XMPP entity')) - self.need_loop=True def add_parser_options(self): self.parser.add_argument( @@ -37,11 +37,13 @@ "-d", "--delay-only", action="store_true", help=_("output delay only (in s)") ) - def _pingCb(self, pong_time): - fmt = "{time}" if self.args.delay_only else "PONG ({time} s)" - self.disp(fmt.format(time=pong_time)) - self.host.quit() - - def start(self): - self.host.bridge.ping(self.args.jid, self.profile, - callback=self._pingCb, errback=self.errback) + async def start(self): + try: + pong_time = await self.host.bridge.ping(self.args.jid, self.profile) + except Exception as e: + self.disp(msg=_(f"can't do the ping: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + msg = pong_time if self.args.delay_only else f"PONG ({pong_time} s)" + self.disp(msg) + self.host.quit() diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_pipe.py --- a/sat_frontends/jp/cmd_pipe.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_pipe.py Wed Sep 25 08:56:41 2019 +0200 @@ -17,17 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import socket +import asyncio +import errno +from functools import partial from sat_frontends.jp import base - from sat_frontends.jp.constants import Const as C +from sat_frontends.jp import xmlui_manager import sys from sat.core.i18n import _ from sat_frontends.tools import jid -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -from functools import partial -import socket -import socketserver -import errno __commands__ = ["Pipe"] @@ -37,59 +36,60 @@ class PipeOut(base.CommandBase): def __init__(self, host): super(PipeOut, self).__init__(host, "out", help=_("send a pipe a stream")) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( "jid", help=_("the destination jid") ) - def streamOutCb(self, port): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(("127.0.0.1", int(port))) - while True: - buf = sys.stdin.read(4096) - if not buf: - break - try: - s.sendall(buf) - except socket.error as e: - if e.errno == errno.EPIPE: - sys.stderr.write(str(e) + "\n") - self.host.quit(1) - else: - raise e - self.host.quit() + async def start(self): + """ Create named pipe, and send stdin to it """ + try: + port = await self.host.bridge.streamOut( + await self.host.get_full_jid(self.args.jid), + self.profile, + ) + except Exception as e: + self.disp(f"can't start stream: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + # FIXME: we use temporarily blocking code here, as it simplify + # asyncio port: "loop.connect_read_pipe(lambda: reader_protocol, + # sys.stdin.buffer)" doesn't work properly when a file is piped in + # (we get a "ValueError: Pipe transport is for pipes/sockets only.") + # while it's working well for simple text sending. - def start(self): - """ Create named pipe, and send stdin to it """ - self.host.bridge.streamOut( - self.host.get_full_jid(self.args.jid), - self.profile, - callback=self.streamOutCb, - errback=partial( - self.errback, - msg=_("can't start stream: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("127.0.0.1", int(port))) + + while True: + buf = sys.stdin.buffer.read(4096) + if not buf: + break + try: + s.sendall(buf) + except socket.error as e: + if e.errno == errno.EPIPE: + sys.stderr.write(f"e\n") + self.host.quit(1) + else: + raise e + self.host.quit() -class StreamServer(socketserver.BaseRequestHandler): - def handle(self): - while True: - data = self.request.recv(4096) - if not data: - break - sys.stdout.write(data) - try: - sys.stdout.flush() - except IOError as e: - sys.stderr.write(str(e) + "\n") - break - #  calling shutdown will do a deadlock as we don't use separate thread - # this is a workaround (cf. https://stackoverflow.com/a/36017741) - self.server._BaseServer__shutdown_request = True +async def handle_stream_in(reader, writer, host): + """Write all received data to stdout""" + while True: + data = await reader.read(4096) + if not data: + break + sys.stdout.buffer.write(data) + try: + sys.stdout.flush() + except IOError as e: + sys.stderr.write(f"{e}\n") + break + host.quitFromSignal() class PipeIn(base.CommandAnswering): @@ -105,35 +105,33 @@ ) def getXmluiId(self, action_data): - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the future - # TODO: XMLUI module try: xml_ui = action_data["xmlui"] except KeyError: self.disp(_("Action has no XMLUI"), 1) else: - ui = ET.fromstring(xml_ui.encode("utf-8")) - xmlui_id = ui.get("submit") - if not xmlui_id: + ui = xmlui_manager.create(self.host, xml_ui) + if not ui.submit_id: self.disp(_("Invalid XMLUI received"), error=True) - return xmlui_id + self.quitFromSignal(C.EXIT_INTERNAL_ERROR) + return ui.submit_id - def onStreamAction(self, action_data, action_id, security_limit, profile): + async def onStreamAction(self, action_data, action_id, security_limit, profile): xmlui_id = self.getXmluiId(action_data) if xmlui_id is None: - return self.host.quitFromSignal(1) + self.host.quitFromSignal(C.EXIT_ERROR) try: from_jid = jid.JID(action_data["meta_from_jid"]) except KeyError: - self.disp(_("Ignoring action without from_jid data"), 1) + self.disp(_("Ignoring action without from_jid data"), error=True) return if not self.bare_jids or from_jid.bare in self.bare_jids: host, port = "localhost", START_PORT while True: try: - server = socketserver.TCPServer((host, port), StreamServer) + server = await asyncio.start_server( + partial(handle_stream_in, host=self.host), host, port) except socket.error as e: if e.errno == errno.EADDRINUSE: port += 1 @@ -142,12 +140,15 @@ else: break xmlui_data = {"answer": C.BOOL_TRUE, "port": str(port)} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - server.serve_forever() + await self.host.bridge.launchAction( + xmlui_id, xmlui_data, profile_key=profile) + async with server: + await server.serve_forever() self.host.quitFromSignal() - def start(self): + async def start(self): self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] + await self.start_answering() class Pipe(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_profile.py --- a/sat_frontends/jp/cmd_profile.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_profile.py Wed Sep 25 08:56:41 2019 +0200 @@ -25,7 +25,6 @@ log = getLogger(__name__) from sat.core.i18n import _ from sat_frontends.jp import base -from functools import partial __commands__ = ["Profile"] @@ -43,18 +42,73 @@ def add_parser_options(self): pass + async def start(self): + # connection is already managed by profile common commands + # so we just need to check arguments and quit + if not self.args.connect and not self.args.start_session: + self.parser.error(_("You need to use either --connect or --start-session")) + self.host.quit() class ProfileDisconnect(base.CommandBase): def __init__(self, host): super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=('disconnect a profile')) - self.need_loop = True def add_parser_options(self): pass - def start(self): - self.host.bridge.disconnect(self.args.profile, callback=self.host.quit) + async def start(self): + try: + await self.host.bridge.disconnect(self.args.profile) + except Exception as e: + self.disp(f"can't disconnect profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class ProfileCreate(base.CommandBase): + def __init__(self, host): + super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=('create a new profile')) + + def add_parser_options(self): + self.parser.add_argument('profile', type=str, help=_('the name of the profile')) + self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile')) + self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile')) + self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'), + metavar='PASSWORD') + self.parser.add_argument('-C', '--component', default='', + help=_('set to component import name (entry point) if this is a component')) + + async def start(self): + """Create a new profile""" + if self.args.profile in await self.host.bridge.profilesListGet(): + self.disp(f"Profile {self.args.profile} already exists.", error=True) + self.host.quit(C.EXIT_BRIDGE_ERROR) + try: + await self.host.bridge.profileCreate( + self.args.profile, self.args.password, self.args.component) + except Exception as e: + self.disp(f"can't create profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.profileStartSession( + self.args.password, self.args.profile) + except Exception as e: + self.disp(f"can't start profile session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if self.args.jid: + await self.host.bridge.setParam( + "JabberID", self.args.jid, "Connection", profile_key=self.args.profile) + xmpp_pwd = self.args.password or self.args.xmpp_password + if xmpp_pwd: + await self.host.bridge.setParam( + "Password", xmpp_pwd, "Connection", profile_key=self.args.profile) + + self.disp(f'profile {self.args.profile} created successfully', 1) + self.host.quit() class ProfileDefault(base.CommandBase): @@ -64,8 +118,9 @@ def add_parser_options(self): pass - def start(self): - print(self.host.bridge.profileNameGet('@DEFAULT@')) + async def start(self): + print(await self.host.bridge.profileNameGet('@DEFAULT@')) + self.host.quit() class ProfileDelete(base.CommandBase): @@ -76,47 +131,51 @@ self.parser.add_argument('profile', type=str, help=PROFILE_HELP) self.parser.add_argument('-f', '--force', action='store_true', help=_('delete profile without confirmation')) - def start(self): - if self.args.profile not in self.host.bridge.profilesListGet(): - log.error("Profile %s doesn't exist." % self.args.profile) - self.host.quit(1) + async def start(self): + if self.args.profile not in await self.host.bridge.profilesListGet(): + log.error(f"Profile {self.args.profile} doesn't exist.") + self.host.quit(C.EXIT_NOT_FOUND) if not self.args.force: - message = "Are you sure to delete profile [{}] ?".format(self.args.profile) - res = input("{} (y/N)? ".format(message)) - if res not in ("y", "Y"): - self.disp(_("Profile deletion cancelled")) - self.host.quit(2) + message = f"Are you sure to delete profile [{self.args.profile}] ?" + cancel_message = "Profile deletion cancelled" + await self.host.confirmOrQuit(message, cancel_message) - self.host.bridge.asyncDeleteProfile(self.args.profile, callback=lambda __: None) + await self.host.bridge.asyncDeleteProfile(self.args.profile) + self.host.quit() class ProfileInfo(base.CommandBase): def __init__(self, host): super(ProfileInfo, self).__init__(host, 'info', need_connect=False, help=_('get information about a profile')) - self.need_loop = True self.to_show = [(_("jid"), "Connection", "JabberID"),] self.largest = max([len(item[0]) for item in self.to_show]) - def add_parser_options(self): self.parser.add_argument('--show-password', action='store_true', help=_('show the XMPP password IN CLEAR TEXT')) - def showNextValue(self, label=None, category=None, value=None): + async def showNextValue(self, label=None, category=None, value=None): """Show next value from self.to_show and quit on last one""" if label is not None: - print((("{label:<"+str(self.largest+2)+"}{value}").format(label=label+": ", value=value))) + print((("{label:<"+str(self.largest+2)+"}{value}").format( + label=label+": ", value=value))) try: label, category, name = self.to_show.pop(0) except IndexError: self.host.quit() else: - self.host.bridge.asyncGetParamA(name, category, profile_key=self.host.profile, - callback=lambda value: self.showNextValue(label, category, value)) + try: + value = await self.host.bridge.asyncGetParamA( + name, category, profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't get {name}/{category} param: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.showNextValue(label, category, value) - def start(self): + async def start(self): if self.args.show_password: self.to_show.append((_("XMPP password"), "Connection", "Password")) - self.showNextValue() + await self.showNextValue() class ProfileList(base.CommandBase): @@ -129,54 +188,19 @@ group.add_argument('-C', '--components', action='store_true', help=('get components profiles only')) - def start(self): + async def start(self): if self.args.clients: clients, components = True, False elif self.args.components: clients, components = False, True else: clients, components = True, True - self.output(self.host.bridge.profilesListGet(clients, components)) - - -class ProfileCreate(base.CommandBase): - def __init__(self, host): - super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=('create a new profile')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('profile', type=str, help=_('the name of the profile')) - self.parser.add_argument('-p', '--password', type=str, default='', help=_('the password of the profile')) - self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile')) - self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'), - metavar='PASSWORD') - self.parser.add_argument('-C', '--component', default='', - help=_('set to component import name (entry point) if this is a component')) - - def _session_started(self, __): - if self.args.jid: - self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.args.profile) - xmpp_pwd = self.args.password or self.args.xmpp_password - if xmpp_pwd: - self.host.bridge.setParam("Password", xmpp_pwd, "Connection", profile_key=self.args.profile) + await self.output(await self.host.bridge.profilesListGet(clients, components)) self.host.quit() - def _profile_created(self): - self.host.bridge.profileStartSession(self.args.password, self.args.profile, callback=self._session_started, errback=None) - - def start(self): - """Create a new profile""" - if self.args.profile in self.host.bridge.profilesListGet(): - log.error("Profile %s already exists." % self.args.profile) - self.host.quit(1) - self.host.bridge.profileCreate(self.args.profile, self.args.password, self.args.component, - callback=self._profile_created, - errback=partial(self.errback, - msg=_("can't create profile: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - class ProfileModify(base.CommandBase): + def __init__(self, host): super(ProfileModify, self).__init__(host, 'modify', need_connect=False, help=_('modify an existing profile')) @@ -189,17 +213,22 @@ metavar='PASSWORD') self.parser.add_argument('-D', '--default', action='store_true', help=_('set as default profile')) - def start(self): + async def start(self): if self.args.disable_password: self.args.password = '' if self.args.password is not None: - self.host.bridge.setParam("Password", self.args.password, "General", profile_key=self.host.profile) + await self.host.bridge.setParam( + "Password", self.args.password, "General", profile_key=self.host.profile) if self.args.jid is not None: - self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.host.profile) + await self.host.bridge.setParam( + "JabberID", self.args.jid, "Connection", profile_key=self.host.profile) if self.args.xmpp_password is not None: - self.host.bridge.setParam("Password", self.args.xmpp_password, "Connection", profile_key=self.host.profile) + await self.host.bridge.setParam( + "Password", self.args.xmpp_password, "Connection", profile_key=self.host.profile) if self.args.default: - self.host.bridge.profileSetDefault(self.host.profile) + await self.host.bridge.profileSetDefault(self.host.profile) + + self.host.quit() class Profile(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_pubsub.py --- a/sat_frontends/jp/cmd_pubsub.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_pubsub.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,6 +18,12 @@ # along with this program. If not, see . +import argparse +import os.path +import re +import sys +import subprocess +import asyncio from . import base from sat.core.i18n import _ from sat.core import exceptions @@ -29,11 +35,6 @@ from sat.tools.common import uri from sat.tools.common.ansi import ANSI as A from sat_frontends.tools import jid, strings -import argparse -import os.path -import re -import subprocess -import sys __commands__ = ["Pubsub"] @@ -55,7 +56,6 @@ pubsub_flags={C.NODE}, help=_("retrieve node configuration"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -72,28 +72,23 @@ def filterKey(self, key): return any((key == k or key == "pubsub#" + k) for k in self.args.keys) - def psNodeConfigurationGetCb(self, config_dict): - key_filter = (lambda k: True) if not self.args.keys else self.filterKey - config_dict = { - self.removePrefix(k): v for k, v in config_dict.items() if key_filter(k) - } - self.output(config_dict) - self.host.quit() - - def psNodeConfigurationGetEb(self, failure_): - self.disp( - "can't get node configuration: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeConfigurationGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeConfigurationGetCb, - errback=self.psNodeConfigurationGetEb, - ) + async def start(self): + try: + config_dict = await self.host.bridge.psNodeConfigurationGet( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get node configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + key_filter = (lambda k: True) if not self.args.keys else self.filterKey + config_dict = { + self.removePrefix(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): @@ -108,7 +103,6 @@ use_verbose=True, help=_("create a node"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -128,35 +122,28 @@ help=_('don\'t prepend "pubsub#" prefix to field names'), ) - def psNodeCreateCb(self, node_id): - if self.host.verbosity: - announce = _("node created successfully: ") - else: - announce = "" - self.disp(announce + node_id) - self.host.quit() - - def psNodeCreateEb(self, failure_): - self.disp("can't create: {reason}".format(reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): + async def start(self): if not self.args.full_prefix: options = {"pubsub#" + k: v for k, v in self.args.fields} else: options = dict(self.args.fields) - self.host.bridge.psNodeCreate( - self.args.service, - self.args.node, - options, - self.profile, - callback=self.psNodeCreateCb, - errback=partial( - self.errback, - msg=_("can't create node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + node_id = await self.host.bridge.psNodeCreate( + self.args.service, + self.args.node, + options, + self.profile, + ) + except Exception as e: + self.disp(msg=_(f"can't create node: {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): @@ -169,7 +156,6 @@ pubsub_flags={C.NODE}, help=_("purge a node (i.e. remove all items from it)"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -179,35 +165,30 @@ help=_("purge node without confirmation"), ) - def psNodePurgeCb(self): - self.disp(_("node [{node}] purged successfully").format(node=self.args.node)) - self.host.quit() - - def start(self): + async def start(self): if not self.args.force: if not self.args.service: - message = _("Are you sure to purge PEP node [{node_id}]? " - "This will delete ALL items from it!").format( - node_id=self.args.node - ) + message = _( + f"Are you sure to purge PEP node [{self.args.node}]? This will " + f"delete ALL items from it!") else: message = _( - "Are you sure to delete node [{node_id}] on service [{service}]? " - "This will delete ALL items from it!" - ).format(node_id=self.args.node, service=self.args.service) - self.host.confirmOrQuit(message, _("node purge cancelled")) + f"Are you sure to delete node [{self.args.node}] on service " + f"[{self.args.service}]? This will delete ALL items from it!") + await self.host.confirmOrQuit(message, _("node purge cancelled")) - self.host.bridge.psNodePurge( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodePurgeCb, - errback=partial( - self.errback, - msg=_("can't purge node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.psNodePurge( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(msg=_(f"can't purge node: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_(f"node [{self.args.node}] purged successfully")) + self.host.quit() class NodeDelete(base.CommandBase): @@ -220,7 +201,6 @@ pubsub_flags={C.NODE}, help=_("delete a node"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -230,33 +210,27 @@ help=_("delete node without confirmation"), ) - def psNodeDeleteCb(self): - self.disp(_("node [{node}] deleted successfully").format(node=self.args.node)) - self.host.quit() - - def start(self): + async def start(self): if not self.args.force: if not self.args.service: - message = _("Are you sure to delete PEP node [{node_id}] ?").format( - node_id=self.args.node - ) + message = _(f"Are you sure to delete PEP node [{self.args.node}] ?") else: - message = _( - "Are you sure to delete node [{node_id}] on service [{service}] ?" - ).format(node_id=self.args.node, service=self.args.service) - self.host.confirmOrQuit(message, _("node deletion cancelled")) + message = _(f"Are you sure to delete node [{self.args.node}] on " + f"service [{self.args.service}]?") + await self.host.confirmOrQuit(message, _("node deletion cancelled")) - self.host.bridge.psNodeDelete( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeDeleteCb, - errback=partial( - self.errback, - msg=_("can't delete node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.psNodeDelete( + 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(_(f"node [{self.args.node}] deleted successfully")) + self.host.quit() class NodeSet(base.CommandBase): @@ -271,7 +245,6 @@ use_verbose=True, help=_("set node configuration"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -284,32 +257,33 @@ metavar=("KEY", "VALUE"), help=_("configuration field to set (required)"), ) - - def psNodeConfigurationSetCb(self): - self.disp(_("node configuration successful"), 1) - self.host.quit() - - def psNodeConfigurationSetEb(self, failure_): - self.disp( - "can't set node configuration: {reason}".format(reason=failure_), error=True + self.parser.add_argument( + "-F", + "--full-prefix", + action="store_true", + help=_('don\'t prepend "pubsub#" prefix to field names'), ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) def getKeyName(self, k): - if not k.startswith("pubsub#"): + if self.args.full_prefix or k.startswith("pubsub#"): + return k + else: return "pubsub#" + k - else: - return k - def start(self): - self.host.bridge.psNodeConfigurationSet( - self.args.service, - self.args.node, - {self.getKeyName(k): v for k, v in self.args.fields}, - self.profile, - callback=self.psNodeConfigurationSetCb, - errback=self.psNodeConfigurationSetEb, - ) + async def start(self): + try: + await self.host.bridge.psNodeConfigurationSet( + self.args.service, + self.args.node, + {self.getKeyName(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): @@ -322,7 +296,6 @@ pubsub_flags={C.NODE}, help=_("import raw XML to a node"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -337,12 +310,7 @@ "whole XML of each item to import."), ) - def psItemsSendCb(self, item_ids): - self.disp(_('items published with id(s) {item_ids}').format( - item_ids=', '.join(item_ids))) - self.host.quit() - - def start(self): + async def start(self): try: element, etree = xml_tools.etreeParse(self, self.args.import_file, reraise=True) @@ -364,38 +332,34 @@ _("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="utf-8") for i in element] + items = [etree.tostring(i, encoding="unicode") for i in element] if self.args.admin: - self.host.bridge.psAdminItemsSend( + method = self.host.bridge.psAdminItemsSend + else: + self.disp(_("Items are imported without using admin mode, publisher can't " + "be changed")) + method = self.host.bridge.psItemsSend + + try: + items_ids = await method( self.args.service, self.args.node, items, "", self.profile, - callback=partial(self.psItemsSendCb), - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), ) + except Exception as e: + self.disp(f"can't send items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - self.disp(_("Items are imported without using admin mode, publisher can't " - "be changed")) - self.host.bridge.psItemsSend( - self.args.service, - self.args.node, - items, - "", - self.profile, - callback=partial(self.psItemsSendCb), - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + 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): @@ -409,29 +373,23 @@ pubsub_flags={C.NODE}, help=_("retrieve node affiliations (for node owner)"), ) - self.need_loop = True def add_parser_options(self): pass - def psNodeAffiliationsGetCb(self, affiliations): - self.output(affiliations) - self.host.quit() - - def psNodeAffiliationsGetEb(self, failure_): - self.disp( - "can't get node affiliations: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeAffiliationsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeAffiliationsGetCb, - errback=self.psNodeAffiliationsGetEb, - ) + async def start(self): + try: + affiliations = await self.host.bridge.psNodeAffiliationsGet( + 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): @@ -445,7 +403,6 @@ use_verbose=True, help=_("set affiliations (for node owner)"), ) - self.need_loop = True def add_parser_options(self): # XXX: we use optional argument syntax for a required one because list of list of 2 elements @@ -461,26 +418,21 @@ help=_("entity/affiliation couple(s)"), ) - def psNodeAffiliationsSetCb(self): - self.disp(_("affiliations have been set"), 1) - self.host.quit() - - def psNodeAffiliationsSetEb(self, failure_): - self.disp( - "can't set node affiliations: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): + async def start(self): affiliations = dict(self.args.affiliations) - self.host.bridge.psNodeAffiliationsSet( - self.args.service, - self.args.node, - affiliations, - self.profile, - callback=self.psNodeAffiliationsSetCb, - errback=self.psNodeAffiliationsSetEb, - ) + try: + await self.host.bridge.psNodeAffiliationsSet( + 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): @@ -506,29 +458,23 @@ pubsub_flags={C.NODE}, help=_("retrieve node subscriptions (for node owner)"), ) - self.need_loop = True def add_parser_options(self): pass - def psNodeSubscriptionsGetCb(self, subscriptions): - self.output(subscriptions) - self.host.quit() - - def psNodeSubscriptionsGetEb(self, failure_): - self.disp( - "can't get node subscriptions: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeSubscriptionsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeSubscriptionsGetCb, - errback=self.psNodeSubscriptionsGetEb, - ) + async def start(self): + try: + subscriptions = await self.host.bridge.psNodeSubscriptionsGet( + 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): @@ -566,7 +512,6 @@ use_verbose=True, help=_("set/modify subscriptions (for node owner)"), ) - self.need_loop = True def add_parser_options(self): # XXX: we use optional argument syntax for a required one because list of list of 2 elements @@ -583,25 +528,20 @@ help=_("entity/subscription couple(s)"), ) - def psNodeSubscriptionsSetCb(self): - self.disp(_("subscriptions have been set"), 1) - self.host.quit() - - def psNodeSubscriptionsSetEb(self, failure_): - self.disp( - "can't set node subscriptions: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeSubscriptionsSet( - self.args.service, - self.args.node, - self.args.subscriptions, - self.profile, - callback=self.psNodeSubscriptionsSetCb, - errback=self.psNodeSubscriptionsSetEb, - ) + async def start(self): + try: + self.host.bridge.psNodeSubscriptionsSet( + 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): @@ -627,28 +567,24 @@ use_verbose=True, help=_("set/replace a schema"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument("schema", help=_("schema to set (must be XML)")) - def psSchemaSetCb(self): - self.disp(_("schema has been set"), 1) - self.host.quit() - - def start(self): - self.host.bridge.psSchemaSet( - self.args.service, - self.args.node, - self.args.schema, - self.profile, - callback=self.psSchemaSetCb, - errback=partial( - self.errback, - msg=_("can't set schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + await self.host.bridge.psSchemaSet( + 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): @@ -666,30 +602,26 @@ help=_("edit a schema"), ) common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR) - self.need_loop = True def add_parser_options(self): pass - def psSchemaSetCb(self): - self.disp(_("schema has been set"), 1) - self.host.quit() + async def publish(self, schema): + try: + await self.host.bridge.psSchemaSet( + 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() - def publish(self, schema): - self.host.bridge.psSchemaSet( - self.args.service, - self.args.node, - schema, - self.profile, - callback=self.psSchemaSetCb, - errback=partial( - self.errback, - msg=_("can't set schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - - def psSchemaGetCb(self, schema): + async def psSchemaGetCb(self, schema): try: from lxml import etree except ImportError: @@ -707,20 +639,20 @@ etree.tostring(schema_elt, encoding="utf-8", pretty_print=True) ) content_file_obj.seek(0) - self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj) + await self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj) - def start(self): - self.host.bridge.psSchemaGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSchemaGetCb, - errback=partial( - self.errback, - msg=_("can't edit schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + schema = await self.host.bridge.psSchemaGet( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't edit schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.psSchemaGetCb(schema) class NodeSchemaGet(base.CommandBase): @@ -735,30 +667,27 @@ use_verbose=True, help=_("get schema"), ) - self.need_loop = True def add_parser_options(self): pass - def psSchemaGetCb(self, schema): - if not schema: - self.disp(_("no schema found"), 1) - self.host.quit(1) - self.output(schema) - self.host.quit() - - def start(self): - self.host.bridge.psSchemaGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSchemaGetCb, - errback=partial( - self.errback, - msg=_("can't get schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + schema = await self.host.bridge.psSchemaGet( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if schema: + await self.output(schema) + self.host.quit() + else: + self.disp(_("no schema found"), 1) + self.host.quit(1) class NodeSchema(base.CommandBase): @@ -799,7 +728,6 @@ pubsub_flags={C.NODE}, help=_("publish a new item or update an existing one"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -809,32 +737,29 @@ help=_("id, URL of the item to update, keyword, or nothing for new item"), ) - def psItemsSendCb(self, published_id): - if published_id: - self.disp("Item published at {pub_id}".format(pub_id=published_id)) - else: - self.disp("Item published") - self.host.quit(C.EXIT_OK) - - def start(self): + async def start(self): element, etree = xml_tools.etreeParse(self, sys.stdin) element = xml_tools.getPayload(self, element) payload = etree.tostring(element, encoding="unicode") - self.host.bridge.psItemSend( - self.args.service, - self.args.node, - payload, - self.args.item, - {}, - self.profile, - callback=self.psItemsSendCb, - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + published_id = await self.host.bridge.psItemSend( + self.args.service, + self.args.node, + payload, + self.args.item, + {}, + self.profile, + ) + except Exception as e: + self.disp(_(f"can't send item: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if published_id: + self.disp("Item published at {pub_id}".format(pub_id=published_id)) + else: + self.disp("Item published") + self.host.quit(C.EXIT_OK) class Get(base.CommandBase): @@ -848,7 +773,6 @@ pubsub_flags={C.NODE, C.MULTI_ITEMS}, help=_("get pubsub item(s)"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -860,26 +784,23 @@ #  TODO: a key(s) argument to select keys to display # TODO: add MAM filters - def psItemsGetCb(self, ps_result): - self.output(ps_result[0]) - self.host.quit(C.EXIT_OK) - - def psItemsGetEb(self, failure_): - self.disp("can't get pubsub items: {reason}".format(reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psItemsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - self.args.sub_id, - self.getPubsubExtra(), - self.profile, - callback=self.psItemsGetCb, - errback=self.psItemsGetEb, - ) + async def start(self): + try: + ps_result = await self.host.bridge.psItemsGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + self.args.sub_id, + self.getPubsubExtra(), + self.profile, + ) + except Exception as e: + self.disp(f"can't get pubsub items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(ps_result[0]) + self.host.quit(C.EXIT_OK) class Delete(base.CommandBase): @@ -892,7 +813,6 @@ pubsub_flags={C.NODE, C.ITEM, C.SINGLE_ITEM}, help=_("delete an item"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -902,31 +822,28 @@ "-N", "--notify", action="store_true", help=_("notify deletion") ) - def psItemsDeleteCb(self): - self.disp(_("item {item_id} has been deleted").format(item_id=self.args.item)) - self.host.quit(C.EXIT_OK) - - def start(self): + async def start(self): if not self.args.item: self.parser.error(_("You need to specify an item to delete")) if not self.args.force: message = _("Are you sure to delete item {item_id} ?").format( item_id=self.args.item ) - self.host.confirmOrQuit(message, _("item deletion cancelled")) - self.host.bridge.psRetractItem( - self.args.service, - self.args.node, - self.args.item, - self.args.notify, - self.profile, - callback=self.psItemsDeleteCb, - errback=partial( - self.errback, - msg=_("can't delete item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + await self.host.confirmOrQuit(message, _("item deletion cancelled")) + try: + await self.host.bridge.psRetractItem( + self.args.service, + self.args.node, + self.args.item, + self.args.notify, + self.profile, + ) + except Exception as e: + self.disp(_(f"can't delete item: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_(f"item {self.args.item} has been deleted")) + self.host.quit(C.EXIT_OK) class Edit(base.CommandBase, common.BaseEdit): @@ -946,12 +863,8 @@ def add_parser_options(self): pass - def edit(self, content_file_path, content_file_obj): - # we launch editor - self.runEditor("pubsub_editor_args", content_file_path, content_file_obj) - - def publish(self, content): - published_id = self.host.bridge.psItemSend( + async def publish(self, content): + published_id = await self.host.bridge.psItemSend( self.pubsub_service, self.pubsub_node, content, @@ -964,7 +877,7 @@ else: self.disp("Item published") - def getItemData(self, service, node, item): + async def getItemData(self, service, node, item): try: from lxml import etree except ImportError: @@ -974,10 +887,10 @@ ) self.host.quit(1) items = [item] if item else [] - item_raw = self.host.bridge.psItemsGet( + item_raw = (await self.host.bridge.psItemsGet( service, node, 1, items, "", {}, self.profile - )[0][0] - parser = etree.XMLParser(remove_blank_text=True) + ))[0][0] + parser = etree.XMLParser(remove_blank_text=True, recover=True) item_elt = etree.fromstring(item_raw, parser) item_id = item_elt.get("id") try: @@ -987,11 +900,14 @@ return "" return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id - def start(self): - self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj = ( - self.getItemPath() - ) - self.edit(content_file_path, content_file_obj) + async def start(self): + (self.pubsub_service, + self.pubsub_node, + self.pubsub_item, + content_file_path, + content_file_obj) = await self.getItemPath() + await self.runEditor("pubsub_editor_args", content_file_path, content_file_obj) + self.host.quit() class Subscribe(base.CommandBase): @@ -1005,34 +921,30 @@ use_verbose=True, help=_("subscribe to a node"), ) - self.need_loop = True def add_parser_options(self): pass - def psSubscribeCb(self, sub_id): - self.disp(_("subscription done"), 1) - if sub_id: - self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id)) - self.host.quit() - - def start(self): - self.host.bridge.psSubscribe( - self.args.service, - self.args.node, - {}, - self.profile, - callback=self.psSubscribeCb, - errback=partial( - self.errback, - msg=_("can't subscribe to node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + sub_id = await self.host.bridge.psSubscribe( + self.args.service, + self.args.node, + {}, + self.profile, + ) + except Exception as e: + self.disp(_(f"can't subscribe to node: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("subscription done"), 1) + if sub_id: + self.disp(_("subscription id: {sub_id}").format(sub_id=sub_id)) + self.host.quit() class Unsubscribe(base.CommandBase): - # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe + # FIXME: check why we get a a NodeNotFound on subscribe just after unsubscribe def __init__(self, host): base.CommandBase.__init__( @@ -1044,27 +956,23 @@ use_verbose=True, help=_("unsubscribe from a node"), ) - self.need_loop = True def add_parser_options(self): pass - def psUnsubscribeCb(self): - self.disp(_("subscription removed"), 1) - self.host.quit() - - def start(self): - self.host.bridge.psUnsubscribe( - self.args.service, - self.args.node, - self.profile, - callback=self.psUnsubscribeCb, - errback=partial( - self.errback, - msg=_("can't unsubscribe from node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + await self.host.bridge.psUnsubscribe( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(_(f"can't unsubscribe from node: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("subscription removed"), 1) + self.host.quit() class Subscriptions(base.CommandBase): @@ -1077,27 +985,23 @@ use_pubsub=True, help=_("retrieve all subscriptions on a service"), ) - self.need_loop = True def add_parser_options(self): pass - def psSubscriptionsGetCb(self, subscriptions): - self.output(subscriptions) - self.host.quit() - - def start(self): - self.host.bridge.psSubscriptionsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSubscriptionsGetCb, - errback=partial( - self.errback, - msg=_("can't retrieve subscriptions: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + subscriptions = await self.host.bridge.psSubscriptionsGet( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(_(f"can't retrieve subscriptions: {e}"), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(subscriptions) + self.host.quit() class Affiliations(base.CommandBase): @@ -1110,33 +1014,29 @@ use_pubsub=True, help=_("retrieve all affiliations on a service"), ) - self.need_loop = True def add_parser_options(self): pass - def psAffiliationsGetCb(self, affiliations): - self.output(affiliations) - self.host.quit() - - def psAffiliationsGetEb(self, failure_): - self.disp( - "can't get node affiliations: {reason}".format(reason=failure_), error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psAffiliationsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psAffiliationsGetCb, - errback=self.psAffiliationsGetEb, - ) + async def start(self): + try: + affiliations = await self.host.bridge.psAffiliationsGet( + 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 Search(base.CommandBase): - """this command do a search without using MAM + """This command do a search without using MAM This commands checks every items it finds by itself, so it may be heavy in resources both for server and client @@ -1159,7 +1059,6 @@ use_verbose=True, help=_("search items corresponding to filters"), ) - self.need_loop = True @property def etree(self): @@ -1317,30 +1216,26 @@ ) self.parser.add_argument("command", nargs=argparse.REMAINDER) - def psItemsGetEb(self, failure_, service, node): - self.disp( - "can't get pubsub items at {service} (node: {node}): {reason}".format( - service=service, node=node, reason=failure_ - ), - error=True, - ) - self.to_get -= 1 - - def getItems(self, depth, service, node, items): - search = partial(self.search, depth=depth) - errback = partial(self.psItemsGetEb, service=service, node=node) - self.host.bridge.psItemsGet( - service, - node, - self.args.node_max, - items, - "", - self.getPubsubExtra(), - self.profile, - callback=search, - errback=errback, - ) + async def getItems(self, depth, service, node, items): self.to_get += 1 + try: + items_data = await self.host.bridge.psItemsGet( + service, + node, + self.args.node_max, + items, + "", + self.getPubsubExtra(), + self.profile, + ) + except Exception as e: + self.disp( + f"can't get pubsub items at {service} (node: {node}): {e}", + error=True, + ) + self.to_get -= 1 + else: + await self.search(items_data, depth) def _checkPubsubURL(self, match, found_nodes): """check that the matched URL is an xmpp: one @@ -1360,13 +1255,13 @@ found_node["item"] = url_data["item"] found_nodes.append(found_node) - def getSubNodes(self, item, depth): + async def getSubNodes(self, item, depth): """look for pubsub URIs in item, and getItems on the linked nodes""" found_nodes = [] checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes) strings.RE_URL.sub(checkURI, item) for data in found_nodes: - self.getItems( + await self.getItems( depth + 1, data["service"], data["node"], @@ -1452,7 +1347,11 @@ elif type_ == "python": if item_xml is None: item_xml = self.parseXml(item) - cmd_ns = {"item": item, "item_xml": item_xml} + cmd_ns = { + "etree": self.etree, + "item": item, + "item_xml": item_xml + } try: keep = eval(value, cmd_ns) except SyntaxError as e: @@ -1483,7 +1382,7 @@ return True, item - def doItemAction(self, item, metadata): + async def doItemAction(self, item, metadata): """called when item has been kepts and the action need to be done @param item(unicode): accepted item @@ -1491,7 +1390,7 @@ action = self.args.action if action == "print" or self.host.verbosity > 0: try: - self.output(item) + await self.output(item) except self.etree.XMLSyntaxError: # item is not valid XML, but a string # can happen when --only-matching is used @@ -1521,22 +1420,22 @@ 2, ) if action == "exec": - ret = subprocess.call(cmd_args) + p = await asyncio.create_subprocess_exec(*cmd_args) + ret = await p.wait() else: - p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE) - p.communicate(item.encode("utf-8")) - ret = p.wait() + p = await asyncio.create_subprocess_exec(*cmd_args, + stdin=subprocess.PIPE) + await p.communicate(item.encode(sys.getfilesystemencoding())) + ret = p.returncode if ret != 0: self.disp( A.color( C.A_FAILURE, - _("executed command failed with exit code {code}").format( - code=ret - ), + _(f"executed command failed with exit code {ret}"), ) ) - def search(self, items_data, depth): + async def search(self, items_data, depth): """callback of getItems this method filters items, get sub nodes if needed, @@ -1548,11 +1447,11 @@ items, metadata = items_data for item in items: if depth < self.args.max_depth: - self.getSubNodes(item, depth) + await self.getSubNodes(item, depth) keep, item = self.filter(item) if not keep: continue - self.doItemAction(item, metadata) + await self.doItemAction(item, metadata) #  we check if we got all getItems results self.to_get -= 1 @@ -1561,7 +1460,7 @@ self.host.quit() assert self.to_get > 0 - def start(self): + async def start(self): if self.args.command: if self.args.action not in self.EXEC_ACTIONS: self.parser.error( @@ -1584,7 +1483,7 @@ self.args.namespace = dict( self.args.namespace + [("pubsub", "http://jabber.org/protocol/pubsub")] ) - self.getItems(0, self.args.service, self.args.node, self.args.items) + await self.getItems(0, self.args.service, self.args.node, self.args.items) class Transform(base.CommandBase): @@ -1597,7 +1496,6 @@ pubsub_flags={C.NODE, C.MULTI_ITEMS}, help=_("modify items of a node using an external command/script"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -1630,18 +1528,18 @@ 'Return "DELETE" string to delete the item, and "SKIP" to ignore it'), ) - def psItemsSendCb(self, item_ids, metadata): + async def psItemsSendCb(self, item_ids, metadata): if item_ids: self.disp(_('items published with ids {item_ids}').format( item_ids=', '.join(item_ids))) else: self.disp(_('items published')) if self.args.all: - return self.handleNextPage(metadata) + return await self.handleNextPage(metadata) else: self.host.quit() - def handleNextPage(self, metadata): + async def handleNextPage(self, metadata): """Retrieve new page through RSM or quit if we're in the last page use to handle --all option @@ -1672,24 +1570,27 @@ extra = self.getPubsubExtra() extra['rsm_after'] = last - self.host.bridge.psItemsGet( - self.args.service, - self.args.node, - self.args.rsm_max, - self.args.items, - "", - extra, - self.profile, - callback=self.psItemsGetCb, - errback=partial( - self.errback, - msg=_("can't retrieve items: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + ps_result = await self.host.bridge.psItemsGet( + self.args.service, + self.args.node, + self.args.rsm_max, + self.args.items, + "", + extra, + self.profile, + ) + except Exception as e: + self.disp( + f"can't retrieve items: {e}", error=True + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.psItemsGetCb(ps_result) - def psItemsGetCb(self, ps_result): + async def psItemsGetCb(self, ps_result): items, metadata = ps_result + encoding = 'utf-8' new_items = [] for item in items: @@ -1707,48 +1608,46 @@ # we launch the command to filter the item try: - p = subprocess.Popen(self.args.command_path, stdin=subprocess.PIPE, - stdout=subprocess.PIPE) + p = await asyncio.create_subprocess_exec( + self.args.command_path, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) except OSError as e: exit_code = C.EXIT_CMD_NOT_FOUND if e.errno == 2 else C.EXIT_ERROR - e = str(e).decode('utf-8', errors="ignore") - self.disp("Can't execute the command: {msg}".format(msg=e), error=True) + self.disp(f"Can't execute the command: {e}", error=True) self.host.quit(exit_code) - cmd_std_out, cmd_std_err = p.communicate(item.encode("utf-8")) - ret = p.wait() + encoding = "utf-8" + cmd_std_out, cmd_std_err = await p.communicate(item.encode(encoding)) + ret = p.returncode if ret != 0: - self.disp("The command returned a non zero status while parsing the " - "following item:\n\n{item}".format(item=item), error=True) + self.disp(f"The command returned a non zero status while parsing the " + f"following item:\n\n{item}", error=True) if self.args.ignore_errors: continue else: self.host.quit(C.EXIT_CMD_ERROR) if cmd_std_err is not None: - cmd_std_err = cmd_std_err.decode('utf-8', errors='ignore') + cmd_std_err = cmd_std_err.decode(encoding, errors='ignore') self.disp(cmd_std_err, error=True) - cmd_std_out = cmd_std_out.strip() + cmd_std_out = cmd_std_out.decode(encoding).strip() if cmd_std_out == "DELETE": item_elt, __ = xml_tools.etreeParse(self, item) item_id = item_elt.get('id') - self.disp(_("Deleting item {item_id}").format(item_id=item_id)) + self.disp(_(f"Deleting item {item_id}")) if self.args.apply: - # FIXME: we don't wait for item to be retracted which can cause - # trouble in case of error just before the end of the command - # (the error message may be missed). - # Once moved to Python 3, we must wait for it by using a - # coroutine. - self.host.bridge.psRetractItem( - self.args.service, - self.args.node, - item_id, - False, - self.profile, - errback=partial( - self.errback, - msg=_("can't delete item [%s]: {}" % item_id), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.psRetractItem( + self.args.service, + self.args.node, + item_id, + False, + self.profile, + ) + except Exception as e: + self.disp( + f"can't delete item {item_id}: {e}", error=True + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) continue elif cmd_std_out == "SKIP": item_elt, __ = xml_tools.etreeParse(self, item) @@ -1774,39 +1673,29 @@ if not self.args.apply: # on dry run we have nothing to wait for, we can quit if self.args.all: - return self.handleNextPage(metadata) + return await self.handleNextPage(metadata) self.host.quit() else: if self.args.admin: - self.host.bridge.psAdminItemsSend( + bridge_method = self.host.bridge.psAdminItemsSend + else: + bridge_method = self.host.bridge.psItemsSend + + try: + ps_result = await bridge_method( self.args.service, self.args.node, new_items, "", self.profile, - callback=partial(self.psItemsSendCb, metadata=metadata), - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), ) + except Exception as e: + self.disp(f"can't send item: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - self.host.bridge.psItemsSend( - self.args.service, - self.args.node, - new_items, - "", - self.profile, - callback=partial(self.psItemsSendCb, metadata=metadata), - errback=partial( - self.errback, - msg=_("can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + await self.psItemsSendCb(ps_result, metadata=metadata) - def start(self): + async def start(self): if self.args.all and self.args.order_by != C.ORDER_BY_CREATION: self.check_duplicates = True self.items_ids = [] @@ -1819,21 +1708,22 @@ "but this method is not safe, and some items may be missed.\n---\n")) else: self.check_duplicates = False - self.host.bridge.psItemsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - self.getPubsubExtra(), - self.profile, - callback=self.psItemsGetCb, - errback=partial( - self.errback, - msg=_("can't retrieve items: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + + try: + ps_result = await self.host.bridge.psItemsGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + self.getPubsubExtra(), + self.profile, + ) + except Exception as e: + self.disp(f"can't retrieve items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.psItemsGetCb(ps_result) class Uri(base.CommandBase): @@ -1847,7 +1737,6 @@ pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_("build URI"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -1871,19 +1760,19 @@ self.disp(uri.buildXMPPUri("pubsub", **uri_args)) self.host.quit() - def start(self): + async def start(self): if not self.args.service: - self.host.bridge.asyncGetParamA( - "JabberID", - "Connection", - profile_key=self.args.profile, - callback=self.display_uri, - errback=partial( - self.errback, - msg=_("can't retrieve jid: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + jid_ = await self.host.bridge.asyncGetParamA( + "JabberID", + "Connection", + profile_key=self.args.profile + ) + except Exception as e: + self.disp(f"can't retrieve jid: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.display_uri(jid_) else: self.display_uri(None) @@ -1898,7 +1787,6 @@ pubsub_flags={C.NODE}, help=_("create a Pubsub hook"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -1928,22 +1816,22 @@ _("{path} is not a file").format(path=self.args.hook_arg) ) - def start(self): + async def start(self): self.checkArgs(self) - self.host.bridge.psHookAdd( - self.args.service, - self.args.node, - self.args.type, - self.args.hook_arg, - self.args.persistent, - self.profile, - callback=self.host.quit, - errback=partial( - self.errback, - msg=_("can't create hook: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + await self.host.bridge.psHookAdd( + self.args.service, + self.args.node, + self.args.type, + self.args.hook_arg, + self.args.persistent, + self.profile, + ) + except Exception as e: + self.disp(f"can't create hook: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() class HookDelete(base.CommandBase): @@ -1956,7 +1844,6 @@ pubsub_flags={C.NODE}, help=_("delete a Pubsub hook"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -1976,27 +1863,22 @@ ), ) - def psHookRemoveCb(self, nb_deleted): - self.disp( - _("{nb_deleted} hook(s) have been deleted").format(nb_deleted=nb_deleted) - ) - self.host.quit() - - def start(self): + async def start(self): HookCreate.checkArgs(self) - self.host.bridge.psHookRemove( - self.args.service, - self.args.node, - self.args.type, - self.args.hook_arg, - self.profile, - callback=self.psHookRemoveCb, - errback=partial( - self.errback, - msg=_("can't delete hook: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + nb_deleted = await self.host.bridge.psHookRemove( + self.args.service, + self.args.node, + self.args.type, + self.args.hook_arg, + self.profile, + ) + except Exception as e: + self.disp(f"can't delete hook: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_(f"{nb_deleted} hook(s) have been deleted")) + self.host.quit() class HookList(base.CommandBase): @@ -2008,27 +1890,23 @@ use_output=C.OUTPUT_LIST_DICT, help=_("list hooks of a profile"), ) - self.need_loop = True def add_parser_options(self): pass - def psHookListCb(self, data): - if not data: - self.disp(_("No hook found.")) - self.output(data) - self.host.quit() - - def start(self): - self.host.bridge.psHookList( - self.profile, - callback=self.psHookListCb, - errback=partial( - self.errback, - msg=_("can't list hooks: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + async def start(self): + try: + data = await self.host.bridge.psHookList( + self.profile, + ) + except Exception as e: + self.disp(f"can't list hooks: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if not data: + self.disp(_("No hook found.")) + await self.output(data) + self.host.quit() class Hook(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_roster.py --- a/sat_frontends/jp/cmd_roster.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_roster.py Wed Sep 25 08:56:41 2019 +0200 @@ -20,77 +20,67 @@ from . import base from collections import OrderedDict -from functools import partial from sat.core.i18n import _ from sat_frontends.jp.constants import Const as C -from twisted.words.protocols.jabber import jid +from sat_frontends.tools import jid +from sat.tools.common.ansi import ANSI as A __commands__ = ["Roster"] - -class Purge(base.CommandBase): +class Get(base.CommandBase): def __init__(self, host): - super(Purge, self).__init__(host, 'purge', help=_('Purge the roster from its contacts with no subscription')) - self.need_loop = True + super().__init__( + host, 'get', use_output=C.OUTPUT_DICT, use_verbose=True, + extra_outputs = {"default": self.default_output}, + help=_('retrieve the roster entities')) def add_parser_options(self): - self.parser.add_argument("--no_from", action="store_true", help=_("Also purge contacts with no 'from' subscription")) - self.parser.add_argument("--no_to", action="store_true", help=_("Also purge contacts with no 'to' subscription")) - - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) + pass - def error(self, failure): - print((_("Error while retrieving the contacts [%s]") % failure)) - self.host.quit(1) + def default_output(self, data): + for contact_jid, contact_data in data.items(): + all_keys = list(contact_data.keys()) + keys_to_show = [] + name = contact_data.get('name', contact_jid.node) - def ask_confirmation(self, no_sub, no_from, no_to): - """Ask the confirmation before removing contacts. + if self.verbosity >= 1: + keys_to_show.append('groups') + all_keys.remove('groups') + if self.verbosity >= 2: + keys_to_show.extend(all_keys) - @param no_sub (list[unicode]): list of contacts with no subscription - @param no_from (list[unicode]): list of contacts with no 'from' subscription - @param no_to (list[unicode]): list of contacts with no 'to' subscription - @return bool - """ - if no_sub: - print("There's no subscription between profile [%s] and the following contacts:" % self.host.profile) - print(" " + "\n ".join(no_sub)) - if no_from: - print("There's no 'from' subscription between profile [%s] and the following contacts:" % self.host.profile) - print(" " + "\n ".join(no_from)) - if no_to: - print("There's no 'to' subscription between profile [%s] and the following contacts:" % self.host.profile) - print(" " + "\n ".join(no_to)) - message = "REMOVE them from profile [%s]'s roster" % self.host.profile - while True: - res = input("%s (y/N)? " % message) - if not res or res.lower() == 'n': - return False - if res.lower() == 'y': - return True + if name is None: + self.disp(A.color(C.A_HEADER, contact_jid)) + else: + self.disp(A.color(C.A_HEADER, name, A.RESET, f" ({contact_jid})")) + for k in keys_to_show: + value = contact_data[k] + if value: + if isinstance(value, list): + value = ', '.join(value) + self.disp(A.color( + " ", C.A_SUBHEADER, f"{k}: ", A.RESET, str(value))) - def gotContacts(self, contacts): - """Process the list of contacts. + async def start(self): + try: + contacts = await self.host.bridge.getContacts(profile_key=self.host.profile) + except Exception as e: + self.disp(f"error while retrieving the contacts: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - @param contacts(list[tuple]): list of contacts with their attributes and groups - """ - no_sub, no_from, no_to = [], [], [] - for contact, attrs, groups in contacts: - from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) - if not from_: - if not to: - no_sub.append(contact) - elif self.args.no_from: - no_from.append(contact) - elif not to and self.args.no_to: - no_to.append(contact) - if not no_sub and not no_from and not no_to: - print("Nothing to do - there's a from and/or to subscription(s) between profile [%s] and each of its contacts" % self.host.profile) - elif self.ask_confirmation(no_sub, no_from, no_to): - for contact in no_sub + no_from + no_to: - self.host.bridge.delContact(contact, profile_key=self.host.profile, callback=lambda __: None, errback=lambda failure: None) + contacts_dict = {} + for contact_jid_s, data, groups in contacts: + # FIXME: we have to convert string to bool here for historical reason + # getContacts format should be changed and serialised properly + for key in ('from', 'to', 'ask'): + if key in data: + data[key] = C.bool(data[key]) + data['groups'] = list(groups) + contacts_dict[jid.JID(contact_jid_s)] = data + + await self.output(contacts_dict) self.host.quit() @@ -98,23 +88,17 @@ def __init__(self, host): super(Stats, self).__init__(host, 'stats', help=_('Show statistics about a roster')) - self.need_loop = True def add_parser_options(self): pass - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) + async def start(self): + try: + contacts = await self.host.bridge.getContacts(profile_key=self.host.profile) + except Exception as e: + self.disp(f"error while retrieving the contacts: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def error(self, failure): - print((_("Error while retrieving the contacts [%s]") % failure)) - self.host.quit(1) - - def gotContacts(self, contacts): - """Process the list of contacts. - - @param contacts(list[tuple]): list of contacts with their attributes and groups - """ hosts = {} unique_groups = set() no_sub, no_from, no_to, no_group, total_group_subscription = 0, 0, 0, 0, 0 @@ -127,7 +111,9 @@ no_from += 1 elif not to: no_to += 1 - host = jid.JID(contact).host + + host = jid.JID(contact).domain + hosts.setdefault(host, 0) hosts[host] += 1 if groups: @@ -142,7 +128,8 @@ print("Number of different hosts: %d" % len(hosts)) print() for host, count in hosts.items(): - print("Contacts on {host}: {count} ({rate:.1f}%)".format(host=host, count=count, rate=100 * float(count) / len(contacts))) + print("Contacts on {host}: {count} ({rate:.1f}%)".format( + host=host, count=count, rate=100 * float(count) / len(contacts))) print() print("Contacts with no 'from' subscription: %d" % no_from) print("Contacts with no 'to' subscription: %d" % no_to) @@ -158,53 +145,90 @@ groups_per_contact = float(total_group_subscription) / len(contacts) except ZeroDivisionError: groups_per_contact = 0 - print("Average groups' subscriptions per contact: {:.1f}".format(groups_per_contact)) + print(f"Average groups' subscriptions per contact: {groups_per_contact:.1f}") print("Contacts not assigned to any group: %d" % no_group) self.host.quit() -class Get(base.CommandBase): +class Purge(base.CommandBase): def __init__(self, host): - super(Get, self).__init__(host, 'get', help=_('Retrieve the roster contacts')) - self.need_loop = True + super(Purge, self).__init__( + host, 'purge', + help=_('purge the roster from its contacts with no subscription')) def add_parser_options(self): - self.parser.add_argument("--subscriptions", action="store_true", help=_("Show the contacts' subscriptions")) - self.parser.add_argument("--groups", action="store_true", help=_("Show the contacts' groups")) - self.parser.add_argument("--name", action="store_true", help=_("Show the contacts' names")) + self.parser.add_argument( + "--no_from", action="store_true", + help=_("also purge contacts with no 'from' subscription")) + self.parser.add_argument( + "--no_to", action="store_true", + help=_("also purge contacts with no 'to' subscription")) - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) + async def start(self): + try: + contacts = await self.host.bridge.getContacts(self.host.profile) + except Exception as e: + self.disp(f"error while retrieving the contacts: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) - def error(self, failure): - print((_("Error while retrieving the contacts [%s]") % failure)) - self.host.quit(1) + no_sub, no_from, no_to = [], [], [] + for contact, attrs, groups in contacts: + from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) + if not from_: + if not to: + no_sub.append(contact) + elif self.args.no_from: + no_from.append(contact) + elif not to and self.args.no_to: + no_to.append(contact) + if not no_sub and not no_from and not no_to: + self.disp( + f"Nothing to do - there's a from and/or to subscription(s) between " + f"profile {self.host.profile!r} and each of its contacts" + ) + elif await self.ask_confirmation(no_sub, no_from, no_to): + for contact in no_sub + no_from + no_to: + try: + await self.host.bridge.delContact( + contact, profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't delete contact {contact!r}: {e}", error=True) + else: + self.disp(f"contact {contact!r} has been removed") - def gotContacts(self, contacts): - """Process the list of contacts. + self.host.quit() + + async def ask_confirmation(self, no_sub, no_from, no_to): + """Ask the confirmation before removing contacts. - @param contacts(list[tuple]): list of contacts with their attributes and groups + @param no_sub (list[unicode]): list of contacts with no subscription + @param no_from (list[unicode]): list of contacts with no 'from' subscription + @param no_to (list[unicode]): list of contacts with no 'to' subscription + @return bool """ - field_count = 1 # only display the contact by default - if self.args.subscriptions: - field_count += 3 # ask, from, to - if self.args.name: - field_count += 1 - if self.args.groups: - field_count += 1 - for contact, attrs, groups in contacts: - args = [contact] - if self.args.subscriptions: - args.append("ask" if C.bool(attrs["ask"]) else "") - args.append("from" if C.bool(attrs["from"]) else "") - args.append("to" if C.bool(attrs["to"]) else "") - if self.args.name: - args.append(str(attrs.get("name", ""))) - if self.args.groups: - args.append("\t".join(groups) if groups else "") - print(";".join(["{}"] * field_count).format(*args).encode("utf-8")) - self.host.quit() + if no_sub: + self.disp( + f"There's no subscription between profile {self.host.profile!r} and the " + f"following contacts:") + self.disp(" " + "\n ".join(no_sub)) + if no_from: + self.disp( + f"There's no 'from' subscription between profile {self.host.profile!r} " + f"and the following contacts:") + self.disp(" " + "\n ".join(no_from)) + if no_to: + self.disp( + f"There's no 'to' subscription between profile {self.host.profile!r} and " + f"the following contacts:") + self.disp(" " + "\n ".join(no_to)) + message = f"REMOVE them from profile {self.host.profile}'s roster" + while True: + res = await self.host.ainput(f"{message} (y/N)? ") + if not res or res.lower() == 'n': + return False + if res.lower() == 'y': + return True class Resync(base.CommandBase): @@ -212,27 +236,24 @@ def __init__(self, host): super(Resync, self).__init__( host, 'resync', help=_('do a full resynchronisation of roster with server')) - self.need_loop = True def add_parser_options(self): pass - def rosterResyncCb(self): - self.disp(_("Roster resynchronized")) - self.host.quit(C.EXIT_OK) - - def start(self): - self.host.bridge.rosterResync(profile_key=self.host.profile, - callback=self.rosterResyncCb, - errback=partial( - self.errback, - msg=_("can't resynchronise roster: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - )) + async def start(self): + try: + await self.host.bridge.rosterResync(profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't resynchronise roster: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("Roster resynchronized")) + self.host.quit(C.EXIT_OK) class Roster(base.CommandBase): subcommands = (Get, Stats, Purge, Resync) def __init__(self, host): - super(Roster, self).__init__(host, 'roster', use_profile=True, help=_("Manage an entity's roster")) + super(Roster, self).__init__( + host, 'roster', use_profile=True, help=_("Manage an entity's roster")) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_shell.py --- a/sat_frontends/jp/cmd_shell.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_shell.py Wed Sep 25 08:56:41 2019 +0200 @@ -18,16 +18,16 @@ # along with this program. If not, see . -from . import base import cmd import sys +import shlex +import subprocess +from . import base from sat.core.i18n import _ from sat.core import exceptions from sat_frontends.jp.constants import Const as C from sat_frontends.jp import arg_tools from sat.tools.common.ansi import ANSI as A -import shlex -import subprocess __commands__ = ["Shell"] INTRO = _( @@ -44,7 +44,8 @@ class Shell(base.CommandBase, cmd.Cmd): def __init__(self, host): base.CommandBase.__init__( - self, host, "shell", help=_("launch jp in shell (REPL) mode") + self, host, "shell", + help=_("launch jp in shell (REPL) mode") ) cmd.Cmd.__init__(self) @@ -152,13 +153,15 @@ help_list = self._cur_parser.format_help().split("\n\n") print(("\n\n".join(help_list[1 if self.path else 2 :]))) - def do_debug(self, args): - """launch internal debugger""" - try: - import ipdb as pdb - except ImportError: - import pdb - pdb.set_trace() + # FIXME: debug crashes on exit and is not that useful, + # keeping it until refactoring, may be removed entirely then + # def do_debug(self, args): + # """launch internal debugger""" + # try: + # import ipdb as pdb + # except ImportError: + # import pdb + # pdb.set_trace() def do_verbose(self, args): """show verbose mode, or (de)activate it""" @@ -188,10 +191,7 @@ def do_version(self, args): """show current SàT/jp version""" - try: - self.host.run(["--version"]) - except SystemExit: - pass + self.run_cmd(['--version']) def do_shell(self, args): """launch an external command (you can use ![command] too)""" @@ -292,7 +292,9 @@ """alias for quit""" self.do_quit(args) - def start(self): + async def start(self): + # FIXME: "shell" is currently kept synchronous as it works well as it + # and it will be refactored soon. default_profile = self.host.bridge.profileNameGet(C.PROF_KEY_DEFAULT) self._not_default_profile = self.profile != default_profile self.path = [] @@ -300,4 +302,4 @@ self.use = {} self.verbose = False self.update_path() - self.cmdloop(INTRO.encode("utf-8")) + self.cmdloop(INTRO) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_ticket.py --- a/sat_frontends/jp/cmd_ticket.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_ticket.py Wed Sep 25 08:56:41 2019 +0200 @@ -22,7 +22,6 @@ from sat.core.i18n import _ from sat_frontends.jp import common from sat_frontends.jp.constants import Const as C -from functools import partial import json import os @@ -44,37 +43,31 @@ use_output=C.OUTPUT_LIST_XMLUI, help=_("get tickets"), ) - self.need_loop = True def add_parser_options(self): pass - def ticketsGetCb(self, tickets_data): - self.output(tickets_data[0]) - self.host.quit(C.EXIT_OK) - - def getTickets(self): - self.host.bridge.ticketsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - self.getPubsubExtra(), - self.profile, - callback=self.ticketsGetCb, - errback=partial( - self.errback, - msg=_("can't get tickets: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - - def start(self): - common.URIFinder(self, os.getcwd(), "tickets", self.getTickets, meta_map={}) + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) + try: + tickets_data = await self.host.bridge.ticketsGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + self.getPubsubExtra(), + self.profile, + ) + except Exception as e: + self.disp(f"can't get tickets: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(tickets_data[0]) + self.host.quit(C.EXIT_OK) -class Import(base.CommandAnswering): +class Import(base.CommandBase): # TODO: factorize with blog/import def __init__(self, host): @@ -82,9 +75,9 @@ host, "import", use_progress=True, + use_verbose=True, help=_("import tickets from external software/dataset"), ) - self.need_loop = True def add_parser_options(self): self.parser.add_argument( @@ -109,7 +102,8 @@ default=[], metavar=("IMPORTED_FIELD", "DEST_FIELD"), help=_( - "specified field in import data will be put in dest field (default: use same field name, or ignore if it doesn't exist)" + "specified field in import data will be put in dest field (default: use " + "same field name, or ignore if it doesn't exist)" ), ) self.parser.add_argument( @@ -125,97 +119,92 @@ default="", metavar="PUBSUB_NODE", help=_( - "PubSub node where the items must be uploaded (default: tickets' defaults)" + "PubSub node where the items must be uploaded (default: tickets' " + "defaults)" ), ) self.parser.add_argument( "location", nargs="?", help=_( - "importer data location (see importer description), nothing to show importer description" + "importer data location (see importer description), nothing to show " + "importer description" ), ) - def onProgressStarted(self, metadata): + async def onProgressStarted(self, metadata): self.disp(_("Tickets upload started"), 2) - def onProgressFinished(self, metadata): + async def onProgressFinished(self, metadata): self.disp(_("Tickets uploaded successfully"), 2) - def onProgressError(self, error_msg): - self.disp(_("Error while uploading tickets: {}").format(error_msg), error=True) + async def onProgressError(self, error_msg): + self.disp(_(f"Error while uploading tickets: {error_msg}"), error=True) - def error(self, failure): - self.disp( - _("Error while trying to upload tickets: {reason}").format(reason=failure), - error=True, - ) - self.host.quit(1) - - def start(self): + async def start(self): if self.args.location is None: + # no location, the list of importer or description is requested for name in ("option", "service", "node"): if getattr(self.args, name): self.parser.error( - _( - "{name} argument can't be used without location argument" - ).format(name=name) - ) + _(f"{name} argument can't be used without location argument")) if self.args.importer is None: self.disp( "\n".join( [ - "{}: {}".format(name, desc) - for name, desc in self.host.bridge.ticketsImportList() + f"{name}: {desc}" + for name, desc in await self.host.bridge.ticketsImportList() ] ) ) else: try: - short_desc, long_desc = self.host.bridge.ticketsImportDesc( + short_desc, long_desc = await self.host.bridge.ticketsImportDesc( self.args.importer ) except Exception as e: - msg = [l for l in str(e).split("\n") if l][ - -1 - ] # we only keep the last line - self.disp(msg) - self.host.quit(1) + self.disp(f"can't get importer description: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) else: - self.disp( - "{name}: {short_desc}\n\n{long_desc}".format( - name=self.args.importer, - short_desc=short_desc, - long_desc=long_desc, - ) - ) + self.disp(f"{name}: {short_desc}\n\n{long_desc}") self.host.quit() else: # we have a location, an import is requested + + if self.args.progress: + # we use a custom progress bar template as we want a counter + self.pbar_template = [ + _("Progress: "), ["Percentage"], " ", ["Bar"], " ", + ["Counter"], " ", ["ETA"] + ] + options = {key: value for key, value in self.args.option} fields_map = dict(self.args.map) if fields_map: if FIELDS_MAP in options: self.parser.error( - _( - "fields_map must be specified either preencoded in --option or using --map, but not both at the same time" - ) + _("fields_map must be specified either preencoded in --option or " + "using --map, but not both at the same time") ) options[FIELDS_MAP] = json.dumps(fields_map) - def gotId(id_): - self.progress_id = id_ - - self.host.bridge.ticketsImport( - self.args.importer, - self.args.location, - options, - self.args.service, - self.args.node, - self.profile, - callback=gotId, - errback=self.error, - ) + try: + progress_id = await self.host.bridge.ticketsImport( + self.args.importer, + self.args.location, + options, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp( + _(f"Error while trying to import tickets: {e}"), + error=True, + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.set_progress_id(progress_id) class Ticket(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/cmd_uri.py --- a/sat_frontends/jp/cmd_uri.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/cmd_uri.py Wed Sep 25 08:56:41 2019 +0200 @@ -42,8 +42,9 @@ "uri", help=_("XMPP URI to parse") ) - def start(self): - self.output(uri.parseXMPPUri(self.args.uri)) + async def start(self): + await self.output(uri.parseXMPPUri(self.args.uri)) + self.host.quit() class Build(base.CommandBase): @@ -65,9 +66,10 @@ help=_("URI fields"), ) - def start(self): + async def start(self): fields = dict(self.args.fields) if self.args.fields else {} self.disp(uri.buildXMPPUri(self.args.type, path=self.args.path, **fields)) + self.host.quit() class Uri(base.CommandBase): diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/common.py --- a/sat_frontends/jp/common.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/common.py Wed Sep 25 08:56:41 2019 +0200 @@ -17,6 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import json +import os +import os.path +import time +import tempfile +import asyncio +import shlex +from pathlib import Path from sat_frontends.jp.constants import Const as C from sat.core.i18n import _ from sat.core import exceptions @@ -26,15 +34,6 @@ from sat.tools import config from configparser import NoSectionError, NoOptionError from collections import namedtuple -from functools import partial -import json -import os -import os.path -import time -import tempfile -import subprocess -import glob -import shlex # defaut arguments used for some known editors (editing with metadata) VIM_SPLIT_ARGS = "-c 'set nospr|vsplit|wincmd w|next|wincmd w'" @@ -76,18 +75,18 @@ """Return directory used to store temporary files @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration - @param cat_dir(unicode): directory of the category (e.g. "blog") + @param cat_dir(str): directory of the category (e.g. "blog") @param sub_dir(str): sub directory where data need to be put profile can be used here, or special directory name sub_dir will be escaped to be usable in path (use regex.pathUnescape to find initial str) - @return (str): path to the dir + @return (Path): path to the dir """ local_dir = config.getConfig(sat_conf, "", "local_dir", Exception) - path = [local_dir.encode("utf-8"), cat_dir.encode("utf-8")] + path_elts = [local_dir, cat_dir] if sub_dir is not None: - path.append(regex.pathEscape(sub_dir)) - return os.path.join(*path) + path_elts.append(regex.pathEscape(sub_dir)) + return Path(*path_elts) def parse_args(host, cmd_line, **format_kw): @@ -121,12 +120,14 @@ @param cat_dir(unicode): directory to use for drafts this will be a sub-directory of SàT's local_dir @param use_metadata(bool): True is edition need a second file for metadata - most of signature change with use_metadata with an additional metadata argument. - This is done to raise error if a command needs metadata but forget the flag, and vice versa + most of signature change with use_metadata with an additional metadata + argument. + This is done to raise error if a command needs metadata but forget the flag, + and vice versa """ self.host = host self.sat_conf = config.parseMainConf() - self.cat_dir_str = cat_dir.encode("utf-8") + self.cat_dir = cat_dir self.use_metadata = use_metadata def secureUnlink(self, path): @@ -135,21 +136,21 @@ This method is used to prevent accidental deletion of a draft If there are more file in SECURE_UNLINK_DIR than SECURE_UNLINK_MAX, older file are deleted - @param path(str): file to unlink + @param path(Path, str): file to unlink """ - if not os.path.isfile(path): + path = Path(path).resolve() + if not path.is_file: raise OSError("path must link to a regular file") - if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)): + if path.parent != getTmpDir(self.sat_conf, self.cat_dir): self.disp( - "File {} is not in SàT temporary hierarchy, we do not remove it".format( - path - ), + f"File {path} is not in SàT temporary hierarchy, we do not remove " + f"it", 2, ) return # we have 2 files per draft with use_metadata, so we double max unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX - backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR) + backup_dir = getTmpDir(self.sat_conf, self.cat_dir, SECURE_UNLINK_DIR) if not os.path.exists(backup_dir): os.makedirs(backup_dir) filename = os.path.basename(path) @@ -170,21 +171,15 @@ self.host.disp("Purging backup file {}".format(path), 2) os.unlink(path) - def runEditor( - self, - editor_args_opt, - content_file_path, - content_file_obj, - meta_file_path=None, - meta_ori=None, - ): - """run editor to edit content and metadata + async def runEditor(self, editor_args_opt, content_file_path, content_file_obj, + meta_file_path=None, meta_ori=None): + """Run editor to edit content and metadata @param editor_args_opt(unicode): option in [jp] section in configuration for specific args @param content_file_path(str): path to the content file @param content_file_obj(file): opened file instance - @param meta_file_path(str, None): metadata file path + @param meta_file_path(str, Path, None): metadata file path if None metadata will not be used @param meta_ori(dict, None): original cotent of metadata can't be used if use_metadata is False @@ -223,26 +218,27 @@ args = [content_file_path] # actual editing - editor_exit = subprocess.call([editor] + args) + editor_process = await asyncio.create_subprocess_exec( + editor, *[str(a) for a in args]) + editor_exit = await editor_process.wait() # edition will now be checked, and data will be sent if it was a success if editor_exit != 0: self.disp( - "Editor exited with an error code, so temporary file has not be deleted, and item is not published.\nYou can find temporary file at {path}".format( - path=content_file_path - ), + f"Editor exited with an error code, so temporary file has not be " + f"deleted, and item is not published.\nYou can find temporary file " + f"at {content_file_path}", error=True, ) else: # main content try: - with open(content_file_path, "rb") as f: + with content_file_path.open("rb") as f: content = f.read() except (OSError, IOError): self.disp( - "Can read file at {content_path}, have it been deleted?\nCancelling edition".format( - content_path=content_file_path - ), + f"Can read file at {content_file_path}, have it been deleted?\n" + f"Cancelling edition", error=True, ) self.host.quit(C.EXIT_NOT_FOUND) @@ -250,50 +246,46 @@ # metadata if self.use_metadata: try: - with open(meta_file_path, "rb") as f: + with meta_file_path.open("rb") as f: metadata = json.load(f) except (OSError, IOError): self.disp( - "Can read file at {meta_file_path}, have it been deleted?\nCancelling edition".format( - content_path=content_file_path, meta_path=meta_file_path - ), + f"Can read file at {meta_file_path}, have it been deleted?\n" + f"Cancelling edition", error=True, ) self.host.quit(C.EXIT_NOT_FOUND) except ValueError: self.disp( - "Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" - + "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format( - content_path=content_file_path, meta_path=meta_file_path - ), + f"Can't parse metadata, please check it is correct JSON format. " + f"Cancelling edition.\nYou can find tmp file at " + f"{content_file_path} and temporary meta file at " + f"{meta_file_path}.", error=True, ) self.host.quit(C.EXIT_DATA_ERROR) if self.use_metadata and not metadata.get("publish", True): self.disp( - 'Publication blocked by "publish" key in metadata, cancelling edition.\n\n' - + "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format( - content_path=content_file_path, meta_path=meta_file_path - ), + f'Publication blocked by "publish" key in metadata, cancelling ' + f'edition.\n\ntemporary file path:\t{content_file_path}\nmetadata ' + f'file path:\t{meta_file_path}', error=True, ) self.host.quit() if len(content) == 0: self.disp("Content is empty, cancelling the edition") - if not content_file_path.startswith( - getTmpDir(self.sat_conf, self.cat_dir_str) - ): + if content_file_path.parent != getTmpDir(self.sat_conf, self.cat_dir): self.disp( "File are not in SàT temporary hierarchy, we do not remove them", 2, ) self.host.quit() - self.disp("Deletion of {}".format(content_file_path), 2) + self.disp(f"Deletion of {content_file_path}", 2) os.unlink(content_file_path) if self.use_metadata: - self.disp("Deletion of {}".format(meta_file_path), 2) + self.disp(f"Deletion of {meta_file_path}".format(meta_file_path), 2) os.unlink(meta_file_path) self.host.quit() @@ -309,24 +301,21 @@ content = content.decode("utf-8-sig") # we use utf-8-sig to avoid BOM try: if self.use_metadata: - self.publish(content, metadata) + await self.publish(content, metadata) else: - self.publish(content) + await self.publish(content) except Exception as e: if self.use_metadata: self.disp( - "Error while sending your item, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format( - content_path=content_file_path, - meta_path=meta_file_path, - reason=e, - ), + f"Error while sending your item, the temporary files have " + f"been kept at {content_file_path} and {meta_file_path}: " + f"{e}", error=True, ) else: self.disp( - "Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format( - content_path=content_file_path, reason=e - ), + f"Error while sending your item, the temporary file has been " + f"kept at {content_file_path}: {e}", error=True, ) self.host.quit(1) @@ -335,41 +324,38 @@ if self.use_metadata: self.secureUnlink(meta_file_path) - def publish(self, content): + async def publish(self, content): # if metadata is needed, publish will be called with it last argument raise NotImplementedError def getTmpFile(self): """Create a temporary file - @param suff (str): suffix to use for the filename - @return (tuple(file, str)): opened (w+b) file object and file path + @return (tuple(file, Path)): opened (w+b) file object and file path """ suff = "." + self.getTmpSuff() - cat_dir_str = self.cat_dir_str - tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode("utf-8")) - if not os.path.exists(tmp_dir): + cat_dir_str = self.cat_dir + tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, self.profile) + if not tmp_dir.exists(): try: - os.makedirs(tmp_dir) + tmp_dir.mkdir(parents=True) except OSError as e: self.disp( - "Can't create {path} directory: {reason}".format( - path=tmp_dir, reason=e - ), + f"Can't create {tmp_dir} directory: {e}", error=True, ) self.host.quit(1) try: fd, path = tempfile.mkstemp( - suffix=suff.encode("utf-8"), + suffix=suff, prefix=time.strftime(cat_dir_str + "_%Y-%m-%d_%H:%M:%S_"), dir=tmp_dir, text=True, ) - return os.fdopen(fd, "w+b"), path + return os.fdopen(fd, "w+b"), Path(path) except OSError as e: self.disp( - "Can't create temporary file: {reason}".format(reason=e), error=True + f"Can't create temporary file: {e}", error=True ) self.host.quit(1) @@ -377,27 +363,26 @@ """Get most recently edited file @param profile(unicode): profile linked to the draft - @return(str): full path of current file + @return(Path): full path of current file """ # we guess the item currently edited by choosing # the most recent file corresponding to temp file pattern # in tmp_dir, excluding metadata files - cat_dir_str = self.cat_dir_str - tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode("utf-8")) + tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, profile) available = [ - path - for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + "_*")) - if not path.endswith(METADATA_SUFF) + p + for p in tmp_dir.glob(f'{self.cat_dir}_*') + if not p.match(f"*{METADATA_SUFF}") ] if not available: self.disp( - "Could not find any content draft in {path}".format(path=tmp_dir), + f"Could not find any content draft in {tmp_dir}", error=True, ) self.host.quit(1) - return max(available, key=lambda path: os.stat(path).st_mtime) + return max(available, key=lambda p: p.stat().st_mtime) - def getItemData(self, service, node, item): + async def getItemData(self, service, node, item): """return formatted content, metadata (or not if use_metadata is false), and item id""" raise NotImplementedError @@ -405,8 +390,8 @@ """return suffix used for content file""" return "xml" - def getItemPath(self): - """retrieve item path (i.e. service and node) from item argument + async def getItemPath(self): + """Retrieve item path (i.e. service and node) from item argument This method is obviously only useful for edition of PubSub based features """ @@ -419,15 +404,15 @@ # user wants to continue current draft content_file_path = self.getCurrentFile(self.profile) self.disp("Continuing edition of current draft", 2) - content_file_obj = open(content_file_path, "r+b") + content_file_obj = content_file_path.open("r+b") # we seek at the end of file in case of an item already exist # this will write content of the existing item at the end of the draft. # This way no data should be lost. content_file_obj.seek(0, os.SEEK_END) elif self.args.draft_path: # there is an existing draft that we use - content_file_path = os.path.expanduser(self.args.draft_path) - content_file_obj = open(content_file_path, "r+b") + content_file_path = self.args.draft_path.expanduser() + content_file_obj = content_file_path.open("r+b") # we seek at the end for the same reason as above content_file_obj.seek(0, os.SEEK_END) else: @@ -438,9 +423,9 @@ self.disp("Editing requested published item", 2) try: if self.use_metadata: - content, metadata, item = self.getItemData(service, node, item) + content, metadata, item = await self.getItemData(service, node, item) else: - content, item = self.getItemData(service, node, item) + content, item = await self.getItemData(service, node, item) except Exception as e: # FIXME: ugly but we have not good may to check errors in bridge if "item-not-found" in str(e): @@ -451,13 +436,14 @@ else: self.disp( _( - 'item "{item_id}" not found, we create a new item with this id' - ).format(item_id=item), + f'item "{item}" not found, we create a new item with' + f'this id' + ), 2, ) content_file_obj.seek(0) else: - self.disp("Error while retrieving item: {}".format(e)) + self.disp(f"Error while retrieving item: {e}") self.host.quit(C.EXIT_ERROR) else: # item exists, we write content @@ -468,7 +454,7 @@ content_file_obj.write(content.encode("utf-8")) content_file_obj.seek(0) self.disp( - _('item "{item_id}" found, we edit it').format(item_id=item), 2 + _(f'item "{item}" found, we edit it'), 2 ) else: self.disp("Editing a new item", 2) @@ -757,89 +743,70 @@ return self.display(**kwargs_) -class URIFinder(object): - """Helper class to find URIs in well-known locations""" +async def fill_well_known_uri(command, path, key, meta_map=None): + """Look for URIs in well-known location and fill appropriate args if suitable + + @param command(CommandBase): command instance + args of this instance will be updated with found values + @param path(unicode): absolute path to use as a starting point to look for URIs + @param key(unicode): key to look for + @param meta_map(dict, None): if not None, map metadata to arg name + key is metadata used attribute name + value is name to actually use, or None to ignore + use empty dict to only retrieve URI + possible keys are currently: + - labels + """ + args = command.args + if args.service or args.node: + # we only look for URIs if a service and a node are not already specified + return + + host = command.host - def __init__(self, command, path, key, callback, meta_map=None): - """ - @param command(CommandBase): command instance - args of this instance will be updated with found values - @param path(unicode): absolute path to use as a starting point to look for URIs - @param key(unicode): key to look for - @param callback(callable): method to call once URIs are found (or not) - @param meta_map(dict, None): if not None, map metadata to arg name - key is metadata used attribute name - value is name to actually use, or None to ignore - use empty dict to only retrieve URI - possible keys are currently: - - labels - """ - if not command.args.service and not command.args.node: - self.host = command.host - self.args = command.args - self.key = key - self.callback = callback - self.meta_map = meta_map - self.host.bridge.URIFind( - path, - [key], - callback=self.URIFindCb, - errback=partial( - command.errback, - msg=_("can't find " + key + " URI: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) - else: - callback() + try: + uris_data = await host.bridge.URIFind(path, [key]) + except Exception as e: + host.disp(f"can't find {key} URI: {e}", error=True) + host.quit(C.EXIT_BRIDGE_ERRBACK) - def setMetadataList(self, uri_data, key): - """Helper method to set list of values from metadata + try: + uri_data = uris_data[key] + except KeyError: + host.disp( + _(f"No {key} URI specified for this project, please specify service and " + f"node"), + error=True, + ) + host.quit(C.EXIT_NOT_FOUND) - @param uri_data(dict): data of the found URI - @param key(unicode): key of the value to retrieve - """ - new_values_json = uri_data.get(key) + uri = uri_data["uri"] + + # set extra metadata if they are specified + for data_key in ['labels']: + new_values_json = uri_data.get(data_key) if uri_data is not None: - if self.meta_map is None: - dest = key + if meta_map is None: + dest = data_key else: - dest = self.meta_map.get(key) + dest = meta_map.get(data_key) if dest is None: - return + continue try: - values = getattr(self.args, key) + values = getattr(args, data_key) except AttributeError: - raise exceptions.InternalError( - 'there is no "{key}" arguments'.format(key=key) - ) + raise exceptions.InternalError(f'there is no {data_key!r} arguments') else: if values is None: values = [] values.extend(json.loads(new_values_json)) - setattr(self.args, dest, values) + setattr(args, dest, values) - def URIFindCb(self, uris_data): - try: - uri_data = uris_data[self.key] - except KeyError: - self.host.disp( - _( - "No {key} URI specified for this project, please specify service and node" - ).format(key=self.key), - error=True, - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - uri = uri_data["uri"] - - self.setMetadataList(uri_data, "labels") - parsed_uri = xmpp_uri.parseXMPPUri(uri) - try: - self.args.service = parsed_uri["path"] - self.args.node = parsed_uri["node"] - except KeyError: - self.host.disp(_("Invalid URI found: {uri}").format(uri=uri), error=True) - self.host.quit(C.EXIT_DATA_ERROR) - self.callback() + parsed_uri = xmpp_uri.parseXMPPUri(uri) + try: + args.service = parsed_uri["path"] + args.node = parsed_uri["node"] + except KeyError: + host.disp(_(f"Invalid URI found: {uri}"), error=True) + host.quit(C.EXIT_DATA_ERROR) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/constants.py --- a/sat_frontends/jp/constants.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/constants.py Wed Sep 25 08:56:41 2019 +0200 @@ -66,6 +66,7 @@ A_LEVEL_COLORS = (A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN) A_SUCCESS = A.BOLD + A.FG_GREEN A_FAILURE = A.BOLD + A.FG_RED + A_WARNING = A.BOLD + A.FG_RED #  A_PROMPT_* is for shell A_PROMPT_PATH = A.BOLD + A.FG_CYAN A_PROMPT_SUF = A.BOLD @@ -83,6 +84,7 @@ EXIT_DATA_ERROR = 17 # data needed for a command is invalid EXIT_MISSING_FEATURE = 18 # a needed plugin or feature is not available EXIT_USER_CANCELLED = 20 # user cancelled action + EXIT_INTERNAL_ERROR = 111 # unexpected error EXIT_FILE_NOT_EXE = ( 126 ) # a file to be executed was found, but it was not an executable utility (cf. man 1 exit) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/jp --- a/sat_frontends/jp/jp Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/jp Wed Sep 25 08:56:41 2019 +0200 @@ -21,5 +21,4 @@ if __name__ == "__main__": jp = base.Jp() - jp.import_plugins() jp.run() diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/output_xmlui.py --- a/sat_frontends/jp/output_xmlui.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/output_xmlui.py Wed Sep 25 08:56:41 2019 +0200 @@ -39,11 +39,11 @@ C.OUTPUT_LIST_XMLUI, "simple", self.xmlui_list, default=True ) - def xmlui(self, data): + async def xmlui(self, data): xmlui = xmlui_manager.create(self.host, data) - xmlui.show(values_only=True, read_only=True) + await xmlui.show(values_only=True, read_only=True) self.host.disp("") - def xmlui_list(self, data): + async def xmlui_list(self, data): for d in data: - self.xmlui(d) + await self.xmlui(d) diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/jp/xmlui_manager.py --- a/sat_frontends/jp/xmlui_manager.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/jp/xmlui_manager.py Wed Sep 25 08:56:41 2019 +0200 @@ -17,14 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from functools import partial from sat.core.log import getLogger - -log = getLogger(__name__) from sat_frontends.tools import xmlui as xmlui_base from sat_frontends.jp.constants import Const as C from sat.tools.common.ansi import ANSI as A from sat.core.i18n import _ -from functools import partial + +log = getLogger(__name__) # workflow constants @@ -67,7 +67,7 @@ def name(self): return self._xmlui_name - def show(self): + async def show(self): """display current widget must be overriden by subclasses @@ -178,14 +178,14 @@ def __init__(self, xmlui_parent): Widget.__init__(self, xmlui_parent) - def show(self): + async def show(self): self.host.disp('') class TextWidget(xmlui_base.TextWidget, ValueWidget): type = "text" - def show(self): + async def show(self): self.host.disp(self.value) @@ -199,7 +199,7 @@ except AttributeError: return None - def show(self, no_lf=False, ansi=""): + async def show(self, no_lf=False, ansi=""): """show label @param no_lf(bool): same as for [JP.disp] @@ -214,16 +214,16 @@ class StringWidget(xmlui_base.StringWidget, InputWidget): type = "string" - def show(self): + async def show(self): if self.read_only or self.root.read_only: self.disp(self.value) else: elems = [] self.verboseName(elems) if self.value: - elems.append(_("(enter: {default})").format(default=self.value)) + elems.append(_(f"(enter: {self.value})")) elems.extend([C.A_HEADER, "> "]) - value = input(A.color(*elems).encode('utf-8')) + value = await self.host.ainput(A.color(*elems)) if value: #  TODO: empty value should be possible # an escape key should be used for default instead of enter with empty value @@ -238,7 +238,7 @@ type = "textbox" # TODO: use a more advanced input method - def show(self): + async def show(self): self.verboseName() if self.read_only or self.root.read_only: self.disp(self.value) @@ -251,9 +251,9 @@ while True: try: if not values: - line = input(A.color(C.A_HEADER, "[Ctrl-D to finish]> ")) + line = await self.host.ainput(A.color(C.A_HEADER, "[Ctrl-D to finish]> ")) else: - line = input() + line = await self.host.ainput() values.append(line) except EOFError: break @@ -264,20 +264,20 @@ class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): type = "xhtmlbox" - def show(self): + async def show(self): # FIXME: we use bridge in a blocking way as permitted by python-dbus # this only for now to make it simpler, it must be refactored # to use async when jp will be fully async (expected for 0.8) - self.value = self.host.bridge.syntaxConvert( + self.value = await self.host.bridge.syntaxConvert( self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile) - super(XHTMLBoxWidget, self).show() + await super(XHTMLBoxWidget, self).show() class ListWidget(xmlui_base.ListWidget, OptionsWidget): type = "list" # TODO: handle flags, notably multi - def show(self): + async def show(self): if self.root.values_only: for value in self.values: self.disp(self.value) @@ -308,8 +308,8 @@ choice = None limit_max = len(self.options) - 1 while choice is None or choice < 0 or choice > limit_max: - choice = input( - A.color(C.A_HEADER, _("your choice (0-{max}): ").format(max=limit_max)) + choice = await self.host.ainput( + A.color(C.A_HEADER, _(f"your choice (0-{limit_max}): ")) ) try: choice = int(choice) @@ -322,7 +322,7 @@ class BoolWidget(xmlui_base.BoolWidget, InputWidget): type = "bool" - def show(self): + async def show(self): disp_true = A.color(A.FG_GREEN, "TRUE") disp_false = A.color(A.FG_RED, "FALSE") if self.read_only or self.root.read_only: @@ -338,7 +338,7 @@ while choice not in ("0", "1"): elems = [C.A_HEADER, _("your choice (0,1): ")] self.verboseName(elems) - choice = input(A.color(*elems)) + choice = await self.host.ainput(A.color(*elems)) self.value = bool(int(choice)) self.disp("") @@ -365,9 +365,9 @@ def _xmluiRemove(self, widget): self.children.remove(widget) - def show(self): + async def show(self): for child in self.children: - child.show() + await child.show() class VerticalContainer(xmlui_base.VerticalContainer, Container): @@ -381,7 +381,7 @@ class LabelContainer(xmlui_base.PairsContainer, Container): type = "label" - def show(self): + async def show(self): for child in self.children: no_lf = False # we check linked widget type @@ -399,9 +399,9 @@ no_lf = True elif wid_type == "bool" and for_widget.read_only: no_lf = True - child.show(no_lf=no_lf, ansi=A.FG_CYAN) + await child.show(no_lf=no_lf, ansi=A.FG_CYAN) else: - child.show() + await child.show() ## Dialogs ## @@ -415,24 +415,61 @@ def disp(self, *args, **kwargs): self.host.disp(*args, **kwargs) - def show(self): + async def show(self): """display current dialog must be overriden by subclasses """ raise NotImplementedError(self.__class__) +class MessageDialog(xmlui_base.MessageDialog, Dialog): + + def __init__(self, xmlui_parent, title, message, level): + Dialog.__init__(self, xmlui_parent) + xmlui_base.MessageDialog.__init__(self, xmlui_parent) + self.title, self.message, self.level = title, message, level + + async def show(self): + # TODO: handle level + if self.title: + self.disp(A.color(C.A_HEADER, self.title)) + self.disp(self.message) + class NoteDialog(xmlui_base.NoteDialog, Dialog): - def show(self): - # TODO: handle title and level - self.disp(self.message) def __init__(self, xmlui_parent, title, message, level): Dialog.__init__(self, xmlui_parent) xmlui_base.NoteDialog.__init__(self, xmlui_parent) self.title, self.message, self.level = title, message, level + async def show(self): + # TODO: handle title and level + self.disp(self.message) + + +class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog): + + def __init__(self, xmlui_parent, title, message, level, buttons_set): + Dialog.__init__(self, xmlui_parent) + xmlui_base.ConfirmDialog.__init__(self, xmlui_parent) + self.title, self.message, self.level, self.buttons_set = ( + title, message, level, buttons_set) + + async def show(self): + # TODO: handle buttons_set and level + self.disp(self.message) + if self.title: + self.disp(A.color(C.A_HEADER, self.title)) + input_ = None + while input_ not in ('y', 'n'): + input_ = await self.host.ainput(f"{self.message} (y/n)? ") + input_ = input_.lower() + if input_ == 'y': + self._xmluiValidated() + else: + self._xmluiCancelled() + ## Factory ## @@ -444,7 +481,7 @@ return cls -class XMLUIPanel(xmlui_base.XMLUIPanel): +class XMLUIPanel(xmlui_base.AIOXMLUIPanel): widget_factory = WidgetFactory() _actions = 0 # use to keep track of bridge's launchAction calls read_only = False @@ -470,7 +507,7 @@ def command(self): return self.host.command - def show(self, workflow=None, read_only=False, values_only=False): + async def show(self, workflow=None, read_only=False, values_only=False): """display the panel @param workflow(list, None): command to execute if not None @@ -489,11 +526,11 @@ if workflow: XMLUIPanel.workflow = workflow if XMLUIPanel.workflow: - self.runWorkflow() + await self.runWorkflow() else: - self.main_cont.show() + await self.main_cont.show() - def runWorkflow(self): + async def runWorkflow(self): """loop into workflow commands and execute commands SUBMIT will interrupt workflow (which will be continue on callback) @@ -506,7 +543,7 @@ except IndexError: break if cmd == SUBMIT: - self.onFormSubmitted() + await self.onFormSubmitted() self.submit_id = None # avoid double submit return elif isinstance(cmd, list): @@ -515,32 +552,32 @@ if widget.type == "bool": value = C.bool(value) widget.value = value - self.show() + await self.show() - def submitForm(self, callback=None): + async def submitForm(self, callback=None): XMLUIPanel._submit_cb = callback - self.onFormSubmitted() + await self.onFormSubmitted() - def onFormSubmitted(self, ignore=None): - #  self.submitted is a Q&D workaround to avoid + async def onFormSubmitted(self, ignore=None): + # self.submitted is a Q&D workaround to avoid # double submit when a workflow is set if self.submitted: return self.submitted = True - super(XMLUIPanel, self).onFormSubmitted(ignore) + await super(XMLUIPanel, self).onFormSubmitted(ignore) def _xmluiClose(self): pass - def _launchActionCb(self, data): + async def _launchActionCb(self, data): XMLUIPanel._actions -= 1 assert XMLUIPanel._actions >= 0 if "xmlui" in data: xmlui_raw = data["xmlui"] xmlui = create(self.host, xmlui_raw) - xmlui.show() + await xmlui.show() if xmlui.submit_id: - xmlui.onFormSubmitted() + await xmlui.onFormSubmitted() # TODO: handle data other than XMLUI if not XMLUIPanel._actions: if self._submit_cb is None: @@ -548,19 +585,19 @@ else: self._submit_cb() - def _xmluiLaunchAction(self, action_id, data): + async def _xmluiLaunchAction(self, action_id, data): XMLUIPanel._actions += 1 - self.host.bridge.launchAction( - action_id, - data, - self.profile, - callback=self._launchActionCb, - errback=partial( - self.command.errback, - msg=_("can't launch XMLUI action: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK, - ), - ) + try: + data = await self.host.bridge.launchAction( + action_id, + data, + self.profile, + ) + except Exception as e: + self.disp(f"can't launch XMLUI action: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self._launchActionCb(data) class XMLUIDialog(xmlui_base.XMLUIDialog): @@ -568,8 +605,8 @@ dialog_factory = WidgetFactory() read_only = False - def show(self, __=None): - self.dlg.show() + async def show(self, __=None): + await self.dlg.show() def _xmluiClose(self): pass diff -r a1bc34f90fa5 -r fee60f17ebac sat_frontends/tools/xmlui.py --- a/sat_frontends/tools/xmlui.py Wed Sep 25 08:53:38 2019 +0200 +++ b/sat_frontends/tools/xmlui.py Wed Sep 25 08:56:41 2019 +0200 @@ -925,6 +925,67 @@ pass +class AIOXMLUIPanel(XMLUIPanel): + """Asyncio compatible version of XMLUIPanel""" + + async def onFormSubmitted(self, ignore=None): + """An XMLUI form has been submited + + call the submit action associated with this form + """ + selected_values = [] + for ctrl_name in self.ctrl_list: + escaped = self.escape(ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl["control"], ListWidget): + selected_values.append( + (escaped, "\t".join(ctrl["control"]._xmluiGetSelectedValues())) + ) + else: + selected_values.append((escaped, ctrl["control"]._xmluiGetValue())) + data = dict(selected_values) + for key, value in self.hidden.items(): + data[self.escape(key)] = value + + if self.submit_id is not None: + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmluiClose() + + async def onFormCancelled(self, *__): + """Called when a form is cancelled""" + log.debug(_("Cancelling form")) + if self.submit_id is not None: + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmluiClose() + + async def submit(self, data): + self._xmluiClose() + if self.submit_id is None: + raise ValueError("Can't submit is self.submit_id is not set") + if "session_id" in data: + raise ValueError( + "session_id must no be used in data, it is automaticaly filled with " + "self.session_id if present" + ) + if self.session_id is not None: + data["session_id"] = self.session_id + await self._xmluiLaunchAction(self.submit_id, data) + + async def _xmluiLaunchAction(self, action_id, data): + await self.host.launchAction( + action_id, data, callback=self.callback, profile=self.profile + ) + + class XMLUIDialog(XMLUIBase): dialog_factory = None diff -r a1bc34f90fa5 -r fee60f17ebac setup.py --- a/setup.py Wed Sep 25 08:53:38 2019 +0200 +++ b/setup.py Wed Sep 25 08:56:41 2019 +0200 @@ -87,7 +87,6 @@ url="https://salut-a-toi.org", classifiers=[ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -110,5 +109,5 @@ use_scm_version=sat_dev_version if is_dev_version else False, install_requires=install_requires, package_data={"sat": ["VERSION"]}, - python_requires=">=3.6", + python_requires=">=3.7", ) diff -r a1bc34f90fa5 -r fee60f17ebac twisted/plugins/sat_plugin.py --- a/twisted/plugins/sat_plugin.py Wed Sep 25 08:53:38 2019 +0200 +++ b/twisted/plugins/sat_plugin.py Wed Sep 25 08:56:41 2019 +0200 @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # SAT: a jabber client