コード例 #1
0
ファイル: package.py プロジェクト: swipswaps/py2deb
    def find_egg_info_file(self, pattern=''):
        """
        Find :pypi:`pip` metadata files in unpacked source distributions.

        :param pattern: The :mod:`glob` pattern to search for (a string).
        :returns: A list of matched filenames (strings).

        When pip unpacks a source distribution archive it creates a directory
        ``pip-egg-info`` which contains the package metadata in a declarative
        and easy to parse format. This method finds such metadata files.
        """
        full_pattern = os.path.join(self.requirement.source_directory,
                                    'pip-egg-info', '*.egg-info', pattern)
        logger.debug("Looking for %r file(s) using pattern %r ..", pattern,
                     full_pattern)
        matches = glob.glob(full_pattern)
        if len(matches) > 1:
            msg = "Source distribution directory of %s (%s) contains multiple *.egg-info directories: %s"
            raise Exception(
                msg % (self.requirement.project_name, self.requirement.version,
                       concatenate(matches)))
        elif matches:
            logger.debug("Matched %s: %s.",
                         pluralize(len(matches), "file", "files"),
                         concatenate(matches))
            return matches[0]
        else:
            logger.debug("No matching %r files found.", pattern)
コード例 #2
0
def ansi_style(**kw):
    """
    Generate ANSI escape sequences for the given color and/or style(s).

    :param color: The name of a color (one of the strings 'black', 'red',
                  'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white') or
                  :data:`None` (the default) which means no escape sequence to
                  switch color will be emitted.
    :param readline_hints: If :data:`True` then :func:`readline_wrap()` is
                           applied to the generated ANSI escape sequences (the
                           default is :data:`False`).
    :param kw: Any additional keyword arguments are expected to match an entry
               in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's
               value evaluates to :data:`True` the respective style will be
               enabled.
    :returns: The ANSI escape sequences to enable the requested text styles or
              an empty string if no styles were requested.
    :raises: :py:exc:`~exceptions.ValueError` when an invalid color name is given.
    """
    # Start with sequences that change text styles.
    sequences = [str(ANSI_TEXT_STYLES[k]) for k, v in kw.items() if k in ANSI_TEXT_STYLES and v]
    # Append the color code (if any).
    color_name = kw.get('color')
    if color_name:
        # Validate the color name.
        if color_name not in ANSI_COLOR_CODES:
            msg = "Invalid color name %r! (expected one of %s)"
            raise ValueError(msg % (color_name, concatenate(sorted(ANSI_COLOR_CODES))))
        sequences.append('3%i' % ANSI_COLOR_CODES[color_name])
    if sequences:
        encoded = ANSI_CSI + ';'.join(sequences) + ANSI_SGR
        return readline_wrap(encoded) if kw.get('readline_hints') else encoded
    else:
        return ''
コード例 #3
0
ファイル: package.py プロジェクト: swipswaps/py2deb
 def __str__(self):
     """The name, version and extras of the package encoded in a human readable string."""
     version = [self.python_version]
     extras = self.requirement.pip_requirement.extras
     if extras:
         version.append("extras: %s" % concatenate(sorted(extras)))
     return "%s (%s)" % (self.python_name, ', '.join(version))
コード例 #4
0
    def smart_search(self, *arguments):
        """
        Perform a smart search on the given keywords or patterns.

        :param arguments: The keywords or patterns to search for.
        :returns: The matched password names (a list of strings).
        :raises: The following exceptions can be raised:

                 - :exc:`.NoMatchingPasswordError` when no matching passwords are found.
                 - :exc:`.EmptyPasswordStoreError` when the password store is empty.

        This method first tries :func:`simple_search()` and if that doesn't
        produce any matches it will fall back to :func:`fuzzy_search()`. If no
        matches are found an exception is raised (see above).
        """
        matches = self.simple_search(*arguments)
        if not matches:
            logger.verbose("Falling back from substring search to fuzzy search ..")
            matches = self.fuzzy_search(*arguments)
        if not matches:
            if len(self.filtered_entries) > 0:
                raise NoMatchingPasswordError(
                    format("No passwords matched the given arguments! (%s)", concatenate(map(repr, arguments)))
                )
            else:
                msg = "You don't have any passwords yet! (no *.gpg files found)"
                raise EmptyPasswordStoreError(msg)
        return matches
コード例 #5
0
def format_timespan(num_seconds, detailed=False, max_units=3):
    """
    Format a timespan in seconds as a human readable string.

    :param num_seconds: Number of seconds (integer or float).
    :param detailed: If :data:`True` milliseconds are represented separately
                     instead of being represented as fractional seconds
                     (defaults to :data:`False`).
    :param max_units: The maximum number of units to show in the formatted time
                      span (an integer, defaults to three).
    :returns: The formatted timespan as a string.

    Some examples:

    >>> from humanfriendly import format_timespan
    >>> format_timespan(0)
    '0 seconds'
    >>> format_timespan(1)
    '1 second'
    >>> import math
    >>> format_timespan(math.pi)
    '3.14 seconds'
    >>> hour = 60 * 60
    >>> day = hour * 24
    >>> week = day * 7
    >>> format_timespan(week * 52 + day * 2 + hour * 3)
    '1 year, 2 days and 3 hours'
    """
    if num_seconds < 60 and not detailed:
        # Fast path.
        return pluralize(round_number(num_seconds), 'second')
    else:
        # Slow path.
        result = []
        num_seconds = decimal.Decimal(str(num_seconds))
        relevant_units = list(reversed(time_units[0 if detailed else 1:]))
        for unit in relevant_units:
            # Extract the unit count from the remaining time.
            divider = decimal.Decimal(str(unit['divider']))
            count = num_seconds / divider
            num_seconds %= divider
            # Round the unit count appropriately.
            if unit != relevant_units[-1]:
                # Integer rounding for all but the smallest unit.
                count = int(count)
            else:
                # Floating point rounding for the smallest unit.
                count = round_number(count)
            # Only include relevant units in the result.
            if count not in (0, '0'):
                result.append(pluralize(count, unit['singular'], unit['plural']))
        if len(result) == 1:
            # A single count/unit combination.
            return result[0]
        else:
            if not detailed:
                # Remove `insignificant' data from the formatted timespan.
                result = result[:max_units]
            # Format the timespan in a readable way.
            return concatenate(result)
コード例 #6
0
    def simple_search(self, *keywords):
        """
        Perform a simple search for case insensitive substring matches.

        :param keywords: The string(s) to search for.
        :returns: The matched password names (a generator of strings).

        Only passwords whose names matches *all*  of the given keywords are
        returned.
        """
        matches = []
        keywords = [kw.lower() for kw in keywords]
        logger.verbose(
            "Performing simple search on %s (%s) ..",
            pluralize(len(keywords), "keyword"),
            concatenate(map(repr, keywords)),
        )
        for entry in self.filtered_entries:
            normalized = entry.name.lower()
            if all(kw in normalized for kw in keywords):
                matches.append(entry)
        logger.log(
            logging.INFO if matches else logging.VERBOSE,
            "Matched %s using simple search.",
            pluralize(len(matches), "password"),
        )
        return matches
コード例 #7
0
def format_timespan(num_seconds, detailed=False, max_units=3):
    """
    Format a timespan in seconds as a human readable string.

    :param num_seconds: Number of seconds (integer or float).
    :param detailed: If :data:`True` milliseconds are represented separately
                     instead of being represented as fractional seconds
                     (defaults to :data:`False`).
    :param max_units: The maximum number of units to show in the formatted time
                      span (an integer, defaults to three).
    :returns: The formatted timespan as a string.

    Some examples:

    >>> from humanfriendly import format_timespan
    >>> format_timespan(0)
    '0 seconds'
    >>> format_timespan(1)
    '1 second'
    >>> import math
    >>> format_timespan(math.pi)
    '3.14 seconds'
    >>> hour = 60 * 60
    >>> day = hour * 24
    >>> week = day * 7
    >>> format_timespan(week * 52 + day * 2 + hour * 3)
    '1 year, 2 days and 3 hours'
    """
    if num_seconds < 60 and not detailed:
        # Fast path.
        return pluralize(round_number(num_seconds), "second")
    else:
        # Slow path.
        result = []
        num_seconds = decimal.Decimal(str(num_seconds))
        relevant_units = list(reversed(time_units[0 if detailed else 1 :]))
        for unit in relevant_units:
            # Extract the unit count from the remaining time.
            divider = decimal.Decimal(str(unit["divider"]))
            count = num_seconds / divider
            num_seconds %= divider
            # Round the unit count appropriately.
            if unit != relevant_units[-1]:
                # Integer rounding for all but the smallest unit.
                count = int(count)
            else:
                # Floating point rounding for the smallest unit.
                count = round_number(count)
            # Only include relevant units in the result.
            if count not in (0, "0"):
                result.append(pluralize(count, unit["singular"], unit["plural"]))
        if len(result) == 1:
            # A single count/unit combination.
            return result[0]
        else:
            if not detailed:
                # Remove `insignificant' data from the formatted timespan.
                result = result[:max_units]
            # Format the timespan in a readable way.
            return concatenate(result)
コード例 #8
0
def wait_for_processes(processes):
    """
    Wait for the given processes to end.

    Prints an overview of running processes to the terminal once a second so
    the user knows what they are waiting for.

    This function is not specific to :mod:`proc.cron` at all (it doesn't
    even need to know what cron jobs are), it just waits until all of the given
    processes have ended.

    :param processes: A list of :class:`~proc.tree.ProcessNode` objects.
    """
    wait_timer = Timer()
    running_processes = list(processes)
    for process in running_processes:
        logger.info("Waiting for process %i: %s (runtime is %s)", process.pid,
                    quote(process.cmdline),
                    format_timespan(round(process.runtime)))
    with Spinner(timer=wait_timer) as spinner:
        while True:
            for process in list(running_processes):
                if not process.is_alive:
                    running_processes.remove(process)
            if not running_processes:
                break
            num_processes = pluralize(len(running_processes), "process",
                                      "processes")
            process_ids = concatenate(str(p.pid) for p in running_processes)
            spinner.step(label="Waiting for %s: %s" %
                         (num_processes, process_ids))
            spinner.sleep()
    logger.info("All processes have finished, we're done waiting (took %s).",
                wait_timer.rounded)
コード例 #9
0
 def overview(self):
     """Render an overview with related members grouped together."""
     return (
         ("Superclass" if len(self.type.__bases__) == 1 else "Superclasses",
          concatenate(format(":class:`~%s.%s`", b.__module__, b.__name__) for b in self.type.__bases__)),
         ("Special methods", self.format_methods(self.special_methods)),
         ("Public methods", self.format_methods(self.public_methods)),
         ("Properties", self.format_properties(n for n, v in self.properties)),
     )
コード例 #10
0
ファイル: rotate_dcm.py プロジェクト: dacopan/autobackup-dcm
    def rotate_backups(self, directory):
        """
        Rotate the backups in a directory according to a flexible rotation scheme.
        :param directory: The pathname of a directory that contains backups to
                          rotate (a string).

        .. note:: This function binds the main methods of the
                  :class:`RotateBackups` class together to implement backup
                  rotation with an easy to use Python API. If you're using
                  `rotate-backups` as a Python API and the default behavior is
                  not satisfactory, consider writing your own
                  :func:`rotate_backups()` function based on the underlying
                  :func:`collect_backups()`, :func:`group_backups()`,
                  :func:`apply_rotation_scheme()` and
                  :func:`find_preservation_criteria()` methods.
        """
        # Load configuration overrides by user?

        # Collect the backups in the given directory. if rotate type is on local or on google drive
        sorted_backups = self.collect_backups(directory, self.rotate_type)
        if not sorted_backups:
            logger.info("No backups found in %s.", self.custom_format_path(directory))
            return
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency, most_recent_backup.datetime)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(backups_by_frequency)
        # Apply the calculated rotation scheme.
        for backup in sorted_backups:
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info("Preserving %s (matches %s retention %s) ..",
                            self.custom_format_path(backup.pathname),
                            concatenate(map(repr, matching_periods)),
                            "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s %s ..", backup.type, self.custom_format_path(backup.pathname))
                if not self.dry_run:
                    timer = Timer()
                    if self.rotate_type == 'local':  # if rotate type is on local or on google drive
                        command = ['rm', '-Rf', backup.pathname]
                        if self.io_scheduling_class:
                            command = ['ionice', '--class', self.io_scheduling_class] + command

                        execute(*command, logger=logger)
                    else:
                        self.gdrivecm.delete_file(backup.pathname.split('_')[0])
                    logger.debug("Deleted %s in %s.", self.custom_format_path(backup.pathname), timer)
        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
コード例 #11
0
    def __init__(self, **kw):
        """
        Initialize a :class:`PropertyManager` object.

        :param kw: Any keyword arguments are passed on to :func:`set_properties()`.
        """
        self.set_properties(**kw)
        missing_properties = self.missing_properties
        if missing_properties:
            msg = "missing %s" % pluralize(len(missing_properties),
                                           "required argument")
            raise TypeError("%s (%s)" % (msg, concatenate(missing_properties)))
コード例 #12
0
ファイル: control.py プロジェクト: xolox/python-deb-pkg-tools
def check_mandatory_fields(control_fields):
    """
    Make sure mandatory binary control fields are defined.

    :param control_fields: A dictionary with control file fields.
    :raises: :exc:`~exceptions.ValueError` when a mandatory binary control
             field is not present in the provided control fields (see also
             :data:`MANDATORY_BINARY_CONTROL_FIELDS`).
    """
    missing_fields = [f for f in MANDATORY_BINARY_CONTROL_FIELDS if not control_fields.get(f)]
    if missing_fields:
        raise ValueError(compact(
            "Missing {fields}! ({details})",
            fields=pluralize(len(missing_fields), "mandatory binary package control field"),
            details=concatenate(sorted(missing_fields)),
        ))
コード例 #13
0
ファイル: __init__.py プロジェクト: yangsongx/doc
def format_timespan(num_seconds, detailed=False):
    """
    Format a timespan in seconds as a human readable string.

    :param num_seconds: Number of seconds (integer or float).
    :param detailed: If :data:`True` milliseconds are represented separately
                     instead of being represented as fractional seconds
                     (defaults to :data:`False`).
    :returns: The formatted timespan as a string.

    Some examples:

    >>> from humanfriendly import format_timespan
    >>> format_timespan(0)
    '0 seconds'
    >>> format_timespan(1)
    '1 second'
    >>> import math
    >>> format_timespan(math.pi)
    '3.14 seconds'
    >>> hour = 60 * 60
    >>> day = hour * 24
    >>> week = day * 7
    >>> format_timespan(week * 52 + day * 2 + hour * 3)
    '1 year, 2 days and 3 hours'
    """
    if num_seconds < 60 and not detailed:
        # Fast path.
        return pluralize(round_number(num_seconds), 'second')
    else:
        # Slow path.
        result = []
        for unit in reversed(time_units):
            if num_seconds >= unit['divider']:
                count = int(num_seconds / unit['divider'])
                num_seconds %= unit['divider']
                result.append(pluralize(count, unit['singular'], unit['plural']))
        if len(result) == 1:
            # A single count/unit combination.
            return result[0]
        else:
            if not detailed:
                # Remove insignificant data from the formatted timespan.
                result = result[:3]
            # Format the timespan in a readable way.
            return concatenate(result)
コード例 #14
0
def format_timespan(num_seconds, detailed=False):
    """
    Format a timespan in seconds as a human readable string.

    :param num_seconds: Number of seconds (integer or float).
    :param detailed: If :data:`True` milliseconds are represented separately
                     instead of being represented as fractional seconds
                     (defaults to :data:`False`).
    :returns: The formatted timespan as a string.

    Some examples:

    >>> from humanfriendly import format_timespan
    >>> format_timespan(0)
    '0 seconds'
    >>> format_timespan(1)
    '1 second'
    >>> import math
    >>> format_timespan(math.pi)
    '3.14 seconds'
    >>> hour = 60 * 60
    >>> day = hour * 24
    >>> week = day * 7
    >>> format_timespan(week * 52 + day * 2 + hour * 3)
    '1 year, 2 days and 3 hours'
    """
    if num_seconds < 60 and not detailed:
        # Fast path.
        return pluralize(round_number(num_seconds), 'second')
    else:
        # Slow path.
        result = []
        for unit in reversed(time_units):
            if num_seconds >= unit['divider']:
                count = int(num_seconds / unit['divider'])
                num_seconds %= unit['divider']
                result.append(pluralize(count, unit['singular'], unit['plural']))
        if len(result) == 1:
            # A single count/unit combination.
            return result[0]
        else:
            if not detailed:
                # Remove insignificant data from the formatted timespan.
                result = result[:3]
            # Format the timespan in a readable way.
            return concatenate(result)
コード例 #15
0
    def fuzzy_search(self, *filters):
        """
        Perform a "fuzzy" search that matches the given characters in the given order.

        :param filters: The pattern(s) to search for.
        :returns: The matched password names (a list of strings).
        """
        matches = []
        logger.verbose("Performing fuzzy search on %s (%s) ..",
                       pluralize(len(filters), "pattern"),
                       concatenate(map(repr, filters)))
        patterns = list(map(create_fuzzy_pattern, filters))
        for entry in self.entries:
            if all(p.search(entry.name) for p in patterns):
                matches.append(entry)
        logger.log(logging.INFO if matches else logging.VERBOSE,
                   "Matched %s using fuzzy search.",
                   pluralize(len(matches), "password"))
        return matches
コード例 #16
0
def cron_graceful(arguments):
    """Command line interface for the ``cron-graceful`` program."""
    runtime_timer = Timer()
    # Initialize logging to the terminal.
    dry_run = parse_arguments(arguments)
    if not dry_run:
        ensure_root_privileges()
    try:
        cron_daemon = find_cron_daemon()
    except CronDaemonNotRunning:
        logger.info(
            "No running cron daemon found, assuming it was previously stopped .."
        )
    else:
        if not dry_run:
            # Prevent the cron daemon from starting new cron jobs.
            cron_daemon.suspend()
            # Enable user defined additional logic.
            run_additions()
        # Identify the running cron jobs based on the process tree _after_ the
        # cron daemon has been paused (assuming we're not performing a dry run)
        # so we know for sure that we see all running cron jobs (also we're not
        # interested in any processes that have already been stopped by
        # cron-graceful-additions).
        cron_daemon = find_cron_daemon()
        cron_jobs = sorted_by_pid(cron_daemon.grandchildren)
        if cron_jobs:
            logger.info("Found %s: %s",
                        pluralize(len(cron_jobs), "running cron job"),
                        concatenate(str(j.pid) for j in cron_jobs))
            # Wait for the running cron jobs to finish.
            wait_for_processes(cron_jobs)
        else:
            logger.info("No running cron jobs found.")
        # Terminate the cron daemon.
        if dry_run:
            logger.info("Stopping cron daemon with process id %i ..",
                        cron_daemon.pid)
        else:
            terminate_cron_daemon(cron_daemon)
        logger.info("Done! Took %s to gracefully terminate cron.",
                    runtime_timer.rounded)
コード例 #17
0
def check_mandatory_fields(control_fields):
    """
    Make sure mandatory binary control fields are defined.

    :param control_fields: A dictionary with control file fields.
    :raises: :exc:`~exceptions.ValueError` when a mandatory binary control
             field is not present in the provided control fields (see also
             :data:`MANDATORY_BINARY_CONTROL_FIELDS`).
    """
    missing_fields = [
        f for f in MANDATORY_BINARY_CONTROL_FIELDS if not control_fields.get(f)
    ]
    if missing_fields:
        raise ValueError(
            compact(
                "Missing {fields}! ({details})",
                fields=pluralize(len(missing_fields),
                                 "mandatory binary package control field"),
                details=concatenate(sorted(missing_fields)),
            ))
コード例 #18
0
    def rotate_backups(self, bucketname, prefix):
        """
        Rotate the backups in a bucket according to a flexible rotation scheme.

        :param bucketname: S3 bucketthat contains backups to rotate (a string).
        """

        bucket = self.conn.get_bucket(bucketname)
        # Collect the backups in the given directory.
        sorted_backups = self.collect_backups(bucketname, prefix)
        if not sorted_backups:
            logger.info("No backups found in %s.", bucketname)
            return
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency,
                                   most_recent_backup.timestamp)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(
            backups_by_frequency)
        # Apply the calculated rotation scheme.
        deleted_files = []
        for backup in sorted_backups:
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info(
                    "Preserving %s (matches %s retention %s) ..",
                    backup.pathname, concatenate(map(repr, matching_periods)),
                    "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s %s ..", backup.type, backup.pathname)
                if not self.dry_run:
                    logger.debug("Marking %s for deletion.", backup.pathname)
                    deleted_files.append(backup.pathname)
        if deleted_files:
            bucket.delete_keys(deleted_files)

        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
コード例 #19
0
def format_timespan(num_seconds):
    """
    Format a timespan in seconds as a human readable string.

    :param num_seconds: Number of seconds (integer or float).
    :returns: The formatted timespan as a string.

    Some examples:

    >>> from humanfriendly import format_timespan
    >>> format_timespan(0)
    '0.00 seconds'
    >>> format_timespan(1)
    '1.00 second'
    >>> format_timespan(math.pi)
    '3.14 seconds'
    >>> hour = 60 * 60
    >>> day = hour * 24
    >>> week = day * 7
    >>> format_timespan(week * 52 + day * 2 + hour * 3)
    '1 year, 2 days and 3 hours'
    """
    if num_seconds < 60:
        # Fast path.
        return pluralize(round_number(num_seconds), 'second')
    else:
        # Slow path.
        result = []
        for unit in reversed(time_units):
            if num_seconds >= unit['divider']:
                count = int(num_seconds / unit['divider'])
                num_seconds %= unit['divider']
                result.append(pluralize(count, unit['singular'], unit['plural']))
        if len(result) == 1:
            # A single count/unit combination.
            return result[0]
        else:
            # Remove insignificant data from the formatted timespan and format
            # it in a readable way.
            return concatenate(result[:3])
コード例 #20
0
def load_config(repository):
    """Load repository configuration from a ``repos.ini`` file."""
    repository = os.path.abspath(repository)
    for config_dir in (config.user_config_directory,
                       config.system_config_directory):
        config_file = os.path.join(config_dir, config.repo_config_file)
        if os.path.isfile(config_file):
            logger.debug("Loading configuration from %s ..",
                         format_path(config_file))
            parser = configparser.RawConfigParser()
            parser.read(config_file)
            sections = dict(
                (n, dict(parser.items(n))) for n in parser.sections())
            defaults = sections.get('default', {})
            logger.debug("Found %i sections: %s", len(sections),
                         concatenate(parser.sections()))
            for name, options in sections.items():
                directory = options.get('directory')
                if directory and fnmatch.fnmatch(repository, directory):
                    defaults.update(options)
                    return defaults
    return {}
コード例 #21
0
def ansi_style(**kw):
    """
    Generate ANSI escape sequences for the given color and/or style(s).

    :param color: The name of a color (one of the strings 'black', 'red',
                  'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white') or
                  :data:`None` (the default) which means no escape sequence to
                  switch color will be emitted.
    :param readline_hints: If :data:`True` then :func:`readline_wrap()` is
                           applied to the generated ANSI escape sequences (the
                           default is :data:`False`).
    :param kw: Any additional keyword arguments are expected to match an entry
               in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's
               value evaluates to :data:`True` the respective style will be
               enabled.
    :returns: The ANSI escape sequences to enable the requested text styles or
              an empty string if no styles were requested.
    :raises: :exc:`~exceptions.ValueError` when an invalid color name is given.
    """
    # Start with sequences that change text styles.
    sequences = [
        str(ANSI_TEXT_STYLES[k]) for k, v in kw.items()
        if k in ANSI_TEXT_STYLES and v
    ]
    # Append the color code (if any).
    color_name = kw.get('color')
    if color_name:
        # Validate the color name.
        if color_name not in ANSI_COLOR_CODES:
            msg = "Invalid color name %r! (expected one of %s)"
            raise ValueError(
                msg % (color_name, concatenate(sorted(ANSI_COLOR_CODES))))
        sequences.append('3%i' % ANSI_COLOR_CODES[color_name])
    if sequences:
        encoded = ANSI_CSI + ';'.join(sequences) + ANSI_SGR
        return readline_wrap(encoded) if kw.get('readline_hints') else encoded
    else:
        return ''
コード例 #22
0
ファイル: prompts.py プロジェクト: ftri/python-humanfriendly
def prompt_for_choice(choices, default=None, padding=True):
    """
    Prompt the user to select a choice from a group of options.

    :param choices: A sequence of strings with available options.
    :param default: The default choice if the user simply presses Enter
                    (expected to be a string, defaults to :data:`None`).
    :param padding: Refer to the documentation of :func:`prompt_for_input()`.
    :returns: The string corresponding to the user's choice.
    :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence.
             - Any exceptions raised by :func:`retry_limit()`.
             - Any exceptions raised by :func:`prompt_for_input()`.

    When no options are given an exception is raised:

    >>> prompt_for_choice([])
    Traceback (most recent call last):
      File "humanfriendly/prompts.py", line 148, in prompt_for_choice
        raise ValueError("Can't prompt for choice without any options!")
    ValueError: Can't prompt for choice without any options!

    If a single option is given the user isn't prompted:

    >>> prompt_for_choice(['only one choice'])
    'only one choice'

    Here's what the actual prompt looks like by default:

    >>> prompt_for_choice(['first option', 'second option'])
     <BLANKLINE>
      1. first option
      2. second option
     <BLANKLINE>
     Enter your choice as a number or unique substring (Control-C aborts): second
     <BLANKLINE>
    'second option'

    If you don't like the whitespace (empty lines and indentation):

    >>> prompt_for_choice(['first option', 'second option'], padding=False)
     1. first option
     2. second option
    Enter your choice as a number or unique substring (Control-C aborts): first
    'first option'
    """
    indent = ' ' if padding else ''
    # Make sure we can use 'choices' more than once (i.e. not a generator).
    choices = list(choices)
    if len(choices) == 1:
        # If there's only one option there's no point in prompting the user.
        logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0])
        return choices[0]
    elif not choices:
        # We can't render a choice prompt without any options.
        raise ValueError("Can't prompt for choice without any options!")
    # Generate the prompt text.
    prompt_text = ('\n\n' if padding else '\n').join([
        # Present the available choices in a user friendly way.
        "\n".join([
            (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "")
            for i, choice in enumerate(choices, start=1)
        ]),
        # Instructions for the user.
        "Enter your choice as a number or unique substring (Control-C aborts): ",
    ])
    if terminal_supports_colors():
        prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True)
    # Loop until a valid choice is made.
    logger.debug("Requesting interactive choice on terminal (options are %s) ..",
                 concatenate(map(repr, choices)))
    for attempt in retry_limit():
        reply = prompt_for_input(prompt_text, '', padding=padding, strip=True)
        if not reply and default is not None:
            logger.debug("Default choice selected by empty reply (%r).", default)
            return default
        elif reply.isdigit():
            index = int(reply) - 1
            if 0 <= index < len(choices):
                logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply)
                return choices[index]
        # Check for substring matches.
        matches = []
        for choice in choices:
            lower_reply = reply.lower()
            lower_choice = choice.lower()
            if lower_reply == lower_choice:
                # If we have an 'exact' match we return it immediately.
                logger.debug("Option (%r) selected by reply (exact match).", choice, reply)
                return choice
            elif lower_reply in lower_choice and len(lower_reply) > 0:
                # Otherwise we gather substring matches.
                matches.append(choice)
        if len(matches) == 1:
            # If a single choice was matched we return it.
            logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply)
            return matches[0]
        else:
            # Give the user a hint about what went wrong.
            if matches:
                details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches))
            elif reply.isdigit():
                details = format("number %i is not a valid choice", int(reply))
            elif reply and not reply.isspace():
                details = format("text '%s' doesn't match any choices", reply)
            else:
                details = "there's no default choice"
            logger.debug("Got %s reply (%s), retrying (%i/%i) ..",
                         "invalid" if reply else "empty", details,
                         attempt, MAX_ATTEMPTS)
            warning("%sError: Invalid input (%s).", indent, details)
コード例 #23
0
ファイル: prompts.py プロジェクト: divyadevadu/CAMstream
def prompt_for_choice(choices, default=None, padding=True):
    """
    Prompt the user to select a choice from a group of options.

    :param choices: A sequence of strings with available options.
    :param default: The default choice if the user simply presses Enter
                    (expected to be a string, defaults to :data:`None`).
    :param padding: Refer to the documentation of :func:`prompt_for_input()`.
    :returns: The string corresponding to the user's choice.
    :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence.
             - Any exceptions raised by :func:`retry_limit()`.
             - Any exceptions raised by :func:`prompt_for_input()`.

    When no options are given an exception is raised:

    >>> prompt_for_choice([])
    Traceback (most recent call last):
      File "humanfriendly/prompts.py", line 148, in prompt_for_choice
        raise ValueError("Can't prompt for choice without any options!")
    ValueError: Can't prompt for choice without any options!

    If a single option is given the user isn't prompted:

    >>> prompt_for_choice(['only one choice'])
    'only one choice'

    Here's what the actual prompt looks like by default:

    >>> prompt_for_choice(['first option', 'second option'])
     <BLANKLINE>
      1. first option
      2. second option
     <BLANKLINE>
     Enter your choice as a number or unique substring (Control-C aborts): second
     <BLANKLINE>
    'second option'

    If you don't like the whitespace (empty lines and indentation):

    >>> prompt_for_choice(['first option', 'second option'], padding=False)
     1. first option
     2. second option
    Enter your choice as a number or unique substring (Control-C aborts): first
    'first option'
    """
    indent = ' ' if padding else ''
    # Make sure we can use 'choices' more than once (i.e. not a generator).
    choices = list(choices)
    if len(choices) == 1:
        # If there's only one option there's no point in prompting the user.
        logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0])
        return choices[0]
    elif not choices:
        # We can't render a choice prompt without any options.
        raise ValueError("Can't prompt for choice without any options!")
    # Generate the prompt text.
    prompt_text = ('\n\n' if padding else '\n').join([
        # Present the available choices in a user friendly way.
        "\n".join([
            (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "")
            for i, choice in enumerate(choices, start=1)
        ]),
        # Instructions for the user.
        "Enter your choice as a number or unique substring (Control-C aborts): ",
    ])
    prompt_text = prepare_prompt_text(prompt_text, bold=True)
    # Loop until a valid choice is made.
    logger.debug("Requesting interactive choice on terminal (options are %s) ..",
                 concatenate(map(repr, choices)))
    for attempt in retry_limit():
        reply = prompt_for_input(prompt_text, '', padding=padding, strip=True)
        if not reply and default is not None:
            logger.debug("Default choice selected by empty reply (%r).", default)
            return default
        elif reply.isdigit():
            index = int(reply) - 1
            if 0 <= index < len(choices):
                logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply)
                return choices[index]
        # Check for substring matches.
        matches = []
        for choice in choices:
            lower_reply = reply.lower()
            lower_choice = choice.lower()
            if lower_reply == lower_choice:
                # If we have an 'exact' match we return it immediately.
                logger.debug("Option (%r) selected by reply (exact match).", choice)
                return choice
            elif lower_reply in lower_choice and len(lower_reply) > 0:
                # Otherwise we gather substring matches.
                matches.append(choice)
        if len(matches) == 1:
            # If a single choice was matched we return it.
            logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply)
            return matches[0]
        else:
            # Give the user a hint about what went wrong.
            if matches:
                details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches))
            elif reply.isdigit():
                details = format("number %i is not a valid choice", int(reply))
            elif reply and not reply.isspace():
                details = format("text '%s' doesn't match any choices", reply)
            else:
                details = "there's no default choice"
            logger.debug("Got %s reply (%s), retrying (%i/%i) ..",
                         "invalid" if reply else "empty", details,
                         attempt, MAX_ATTEMPTS)
            warning("%sError: Invalid input (%s).", indent, details)
コード例 #24
0
def ansi_style(**kw):
    """
    Generate ANSI escape sequences for the given color and/or style(s).

    :param color: The foreground color. Three types of values are supported:

                  - The name of a color (one of the strings 'black', 'red',
                    'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white').
                  - An integer that refers to the 256 color mode palette.
                  - A tuple or list with three integers representing an RGB
                    (red, green, blue) value.

                  The value :data:`None` (the default) means no escape
                  sequence to switch color will be emitted.
    :param background: The background color (see the description
                       of the `color` argument).
    :param bright: Use high intensity colors instead of default colors
                   (a boolean, defaults to :data:`False`).
    :param readline_hints: If :data:`True` then :func:`readline_wrap()` is
                           applied to the generated ANSI escape sequences (the
                           default is :data:`False`).
    :param kw: Any additional keyword arguments are expected to match a key
               in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's
               value evaluates to :data:`True` the respective style will be
               enabled.
    :returns: The ANSI escape sequences to enable the requested text styles or
              an empty string if no styles were requested.
    :raises: :exc:`~exceptions.ValueError` when an invalid color name is given.

    Even though only eight named colors are supported, the use of `bright=True`
    and `faint=True` increases the number of available colors to around 24 (it
    may be slightly lower, for example because faint black is just black).

    **Support for 8-bit colors**

    In `release 4.7`_ support for 256 color mode was added. While this
    significantly increases the available colors it's not very human friendly
    in usage because you need to look up color codes in the `256 color mode
    palette <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>`_.

    You can use the ``humanfriendly --demo`` command to get a demonstration of
    the available colors, see also the screen shot below. Note that the small
    font size in the screen shot was so that the demonstration of 256 color
    mode support would fit into a single screen shot without scrolling :-)
    (I wasn't feeling very creative).

      .. image:: images/ansi-demo.png

    **Support for 24-bit colors**

    In `release 4.14`_ support for 24-bit colors was added by accepting a tuple
    or list with three integers representing the RGB (red, green, blue) value
    of a color. This is not included in the demo because rendering millions of
    colors was deemed unpractical ;-).

    .. _release 4.7: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-7-2018-01-14
    .. _release 4.14: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-14-2018-07-13
    """
    # Start with sequences that change text styles.
    sequences = [ANSI_TEXT_STYLES[k] for k, v in kw.items() if k in ANSI_TEXT_STYLES and v]
    # Append the color code (if any).
    for color_type in 'color', 'background':
        color_value = kw.get(color_type)
        if isinstance(color_value, (tuple, list)):
            if len(color_value) != 3:
                msg = "Invalid color value %r! (expected tuple or list with three numbers)"
                raise ValueError(msg % color_value)
            sequences.append(48 if color_type == 'background' else 38)
            sequences.append(2)
            sequences.extend(map(int, color_value))
        elif isinstance(color_value, numbers.Number):
            # Numeric values are assumed to be 256 color codes.
            sequences.extend((
                39 if color_type == 'background' else 38,
                5, int(color_value)
            ))
        elif color_value:
            # Other values are assumed to be strings containing one of the known color names.
            if color_value not in ANSI_COLOR_CODES:
                msg = "Invalid color value %r! (expected an integer or one of the strings %s)"
                raise ValueError(msg % (color_value, concatenate(map(repr, sorted(ANSI_COLOR_CODES)))))
            # Pick the right offset for foreground versus background
            # colors and regular intensity versus bright colors.
            offset = (
                (100 if kw.get('bright') else 40)
                if color_type == 'background'
                else (90 if kw.get('bright') else 30)
            )
            # Combine the offset and color code into a single integer.
            sequences.append(offset + ANSI_COLOR_CODES[color_value])
    if sequences:
        encoded = ANSI_CSI + ';'.join(map(str, sequences)) + ANSI_SGR
        return readline_wrap(encoded) if kw.get('readline_hints') else encoded
    else:
        return ''
コード例 #25
0
    def rotate_backups(self, location, load_config=True, prepare=False):
        """
        Rotate the backups in a directory according to a flexible rotation scheme.

        :param location: Any value accepted by :func:`coerce_location()`.
        :param load_config: If :data:`True` (so by default) the rotation scheme
                            and other options can be customized by the user in
                            a configuration file. In this case the caller's
                            arguments are only used when the configuration file
                            doesn't define a configuration for the location.
        :param prepare: If this is :data:`True` (not the default) then
                        :func:`rotate_backups()` will prepare the required
                        rotation commands without running them.
        :returns: A list with the rotation commands
                  (:class:`~executor.ExternalCommand` objects).
        :raises: :exc:`~exceptions.ValueError` when the given location doesn't
                 exist, isn't readable or isn't writable. The third check is
                 only performed when dry run isn't enabled.

        This function binds the main methods of the :class:`RotateBackups`
        class together to implement backup rotation with an easy to use Python
        API. If you're using `rotate-backups` as a Python API and the default
        behavior is not satisfactory, consider writing your own
        :func:`rotate_backups()` function based on the underlying
        :func:`collect_backups()`, :func:`group_backups()`,
        :func:`apply_rotation_scheme()` and
        :func:`find_preservation_criteria()` methods.
        """
        rotation_commands = []
        location = coerce_location(location)
        # Load configuration overrides by user?
        if load_config:
            location = self.load_config_file(location)
        # Collect the backups in the given directory.
        sorted_backups = self.collect_backups(location)
        if not sorted_backups:
            logger.info("No backups found in %s.", location)
            return
        # Make sure the directory is writable.
        if not self.dry_run:
            location.ensure_writable()
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency, most_recent_backup.timestamp)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(backups_by_frequency)
        # Apply the calculated rotation scheme.
        for backup in sorted_backups:
            friendly_name = backup.pathname
            if not location.is_remote:
                # Use human friendly pathname formatting for local backups.
                friendly_name = format_path(backup.pathname)
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info("Preserving %s (matches %s retention %s) ..",
                            friendly_name, concatenate(map(repr, matching_periods)),
                            "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s ..", friendly_name)
                if not self.dry_run:
                    # Copy the list with the (possibly user defined) removal command.
                    removal_command = list(self.removal_command)
                    # Add the pathname of the backup as the final argument.
                    removal_command.append(backup.pathname)
                    # Construct the command object.
                    command = location.context.prepare(
                        command=removal_command,
                        group_by=(location.ssh_alias, location.mount_point),
                        ionice=self.io_scheduling_class,
                    )
                    rotation_commands.append(command)
                    if not prepare:
                        timer = Timer()
                        command.wait()
                        logger.verbose("Deleted %s in %s.", friendly_name, timer)
        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
        return rotation_commands
コード例 #26
0
    def rotate_backups(self, location, load_config=True):
        """
        Rotate the backups in a directory according to a flexible rotation scheme.

        :param location: Any value accepted by :func:`coerce_location()`.
        :param load_config: If :data:`True` (so by default) the rotation scheme
                            and other options can be customized by the user in
                            a configuration file. In this case the caller's
                            arguments are only used when the configuration file
                            doesn't define a configuration for the location.
        :raises: :exc:`~exceptions.ValueError` when the given location doesn't
                 exist, isn't readable or isn't writable. The third check is
                 only performed when dry run isn't enabled.

        This function binds the main methods of the :class:`RotateBackups`
        class together to implement backup rotation with an easy to use Python
        API. If you're using `rotate-backups` as a Python API and the default
        behavior is not satisfactory, consider writing your own
        :func:`rotate_backups()` function based on the underlying
        :func:`collect_backups()`, :func:`group_backups()`,
        :func:`apply_rotation_scheme()` and
        :func:`find_preservation_criteria()` methods.
        """
        location = coerce_location(location)
        # Load configuration overrides by user?
        if load_config:
            location = self.load_config_file(location)
        # Collect the backups in the given directory.
        sorted_backups = self.collect_backups(location)
        if not sorted_backups:
            logger.info("No backups found in %s.", location)
            return
        # Make sure the directory is writable.
        if not self.dry_run:
            location.ensure_writable()
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency, most_recent_backup.timestamp)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(backups_by_frequency)
        # Apply the calculated rotation scheme.
        for backup in sorted_backups:
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info("Preserving %s (matches %s retention %s) ..",
                            format_path(backup.pathname),
                            concatenate(map(repr, matching_periods)),
                            "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s ..", format_path(backup.pathname))
                if not self.dry_run:
                    command = ['rm', '-Rf', backup.pathname]
                    if self.io_scheduling_class:
                        command = ['ionice', '--class', self.io_scheduling_class] + command
                    timer = Timer()
                    location.context.execute(*command)
                    logger.debug("Deleted %s in %s.", format_path(backup.pathname), timer)
        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
コード例 #27
0
    def rotate_backups(self, location, load_config=True, prepare=False):
        """
        Rotate the backups in a directory according to a flexible rotation scheme.

        :param location: Any value accepted by :func:`coerce_location()`.
        :param load_config: If :data:`True` (so by default) the rotation scheme
                            and other options can be customized by the user in
                            a configuration file. In this case the caller's
                            arguments are only used when the configuration file
                            doesn't define a configuration for the location.
        :param prepare: If this is :data:`True` (not the default) then
                        :func:`rotate_backups()` will prepare the required
                        rotation commands without running them.
        :returns: A list with the rotation commands (:class:`ExternalCommand`
                  objects).
        :raises: :exc:`~exceptions.ValueError` when the given location doesn't
                 exist, isn't readable or isn't writable. The third check is
                 only performed when dry run isn't enabled.

        This function binds the main methods of the :class:`RotateBackups`
        class together to implement backup rotation with an easy to use Python
        API. If you're using `rotate-backups` as a Python API and the default
        behavior is not satisfactory, consider writing your own
        :func:`rotate_backups()` function based on the underlying
        :func:`collect_backups()`, :func:`group_backups()`,
        :func:`apply_rotation_scheme()` and
        :func:`find_preservation_criteria()` methods.
        """
        rotation_commands = []
        location = coerce_location(location)
        # Load configuration overrides by user?
        if load_config:
            location = self.load_config_file(location)
        # Collect the backups in the given directory.
        sorted_backups = self.collect_backups(location)
        if not sorted_backups:
            logger.info("No backups found in %s.", location)
            return
        # Make sure the directory is writable.
        if not self.dry_run:
            location.ensure_writable()
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency,
                                   most_recent_backup.timestamp)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(
            backups_by_frequency)
        # Apply the calculated rotation scheme.
        for backup in sorted_backups:
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info(
                    "Preserving %s (matches %s retention %s) ..",
                    format_path(backup.pathname),
                    concatenate(map(repr, matching_periods)),
                    "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s ..", format_path(backup.pathname))
                if not self.dry_run:
                    command = location.context.prepare(
                        'rm',
                        '-Rf',
                        backup.pathname,
                        group_by=(location.ssh_alias, location.mount_point),
                        ionice=self.io_scheduling_class,
                    )
                    rotation_commands.append(command)
                    if not prepare:
                        timer = Timer()
                        command.wait()
                        logger.verbose("Deleted %s in %s.",
                                       format_path(backup.pathname), timer)
        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
        return rotation_commands
コード例 #28
0
def foreach(hosts, *command, **options):
    """
    Execute a command simultaneously on a group of remote hosts using SSH.

    :param hosts: An iterable of strings with SSH host aliases.
    :param command: Any positional arguments are converted to a list and used
                    to set the :attr:`~.ExternalCommand.command` property of
                    the :class:`RemoteCommand` objects constructed by
                    :func:`foreach()`.
    :param concurrency: The value of :attr:`.concurrency` to use
                        (defaults to :data:`DEFAULT_CONCURRENCY`).
    :param delay_checks: The value of :attr:`.delay_checks` to use
                         (defaults to :data:`True`).
    :param logs_directory: The value of :attr:`.logs_directory` to
                           use (defaults to :data:`None`).
    :param options: Additional keyword arguments can be used to conveniently
                    override the default values of the writable properties of
                    the :class:`RemoteCommand` objects constructed by
                    :func:`foreach()` (see :func:`RemoteCommand.__init__()` for
                    details).
    :returns: The list of :class:`RemoteCommand` objects constructed by
              :func:`foreach()`.
    :raises: Any of the following exceptions can be raised:

             - :exc:`.CommandPoolFailed` if :attr:`.delay_checks` is enabled
               (the default) and a command in the pool that has :attr:`.check`
               enabled (the default) fails.
             - :exc:`RemoteCommandFailed` if :attr:`.delay_checks` is disabled
               (not the default) and an SSH connection was successful but the
               remote command failed (the exit code of the ``ssh`` command was
               neither zero nor 255). Use the keyword argument ``check=False``
               to disable raising of this exception.
             - :exc:`RemoteConnectFailed` if :attr:`.delay_checks` is disabled
               (not the default) and an SSH connection failed (the exit code of
               the ``ssh`` command is 255). Use the keyword argument
               ``check=False`` to disable raising of this exception.

    .. note:: The :func:`foreach()` function enables the :attr:`.check` and
              :attr:`.delay_checks` options by default in an attempt to make it
              easy to do "the right thing". My assumption here is that if you
              are running *the same command* on multiple remote hosts:

              - You definitely want to know when a remote command has failed,
                ideally without manually checking the :attr:`.succeeded`
                property of each command.

              - Regardless of whether some remote commands fail you want to
                know that the command was at least executed on all hosts,
                otherwise your cluster of hosts will end up in a very
                inconsistent state.

              - If remote commands fail and an exception is raised the
                exception message should explain *which* remote commands
                failed.

              If these assumptions are incorrect then you can use the keyword
              arguments ``check=False`` and/or ``delay_checks=False`` to opt
              out of "doing the right thing" ;-)
    """
    hosts = list(hosts)
    # Separate command pool options from command options.
    concurrency = options.pop('concurrency', DEFAULT_CONCURRENCY)
    delay_checks = options.pop('delay_checks', True)
    logs_directory = options.pop('logs_directory', None)
    # Capture the output of remote commands by default
    # (unless the caller requested capture=False).
    if options.get('capture') is not False:
        options['capture'] = True
    # Enable error checking of remote commands by default
    # (unless the caller requested check=False).
    if options.get('check') is not False:
        options['check'] = True
    # Create a command pool.
    timer = Timer()
    pool = RemoteCommandPool(concurrency=concurrency,
                             delay_checks=delay_checks,
                             logs_directory=logs_directory)
    hosts_pluralized = pluralize(len(hosts), "host")
    logger.debug(
        "Preparing to run remote command on %s (%s) with a concurrency of %i: %s",
        hosts_pluralized, concatenate(hosts), concurrency, quote(command))
    # Populate the pool with remote commands to execute.
    for ssh_alias in hosts:
        pool.add(identifier=ssh_alias,
                 command=RemoteCommand(ssh_alias, *command, **options))
    # Run all commands in the pool.
    pool.run()
    # Report the results to the caller.
    logger.debug("Finished running remote command on %s in %s.",
                 hosts_pluralized, timer)
    return dict(pool.commands).values()
コード例 #29
0
def prompt_for_choice(choices, default=None):
    """
    Prompt the user to select a choice from a list of options.

    :param choices: A list of strings with available options.
    :param default: The default choice if the user simply presses Enter
                    (expected to be a string, defaults to ``None``).
    :returns: The string corresponding to the user's choice.
    """
    # By default the raw_input() prompt is very unfriendly, for example the
    # `Home' key enters `^[OH' and the `End' key enters `^[OF'. By simply
    # importing the `readline' module the prompt becomes much friendlier.
    import readline  # NOQA
    # Make sure we can use 'choices' more than once (i.e. not a generator).
    choices = list(choices)
    # Present the available choices in a user friendly way.
    for i, choice in enumerate(choices, start=1):
        text = u" %i. %s" % (i, choice)
        if choice == default:
            text += " (default choice)"
        print(text)
    # Loop until a valid choice is made.
    prompt = "Enter your choice as a number or unique substring (Ctrl-C aborts): "
    while True:
        input = interactive_prompt(prompt).strip()
        # Make sure the user entered something.
        if not input:
            if default is not None:
                return default
            continue
        # Check for a valid number.
        if input.isdigit():
            index = int(input) - 1
            if 0 <= index < len(choices):
                return choices[index]
        # Check for substring matches.
        matches = []
        for choice in choices:
            lower_input = input.lower()
            lower_choice = choice.lower()
            if lower_input == lower_choice:
                # If we have an 'exact' match we return it immediately.
                return choice
            elif lower_input in lower_choice:
                # Otherwise we gather substring matches.
                matches.append(choice)
        # If a single choice was matched we return it, otherwise we give the
        # user a hint about what went wrong.
        if len(matches) == 1:
            return matches[0]
        elif matches:
            print("Error: The string '%s' matches more than one choice (%s)." % (input, concatenate(matches)))
        elif input.isdigit():
            print("Error: The number %i is not a valid choice." % int(input))
        else:
            print("Error: The string '%s' doesn't match any choices." % input)
コード例 #30
0
 def render_summary(self):
     """Render a summary of installed and removable kernel packages on the terminal."""
     logger.verbose("Sanity checking meta packages on %s ..", self.context)
     with AutomaticSpinner(label="Gathering information about %s" %
                           self.context):
         # Report the installed Linux kernel image meta package(s).
         if self.installed_image_meta_packages:
             logger.info(
                 "Found %s installed:",
                 pluralize(len(self.installed_image_meta_packages),
                           "Linux kernel image meta package"),
             )
             for package in self.installed_image_meta_packages:
                 logger.info(" - %s (%s)", package.name, package.version)
             if len(self.installed_image_meta_packages) > 1:
                 names = concatenate(
                     pkg.name for pkg in self.installed_image_meta_packages)
                 logger.warning(
                     compact(
                         """
                         You have more than one Linux kernel image meta
                         package installed ({names}) which means automatic
                         package removal can be unreliable!
                         """,
                         names=names,
                     ))
                 logger.verbose(
                     compact("""
                         I would suggest to stick to one Linux kernel image
                         meta package, preferably the one that matches the
                         newest kernel :-)
                         """))
         else:
             logger.warning(
                 compact("""
                     It looks like there's no Linux kernel image meta
                     package installed! I hope you've thought about how to
                     handle security updates?
                     """))
         # Report the installed Linux kernel header/image package(s).
         logger.verbose("Checking for removable packages on %s ..",
                        self.context)
         package_types = (
             (self.installed_kernel_packages, "image", True),
             (self.installed_header_packages, "header", False),
             (self.installed_modules_packages, "modules", False),
         )
         for collection, label, expected in package_types:
             if collection:
                 logger.info(
                     "Found %s:",
                     pluralize(len(collection),
                               "installed Linux kernel %s package" % label))
                 for group in self.installed_package_groups:
                     matching_packages = sorted(package.name
                                                for package in group
                                                if package in collection)
                     active_group = any(
                         package.name == self.active_kernel_package
                         for package in group)
                     removable_group = group in self.removable_package_groups
                     if matching_packages:
                         logger.info(
                             " - %s (%s)",
                             concatenate(matching_packages),
                             ansi_wrap("removable", color="green")
                             if removable_group else ansi_wrap(
                                 "the active kernel" if active_group else
                                 ("one of %i newest kernels" %
                                  self.preserve_count),
                                 color="blue",
                             ),
                         )
             elif expected:
                 logger.warning(
                     "No installed %s packages found, this can't be right?!",
                     label)
         # Report the removable packages.
         if self.removable_packages:
             logger.info("Found %s that can be removed.",
                         pluralize(len(self.removable_packages), "package"))
             # Report the shell command to remove the packages.
             logger.verbose("Command to remove packages: %s",
                            " ".join(self.cleanup_command))
         else:
             logger.info("No packages need to be removed! :-)")
コード例 #31
0
 def format_properties(self, names):
     """Format a list of property names as reStructuredText."""
     return concatenate(format(":attr:`%s`", n) for n in sorted(names))
コード例 #32
0
 def format_methods(self, names):
     """Format a list of method names as reStructuredText."""
     return concatenate(format(":func:`%s()`", n) for n in sorted(names))
コード例 #33
0
def ansi_style(**kw):
    """
    Generate ANSI escape sequences for the given color and/or style(s).

    :param color: The foreground color. Two types of values are supported:

                  - The name of a color (one of the strings 'black', 'red',
                    'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white').
                  - An integer that refers to the 256 color mode palette.

                  The value :data:`None` (the default) means no escape
                  sequence to switch color will be emitted.
    :param background: The background color (see the description
                       of the `color` argument).
    :param bright: Use high intensity colors instead of default colors
                   (a boolean, defaults to :data:`False`).
    :param readline_hints: If :data:`True` then :func:`readline_wrap()` is
                           applied to the generated ANSI escape sequences (the
                           default is :data:`False`).
    :param kw: Any additional keyword arguments are expected to match an entry
               in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's
               value evaluates to :data:`True` the respective style will be
               enabled.
    :returns: The ANSI escape sequences to enable the requested text styles or
              an empty string if no styles were requested.
    :raises: :exc:`~exceptions.ValueError` when an invalid color name is given.

    Even though only eight named colors are supported, the use of `bright=True`
    and `faint=True` increases the number of available colors to around 24 (it
    may be slightly lower, for example because faint black is just black).

    Starting in version 4.7 support for 256 color mode was added. While this
    significantly increases the available colors it's not very human friendly
    in usage because you need to look up color codes in the `256 color mode
    palette <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>`_.

    You can use the ``humanfriendly --demo`` command to get a demonstration of
    the available colors, see also the screen shot below. Note that the small
    font size in the screen shot was so that the demonstration of 256 color
    mode support would fit into a single screen shot without scrolling :-)
    (I wasn't feeling very creative).

    .. image:: images/ansi-demo.png
    """
    # Start with sequences that change text styles.
    sequences = [
        ANSI_TEXT_STYLES[k] for k, v in kw.items()
        if k in ANSI_TEXT_STYLES and v
    ]
    # Append the color code (if any).
    for color_type in 'color', 'background':
        color_value = kw.get(color_type)
        if isinstance(color_value, numbers.Number):
            # Numeric values are assumed to be 256 color codes.
            sequences.extend((39 if color_type == 'background' else 38, 5,
                              int(color_value)))
        elif color_value:
            # Other values are assumed to be strings containing one of the known color names.
            if color_value not in ANSI_COLOR_CODES:
                msg = "Invalid color value %r! (expected an integer or one of the strings %s)"
                raise ValueError(
                    msg % (color_value,
                           concatenate(map(repr, sorted(ANSI_COLOR_CODES)))))
            # Pick the right offset for foreground versus background
            # colors and regular intensity versus bright colors.
            offset = ((100 if kw.get('bright') else 40) if color_type
                      == 'background' else (90 if kw.get('bright') else 30))
            # Combine the offset and color code into a single integer.
            sequences.append(offset + ANSI_COLOR_CODES[color_value])
    if sequences:
        encoded = ANSI_CSI + ';'.join(map(str, sequences)) + ANSI_SGR
        return readline_wrap(encoded) if kw.get('readline_hints') else encoded
    else:
        return ''
コード例 #34
0
    def rotate_backups(self, location, load_config=True, prepare=False):
        """
        Rotate the backups in a directory according to a flexible rotation scheme.

        :param location: Any value accepted by :func:`coerce_location()`.
        :param load_config: If :data:`True` (so by default) the rotation scheme
                            and other options can be customized by the user in
                            a configuration file. In this case the caller's
                            arguments are only used when the configuration file
                            doesn't define a configuration for the location.
        :param prepare: If this is :data:`True` (not the default) then
                        :func:`rotate_backups()` will prepare the required
                        rotation commands without running them.
        :returns: A list with the rotation commands
                  (:class:`~executor.ExternalCommand` objects).
        :raises: :exc:`~exceptions.ValueError` when the given location doesn't
                 exist, isn't readable or isn't writable. The third check is
                 only performed when dry run isn't enabled.

        This function binds the main methods of the :class:`RotateBackups`
        class together to implement backup rotation with an easy to use Python
        API. If you're using `rotate-backups` as a Python API and the default
        behavior is not satisfactory, consider writing your own
        :func:`rotate_backups()` function based on the underlying
        :func:`collect_backups()`, :func:`group_backups()`,
        :func:`apply_rotation_scheme()` and
        :func:`find_preservation_criteria()` methods.
        """
        rotation_commands = []
        location = coerce_location(location)
        # Load configuration overrides by user?
        if load_config:
            location = self.load_config_file(location)
        # Collect the backups in the given directory.
        sorted_backups = self.collect_backups(location)
        if not sorted_backups:
            logger.info("No backups found in %s.", location)
            return
        # Make sure the directory is writable, but only when the default
        # removal command is being used (because custom removal commands
        # imply custom semantics that we shouldn't get in the way of, see
        # https://github.com/xolox/python-rotate-backups/issues/18 for
        # more details about one such use case).
        if not self.dry_run and (self.removal_command
                                 == DEFAULT_REMOVAL_COMMAND):
            location.ensure_writable(self.force)
        most_recent_backup = sorted_backups[-1]
        # Group the backups by the rotation frequencies.
        backups_by_frequency = self.group_backups(sorted_backups)
        # Apply the user defined rotation scheme.
        self.apply_rotation_scheme(backups_by_frequency,
                                   most_recent_backup.timestamp)
        # Find which backups to preserve and why.
        backups_to_preserve = self.find_preservation_criteria(
            backups_by_frequency)
        # Apply the calculated rotation scheme.
        for backup in sorted_backups:
            friendly_name = backup.pathname
            if not location.is_remote:
                # Use human friendly pathname formatting for local backups.
                friendly_name = format_path(backup.pathname)
            if backup in backups_to_preserve:
                matching_periods = backups_to_preserve[backup]
                logger.info(
                    "Preserving %s (matches %s retention %s) ..",
                    friendly_name, concatenate(map(repr, matching_periods)),
                    "period" if len(matching_periods) == 1 else "periods")
            else:
                logger.info("Deleting %s ..", friendly_name)
                if not self.dry_run:
                    # Copy the list with the (possibly user defined) removal command.
                    removal_command = list(self.removal_command)
                    # Add the pathname of the backup as the final argument.
                    removal_command.append(backup.pathname)
                    # Construct the command object.
                    command = location.context.prepare(
                        command=removal_command,
                        group_by=(location.ssh_alias, location.mount_point),
                        ionice=self.io_scheduling_class,
                    )
                    rotation_commands.append(command)
                    if not prepare:
                        timer = Timer()
                        command.wait()
                        logger.verbose("Deleted %s in %s.", friendly_name,
                                       timer)
        if len(backups_to_preserve) == len(sorted_backups):
            logger.info("Nothing to do! (all backups preserved)")
        return rotation_commands