element")
file_elt.addElement("range")
elif action == self._j.A_SESSION_ACCEPT:
@@ -596,21 +579,32 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 --git a/sat/plugins/plugin_xep_0300.py b/sat/plugins/plugin_xep_0300.py
index 58c60727..ca50cb7d 100644
--- a/sat/plugins/plugin_xep_0300.py
+++ b/sat/plugins/plugin_xep_0300.py
@@ -196,13 +196,13 @@ class XEP_0300(object):
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 --git a/sat/plugins/plugin_xep_0329.py b/sat/plugins/plugin_xep_0329.py
index a581d981..ffa6073d 100644
--- a/sat/plugins/plugin_xep_0329.py
+++ b/sat/plugins/plugin_xep_0329.py
@@ -595,10 +595,14 @@ class XEP_0329(object):
):
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 @@ class XEP_0329(object):
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 @@ class XEP_0329(object):
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 --git a/sat/plugins/plugin_xep_0363.py b/sat/plugins/plugin_xep_0363.py
index 3a567a3c..a491bbe1 100644
--- a/sat/plugins/plugin_xep_0363.py
+++ b/sat/plugins/plugin_xep_0363.py
@@ -36,7 +36,6 @@ from twisted.web import http_headers
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 @@ class XEP_0363(object):
@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 @@ class XEP_0363(object):
headers[name] = value
d = agent.request(
- "PUT",
+ b"PUT",
slot.put.encode("utf-8"),
http_headers.Headers(headers),
file_producer,
diff --git a/sat/tools/common/async_process.py b/sat/tools/common/async_process.py
index 686673e3..1cd6e362 100644
--- a/sat/tools/common/async_process.py
+++ b/sat/tools/common/async_process.py
@@ -76,8 +76,7 @@ class CommandProtocol(protocol.ProcessProtocol):
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 @@ class CommandProtocol(protocol.ProcessProtocol):
- 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 @@ class CommandProtocol(protocol.ProcessProtocol):
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 --git a/sat_frontends/jp/base.py b/sat_frontends/jp/base.py
index a946842f..1a6366af 100644
--- a/sat_frontends/jp/base.py
+++ b/sat_frontends/jp/base.py
@@ -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 @@ import sat_frontends.jp
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 @@ if 'dbus' in bridge_name:
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 @@ if 'dbus' in bridge_name:
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 run(self):
- # reactor.run()
+ 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 quit(self):
- # reactor.stop()
+ 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 _timeout_cb(self, args, callback, delay):
- # ret = callback(*args)
- # if ret:
- # reactor.callLater(delay, self._timeout_cb, args, callback, delay)
+ 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):
+ 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 _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)
- # def call_later(self, delay, callback, *args):
- # delay = float(delay) / 1000
- # reactor.callLater(delay, self._timeout_cb, args, callback, delay)
if bridge_name == "embedded":
from sat.core import sat_main
@@ -107,9 +157,10 @@ if bridge_name == "embedded":
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 program comes with ABSOLUTELY NO WARRANTY;
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
}
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 @@ class Jp(object):
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 @@ class Jp(object):
"""
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
# 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 @@ class Jp(object):
if self.args.max is None:
self.args.max = C.NO_LIMIT
+ async def main(self, args, namespace):
+ try:
+ 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 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()
- try:
- self.args._cmd.run()
- if self._need_loop or self._auto_loop:
- self._start_loop()
- except KeyboardInterrupt:
- self.disp(_("User interruption: good bye"))
-
- def _start_loop(self):
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, errcode=0):
+ def quit(self, exit_code=0, raise_exc=True):
+ """Terminate the execution with specified exit_code
+
+ 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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class Jp(object):
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 @@ class CommandBase(object):
@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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
# --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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
# 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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
"""
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 @@ class CommandBase(object):
"""
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 @@ class CommandBase(object):
"""
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
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 @@ class CommandBase(object):
"""
# 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 @@ class CommandBase(object):
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()
+ await self.host.connect_profile()
+ await self.start()
- def connected(self):
- """this method is called when profile is connected (or session is started)
-
- 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 @@ class CommandAnswering(CommandBase):
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 @@ class CommandAnswering(CommandBase):
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 @@ class CommandAnswering(CommandBase):
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 --git a/sat_frontends/jp/cmd_account.py b/sat_frontends/jp/cmd_account.py
index e1e4e46b..f911190e 100644
--- a/sat_frontends/jp/cmd_account.py
+++ b/sat_frontends/jp/cmd_account.py
@@ -39,7 +39,6 @@ class AccountCreate(base.CommandBase):
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 @@ class AccountCreate(base.CommandBase):
"--port",
type=int,
default=0,
- help=_("server port (IP address or domain, default: use localhost)"),
+ help=_(f"server port (default: {C.XMPP_C2S_PORT})"),
)
- def _setParamCb(self):
- self.host.bridge.setParam(
- "Password",
- self.args.password,
- "Connection",
- profile_key=self.args.profile,
- callback=self.host.quit,
- 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 _session_started(self, __):
- self.host.bridge.setParam(
- "JabberID",
- self.args.jid,
- "Connection",
- profile_key=self.args.profile,
- callback=self._setParamCb,
- errback=self.errback,
- )
-
- 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,
- )
-
- 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)
-
- 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(
+
+ if self.args.profile is None:
+ self.host.quit()
+
+
+ self.disp(_("creating profile"), 2)
+ try:
+ await self.host.bridge.profileCreate(
self.args.profile,
self.args.password,
"",
- callback=self._profileCreateCb,
- errback=self._profileCreateEb,
)
- else:
- self.host.quit()
+ 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,
+ )
+ except Exception as e:
+ self.disp(f"can't start profile session: {e}", error=True)
+ self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+
+ 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)
- def accountNewEb(self, failure_):
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 @@ class AccountModify(base.CommandBase):
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 @@ class AccountDelete(base.CommandBase):
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 @@ class AccountDelete(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_adhoc.py b/sat_frontends/jp/cmd_adhoc.py
index c2baf202..adc6713f 100644
--- a/sat_frontends/jp/cmd_adhoc.py
+++ b/sat_frontends/jp/cmd_adhoc.py
@@ -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 @@ class Remote(base.CommandBase):
"-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 @@ class Remote(base.CommandBase):
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 @@ class Run(base.CommandBase):
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 @@ class Run(base.CommandBase):
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 @@ class List(base.CommandBase):
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 @@ class List(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_avatar.py b/sat_frontends/jp/cmd_avatar.py
index b29c0e3d..dfababa4 100644
--- a/sat_frontends/jp/cmd_avatar.py
+++ b/sat_frontends/jp/cmd_avatar.py
@@ -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 @@ class Get(base.CommandBase):
"-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)
- def start(self):
- self.host.bridge.avatarGet(
- self.args.jid,
- False,
- False,
- self.profile,
- callback=self._avatarGetCb,
- errback=self._avatarGetEb,
+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")
+ )
+
+ 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 --git a/sat_frontends/jp/cmd_blog.py b/sat_frontends/jp/cmd_blog.py
index d4f84acf..34205a32 100644
--- a/sat_frontends/jp/cmd_blog.py
+++ b/sat_frontends/jp/cmd_blog.py
@@ -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 data_objects
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 @@ KEY_TO_REMOVE_METADATA = (
)
URL_REDIRECT_PREFIX = "url_redirect_"
-INOTIFY_INSTALL = '"pip install inotify"'
+AIONOTIFY_INSTALL = '"pip install aionotify"'
MB_KEYS = (
"id",
"url",
@@ -86,7 +87,7 @@ MB_KEYS = (
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 @@ def guessSyntaxFromPath(host, sat_conf, path):
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 @@ class BlogPublishCommon(object):
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 @@ class Set(base.CommandBase, BlogPublishCommon):
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 @@ class Get(base.CommandBase):
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 @@ class Get(base.CommandBase):
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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
"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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
# 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 @@ class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
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 @@ class Preview(base.CommandBase, common.BaseEdit):
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 @@ class Preview(base.CommandBase, common.BaseEdit):
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 @@ class Preview(base.CommandBase, common.BaseEdit):
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 @@ class Preview(base.CommandBase, common.BaseEdit):
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 @@ class Import(base.CommandAnswering):
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 @@ class Import(base.CommandAnswering):
),
)
- 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 @@ class Import(base.CommandAnswering):
)
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 @@ class Import(base.CommandAnswering):
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 @@ class Import(base.CommandAnswering):
elif self.args.upload_ignore_host:
options["upload_ignore_host"] = self.args.upload_ignore_host
- def gotId(id_):
- self.progress_id = id_
-
- self.host.bridge.blogImport(
- 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.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)
+ await self.set_progress_id(progress_id)
class Blog(base.CommandBase):
subcommands = (Set, Get, Edit, Preview, Import)
diff --git a/sat_frontends/jp/cmd_bookmarks.py b/sat_frontends/jp/cmd_bookmarks.py
index 701c90be..c2836423 100644
--- a/sat_frontends/jp/cmd_bookmarks.py
+++ b/sat_frontends/jp/cmd_bookmarks.py
@@ -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 @@ class BookmarksCommon(base.CommandBase):
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 @@ class BookmarksList(BookmarksCommon):
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"),)
- 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)
+ async def start(self):
+ if not self.args.force:
+ await self.host.confirmOrQuit(_("Are you sure to delete this bookmark?"))
+
+ 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 @@ class BookmarksAdd(BookmarksCommon):
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 --git a/sat_frontends/jp/cmd_debug.py b/sat_frontends/jp/cmd_debug.py
index 84684ac8..bd5161f8 100644
--- a/sat_frontends/jp/cmd_debug.py
+++ b/sat_frontends/jp/cmd_debug.py
@@ -48,7 +48,6 @@ class Method(base.CommandBase, BridgeCommon):
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 @@ class Method(base.CommandBase, BridgeCommon):
"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 @@ class Signal(base.CommandBase, BridgeCommon):
"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 @@ class Monitor(base.CommandBase):
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 @@ class Monitor(base.CommandBase):
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 @@ class Monitor(base.CommandBase):
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 @@ class Monitor(base.CommandBase):
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 @@ class Monitor(base.CommandBase):
self.disp(xml_data)
self.disp("")
- def start(self):
+ async def start(self):
self.host.bridge.register_signal("xmlLog", self.printXML, "plugin")
diff --git a/sat_frontends/jp/cmd_encryption.py b/sat_frontends/jp/cmd_encryption.py
index ab21b007..68824d22 100644
--- a/sat_frontends/jp/cmd_encryption.py
+++ b/sat_frontends/jp/cmd_encryption.py
@@ -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,33 +36,26 @@ class EncryptionAlgorithms(base.CommandBase):
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])))
- 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,
- ),
- )
+ 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()
class EncryptionGet(base.CommandBase):
@@ -73,7 +65,6 @@ class EncryptionGet(base.CommandBase):
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 @@ class EncryptionGet(base.CommandBase):
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 @@ class EncryptionStart(base.CommandBase):
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 @@ class EncryptionStart(base.CommandBase):
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,
- ))
-
- def start(self):
+ async 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,
- ))
+ 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:
- self.encryptionNamespaceGetCb(self.args.namespace)
+ namespace = self.args.namespace
else:
- self.encryptionNamespaceGetCb("")
+ namespace = ""
+
+ 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 @@ class EncryptionStop(base.CommandBase):
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 @@ class EncryptionStop(base.CommandBase):
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 @@ class TrustUI(base.CommandBase):
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 @@ class TrustUI(base.CommandBase):
"-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()
-
- def encryptionNamespaceGetCb(self, namespace):
- jids = 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):
+ async 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,
- ))
+ 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:
- self.encryptionNamespaceGetCb(self.args.namespace)
+ namespace = self.args.namespace
else:
- self.encryptionNamespaceGetCb("")
+ namespace = ""
+ jids = await self.host.check_jids([self.args.jid])
+ jid = jids[0]
+
+ 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 --git a/sat_frontends/jp/cmd_event.py b/sat_frontends/jp/cmd_event.py
index 7b6f4c4c..c5c89543 100644
--- a/sat_frontends/jp/cmd_event.py
+++ b/sat_frontends/jp/cmd_event.py
@@ -23,7 +23,6 @@ from sat.core.i18n import _
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 @@ class Get(base.CommandBase):
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 @@ class Create(EventBase, base.CommandBase):
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 @@ class Modify(EventBase, base.CommandBase):
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,32 +180,31 @@ class InviteeGet(base.CommandBase):
"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()
-
- 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,
- ),
+ self.parser.add_argument(
+ "-j", "--jid", default="", help=_("bare jid of the invitee")
)
+ 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):
def __init__(self, host):
@@ -227,7 +216,6 @@ class InviteeSet(base.CommandBase):
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 @@ class InviteeSet(base.CommandBase):
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 @@ class InviteesList(base.CommandBase):
use_verbose=True,
help=_("get event attendance"),
)
- self.need_loop = True
def add_parser_options(self):
self.parser.add_argument(
@@ -399,72 +386,68 @@ class InviteesList(base.CommandBase):
)
)
- 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 @@ class InviteeInvite(base.CommandBase):
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 @@ class InviteeInvite(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_file.py b/sat_frontends/jp/cmd_file.py
index 9441f5b5..80fb79eb 100644
--- a/sat_frontends/jp/cmd_file.py
+++ b/sat_frontends/jp/cmd_file.py
@@ -31,7 +31,6 @@ from sat_frontends.tools import jid
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 @@ class Send(base.CommandBase):
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 @@ class Send(base.CommandBase):
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 @@ class Send(base.CommandBase):
# 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 @@ class Send(base.CommandBase):
)
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 @@ class Send(base.CommandBase):
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 @@ class Request(base.CommandBase):
use_verbose=True,
help=_("request a file from a contact"),
)
- self.need_loop = True
@property
def filename(self):
@@ -200,7 +192,8 @@ class Request(base.CommandBase):
"-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 @@ class Request(base.CommandBase):
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 @@ class Request(base.CommandBase):
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 @@ class Receive(base.CommandAnswering):
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 @@ class Receive(base.CommandAnswering):
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 @@ class Upload(base.CommandBase):
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 @@ class Upload(base.CommandBase):
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 @@ class Upload(base.CommandBase):
# 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)
+
+ 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)
- self.full_dest_jid = (
- self.host.get_full_jid(self.args.jid) if self.args.jid is not None else ""
- )
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 @@ class ShareList(base.CommandBase):
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 @@ class ShareList(base.CommandBase):
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 @@ class ShareList(base.CommandBase):
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 @@ class SharePath(base.CommandBase):
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 @@ class SharePath(base.CommandBase):
perm_group.add_argument(
"-j",
"--jid",
+ metavar="JID",
action="append",
dest="jids",
default=[],
@@ -649,7 +609,8 @@ class SharePath(base.CommandBase):
"--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 @@ class SharePath(base.CommandBase):
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 @@ class SharePath(base.CommandBase):
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 @@ class ShareInvite(base.CommandBase):
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 @@ class ShareInvite(base.CommandBase):
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 @@ class ShareInvite(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_forums.py b/sat_frontends/jp/cmd_forums.py
index 02fc806b..7137b8d3 100644
--- a/sat_frontends/jp/cmd_forums.py
+++ b/sat_frontends/jp/cmd_forums.py
@@ -23,7 +23,6 @@ from sat.core.i18n import _
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 @@ class Edit(base.CommandBase, common.BaseEdit):
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 @@ class Edit(base.CommandBase, common.BaseEdit):
"""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 @@ class Edit(base.CommandBase, common.BaseEdit):
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 @@ class Get(base.CommandBase):
use_verbose=True,
help=_("get forums structure"),
)
- self.need_loop = True
def add_parser_options(self):
self.parser.add_argument(
@@ -165,27 +152,24 @@ class Get(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_identity.py b/sat_frontends/jp/cmd_identity.py
index f624c78d..cd363c9c 100644
--- a/sat_frontends/jp/cmd_identity.py
+++ b/sat_frontends/jp/cmd_identity.py
@@ -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 @@ class Get(base.CommandBase):
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 @@ class Set(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_info.py b/sat_frontends/jp/cmd_info.py
index 65541219..431fc50e 100644
--- a/sat_frontends/jp/cmd_info.py
+++ b/sat_frontends/jp/cmd_info.py
@@ -31,53 +31,19 @@ class Disco(base.CommandBase):
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 @@ class Disco(base.CommandBase):
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 @@ class Disco(base.CommandBase):
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)
+ 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 error(self, failure):
- print((_("Error while trying to get version [%s]") % failure))
- self.host.quit(1)
-
- 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
-
- 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)
+ super(Session, self).__init__(
+ host, 'session', use_output='dict', extra_outputs=extra_outputs,
+ help=_('running session'))
def add_parser_options(self):
pass
- def start(self):
- self.host.bridge.sessionInfosGet(self.host.profile, callback=self._sessionInfoGetCb, errback=self._sessionInfoGetEb)
+ 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'))
+ 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 --git a/sat_frontends/jp/cmd_input.py b/sat_frontends/jp/cmd_input.py
index 5f5cc0bb..81103ddc 100644
--- a/sat_frontends/jp/cmd_input.py
+++ b/sat_frontends/jp/cmd_input.py
@@ -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 @@ class InputCommon(base.CommandBase):
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 @@ class InputCommon(base.CommandBase):
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 @@ class InputCommon(base.CommandBase):
).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 @@ class InputCommon(base.CommandBase):
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 @@ class InputCommon(base.CommandBase):
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 @@ class Csv(InputCommon):
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 --git a/sat_frontends/jp/cmd_invitation.py b/sat_frontends/jp/cmd_invitation.py
index e312ae00..8baaac70 100644
--- a/sat_frontends/jp/cmd_invitation.py
+++ b/sat_frontends/jp/cmd_invitation.py
@@ -23,7 +23,6 @@ from sat.core.i18n import _
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 @@ class Create(base.CommandBase):
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 @@ class Create(base.CommandBase):
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 @@ class Create(base.CommandBase):
_("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 @@ class Get(base.CommandBase):
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 @@ class Get(base.CommandBase):
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:
- profile = invitation_data["guest_profile"]
+ 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)
- def session_started(__):
- self.host.bridge.asyncGetParamA(
+ 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)
+
+ 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 @@ class Modify(base.CommandBase):
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 @@ class Modify(base.CommandBase):
"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 @@ class Modify(base.CommandBase):
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 @@ class List(base.CommandBase):
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 @@ class List(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_merge_request.py b/sat_frontends/jp/cmd_merge_request.py
index 98f5195e..a18b9c1a 100644
--- a/sat_frontends/jp/cmd_merge_request.py
+++ b/sat_frontends/jp/cmd_merge_request.py
@@ -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 @@ class Set(base.CommandBase):
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 @@ class Set(base.CommandBase):
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 @@ class Get(base.CommandBase):
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 @@ class Import(base.CommandBase):
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 @@ class Import(base.CommandBase):
help=_("path of the repository (DEFAULT: current directory)"),
)
- def mergeRequestImportCb(self):
- self.host.quit(C.EXIT_OK)
-
- def importRequest(self):
- 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):
+ async 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={}
- )
+ await common.fill_well_known_uri(
+ self, self.repository, "merge requests", meta_map={})
+ extra = {}
+ 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 --git a/sat_frontends/jp/cmd_message.py b/sat_frontends/jp/cmd_message.py
index a7bb5b28..e94e391e 100644
--- a/sat_frontends/jp/cmd_message.py
+++ b/sat_frontends/jp/cmd_message.py
@@ -25,7 +25,6 @@ from sat.core.i18n import _
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 @@ __commands__ = ["Message"]
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 @@ class Send(base.CommandBase):
"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 @@ class Send(base.CommandBase):
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(
- dest_jid,
- {self.args.lang: header},
- subject,
- self.args.type,
- profile_key=self.profile,
- callback=lambda: None,
- errback=lambda ignore: ignore,
- )
+ # 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
- self.to_send += len(stdin_lines)
- for line in stdin_lines:
- self.host.bridge.messageSend(
+ 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: 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,28 +222,7 @@ class MAM(base.CommandBase):
"--index", dest="rsm_index", type=int,
help=_("index of the page to retrieve"))
- def _sessionInfosGetCb(self, session_info, data, metadata):
- self.host.own_jid = jid.JID(session_info["jid"])
- 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:
- for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count",
- "mam_complete", "mam_stable"):
- if value in metadata:
- label = value.split("_")[1]
- self.disp(A.color(
- C.A_HEADER, label, ': ' , A.RESET, metadata[value]))
-
- 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):
+ async def start(self):
extra = {}
if self.args.mam_start is not None:
extra["mam_start"] = float(self.args.mam_start)
@@ -269,9 +235,35 @@ class MAM(base.CommandBase):
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)
+ 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"])
+
+ 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:
+ for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count",
+ "mam_complete", "mam_stable"):
+ if value in metadata:
+ label = value.split("_")[1]
+ self.disp(A.color(
+ C.A_HEADER, label, ': ' , A.RESET, metadata[value]))
+
+ self.host.quit()
class Message(base.CommandBase):
diff --git a/sat_frontends/jp/cmd_param.py b/sat_frontends/jp/cmd_param.py
index 8557165b..5909e62a 100644
--- a/sat_frontends/jp/cmd_param.py
+++ b/sat_frontends/jp/cmd_param.py
@@ -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 --git a/sat_frontends/jp/cmd_ping.py b/sat_frontends/jp/cmd_ping.py
index 4ac2c652..2aa42119 100644
--- a/sat_frontends/jp/cmd_ping.py
+++ b/sat_frontends/jp/cmd_ping.py
@@ -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 @@ class Ping(base.CommandBase):
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 @@ class Ping(base.CommandBase):
"-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 --git a/sat_frontends/jp/cmd_pipe.py b/sat_frontends/jp/cmd_pipe.py
index 8e977ed7..e61d2da5 100644
--- a/sat_frontends/jp/cmd_pipe.py
+++ b/sat_frontends/jp/cmd_pipe.py
@@ -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 @@ START_PORT = 9999
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()
-
- def start(self):
+ async 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,
- ),
- )
+ 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.
+
+ 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 @@ class PipeIn(base.CommandAnswering):
)
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 @@ class PipeIn(base.CommandAnswering):
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 --git a/sat_frontends/jp/cmd_profile.py b/sat_frontends/jp/cmd_profile.py
index eeeb3065..026a69b9 100644
--- a/sat_frontends/jp/cmd_profile.py
+++ b/sat_frontends/jp/cmd_profile.py
@@ -25,7 +25,6 @@ from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core.i18n import _
from sat_frontends.jp import base
-from functools import partial
__commands__ = ["Profile"]
@@ -43,18 +42,73 @@ class ProfileConnect(base.CommandBase):
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 @@ class ProfileDefault(base.CommandBase):
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 @@ class ProfileDelete(base.CommandBase):
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 @@ class ProfileList(base.CommandBase):
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 @@ class ProfileModify(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_pubsub.py b/sat_frontends/jp/cmd_pubsub.py
index f9e63a91..c7c1215a 100644
--- a/sat_frontends/jp/cmd_pubsub.py
+++ b/sat_frontends/jp/cmd_pubsub.py
@@ -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 functools import partial
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 @@ class NodeInfo(base.CommandBase):
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 @@ class NodeInfo(base.CommandBase):
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 @@ class NodeCreate(base.CommandBase):
use_verbose=True,
help=_("create a node"),
)
- self.need_loop = True
def add_parser_options(self):
self.parser.add_argument(
@@ -128,35 +122,28 @@ class NodeCreate(base.CommandBase):
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 @@ class NodePurge(base.CommandBase):
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 @@ class NodePurge(base.CommandBase):
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 @@ class NodeDelete(base.CommandBase):
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 @@ class NodeDelete(base.CommandBase):
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 @@ class NodeSet(base.CommandBase):
use_verbose=True,
help=_("set node configuration"),
)
- self.need_loop = True
def add_parser_options(self):
self.parser.add_argument(
@@ -284,32 +257,33 @@ class NodeSet(base.CommandBase):
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#"):
- return "pubsub#" + k
- else:
+ if self.args.full_prefix or k.startswith("pubsub#"):
return k
+ else:
+ return "pubsub#" + 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 @@ class NodeImport(base.CommandBase):
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 @@ class NodeImport(base.CommandBase):
"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 @@ class NodeImport(base.CommandBase):
_("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(
- 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,
- ),
- )
+ method = self.host.bridge.psAdminItemsSend
else:
self.disp(_("Items are imported without using admin mode, publisher can't "
"be changed"))
- self.host.bridge.psItemsSend(
+ 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:
+ 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 @@ class NodeAffiliationsGet(base.CommandBase):
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 @@ class NodeAffiliationsSet(base.CommandBase):
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 @@ class NodeAffiliationsSet(base.CommandBase):
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 @@ class NodeSubscriptionsGet(base.CommandBase):
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 @@ class NodeSubscriptionsSet(base.CommandBase):
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 @@ class NodeSubscriptionsSet(base.CommandBase):
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 @@ class NodeSchemaSet(base.CommandBase):
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 @@ class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
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 @@ class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
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 @@ class NodeSchemaGet(base.CommandBase):
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 @@ class Set(base.CommandBase):
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 @@ class Set(base.CommandBase):
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 @@ class Get(base.CommandBase):
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 @@ class Get(base.CommandBase):
# 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 @@ class Delete(base.CommandBase):
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 @@ class Delete(base.CommandBase):
"-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 @@ class Edit(base.CommandBase, common.BaseEdit):
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 @@ class Edit(base.CommandBase, common.BaseEdit):
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 @@ class Edit(base.CommandBase, common.BaseEdit):
)
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 @@ class Edit(base.CommandBase, common.BaseEdit):
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 @@ class Subscribe(base.CommandBase):
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 @@ class Unsubscribe(base.CommandBase):
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 @@ class Subscriptions(base.CommandBase):
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 @@ class Affiliations(base.CommandBase):
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 @@ class Search(base.CommandBase):
use_verbose=True,
help=_("search items corresponding to filters"),
)
- self.need_loop = True
@property
def etree(self):
@@ -1317,30 +1216,26 @@ class Search(base.CommandBase):
)
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Search(base.CommandBase):
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 @@ class Transform(base.CommandBase):
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 @@ class Transform(base.CommandBase):
'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 @@ class Transform(base.CommandBase):
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 @@ class Transform(base.CommandBase):
# 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 @@ class Transform(base.CommandBase):
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(
- 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,
- ),
- )
+ bridge_method = self.host.bridge.psAdminItemsSend
else:
- self.host.bridge.psItemsSend(
+ 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:
+ 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 @@ class Transform(base.CommandBase):
"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 @@ class Uri(base.CommandBase):
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 @@ class Uri(base.CommandBase):
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 @@ class HookCreate(base.CommandBase):
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 @@ class HookCreate(base.CommandBase):
_("{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 @@ class HookDelete(base.CommandBase):
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 @@ class HookDelete(base.CommandBase):
),
)
- 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 @@ class HookList(base.CommandBase):
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 --git a/sat_frontends/jp/cmd_roster.py b/sat_frontends/jp/cmd_roster.py
index aa312a55..b260ee5a 100644
--- a/sat_frontends/jp/cmd_roster.py
+++ b/sat_frontends/jp/cmd_roster.py
@@ -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"))
+ pass
- def start(self):
- self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error)
+ 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 error(self, failure):
- print((_("Error while retrieving the contacts [%s]") % failure))
- self.host.quit(1)
+ if self.verbosity >= 1:
+ keys_to_show.append('groups')
+ all_keys.remove('groups')
+ if self.verbosity >= 2:
+ keys_to_show.extend(all_keys)
- def ask_confirmation(self, no_sub, no_from, no_to):
- """Ask the confirmation before removing contacts.
+ 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)))
- @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
+ 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 gotContacts(self, contacts):
- """Process the list of contacts.
+ 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
- @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)
+ await self.output(contacts_dict)
self.host.quit()
@@ -98,23 +88,17 @@ class Stats(base.CommandBase):
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 @@ class Stats(base.CommandBase):
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 @@ class Stats(base.CommandBase):
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,81 +145,115 @@ class Stats(base.CommandBase):
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)
-
- def gotContacts(self, contacts):
- """Process the list of contacts.
-
- @param contacts(list[tuple]): list of contacts with their attributes and groups
- """
- 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
+ no_sub, no_from, no_to = [], [], []
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"))
+ 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")
+
self.host.quit()
+ async def ask_confirmation(self, no_sub, no_from, no_to):
+ """Ask the confirmation before removing contacts.
+
+ @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:
+ 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):
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 --git a/sat_frontends/jp/cmd_shell.py b/sat_frontends/jp/cmd_shell.py
index 9635b5c3..b595419e 100644
--- a/sat_frontends/jp/cmd_shell.py
+++ b/sat_frontends/jp/cmd_shell.py
@@ -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 @@ Enter "help" or "?" to know what to do
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 @@ class Shell(base.CommandBase, cmd.Cmd):
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 @@ class Shell(base.CommandBase, cmd.Cmd):
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 @@ class Shell(base.CommandBase, cmd.Cmd):
"""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 @@ class Shell(base.CommandBase, cmd.Cmd):
self.use = {}
self.verbose = False
self.update_path()
- self.cmdloop(INTRO.encode("utf-8"))
+ self.cmdloop(INTRO)
diff --git a/sat_frontends/jp/cmd_ticket.py b/sat_frontends/jp/cmd_ticket.py
index bb263e22..d583b9f5 100644
--- a/sat_frontends/jp/cmd_ticket.py
+++ b/sat_frontends/jp/cmd_ticket.py
@@ -22,7 +22,6 @@ from . import base
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 @@ class Get(base.CommandBase):
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 @@ class Import(base.CommandAnswering):
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 @@ class Import(base.CommandAnswering):
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 @@ class Import(base.CommandAnswering):
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 --git a/sat_frontends/jp/cmd_uri.py b/sat_frontends/jp/cmd_uri.py
index d274f28e..a45d1829 100644
--- a/sat_frontends/jp/cmd_uri.py
+++ b/sat_frontends/jp/cmd_uri.py
@@ -42,8 +42,9 @@ class Parse(base.CommandBase):
"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 @@ class Build(base.CommandBase):
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 --git a/sat_frontends/jp/common.py b/sat_frontends/jp/common.py
index 89545d0e..6de59f44 100644
--- a/sat_frontends/jp/common.py
+++ b/sat_frontends/jp/common.py
@@ -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.common import uri as xmpp_uri
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 @@ def getTmpDir(sat_conf, cat_dir, sub_dir=None):
"""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 @@ class BaseEdit(object):
@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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
# 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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
"""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 @@ class BaseEdit(object):
"""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 @@ class BaseEdit(object):
# 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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
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 @@ class BaseEdit(object):
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 @@ class Table(object):
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
- 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()
+ @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
- def setMetadataList(self, uri_data, key):
- """Helper method to set list of values from metadata
+ host = command.host
- @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)
+ 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)
+
+ 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)
+
+ 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 --git a/sat_frontends/jp/constants.py b/sat_frontends/jp/constants.py
index 243e8590..004fbe7b 100644
--- a/sat_frontends/jp/constants.py
+++ b/sat_frontends/jp/constants.py
@@ -66,6 +66,7 @@ class Const(constants.Const):
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 @@ class Const(constants.Const):
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 --git a/sat_frontends/jp/jp b/sat_frontends/jp/jp
index 9b014761..9c6d40f6 100755
--- a/sat_frontends/jp/jp
+++ b/sat_frontends/jp/jp
@@ -21,5 +21,4 @@ from sat_frontends.jp import base
if __name__ == "__main__":
jp = base.Jp()
- jp.import_plugins()
jp.run()
diff --git a/sat_frontends/jp/output_xmlui.py b/sat_frontends/jp/output_xmlui.py
index 978d10f6..d596bcaf 100644
--- a/sat_frontends/jp/output_xmlui.py
+++ b/sat_frontends/jp/output_xmlui.py
@@ -39,11 +39,11 @@ class XMLUI(object):
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 --git a/sat_frontends/jp/xmlui_manager.py b/sat_frontends/jp/xmlui_manager.py
index 0a1c6569..265e636d 100644
--- a/sat_frontends/jp/xmlui_manager.py
+++ b/sat_frontends/jp/xmlui_manager.py
@@ -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 @@ class Widget(Base):
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 @@ class EmptyWidget(xmlui_base.EmptyWidget, Widget):
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 @@ class LabelWidget(xmlui_base.LabelWidget, ValueWidget):
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 JidWidget(xmlui_base.JidWidget, TextWidget):
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 @@ class TextBoxWidget(xmlui_base.TextWidget, StringWidget):
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 @@ class TextBoxWidget(xmlui_base.TextWidget, StringWidget):
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 TextBoxWidget(xmlui_base.TextWidget, StringWidget):
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 @@ class ListWidget(xmlui_base.ListWidget, OptionsWidget):
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 ListWidget(xmlui_base.ListWidget, OptionsWidget):
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 @@ class BoolWidget(xmlui_base.BoolWidget, InputWidget):
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 @@ class Container(Base):
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 PairsContainer(xmlui_base.PairsContainer, Container):
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 @@ class LabelContainer(xmlui_base.PairsContainer, Container):
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 @@ class Dialog(object):
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 @@ class WidgetFactory(object):
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 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
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 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
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 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
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 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
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 @@ class XMLUIPanel(xmlui_base.XMLUIPanel):
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 @@ class XMLUIDialog(xmlui_base.XMLUIDialog):
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 --git a/sat_frontends/tools/xmlui.py b/sat_frontends/tools/xmlui.py
index 5ac74727..97e1c909 100644
--- a/sat_frontends/tools/xmlui.py
+++ b/sat_frontends/tools/xmlui.py
@@ -925,6 +925,67 @@ class XMLUIPanel(XMLUIBase):
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 --git a/setup.py b/setup.py
index f5c22a74..4bec01f0 100644
--- a/setup.py
+++ b/setup.py
@@ -87,7 +87,6 @@ setup(
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 @@ setup(
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 --git a/twisted/plugins/sat_plugin.py b/twisted/plugins/sat_plugin.py
index 7e79e342..fd4054fd 100644
--- a/twisted/plugins/sat_plugin.py
+++ b/twisted/plugins/sat_plugin.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SAT: a jabber client