def test_connected_to_terminal(self): for stream in [sys.stdin, sys.stdout, sys.stderr]: result = connected_to_terminal(stream) # We really can't assert a True or False value here because this # test suite should be able to run both interactively and # non-interactively :-). assert isinstance(result, bool) # We can at least verify that e.g. /dev/null is never a terminal :-). with open(os.devnull) as handle: assert not connected_to_terminal(handle) # We can also verify that objects without isatty() don't raise an exception. assert not connected_to_terminal(object())
def report_available_mirrors(updater): """Print the available mirrors to the terminal (in a human friendly format).""" if connected_to_terminal(): have_bandwidth = any(c.bandwidth for c in updater.ranked_mirrors) have_last_updated = any(c.last_updated is not None for c in updater.ranked_mirrors) column_names = ["Rank", "Mirror URL", "Available?", "Updating?"] if have_last_updated: column_names.append("Last updated") if have_bandwidth: column_names.append("Bandwidth") data = [] for i, candidate in enumerate(updater.ranked_mirrors, start=1): row = [ i, candidate.mirror_url, "Yes" if candidate.is_available else "No", "Yes" if candidate.is_updating else "No" ] if have_last_updated: row.append("Up to date" if candidate.last_updated == 0 else ( "%s behind" % format_timespan(candidate.last_updated) if candidate. last_updated else "Unknown")) if have_bandwidth: row.append("%s/s" % format_size(round(candidate.bandwidth, 2)) if candidate.bandwidth else "Unknown") data.append(row) output(format_smart_table(data, column_names=column_names)) else: output(u"\n".join( candidate.mirror_url for candidate in updater.ranked_mirrors if candidate.is_available and not candidate.is_updating))
def parse_feedback(status, msg, step, **kwargs): """ This is called once per step execution. It provides a hook into print messages to the terminal, log files and the JIRA Client. """ # nice_output = '{}\n{}'.format(self.complete_msg, result) # pass_back['result'] = result # pass_back['exp'] = exp the_msg = msg task_referal = None if status > logging.INFO: exp = kwargs['exp'] if exp.args: if isinstance(exp.args[0], TaskReferralBase): task_referal = exp.args[0] # error_str = task_referal.get_task_unique_summary() else: error_str = '\n'.join( str(s) for s in exp.args if isinstance(s, six.string_types)) stack_trace = kwargs['stack_trace'] # error_str = '\n'.join(str(s) for s in exp.args if isinstance(s, six.string_types)) the_msg = '{}\nerror message={}\n\n{}'.format( msg, error_str, stack_trace) if jira_client: jira_client.task_handler(status, msg, task_referal) if hft.connected_to_terminal(): hft.output('{} {} {}'.format(hft.ANSI_ERASE_LINE, terminal_checkboxs[status], the_msg)) else: logger.log(status, the_msg)
def interactive(self): """ :data:`True` to allow user interaction, :data:`False` otherwise. The value of :attr:`interactive` defaults to the return value of :func:`~humanfriendly.terminal.connected_to_terminal()` when given :data:`sys.stdin`. """ return connected_to_terminal(sys.stdin)
def report_available_mirrors(updater): """Print the available mirrors to the terminal (in a human friendly format).""" if connected_to_terminal() or os.getenv( 'TRAVIS') == 'true': # make Travis CI test this code # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables have_bandwidth = any(c.bandwidth for c in updater.ranked_mirrors) have_last_updated = any(c.last_updated is not None for c in updater.ranked_mirrors) column_names = ["Rank", "Mirror URL", "Available?", "Updating?"] if have_last_updated: column_names.append("Last updated") if have_bandwidth: column_names.append("Bandwidth") data = [] long_mirror_urls = {} if os.getenv('TRAVIS') == 'true': updater.url_char_len = 50 for i, candidate in enumerate(updater.ranked_mirrors, start=1): if len(candidate.mirror_url) <= updater.url_char_len: stripped_mirror_url = candidate.mirror_url else: # the mirror_url is too long, strip it stripped_mirror_url = candidate.mirror_url[:updater. url_char_len - 3] stripped_mirror_url = stripped_mirror_url + "..." long_mirror_urls[ i] = candidate.mirror_url # store it, output as full afterwards row = [ i, stripped_mirror_url, "Yes" if candidate.is_available else "No", "Yes" if candidate.is_updating else "No" ] if have_last_updated: row.append("Up to date" if candidate.last_updated == 0 else ( "%s behind" % format_timespan(candidate.last_updated, max_units=1) if candidate.last_updated else "Unknown")) if have_bandwidth: row.append("%s/s" % format_size(round(candidate.bandwidth, 0)) if candidate.bandwidth else "Unknown") data.append(row) output(format_table(data, column_names=column_names)) if long_mirror_urls: output(u"Full URLs which are too long to be shown in above table:") for key in long_mirror_urls: output(u"%i: %s", key, long_mirror_urls[key]) else: output(u"\n".join( candidate.mirror_url for candidate in updater.ranked_mirrors if candidate.is_available and not candidate.is_updating))
def prepare_prompt_text(prompt_text, **options): """ Wrap a text to be rendered as an interactive prompt in ANSI escape sequences. :param prompt_text: The text to render on the prompt (a string). :param options: Any keyword arguments are passed on to :func:`.ansi_wrap()`. :returns: The resulting prompt text (a string). ANSI escape sequences are only used when the standard output stream is connected to a terminal. When the standard input stream is connected to a terminal any escape sequences are wrapped in "readline hints". """ return (ansi_wrap(prompt_text, readline_hints=connected_to_terminal(sys.stdin), **options) if terminal_supports_colors(sys.stdout) else prompt_text)
def convert_command_output(*command): """ Command line interface for ``coloredlogs --to-html``. Takes a command (and its arguments) and runs the program under ``script`` (emulating an interactive terminal), intercepts the output of the command and converts ANSI escape sequences in the output to HTML. """ html_output = convert(capture(command)) if connected_to_terminal(): fd, temporary_file = tempfile.mkstemp(suffix='.html') with open(temporary_file, 'w') as handle: handle.write(html_output) webbrowser.open(temporary_file) else: print(html_output)
def process_stack(step_list, initial_state): """ This is the principal function which executes the stack of `Step` objects. For each step in the stack its `func` is called. * If `func` returns one or more Step objects (either as a single object or as a list) then these are added to the top of the stack. The `state` remains unaltered and is passed to the next Step. * If `func` returns any other value (including None and other falsey values) then this is passed as the state object to the `func` of the next Step item in the stack. :param step_list: A list of initial steps which are used to populate the stack. :param initial_state: This value will be passed a the 'state' keyword arg to the first step's `func`. :returns: The return value of the final step's `func`. """ hft.enable_ansi_support() n_state = initial_state step_list.reverse() stack = deque(step_list) try: while stack: # Definitions: # `n_state` = the state for the current iteration # `nplus_state` = the state for the next iteraction (eg N+1) step = stack.pop() kwargs = {'state': n_state} if hft.connected_to_terminal(): with spinners.AutomaticSpinner(step.running_msg, show_time=True): nplus_state = step.run(parse_feedback, **kwargs) else: logger.info('Starting: {}'.format(step.running_msg)) nplus_state = step.run(parse_feedback, **kwargs) # Used to increment the state *only* if no new Steps where returned n_state = _add_steps_from_state_to_stack(nplus_state, stack, n_state) return n_state except Exception as exp: pass_back = {'exp': exp, 'stack_trace': traceback.format_exc()} # print error and then exit with a non-zero exit code parse_feedback(logging.ERROR, 'Unable to continue following the previous error', None, **pass_back) sys.exit(1)
def __init__(self, stream=sys.stderr, level=logging.NOTSET, isatty=None, show_name=True, show_severity=True, show_timestamps=True, show_hostname=True, use_chroot=True, severity_to_style=None): logging.StreamHandler.__init__(self, stream) self.level = level self.show_timestamps = show_timestamps self.show_hostname = show_hostname self.show_name = show_name self.show_severity = show_severity self.severity_to_style = self.default_severity_to_style.copy() if severity_to_style: self.severity_to_style.update(severity_to_style) self.isatty = connected_to_terminal(stream) if isatty is None else isatty if show_hostname: chroot_file = '/etc/debian_chroot' if use_chroot and os.path.isfile(chroot_file): with open(chroot_file) as handle: self.hostname = handle.read().strip() else: self.hostname = re.sub(r'\.local$', '', socket.gethostname()) if show_name: self.pid = os.getpid()
def main(): """Command line interface for the ``apache-manager`` program.""" # Configure logging output. coloredlogs.install(syslog=True) # Command line option defaults. data_file = '/tmp/apache-manager.txt' dry_run = False max_memory_active = None max_memory_idle = None max_ss = None watch = False zabbix_discovery = False verbosity = 0 # Parse the command line options. try: options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [ 'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=', 'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run', 'simulate', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-w', '--watch'): watch = True elif option in ('-a', '--max-memory-active'): max_memory_active = parse_size(value) elif option in ('-i', '--max-memory-idle'): max_memory_idle = parse_size(value) elif option in ('-t', '--max-ss', '--max-time'): max_ss = parse_timespan(value) elif option in ('-f', '--data-file'): data_file = value elif option in ('-z', '--zabbix-discovery'): zabbix_discovery = True elif option in ('-n', '--dry-run', '--simulate'): logger.info("Performing a dry run ..") dry_run = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() verbosity += 1 elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() verbosity -= 1 elif option in ('-h', '--help'): usage(__doc__) return except Exception as e: sys.stderr.write("Error: %s!\n" % e) sys.exit(1) # Execute the requested action(s). manager = ApacheManager() try: if max_memory_active or max_memory_idle or max_ss: manager.kill_workers( max_memory_active=max_memory_active, max_memory_idle=max_memory_idle, timeout=max_ss, dry_run=dry_run, ) elif watch and connected_to_terminal(sys.stdout): watch_metrics(manager) elif zabbix_discovery: report_zabbix_discovery(manager) elif data_file != '-' and verbosity >= 0: for line in report_metrics(manager): if line_is_heading(line): line = ansi_wrap(line, color=HIGHLIGHT_COLOR) print(line) finally: if (not watch) and (data_file == '-' or not dry_run): manager.save_metrics(data_file)
def use_colors(self): """Whether to output ANSI escape sequences for text colors and styles (a boolean).""" return connected_to_terminal()
def main(): """Command line interface for the ``rsync-system-backup`` program.""" # Initialize logging to the terminal and system log. coloredlogs.install(syslog=True) # Parse the command line arguments. context_opts = dict() program_opts = dict() dest_opts = dict() try: options, arguments = getopt.gnu_getopt(sys.argv[1:], 'bsrm:c:t:i:unx:fvqh', [ 'backup', 'snapshot', 'rotate', 'mount=', 'crypto=', 'tunnel=', 'ionice=', 'no-sudo', 'dry-run', 'multi-fs', 'exclude=', 'force', 'disable-notifications', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-b', '--backup'): enable_explicit_action(program_opts, 'backup_enabled') elif option in ('-s', '--snapshot'): enable_explicit_action(program_opts, 'snapshot_enabled') elif option in ('-r', '--rotate'): enable_explicit_action(program_opts, 'rotate_enabled') elif option in ('-m', '--mount'): program_opts['mount_point'] = value elif option in ('-c', '--crypto'): program_opts['crypto_device'] = value elif option in ('-t', '--tunnel'): ssh_user, _, value = value.rpartition('@') ssh_alias, _, port_number = value.partition(':') tunnel_opts = dict( ssh_alias=ssh_alias, ssh_user=ssh_user, # The port number of the rsync daemon. remote_port=RSYNCD_PORT, ) if port_number: # The port number of the SSH server. tunnel_opts['port'] = int(port_number) dest_opts['ssh_tunnel'] = SecureTunnel(**tunnel_opts) elif option in ('-i', '--ionice'): value = value.lower().strip() validate_ionice_class(value) program_opts['ionice'] = value elif option in ('-u', '--no-sudo'): program_opts['sudo_enabled'] = False elif option in ('-n', '--dry-run'): logger.info("Performing a dry run (because of %s option) ..", option) program_opts['dry_run'] = True elif option in ('-f', '--force'): program_opts['force'] = True elif option in ('-x', '--exclude'): program_opts.setdefault('exclude_list', []) program_opts['exclude_list'].append(value) elif option == '--multi-fs': program_opts['multi_fs'] = True elif option == '--disable-notifications': program_opts['notifications_enabled'] = False elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() elif option in ('-h', '--help'): usage(__doc__) return else: raise Exception("Unhandled option! (programming error)") if len(arguments) > 2: msg = "Expected one or two positional arguments! (got %i)" raise Exception(msg % len(arguments)) if len(arguments) == 2: # Get the source from the first of two arguments. program_opts['source'] = arguments.pop(0) if arguments: # Get the destination from the second (or only) argument. dest_opts['expression'] = arguments[0] program_opts['destination'] = Destination(**dest_opts) elif not os.environ.get('RSYNC_MODULE_PATH'): # Show a usage message when no destination is given. usage(__doc__) return except Exception as e: warning("Error: %s", e) sys.exit(1) try: # Inject the source context into the program options. program_opts['source_context'] = create_context(**context_opts) # Initialize the program with the command line # options and execute the requested action(s). RsyncSystemBackup(**program_opts).execute() except Exception as e: if isinstance(e, RsyncSystemBackupError): # Special handling when the backup disk isn't available. if isinstance(e, MissingBackupDiskError): # Check if we're connected to a terminal to decide whether the # error should be propagated or silenced, the idea being that # rsync-system-backup should keep quiet when it's being run # from cron and the backup disk isn't available. if not connected_to_terminal(): logger.info("Skipping backup: %s", e) sys.exit(0) # Known problems shouldn't produce # an intimidating traceback to users. logger.error("Aborting due to error: %s", e) else: # Unhandled exceptions do get a traceback, # because it may help fix programming errors. logger.exception("Aborting due to unhandled exception!") sys.exit(1)
def main(): """Command line interface for the ``apache-manager`` program.""" # Configure logging output. coloredlogs.install() # Command line option defaults. data_file = '/tmp/apache-manager.txt' dry_run = False max_memory_active = None max_memory_idle = None max_ss = None watch = False zabbix_discovery = False verbosity = 0 # Parse the command line options. try: options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [ 'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=', 'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run', 'simulate', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-w', '--watch'): watch = True elif option in ('-a', '--max-memory-active'): max_memory_active = parse_size(value) elif option in ('-i', '--max-memory-idle'): max_memory_idle = parse_size(value) elif option in ('-t', '--max-ss', '--max-time'): max_ss = parse_timespan(value) elif option in ('-f', '--data-file'): data_file = value elif option in ('-z', '--zabbix-discovery'): zabbix_discovery = True elif option in ('-n', '--dry-run', '--simulate'): logger.info("Performing a dry run ..") dry_run = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() verbosity += 1 elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() verbosity -= 1 elif option in ('-h', '--help'): usage(__doc__) return except Exception as e: sys.stderr.write("Error: %s!\n" % e) sys.exit(1) # Execute the requested action(s). manager = ApacheManager() try: if max_memory_active or max_memory_idle or max_ss: manager.kill_workers( max_memory_active=max_memory_active, max_memory_idle=max_memory_idle, timeout=max_ss, dry_run=dry_run, ) if watch and connected_to_terminal(sys.stdout): watch_metrics(manager) elif zabbix_discovery: report_zabbix_discovery(manager) elif data_file != '-' and verbosity >= 0: for line in report_metrics(manager): if line_is_heading(line): line = ansi_wrap(line, color=HIGHLIGHT_COLOR) print(line) finally: if (not watch) and (data_file == '-' or not dry_run): manager.save_metrics(data_file)
def format_robust_table(data, column_names): """ Render tabular data with one column per line (allowing columns with line breaks). :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_robust_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_robust_table(humanfriendly_releases, column_names)) ----------------------- Version: 1.23 Uploaded on: 2015-05-25 Downloads: 218 ----------------------- Version: 1.23.1 Uploaded on: 2015-05-26 Downloads: 1354 ----------------------- Version: 1.24 Uploaded on: 2015-05-26 Downloads: 223 ----------------------- Version: 1.25 Uploaded on: 2015-05-26 Downloads: 4319 ----------------------- Version: 1.25.1 Uploaded on: 2015-06-02 Downloads: 197 ----------------------- The column names are highlighted in bold font and color so they stand out a bit more (see :data:`.HIGHLIGHT_COLOR`). """ blocks = [] column_names = ["%s:" % n for n in normalize_columns(column_names)] if connected_to_terminal(): column_names = [highlight_column_name(n) for n in column_names] # Convert each row into one or more `name: value' lines (one per column) # and group each `row of lines' into a block (i.e. rows become blocks). for row in data: lines = [] for column_index, column_text in enumerate(normalize_columns(row)): stripped_column = column_text.strip() if '\n' not in stripped_column: # Columns without line breaks are formatted inline. lines.append("%s %s" % (column_names[column_index], stripped_column)) else: # Columns with line breaks could very well contain indented # lines, so we'll put the column name on a separate line. This # way any indentation remains intact, and it's easier to # copy/paste the text. lines.append(column_names[column_index]) lines.extend(column_text.rstrip().splitlines()) blocks.append(lines) # Calculate the width of the row delimiter. num_rows, num_columns = find_terminal_size() longest_line = max(max(map(ansi_width, lines)) for lines in blocks) delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns)) # Force a delimiter at the start and end of the table. blocks.insert(0, "") blocks.append("") # Embed the row delimiter between every two blocks. return delimiter.join(u"\n".join(b) for b in blocks).strip()
def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'): """ Render a table using characters like dashes and vertical bars to emulate borders. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :param horizontal_bar: The character used to represent a horizontal bar (a string). :param vertical_bar: The character used to represent a vertical bar (a string). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_pretty_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_pretty_table(humanfriendly_releases, column_names)) ------------------------------------- | Version | Uploaded on | Downloads | ------------------------------------- | 1.23 | 2015-05-25 | 218 | | 1.23.1 | 2015-05-26 | 1354 | | 1.24 | 2015-05-26 | 223 | | 1.25 | 2015-05-26 | 4319 | | 1.25.1 | 2015-06-02 | 197 | ------------------------------------- Notes about the resulting table: - If a column contains numeric data (integer and/or floating point numbers) in all rows (ignoring column names of course) then the content of that column is right-aligned, as can be seen in the example above. The idea here is to make it easier to compare the numbers in different columns to each other. - The column names are highlighted in color so they stand out a bit more (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what that looks like (my terminals are always set to white text on a black background): .. image:: images/pretty-table.png """ # Normalize the input because we'll have to iterate it more than once. data = [normalize_columns(r) for r in data] if column_names is not None: column_names = normalize_columns(column_names) if column_names: if connected_to_terminal(): column_names = [highlight_column_name(n) for n in column_names] data.insert(0, column_names) # Calculate the maximum width of each column. widths = collections.defaultdict(int) numeric_data = collections.defaultdict(list) for row_index, row in enumerate(data): for column_index, column in enumerate(row): widths[column_index] = max(widths[column_index], ansi_width(column)) if not (column_names and row_index == 0): numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column)))) # Create a horizontal bar of dashes as a delimiter. line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1) # Start the table with a vertical bar. lines = [line_delimiter] # Format the rows and columns. for row_index, row in enumerate(data): line = [vertical_bar] for column_index, column in enumerate(row): padding = ' ' * (widths[column_index] - ansi_width(column)) if all(numeric_data[column_index]): line.append(' ' + padding + column + ' ') else: line.append(' ' + column + padding + ' ') line.append(vertical_bar) lines.append(u''.join(line)) if column_names and row_index == 0: lines.append(line_delimiter) # End the table with a vertical bar. lines.append(line_delimiter) # Join the lines, returning a single string. return u'\n'.join(lines)