summaryrefslogtreecommitdiff
path: root/contrib/python/pypodman/lib
diff options
context:
space:
mode:
authorJhon Honce <jhonce@redhat.com>2018-07-16 17:29:50 -0700
committerAtomic Bot <atomic-devel@projectatomic.io>2018-07-23 18:53:44 +0000
commit9a18681ba62d1a297809c243607a7b3763131c36 (patch)
tree8333f8727fd7d32f81cb1f54754ccd138a7e1063 /contrib/python/pypodman/lib
parent8569ed03056ce39e0dc163747089ed4b60b1b9b1 (diff)
downloadpodman-9a18681ba62d1a297809c243607a7b3763131c36.tar.gz
podman-9a18681ba62d1a297809c243607a7b3763131c36.tar.bz2
podman-9a18681ba62d1a297809c243607a7b3763131c36.zip
[WIP] Refactor and simplify python builds
* pypodman namespaced in site-packages * version numbers pulled from requirements.txt * add python-podman spec file to install eggs Signed-off-by: Jhon Honce <jhonce@redhat.com> Closes: #1106 Approved by: rhatdan
Diffstat (limited to 'contrib/python/pypodman/lib')
-rw-r--r--contrib/python/pypodman/lib/__init__.py11
-rw-r--r--contrib/python/pypodman/lib/action_base.py84
-rw-r--r--contrib/python/pypodman/lib/actions/__init__.py7
-rw-r--r--contrib/python/pypodman/lib/actions/images_action.py88
-rw-r--r--contrib/python/pypodman/lib/actions/ps_action.py76
-rw-r--r--contrib/python/pypodman/lib/actions/rm_action.py51
-rw-r--r--contrib/python/pypodman/lib/actions/rmi_action.py50
-rw-r--r--contrib/python/pypodman/lib/config.py212
-rw-r--r--contrib/python/pypodman/lib/future_abstract.py29
-rwxr-xr-xcontrib/python/pypodman/lib/pypodman.py76
-rw-r--r--contrib/python/pypodman/lib/report.py67
11 files changed, 0 insertions, 751 deletions
diff --git a/contrib/python/pypodman/lib/__init__.py b/contrib/python/pypodman/lib/__init__.py
deleted file mode 100644
index 5a8303668..000000000
--- a/contrib/python/pypodman/lib/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Remote podman client support library."""
-from .action_base import AbstractActionBase
-from .config import PodmanArgumentParser
-from .report import Report, ReportColumn
-
-__all__ = [
- 'AbstractActionBase',
- 'PodmanArgumentParser',
- 'Report',
- 'ReportColumn',
-]
diff --git a/contrib/python/pypodman/lib/action_base.py b/contrib/python/pypodman/lib/action_base.py
deleted file mode 100644
index ff2922262..000000000
--- a/contrib/python/pypodman/lib/action_base.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""Base class for all actions of remote client."""
-import abc
-from functools import lru_cache
-
-import podman
-
-
-class AbstractActionBase(abc.ABC):
- """Base class for all actions of remote client."""
-
- @classmethod
- @abc.abstractmethod
- def subparser(cls, parser):
- """Define parser for this action. Subclasses must implement.
-
- API:
- Use set_defaults() to set attributes "class_" and "method". These will
- be invoked as class_(parsed_args).method()
- """
- parser.add_argument(
- '--all',
- action='store_true',
- help=('list all items.'
- ' (default: no-op, included for compatibility.)'))
- parser.add_argument(
- '--no-trunc',
- '--notruncate',
- action='store_false',
- dest='truncate',
- default=True,
- help='Display extended information. (default: False)')
- parser.add_argument(
- '--noheading',
- action='store_false',
- dest='heading',
- default=True,
- help=('Omit the table headings from the output.'
- ' (default: False)'))
- parser.add_argument(
- '--quiet',
- action='store_true',
- help='List only the IDs. (default: %(default)s)')
-
- def __init__(self, args):
- """Construct class."""
- self._args = args
-
- @property
- def remote_uri(self):
- """URI for remote side of connection."""
- return self._args.remote_uri
-
- @property
- def local_uri(self):
- """URI for local side of connection."""
- return self._args.local_uri
-
- @property
- def identity_file(self):
- """Key for authenication."""
- return self._args.identity_file
-
- @property
- @lru_cache(maxsize=1)
- def client(self):
- """Podman remote client for communicating."""
- if self._args.host is None:
- return podman.Client(
- uri=self.local_uri)
- else:
- return podman.Client(
- uri=self.local_uri,
- remote_uri=self.remote_uri,
- identity_file=self.identity_file)
-
- def __repr__(self):
- """Compute the “official” string representation of object."""
- return ("{}(local_uri='{}', remote_uri='{}',"
- " identity_file='{}')").format(
- self.__class__,
- self.local_uri,
- self.remote_uri,
- self.identity_file,
- )
diff --git a/contrib/python/pypodman/lib/actions/__init__.py b/contrib/python/pypodman/lib/actions/__init__.py
deleted file mode 100644
index cdc58b6ab..000000000
--- a/contrib/python/pypodman/lib/actions/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""Module to export all the podman subcommands."""
-from .images_action import Images
-from .ps_action import Ps
-from .rm_action import Rm
-from .rmi_action import Rmi
-
-__all__ = ['Images', 'Ps', 'Rm', 'Rmi']
diff --git a/contrib/python/pypodman/lib/actions/images_action.py b/contrib/python/pypodman/lib/actions/images_action.py
deleted file mode 100644
index f6a7497e5..000000000
--- a/contrib/python/pypodman/lib/actions/images_action.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Remote client commands dealing with images."""
-import operator
-from collections import OrderedDict
-
-import humanize
-import podman
-
-from .. import AbstractActionBase, Report, ReportColumn
-
-
-class Images(AbstractActionBase):
- """Class for Image manipulation."""
-
- @classmethod
- def subparser(cls, parent):
- """Add Images commands to parent parser."""
- parser = parent.add_parser('images', help='list images')
- super().subparser(parser)
- parser.add_argument(
- '--sort',
- choices=['created', 'id', 'repository', 'size', 'tag'],
- default='created',
- type=str.lower,
- help=('Change sort ordered of displayed images.'
- ' (default: %(default)s)'))
-
- group = parser.add_mutually_exclusive_group()
- group.add_argument(
- '--digests',
- action='store_true',
- help='Include digests with images. (default: %(default)s)')
- parser.set_defaults(class_=cls, method='list')
-
- def __init__(self, args):
- """Construct Images class."""
- super().__init__(args)
-
- self.columns = OrderedDict({
- 'name':
- ReportColumn('name', 'REPOSITORY', 40),
- 'tag':
- ReportColumn('tag', 'TAG', 10),
- 'id':
- ReportColumn('id', 'IMAGE ID', 12),
- 'created':
- ReportColumn('created', 'CREATED', 12),
- 'size':
- ReportColumn('size', 'SIZE', 8),
- 'repoDigests':
- ReportColumn('repoDigests', 'DIGESTS', 35),
- })
-
- def list(self):
- """List images."""
- images = sorted(
- self.client.images.list(),
- key=operator.attrgetter(self._args.sort))
- if len(images) == 0:
- return 0
-
- rows = list()
- for image in images:
- fields = dict(image)
- fields.update({
- 'created':
- humanize.naturaldate(podman.datetime_parse(image.created)),
- 'size':
- humanize.naturalsize(int(image.size)),
- 'repoDigests':
- ' '.join(image.repoDigests),
- })
-
- for r in image.repoTags:
- name, tag = r.split(':', 1)
- fields.update({
- 'name': name,
- 'tag': tag,
- })
- rows.append(fields)
-
- if not self._args.digests:
- del self.columns['repoDigests']
-
- 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/lib/actions/ps_action.py b/contrib/python/pypodman/lib/actions/ps_action.py
deleted file mode 100644
index 4bbec5578..000000000
--- a/contrib/python/pypodman/lib/actions/ps_action.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Remote client commands dealing with containers."""
-import operator
-from collections import OrderedDict
-
-import humanize
-import podman
-
-from .. import AbstractActionBase, Report, ReportColumn
-
-
-class Ps(AbstractActionBase):
- """Class for Container manipulation."""
-
- @classmethod
- def subparser(cls, parent):
- """Add Images command to parent parser."""
- parser = parent.add_parser('ps', help='list containers')
- super().subparser(parser)
- parser.add_argument(
- '--sort',
- choices=[
- 'createdat', 'id', 'image', 'names', 'runningfor', 'size',
- 'status'
- ],
- default='createdat',
- type=str.lower,
- help=('Change sort ordered of displayed containers.'
- ' (default: %(default)s)'))
- parser.set_defaults(class_=cls, method='list')
-
- def __init__(self, args):
- """Construct Ps class."""
- super().__init__(args)
-
- self.columns = OrderedDict({
- 'id':
- ReportColumn('id', 'CONTAINER ID', 14),
- 'image':
- ReportColumn('image', 'IMAGE', 30),
- 'command':
- ReportColumn('column', 'COMMAND', 20),
- 'createdat':
- ReportColumn('createdat', 'CREATED', 12),
- 'status':
- ReportColumn('status', 'STATUS', 10),
- 'ports':
- ReportColumn('ports', 'PORTS', 28),
- 'names':
- ReportColumn('names', 'NAMES', 18)
- })
-
- def list(self):
- """List containers."""
- # TODO: Verify sorting on dates and size
- ctnrs = sorted(
- self.client.containers.list(),
- key=operator.attrgetter(self._args.sort))
- if len(ctnrs) == 0:
- return 0
-
- rows = list()
- for ctnr in ctnrs:
- fields = dict(ctnr)
- fields.update({
- 'command':
- ' '.join(ctnr.command),
- 'createdat':
- humanize.naturaldate(podman.datetime_parse(ctnr.createdat)),
- })
- rows.append(fields)
-
- 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/lib/actions/rm_action.py b/contrib/python/pypodman/lib/actions/rm_action.py
deleted file mode 100644
index bd8950bd6..000000000
--- a/contrib/python/pypodman/lib/actions/rm_action.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""Remote client command for deleting containers."""
-import sys
-
-import podman
-
-from .. import AbstractActionBase
-
-
-class Rm(AbstractActionBase):
- """Class for removing containers from storage."""
-
- @classmethod
- def subparser(cls, parent):
- """Add Rm command to parent parser."""
- parser = parent.add_parser('rm', help='delete container(s)')
- parser.add_argument(
- '-f',
- '--force',
- action='store_true',
- help=('force delete of running container(s).'
- ' (default: %(default)s)'))
- parser.add_argument(
- 'targets', nargs='*', help='container id(s) to delete')
- parser.set_defaults(class_=cls, method='remove')
-
- def __init__(self, args):
- """Construct Rm class."""
- super().__init__(args)
- if len(args.targets) < 1:
- raise ValueError('You must supply at least one container id'
- ' or name to be deleted.')
-
- def remove(self):
- """Remove container(s)."""
- for id in self._args.targets:
- try:
- ctnr = self.client.containers.get(id)
- ctnr.remove(self._args.force)
- print(id)
- 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)
diff --git a/contrib/python/pypodman/lib/actions/rmi_action.py b/contrib/python/pypodman/lib/actions/rmi_action.py
deleted file mode 100644
index 91f0deeaf..000000000
--- a/contrib/python/pypodman/lib/actions/rmi_action.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Remote client command for deleting images."""
-import sys
-
-import podman
-
-from .. import AbstractActionBase
-
-
-class Rmi(AbstractActionBase):
- """Clas for removing images from storage."""
-
- @classmethod
- def subparser(cls, parent):
- """Add Rmi command to parent parser."""
- parser = parent.add_parser('rmi', help='delete image(s)')
- parser.add_argument(
- '-f',
- '--force',
- action='store_true',
- help=('force delete of image(s) and associated containers.'
- ' (default: %(default)s)'))
- parser.add_argument('targets', nargs='*', help='image id(s) to delete')
- parser.set_defaults(class_=cls, method='remove')
-
- def __init__(self, args):
- """Construct Rmi class."""
- super().__init__(args)
- if len(args.targets) < 1:
- raise ValueError('You must supply at least one image id'
- ' or name to be deleted.')
-
- def remove(self):
- """Remove image(s)."""
- for id in self._args.targets:
- try:
- img = self.client.images.get(id)
- img.remove(self._args.force)
- print(id)
- except podman.ImageNotFound as e:
- sys.stdout.flush()
- print(
- 'Image {} 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)
diff --git a/contrib/python/pypodman/lib/config.py b/contrib/python/pypodman/lib/config.py
deleted file mode 100644
index e687697ef..000000000
--- a/contrib/python/pypodman/lib/config.py
+++ /dev/null
@@ -1,212 +0,0 @@
-import argparse
-import curses
-import getpass
-import inspect
-import logging
-import os
-import sys
-
-import pkg_resources
-
-import pytoml
-
-# TODO: setup.py and obtain __version__ from rpm.spec
-try:
- __version__ = pkg_resources.get_distribution('pypodman').version
-except Exception:
- __version__ = '0.0.0'
-
-
-class HelpFormatter(argparse.RawDescriptionHelpFormatter):
- """Set help width to screen size."""
-
- def __init__(self, *args, **kwargs):
- """Construct HelpFormatter using screen width."""
- if 'width' not in kwargs:
- kwargs['width'] = 80
- try:
- height, width = curses.initscr().getmaxyx()
- kwargs['width'] = width
- finally:
- curses.endwin()
- super().__init__(*args, **kwargs)
-
-
-class PodmanArgumentParser(argparse.ArgumentParser):
- """Default remote podman configuration."""
-
- def __init__(self, **kwargs):
- """Construct the parser."""
- kwargs['add_help'] = True
- kwargs['allow_abbrev'] = True
- kwargs['description'] = __doc__
- kwargs['formatter_class'] = HelpFormatter
-
- super().__init__(**kwargs)
-
- def initialize_parser(self):
- """Initialize parser without causing recursion meltdown."""
- self.add_argument(
- '--version',
- action='version',
- version='%(prog)s v. ' + __version__)
- self.add_argument(
- '--log-level',
- choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
- default='WARNING',
- type=str.upper,
- help='set logging level for events. (default: %(default)s)',
- )
- self.add_argument(
- '--run-dir',
- metavar='DIRECTORY',
- help=('directory to place local socket bindings.'
- ' (default: XDG_RUNTIME_DIR/pypodman'))
- self.add_argument(
- '--user',
- default=getpass.getuser(),
- help='Authenicating user on remote host. (default: %(default)s)')
- self.add_argument(
- '--host', help='name of remote host. (default: None)')
- self.add_argument(
- '--remote-socket-path',
- metavar='PATH',
- help=('path of podman socket on remote host'
- ' (default: /run/podman/io.projectatomic.podman)'))
- self.add_argument(
- '--identity-file',
- metavar='PATH',
- help=('path to ssh identity file. (default: ~user/.ssh/id_dsa)'))
- self.add_argument(
- '--config-home',
- metavar='DIRECTORY',
- help=('home of configuration "pypodman.conf".'
- ' (default: XDG_CONFIG_HOME/pypodman'))
-
- actions_parser = self.add_subparsers(
- dest='subparser_name', help='actions')
-
- # pull in plugin(s) code for each subcommand
- for name, obj in inspect.getmembers(
- sys.modules['lib.actions'],
- lambda member: inspect.isclass(member)):
- if hasattr(obj, 'subparser'):
- try:
- obj.subparser(actions_parser)
- except NameError as e:
- logging.critical(e)
- logging.warning(
- 'See subparser configuration for Class "{}"'.format(
- name))
- sys.exit(3)
-
- def parse_args(self, args=None, namespace=None):
- """Parse command line arguments, backed by env var and config_file."""
- self.initialize_parser()
- cooked = super().parse_args(args, namespace)
- return self.resolve_configuration(cooked)
-
- def resolve_configuration(self, args):
- """Find and fill in any arguments not passed on command line."""
- args.xdg_runtime_dir = os.environ.get('XDG_RUNTIME_DIR', '/tmp')
- args.xdg_config_home = os.environ.get('XDG_CONFIG_HOME',
- os.path.expanduser('~/.config'))
- args.xdg_config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg')
-
- # Configuration file(s) are optional,
- # required arguments may be provided elsewhere
- config = {'default': {}}
- dirs = args.xdg_config_dirs.split(':')
- dirs.extend((args.xdg_config_home, args.config_home))
- for dir_ in dirs:
- if dir_ is None:
- continue
- try:
- with open(os.path.join(dir_, 'pypodman/pypodman.conf'),
- 'r') as stream:
- config.update(pytoml.load(stream))
- except OSError:
- pass
-
- def reqattr(name, value):
- if value:
- setattr(args, name, value)
- return value
- self.error('Required argument "%s" is not configured.' % name)
-
- reqattr(
- 'run_dir',
- getattr(args, 'run_dir')
- or os.environ.get('RUN_DIR')
- or config['default'].get('run_dir')
- or os.path.join(args.xdg_runtime_dir, 'pypodman')
- ) # yapf: disable
-
- setattr(
- args,
- 'host',
- getattr(args, 'host')
- or os.environ.get('HOST')
- or config['default'].get('host')
- ) # yapf:disable
-
- reqattr(
- 'user',
- getattr(args, 'user')
- or os.environ.get('USER')
- or config['default'].get('user')
- or getpass.getuser()
- ) # yapf:disable
-
- reqattr(
- 'remote_socket_path',
- getattr(args, 'remote_socket_path')
- or os.environ.get('REMOTE_SOCKET_PATH')
- or config['default'].get('remote_socket_path')
- or '/run/podman/io.projectatomic.podman'
- ) # yapf:disable
-
- reqattr(
- 'log_level',
- getattr(args, 'log_level')
- or os.environ.get('LOG_LEVEL')
- or config['default'].get('log_level')
- or logging.WARNING
- ) # yapf:disable
-
- setattr(
- args,
- 'identity_file',
- getattr(args, 'identity_file')
- or os.environ.get('IDENTITY_FILE')
- or config['default'].get('identity_file')
- or os.path.expanduser('~{}/.ssh/id_dsa'.format(args.user))
- ) # yapf:disable
-
- if not os.path.isfile(args.identity_file):
- args.identity_file = None
-
- if args.host:
- args.local_socket_path = os.path.join(args.run_dir,
- "podman.socket")
- else:
- args.local_socket_path = args.remote_socket_path
-
- args.local_uri = "unix:{}".format(args.local_socket_path)
- args.remote_uri = "ssh://{}@{}{}".format(args.user, args.host,
- args.remote_socket_path)
- return args
-
- def exit(self, status=0, message=None):
- """Capture message and route to logger."""
- if message:
- log = logging.info if status == 0 else logging.error
- log(message)
- super().exit(status)
-
- def error(self, message):
- """Capture message and route to logger."""
- logging.error('{}: {}'.format(self.prog, message))
- logging.error("Try '{} --help' for more information.".format(
- self.prog))
- super().exit(2)
diff --git a/contrib/python/pypodman/lib/future_abstract.py b/contrib/python/pypodman/lib/future_abstract.py
deleted file mode 100644
index 75a1d42db..000000000
--- a/contrib/python/pypodman/lib/future_abstract.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Utilities for with-statement contexts. See PEP 343."""
-
-import abc
-
-import _collections_abc
-
-try:
- from contextlib import AbstractContextManager
-except ImportError:
- # Copied from python3.7 library as "backport"
- class AbstractContextManager(abc.ABC):
- """An abstract base class for context managers."""
-
- def __enter__(self):
- """Return `self` upon entering the runtime context."""
- return self
-
- @abc.abstractmethod
- def __exit__(self, exc_type, exc_value, traceback):
- """Raise any exception triggered within the runtime context."""
- return None
-
- @classmethod
- def __subclasshook__(cls, C):
- """Check whether subclass is considered a subclass of this ABC."""
- if cls is AbstractContextManager:
- return _collections_abc._check_methods(C, "__enter__",
- "__exit__")
- return NotImplemented
diff --git a/contrib/python/pypodman/lib/pypodman.py b/contrib/python/pypodman/lib/pypodman.py
deleted file mode 100755
index 4bc71a9cc..000000000
--- a/contrib/python/pypodman/lib/pypodman.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-"""Remote podman client."""
-
-import logging
-import os
-import sys
-
-import lib.actions
-from lib import PodmanArgumentParser
-
-assert lib.actions # silence pyflakes
-
-
-def main():
- """Entry point."""
- # Setup logging so we use stderr and can change logging level later
- # Do it now before there is any chance of a default setup hardcoding crap.
- log = logging.getLogger()
- fmt = logging.Formatter('%(asctime)s | %(levelname)-8s | %(message)s',
- '%Y-%m-%d %H:%M:%S %Z')
- stderr = logging.StreamHandler(stream=sys.stderr)
- stderr.setFormatter(fmt)
- log.addHandler(stderr)
- log.setLevel(logging.WARNING)
-
- parser = PodmanArgumentParser()
- args = parser.parse_args()
-
- log.setLevel(args.log_level)
- logging.debug('Logging initialized at level {}'.format(
- logging.getLevelName(logging.getLogger().getEffectiveLevel())))
-
- def want_tb():
- """Add traceback when logging events."""
- return log.getEffectiveLevel() == logging.DEBUG
-
- try:
- if not os.path.exists(args.run_dir):
- os.makedirs(args.run_dir)
- except PermissionError as e:
- logging.critical(e, exc_info=want_tb())
- sys.exit(6)
-
- # class_(args).method() are set by the sub-command's parser
- returncode = None
- try:
- obj = args.class_(args)
- except Exception as e:
- logging.critical(repr(e), exc_info=want_tb())
- logging.warning('See subparser "{}" configuration.'.format(
- args.subparser_name))
- sys.exit(5)
-
- try:
- returncode = getattr(obj, args.method)()
- except AttributeError as e:
- logging.critical(e, exc_info=want_tb())
- logging.warning('See subparser "{}" configuration.'.format(
- args.subparser_name))
- returncode = 3
- except KeyboardInterrupt:
- pass
- except (
- ConnectionRefusedError,
- ConnectionResetError,
- TimeoutError,
- ) as e:
- logging.critical(e, exc_info=want_tb())
- logging.info('Review connection arguments for correctness.')
- returncode = 4
-
- return 0 if returncode is None else returncode
-
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/contrib/python/pypodman/lib/report.py b/contrib/python/pypodman/lib/report.py
deleted file mode 100644
index 25fe2ae0d..000000000
--- a/contrib/python/pypodman/lib/report.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Report Manager."""
-import sys
-from collections import namedtuple
-
-from .future_abstract import AbstractContextManager
-
-
-class ReportColumn(namedtuple('ReportColumn', 'key display width default')):
- """Hold attributes of output column."""
-
- __slots__ = ()
-
- def __new__(cls, key, display, width, default=None):
- """Add defaults for attributes."""
- return super(ReportColumn, cls).__new__(cls, key, display, width,
- default)
-
-
-class Report(AbstractContextManager):
- """Report Manager."""
-
- def __init__(self, columns, heading=True, epilog=None, file=sys.stdout):
- """Construct Report.
-
- columns is a mapping for named fields to column headings.
- headers True prints headers on table.
- epilog will be printed when the report context is closed.
- """
- self._columns = columns
- self._file = file
- self._heading = heading
- self.epilog = epilog
- self._format = None
-
- def row(self, **fields):
- """Print row for report."""
- if self._heading:
- hdrs = {k: v.display for (k, v) in self._columns.items()}
- print(self._format.format(**hdrs), flush=True, file=self._file)
- self._heading = False
- fields = {k: str(v) for k, v in fields.items()}
- print(self._format.format(**fields))
-
- def __exit__(self, exc_type, exc_value, traceback):
- """Leave Report context and print epilog if provided."""
- if self.epilog:
- print(self.epilog, flush=True, file=self._file)
-
- def layout(self, iterable, keys, truncate=True):
- """Use data and headings build format for table to fit."""
- format = []
-
- for key in keys:
- value = max(map(lambda x: len(str(x.get(key, ''))), iterable))
- # print('key', key, 'value', value)
-
- 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
-
- format.append('{{{0}:{1}.{1}}}'.format(key, value))
- self._format = ' '.join(format)