From e4874f7a76b27e00c0713ac7c421ef110e2ae3c6 Mon Sep 17 00:00:00 2001 From: Timur Gadiev Date: Tue, 1 Apr 2025 17:27:41 +0400 Subject: [PATCH] Add new action plugin 'prettytable' --- plugins/action/prettytable.py | 145 +++++++++++++++++++++++++++++++++ plugins/modules/prettytable.py | 145 +++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 plugins/action/prettytable.py create mode 100644 plugins/modules/prettytable.py diff --git a/plugins/action/prettytable.py b/plugins/action/prettytable.py new file mode 100644 index 0000000000..cb210e30bc --- /dev/null +++ b/plugins/action/prettytable.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, quidame +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +__metaclass__ = type # pylint: disable=C0103 + +from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.module_utils._text import to_text +from ansible.plugins.action import ActionBase + +try: + import prettytable + + HAS_PRETTYTABLE = True +except ImportError: + HAS_PRETTYTABLE = False + + +class ActionModule(ActionBase): + """Print prettytable from list of dicts.""" + + TRANSFERS_FILES = False + _VALID_ARGS = frozenset( + ("data", "column_order", "header_names", "column_alignments") + ) + _VALID_ALIGNMENTS = {"left", "center", "right", "l", "c", "r"} + + def run( + self, tmp: Optional[str] = None, task_vars: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) # pylint: disable=R1725 + del tmp # tmp no longer has any effect + + try: + self._handle_options() + + if not HAS_PRETTYTABLE: + raise AnsibleError( + 'You need to install "prettytable" python module before running this module' + ) + + data = self._task.args.get("data") + if data is None: # Only check for None, allow empty list + raise AnsibleError("The 'data' parameter is required") + + if not isinstance(data, list): + raise AnsibleError( + "Expected a list of dictionaries, got a string" + if isinstance(data, (str, bytes)) + else f"Expected a list of dictionaries, got {type(data).__name__}" + ) + + if data and not all(isinstance(item, dict) for item in data): + invalid_item = next(item for item in data if not isinstance(item, dict)) + raise AnsibleError( + "All items in the list must be dictionaries, got a string" + if isinstance(invalid_item, (str, bytes)) + else f"All items in the list must be dictionaries, got {type(invalid_item).__name__}" + ) + + # Special handling for empty data + if not data: + result["pretty_table"] = "++\n++" + else: + table = self._create_table(data) + result["pretty_table"] = to_text(table) + + result["_ansible_verbose_always"] = True + result["failed"] = False + + except (AnsibleError, AnsibleUndefinedVariable) as e: + result["failed"] = True + result["msg"] = str(e) + + return result + + def _handle_options(self) -> None: + """Validate module arguments.""" + argument_spec = { + "data": {"type": "list", "required": True}, + "column_order": {"type": "list", "elements": "str"}, + "header_names": {"type": "list", "elements": "str"}, + "column_alignments": {"type": "dict"}, + } + + self._options_context = self.validate_argument_spec(argument_spec) + + def _create_table(self, data: List[Dict[str, Any]]) -> prettytable.PrettyTable: + """Create and configure the prettytable instance. + + Args: + data: List of dictionaries to format into a table + + Returns: + Configured PrettyTable instance + """ + table = prettytable.PrettyTable() + + # Determine field names from data or column_order + field_names = self._task.args.get("column_order") or list(data[0].keys()) + + # Set headers + header_names = self._task.args.get("header_names") + table.field_names = header_names if header_names else field_names + + # Configure alignments + self._configure_alignments(table, field_names) + + # Add rows + rows = [[item.get(col, "") for col in field_names] for item in data] + table.add_rows(rows) + + return table + + def _configure_alignments( + self, table: prettytable.PrettyTable, field_names: List[str] + ) -> None: + """Configure column alignments for the table. + + Args: + table: The PrettyTable instance to configure + field_names: List of field names to align + """ + column_alignments = self._task.args.get("column_alignments", {}) + if not isinstance(column_alignments, dict): + return + + for col_name, alignment in column_alignments.items(): + if col_name in field_names: + alignment = alignment.lower() + if alignment in self._VALID_ALIGNMENTS: + table.align[col_name] = alignment[0] + else: + self._display.warning( + f"Ignored invalid alignment '{alignment}' for column '{col_name}'. " + f"Valid alignments are {list(self._VALID_ALIGNMENTS)}" + ) diff --git a/plugins/modules/prettytable.py b/plugins/modules/prettytable.py new file mode 100644 index 0000000000..c72d21d805 --- /dev/null +++ b/plugins/modules/prettytable.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: prettytable +short_description: Format data into an ASCII table using prettytable +version_added: '10.6.0' +author: + - Timur Gadiev (@tgadiev) +extends_documentation_fragment: + - action_common_attributes + - action_common_attributes.conn + - action_common_attributes.flow + - community.general.attributes + - community.general.attributes.flow +description: + - This module formats a list of dictionaries into an ASCII table using the B(prettytable) Python library. + - Useful for creating human-readable output from structured data. + - Allows customization of column order, headers, and alignments. + - The table is created with a clean, readable format suitable for terminal output. +requirements: + - prettytable +attributes: + action: + support: full + async: + support: none + become: + support: none + bypass_host_loop: + support: none + check_mode: + support: full + connection: + support: none + delegation: + support: none + details: This is a pure action plugin that runs entirely on the controller. Delegation has no effect as no tasks are executed on remote hosts. + diff_mode: + support: none + platform: + support: full + platforms: all +options: + data: + description: + - List of dictionaries to format into a table. + - Each dictionary in the list represents a row in the table. + - Dictionary keys become column headers unless overridden by I(header_names). + - If the list is empty, an empty table will be created. + - All items in the list must be dictionaries. + type: list + elements: dict + required: true + column_order: + description: + - List of dictionary keys specifying the order of columns in the output table. + - If not specified, uses the keys from the first dictionary in the input list. + - Only the columns specified in this list will be included in the table. + - Keys must exist in the input dictionaries. + type: list + elements: str + required: false + header_names: + description: + - List of custom header names for the columns. + - Must match the length of columns being displayed. + - If not specified, uses the dictionary keys or I(column_order) values as headers. + - Use this to provide more readable or formatted column headers. + type: list + elements: str + required: false + column_alignments: + description: + - Dictionary mapping column names to their alignment. + - Keys should be column names (either from input data or I(column_order)). + - Values must be one of 'left', 'center', 'right' (or 'l', 'c', 'r'). + - Invalid alignment values will be ignored with a warning. + - Columns not specified default to left alignment. + - Alignments for non-existent columns are ignored. + type: dict + required: false +notes: + - This is an action plugin, meaning the plugin executes on the controller, rather than on the target host. + - The prettytable Python library must be installed on the controller. + - Column alignments are case-insensitive. + - Missing values in input dictionaries are displayed as empty strings. +seealso: + - module: ansible.builtin.debug +''' + +EXAMPLES = r''' +# Basic usage with a list of dictionaries +- name: Create a table from user data + community.general.prettytable: + data: + - name: Alice + age: 25 + role: admin + - name: Bob + age: 30 + role: user + +# Specify column order and custom headers +- name: Create a formatted table with custom headers + community.general.prettytable: + data: + - name: Alice + age: 25 + role: admin + - name: Bob + age: 30 + role: user + column_order: + - name + - role + - age + header_names: + - "User Name" + - "User Role" + - "User Age" + +# Set column alignments for better number and text formatting +- name: Create table with specific alignments + community.general.prettytable: + data: + - date: "2023-01-01" + description: "Office supplies" + amount: 123.45 + - date: "2023-01-02" + description: "Software license" + amount: 500.00 + column_alignments: + amount: right # Numbers right-aligned + description: left # Text left-aligned + date: center # Dates centered +'''