summaryrefslogtreecommitdiff
path: root/contrib/python/pypodman
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python/pypodman')
-rw-r--r--contrib/python/pypodman/pypodman/lib/__init__.py2
-rw-r--r--contrib/python/pypodman/pypodman/lib/action_base.py3
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/__init__.py16
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/commit_action.py102
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/export_action.py60
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/inspect_action.py90
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/kill_action.py55
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/logs_action.py75
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/mount_action.py78
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/pause_action.py47
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/port_action.py63
-rw-r--r--contrib/python/pypodman/pypodman/lib/actions/ps_action.py7
-rw-r--r--contrib/python/pypodman/pypodman/lib/report.py19
13 files changed, 601 insertions, 16 deletions
diff --git a/contrib/python/pypodman/pypodman/lib/__init__.py b/contrib/python/pypodman/pypodman/lib/__init__.py
index e3654dc2b..80fa0e1e9 100644
--- a/contrib/python/pypodman/pypodman/lib/__init__.py
+++ b/contrib/python/pypodman/pypodman/lib/__init__.py
@@ -1,7 +1,7 @@
"""Remote podman client support library."""
from pypodman.lib.action_base import AbstractActionBase
from pypodman.lib.config import PodmanArgumentParser
-from pypodman.lib.report import (Report, ReportColumn)
+from pypodman.lib.report import Report, ReportColumn
__all__ = [
'AbstractActionBase',
diff --git a/contrib/python/pypodman/pypodman/lib/action_base.py b/contrib/python/pypodman/pypodman/lib/action_base.py
index 8b86c02df..5cc4c22a9 100644
--- a/contrib/python/pypodman/pypodman/lib/action_base.py
+++ b/contrib/python/pypodman/pypodman/lib/action_base.py
@@ -65,8 +65,7 @@ class AbstractActionBase(abc.ABC):
def client(self):
"""Podman remote client for communicating."""
if self._args.host is None:
- return podman.Client(
- uri=self.local_uri)
+ return podman.Client(uri=self.local_uri)
return podman.Client(
uri=self.local_uri,
remote_uri=self.remote_uri,
diff --git a/contrib/python/pypodman/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/pypodman/lib/actions/__init__.py
index 098f033e9..b0af3c589 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/__init__.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/__init__.py
@@ -1,7 +1,15 @@
"""Module to export all the podman subcommands."""
from pypodman.lib.actions.attach_action import Attach
+from pypodman.lib.actions.commit_action import Commit
from pypodman.lib.actions.create_action import Create
+from pypodman.lib.actions.export_action import Export
from pypodman.lib.actions.images_action import Images
+from pypodman.lib.actions.inspect_action import Inspect
+from pypodman.lib.actions.kill_action import Kill
+from pypodman.lib.actions.logs_action import Logs
+from pypodman.lib.actions.mount_action import Mount
+from pypodman.lib.actions.pause_action import Pause
+from pypodman.lib.actions.port_action import Port
from pypodman.lib.actions.ps_action import Ps
from pypodman.lib.actions.pull_action import Pull
from pypodman.lib.actions.rm_action import Rm
@@ -9,8 +17,16 @@ from pypodman.lib.actions.rmi_action import Rmi
__all__ = [
'Attach',
+ 'Commit',
'Create',
+ 'Export',
'Images',
+ 'Inspect',
+ 'Kill',
+ 'Logs',
+ 'Mount',
+ 'Pause',
+ 'Port',
'Ps',
'Pull',
'Rm',
diff --git a/contrib/python/pypodman/pypodman/lib/actions/commit_action.py b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py
new file mode 100644
index 000000000..1e16550ad
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/commit_action.py
@@ -0,0 +1,102 @@
+"""Remote client command for creating image from container."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Commit(AbstractActionBase):
+ """Class for creating image from container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Commit command to parent parser."""
+ parser = parent.add_parser(
+ 'commit', help='create image from container')
+ parser.add_argument(
+ '--author',
+ help='Set the author for the committed image',
+ )
+ parser.add_argument(
+ '--change',
+ '-c',
+ choices=('CMD', 'ENTRYPOINT', 'ENV', 'EXPOSE', 'LABEL', 'ONBUILD',
+ 'STOPSIGNAL', 'USER', 'VOLUME', 'WORKDIR'),
+ action='append',
+ type=str.upper,
+ help='Apply the following possible changes to the created image',
+ )
+ parser.add_argument(
+ '--format',
+ '-f',
+ choices=('oci', 'docker'),
+ default='oci',
+ type=str.lower,
+ help='Set the format of the image manifest and metadata',
+ )
+ parser.add_argument(
+ '--iidfile',
+ metavar='PATH',
+ help='Write the image ID to the file',
+ )
+ parser.add_argument(
+ '--message',
+ '-m',
+ help='Set commit message for committed image',
+ )
+ parser.add_argument(
+ '--pause',
+ '-p',
+ choices=('True', 'False'),
+ default=True,
+ type=bool,
+ help='Pause the container when creating an image',
+ )
+ parser.add_argument(
+ '--quiet',
+ '-q',
+ help='Suppress output',
+ )
+ parser.add_argument(
+ 'container',
+ nargs=1,
+ help='container to use as source',
+ )
+ parser.add_argument(
+ 'image',
+ nargs=1,
+ help='image name to create',
+ )
+ parser.set_defaults(class_=cls, method='commit')
+
+ def __init__(self, args):
+ """Construct Commit class."""
+ super().__init__(args)
+ if not args.container:
+ raise ValueError('You must supply one container id'
+ ' or name to be used as source.')
+ if not args.image:
+ raise ValueError('You must supply one image id'
+ ' or name to be created.')
+
+ def commit(self):
+ """Create image from container."""
+ try:
+ try:
+ ctnr = self.client.containers.get(self._args.container[0])
+ ident = ctnr.commit(**self._args)
+ print(ident)
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ return 1
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/export_action.py b/contrib/python/pypodman/pypodman/lib/actions/export_action.py
new file mode 100644
index 000000000..2a6c2a3cf
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/export_action.py
@@ -0,0 +1,60 @@
+"""Remote client command for export container filesystem to tarball."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Export(AbstractActionBase):
+ """Class for exporting container filesystem to tarball."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Export command to parent parser."""
+ parser = parent.add_parser(
+ 'export', help='export container to tarball')
+ parser.add_argument(
+ '--output',
+ '-o',
+ metavar='PATH',
+ nargs=1,
+ help='Write to a file',
+ )
+ parser.add_argument(
+ 'container',
+ nargs=1,
+ help='container to use as source',
+ )
+ parser.set_defaults(class_=cls, method='export')
+
+ def __init__(self, args):
+ """Construct Export class."""
+ super().__init__(args)
+ if not args.container:
+ raise ValueError('You must supply one container id'
+ ' or name to be used as source.')
+
+ if not args.output:
+ raise ValueError('You must supply one filename'
+ ' to be created as tarball using --output.')
+
+ def export(self):
+ """Create tarball from container filesystem."""
+ try:
+ try:
+ ctnr = self.client.containers.get(self._args.container[0])
+ ctnr.export(self._args.output[0])
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container {} not found.'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ return 1
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py
new file mode 100644
index 000000000..0559cd40a
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/inspect_action.py
@@ -0,0 +1,90 @@
+"""Remote client command for inspecting podman objects."""
+import json
+import logging
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Inspect(AbstractActionBase):
+ """Class for inspecting podman objects."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Inspect command to parent parser."""
+ parser = parent.add_parser('inspect', help='inspect objects')
+ parser.add_argument(
+ '--type',
+ '-t',
+ choices=('all', 'container', 'image'),
+ default='all',
+ type=str.lower,
+ help='Type of object to inspect',
+ )
+ parser.add_argument(
+ 'size',
+ action='store_true',
+ default=True,
+ help='Display the total file size if the type is a container.'
+ ' Always True.')
+ parser.add_argument(
+ 'objects',
+ nargs='+',
+ help='objects to inspect',
+ )
+ parser.set_defaults(class_=cls, method='inspect')
+
+ def __init__(self, args):
+ """Construct Inspect class."""
+ super().__init__(args)
+
+ def _get_container(self, ident):
+ try:
+ logging.debug("Get container %s", ident)
+ ctnr = self.client.containers.get(ident)
+ except podman.ContainerNotFound:
+ pass
+ else:
+ return ctnr.inspect()
+
+ def _get_image(self, ident):
+ try:
+ logging.debug("Get image %s", ident)
+ img = self.client.images.get(ident)
+ except podman.ImageNotFound:
+ pass
+ else:
+ return img.inspect()
+
+ def inspect(self):
+ """Inspect provided podman objects."""
+ output = {}
+ try:
+ for ident in self._args.objects:
+ obj = None
+
+ if self._args.type in ('all', 'container'):
+ obj = self._get_container(ident)
+ if obj is None and self._args.type in ('all', 'image'):
+ obj = self._get_image(ident)
+
+ if obj is None:
+ if self._args.type == 'container':
+ msg = 'Container "{}" not found'.format(ident)
+ elif self._args.type == 'image':
+ msg = 'Image "{}" not found'.format(ident)
+ else:
+ msg = 'Object "{}" not found'.format(ident)
+ print(msg, file=sys.stderr, flush=True)
+ else:
+ output.update(obj._asdict())
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
+ else:
+ print(json.dumps(output, indent=2))
diff --git a/contrib/python/pypodman/pypodman/lib/actions/kill_action.py b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py
new file mode 100644
index 000000000..3caa42cf0
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/kill_action.py
@@ -0,0 +1,55 @@
+"""Remote client command for signaling podman containers."""
+import signal
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Kill(AbstractActionBase):
+ """Class for sending signal to main process in container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Kill command to parent parser."""
+ parser = parent.add_parser('kill', help='signal container')
+ parser.add_argument(
+ '--signal',
+ '-s',
+ choices=range(1, signal.NSIG),
+ metavar='[1,{}]'.format(signal.NSIG),
+ default=9,
+ help='Signal to send to the container. (Default: 9)')
+ parser.add_argument(
+ 'containers',
+ nargs='+',
+ help='containers to signal',
+ )
+ parser.set_defaults(class_=cls, method='kill')
+
+ def __init__(self, args):
+ """Construct Kill class."""
+ super().__init__(args)
+
+ def kill(self):
+ """Signal provided containers."""
+ try:
+ for ident in self._args.containers:
+ try:
+ ctnr = self.client.containers.get(ident)
+ ctnr.kill(self._args.signal)
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container "{}" not found'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ else:
+ print(ident)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/logs_action.py b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py
new file mode 100644
index 000000000..764a4b9c7
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/logs_action.py
@@ -0,0 +1,75 @@
+"""Remote client command for retrieving container logs."""
+import argparse
+import logging
+import sys
+from collections import deque
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class PositiveIntAction(argparse.Action):
+ """Validate number given is positive integer."""
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """Validate input."""
+ if values > 0:
+ setattr(namespace, self.dest, values)
+ return
+
+ msg = 'Must be a positive integer.'
+ raise argparse.ArgumentError(self, msg)
+
+
+class Logs(AbstractActionBase):
+ """Class for retrieving logs from container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Logs command to parent parser."""
+ parser = parent.add_parser('logs', help='retrieve logs from container')
+ parser.add_argument(
+ '--tail',
+ metavar='LINES',
+ action=PositiveIntAction,
+ type=int,
+ help='Output the specified number of LINES at the end of the logs')
+ parser.add_argument(
+ 'container',
+ nargs=1,
+ help='retrieve container logs',
+ )
+ parser.set_defaults(class_=cls, method='logs')
+
+ def __init__(self, args):
+ """Construct Logs class."""
+ super().__init__(args)
+
+ def logs(self):
+ """Retrieve logs from containers."""
+ try:
+ ident = self._args.container[0]
+ try:
+ logging.debug('Get container "%s" logs', ident)
+ ctnr = self.client.containers.get(ident)
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container "{}" not found'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ else:
+ if self._args.tail:
+ logs = iter(deque(ctnr.logs(), maxlen=self._args.tail))
+ else:
+ logs = ctnr.logs()
+
+ for line in logs:
+ sys.stdout.write(line)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/mount_action.py b/contrib/python/pypodman/pypodman/lib/actions/mount_action.py
new file mode 100644
index 000000000..905eda6da
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/mount_action.py
@@ -0,0 +1,78 @@
+"""Remote client command for retrieving mounts from containers."""
+import sys
+from collections import OrderedDict
+
+import podman
+from pypodman.lib import AbstractActionBase, Report, ReportColumn
+
+
+class Mount(AbstractActionBase):
+ """Class for retrieving mounts from container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add mount command to parent parser."""
+ parser = parent.add_parser(
+ 'mount', help='retrieve mounts from containers.')
+ super().subparser(parser)
+ parser.add_argument(
+ 'containers',
+ nargs='*',
+ help='containers to list ports',
+ )
+ parser.set_defaults(class_=cls, method='mount')
+
+ def __init__(self, args):
+ """Construct Mount class."""
+ super().__init__(args)
+
+ self.columns = OrderedDict({
+ 'id':
+ ReportColumn('id', 'CONTAINER ID', 14),
+ 'destination':
+ ReportColumn('destination', 'DESTINATION', 0)
+ })
+
+ def mount(self):
+ """Retrieve mounts from containers."""
+ try:
+ ctnrs = []
+ if not self._args.containers:
+ ctnrs = self.client.containers.list()
+ else:
+ for ident in self._args.containers:
+ try:
+ ctnrs.append(self.client.containers.get(ident))
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container "{}" not found'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
+
+ if not ctnrs:
+ print(
+ 'Unable to find any containers.', file=sys.stderr, flush=True)
+ return 1
+
+ rows = list()
+ for ctnr in ctnrs:
+ details = ctnr.inspect()
+ rows.append({
+ 'id': ctnr.id,
+ 'destination': details.graphdriver['data']['mergeddir']
+ })
+
+ with Report(self.columns, heading=self._args.heading) as report:
+ report.layout(
+ rows, self.columns.keys(), truncate=self._args.truncate)
+ for row in rows:
+ report.row(**row)
diff --git a/contrib/python/pypodman/pypodman/lib/actions/pause_action.py b/contrib/python/pypodman/pypodman/lib/actions/pause_action.py
new file mode 100644
index 000000000..ab64d8b81
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/pause_action.py
@@ -0,0 +1,47 @@
+"""Remote client command for pausing processes in containers."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Pause(AbstractActionBase):
+ """Class for pausing processes in container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Pause command to parent parser."""
+ parser = parent.add_parser('pause', help='pause container processes')
+ parser.add_argument(
+ 'containers',
+ nargs='+',
+ help='containers to pause',
+ )
+ parser.set_defaults(class_=cls, method='pause')
+
+ def __init__(self, args):
+ """Construct Pause class."""
+ super().__init__(args)
+
+ def pause(self):
+ """Pause provided containers."""
+ try:
+ for ident in self._args.containers:
+ try:
+ ctnr = self.client.containers.get(ident)
+ ctnr.pause()
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container "{}" not found'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+ else:
+ print(ident)
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/port_action.py b/contrib/python/pypodman/pypodman/lib/actions/port_action.py
new file mode 100644
index 000000000..60bbe12c4
--- /dev/null
+++ b/contrib/python/pypodman/pypodman/lib/actions/port_action.py
@@ -0,0 +1,63 @@
+"""Remote client command for retrieving ports from containers."""
+import sys
+
+import podman
+from pypodman.lib import AbstractActionBase
+
+
+class Port(AbstractActionBase):
+ """Class for retrieving ports from container."""
+
+ @classmethod
+ def subparser(cls, parent):
+ """Add Port command to parent parser."""
+ parser = parent.add_parser(
+ 'port', help='retrieve ports from containers.')
+ parser.add_argument(
+ '--all',
+ '-a',
+ action='store_true',
+ default=False,
+ help='List all known port mappings for running containers')
+ parser.add_argument(
+ 'containers',
+ nargs='*',
+ default=None,
+ help='containers to list ports',
+ )
+ parser.set_defaults(class_=cls, method='port')
+
+ def __init__(self, args):
+ """Construct Port class."""
+ super().__init__(args)
+ if not args.all and not args.containers:
+ ValueError('You must supply at least one'
+ ' container id or name, or --all.')
+
+ def port(self):
+ """Retrieve ports from containers."""
+ try:
+ ctnrs = []
+ if self._args.all:
+ ctnrs = self.client.containers.list()
+ else:
+ for ident in self._args.containers:
+ try:
+ ctnrs.append(self.client.containers.get(ident))
+ except podman.ContainerNotFound as e:
+ sys.stdout.flush()
+ print(
+ 'Container "{}" not found'.format(e.name),
+ file=sys.stderr,
+ flush=True)
+
+ for ctnr in ctnrs:
+ print("{}\n{}".format(ctnr.id, ctnr.ports))
+
+ except podman.ErrorOccurred as e:
+ sys.stdout.flush()
+ print(
+ '{}'.format(e.reason).capitalize(),
+ file=sys.stderr,
+ flush=True)
+ return 1
diff --git a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
index 83954479c..8c0a739d7 100644
--- a/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
+++ b/contrib/python/pypodman/pypodman/lib/actions/ps_action.py
@@ -3,8 +3,8 @@ import operator
from collections import OrderedDict
import humanize
-import podman
+import podman
from pypodman.lib import AbstractActionBase, Report, ReportColumn
@@ -44,7 +44,7 @@ class Ps(AbstractActionBase):
'status':
ReportColumn('status', 'STATUS', 10),
'ports':
- ReportColumn('ports', 'PORTS', 28),
+ ReportColumn('ports', 'PORTS', 0),
'names':
ReportColumn('names', 'NAMES', 18)
})
@@ -67,6 +67,9 @@ class Ps(AbstractActionBase):
'createdat':
humanize.naturaldate(podman.datetime_parse(ctnr.createdat)),
})
+
+ if self._args.truncate:
+ fields.update({'image': ctnr.image[-30:]})
rows.append(fields)
with Report(self.columns, heading=self._args.heading) as report:
diff --git a/contrib/python/pypodman/pypodman/lib/report.py b/contrib/python/pypodman/pypodman/lib/report.py
index 0fa06f22c..a06e6055e 100644
--- a/contrib/python/pypodman/pypodman/lib/report.py
+++ b/contrib/python/pypodman/pypodman/lib/report.py
@@ -53,17 +53,14 @@ class Report():
fmt = []
for key in keys:
- value = max(map(lambda x: len(str(x.get(key, ''))), iterable))
- # print('key', key, 'value', value)
+ slice_ = [i.get(key, '') for i in iterable]
+ data_len = len(max(slice_, key=len))
- if truncate:
- row = self._columns.get(
- key, ReportColumn(key, key.upper(), len(key)))
- if value < row.width:
- step = row.width if value == 0 else value
- value = max(len(key), step)
- elif value > row.width:
- value = row.width if row.width != 0 else value
+ info = self._columns.get(key,
+ ReportColumn(key, key.upper(), data_len))
+ display_len = max(data_len, len(info.display))
+ if truncate and info.width != 0:
+ display_len = info.width
- fmt.append('{{{0}:{1}.{1}}}'.format(key, value))
+ fmt.append('{{{0}:{1}.{1}}}'.format(key, display_len))
self._format = ' '.join(fmt)