From e4874f7a76b27e00c0713ac7c421ef110e2ae3c6 Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 17:27:41 +0400
Subject: [PATCH 1/8] 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 <quidame@poivron.org>
+# 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
+'''

From f316fc99e2b28981da1aa278f136032da6cc3eac Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 17:28:36 +0400
Subject: [PATCH 2/8] Add integration tests for 'prettytable' plugin

---
 plugins/modules/prettytable.py                |   2 +-
 tests/integration/targets/prettytable/aliases |   1 +
 .../prettytable/files/aligned_table.txt       |   5 +
 .../targets/prettytable/files/basic_table.txt |   6 +
 .../prettytable/files/custom_headers.txt      |   5 +
 .../targets/prettytable/files/empty_table.txt |   2 +
 .../prettytable/files/ordered_table.txt       |   6 +
 .../targets/prettytable/meta/main.yml         |   1 +
 .../targets/prettytable/tasks/main.yml        | 176 ++++++++++++++++++
 9 files changed, 203 insertions(+), 1 deletion(-)
 create mode 100644 tests/integration/targets/prettytable/aliases
 create mode 100644 tests/integration/targets/prettytable/files/aligned_table.txt
 create mode 100644 tests/integration/targets/prettytable/files/basic_table.txt
 create mode 100644 tests/integration/targets/prettytable/files/custom_headers.txt
 create mode 100644 tests/integration/targets/prettytable/files/empty_table.txt
 create mode 100644 tests/integration/targets/prettytable/files/ordered_table.txt
 create mode 100644 tests/integration/targets/prettytable/meta/main.yml
 create mode 100644 tests/integration/targets/prettytable/tasks/main.yml

diff --git a/plugins/modules/prettytable.py b/plugins/modules/prettytable.py
index c72d21d805..8239ad8b65 100644
--- a/plugins/modules/prettytable.py
+++ b/plugins/modules/prettytable.py
@@ -119,7 +119,7 @@ EXAMPLES = r'''
       - name: Bob
         age: 30
         role: user
-    column_order: 
+    column_order:
       - name
       - role
       - age
diff --git a/tests/integration/targets/prettytable/aliases b/tests/integration/targets/prettytable/aliases
new file mode 100644
index 0000000000..4434c7969e
--- /dev/null
+++ b/tests/integration/targets/prettytable/aliases
@@ -0,0 +1 @@
+azp/posix/1
\ No newline at end of file
diff --git a/tests/integration/targets/prettytable/files/aligned_table.txt b/tests/integration/targets/prettytable/files/aligned_table.txt
new file mode 100644
index 0000000000..c0394afe70
--- /dev/null
+++ b/tests/integration/targets/prettytable/files/aligned_table.txt
@@ -0,0 +1,5 @@
++------------+-----------------+--------+
+|    date    | description     | amount |
++------------+-----------------+--------+
+| 2023-01-01 | Office supplies | 123.45 |
++------------+-----------------+--------+
diff --git a/tests/integration/targets/prettytable/files/basic_table.txt b/tests/integration/targets/prettytable/files/basic_table.txt
new file mode 100644
index 0000000000..29f39077ec
--- /dev/null
+++ b/tests/integration/targets/prettytable/files/basic_table.txt
@@ -0,0 +1,6 @@
++-------+-----+-------+
+|  name | age |  role |
++-------+-----+-------+
+| Alice |  25 | admin |
+|  Bob  |  30 |  user |
++-------+-----+-------+
diff --git a/tests/integration/targets/prettytable/files/custom_headers.txt b/tests/integration/targets/prettytable/files/custom_headers.txt
new file mode 100644
index 0000000000..1630b327db
--- /dev/null
+++ b/tests/integration/targets/prettytable/files/custom_headers.txt
@@ -0,0 +1,5 @@
++-----------+----------+-----------+
+| User Name | User Age | User Role |
++-----------+----------+-----------+
+|   Alice   |    25    |   admin   |
++-----------+----------+-----------+
diff --git a/tests/integration/targets/prettytable/files/empty_table.txt b/tests/integration/targets/prettytable/files/empty_table.txt
new file mode 100644
index 0000000000..9b52315d4f
--- /dev/null
+++ b/tests/integration/targets/prettytable/files/empty_table.txt
@@ -0,0 +1,2 @@
+++
+++
diff --git a/tests/integration/targets/prettytable/files/ordered_table.txt b/tests/integration/targets/prettytable/files/ordered_table.txt
new file mode 100644
index 0000000000..de7fab56f6
--- /dev/null
+++ b/tests/integration/targets/prettytable/files/ordered_table.txt
@@ -0,0 +1,6 @@
++-------+-------+-----+
+|  role |  name | age |
++-------+-------+-----+
+| admin | Alice |  25 |
+|  user |  Bob  |  30 |
++-------+-------+-----+
diff --git a/tests/integration/targets/prettytable/meta/main.yml b/tests/integration/targets/prettytable/meta/main.yml
new file mode 100644
index 0000000000..ed97d539c0
--- /dev/null
+++ b/tests/integration/targets/prettytable/meta/main.yml
@@ -0,0 +1 @@
+---
diff --git a/tests/integration/targets/prettytable/tasks/main.yml b/tests/integration/targets/prettytable/tasks/main.yml
new file mode 100644
index 0000000000..6ced925790
--- /dev/null
+++ b/tests/integration/targets/prettytable/tasks/main.yml
@@ -0,0 +1,176 @@
+---
+- name: Install required libs
+  pip:
+    name: prettytable
+    state: present
+  delegate_to: localhost
+  become: false
+
+# Test basic functionality
+- name: Test basic table creation
+  prettytable:
+    data:
+      - name: Alice
+        age: 25
+        role: admin
+      - name: Bob
+        age: 30
+        role: user
+  register: basic_result
+
+- name: Load expected basic table output
+  set_fact:
+    expected_basic_table: "{{ lookup('file', 'basic_table.txt') }}"
+
+- name: Verify basic table output
+  assert:
+    that:
+      - basic_result.pretty_table | trim == expected_basic_table | trim
+      - not basic_result.failed
+
+# Test column ordering
+- name: Test table with custom column order
+  prettytable:
+    data:
+      - name: Alice
+        age: 25
+        role: admin
+      - name: Bob
+        age: 30
+        role: user
+    column_order:
+      - role
+      - name
+      - age
+  register: ordered_result
+
+- name: Load expected ordered table output
+  set_fact:
+    expected_ordered_table: "{{ lookup('file', 'ordered_table.txt') }}"
+
+- name: Verify ordered table output
+  assert:
+    that:
+      - ordered_result.pretty_table | trim == expected_ordered_table | trim
+      - not ordered_result.failed
+
+# Test custom headers
+- name: Test table with custom headers
+  prettytable:
+    data:
+      - name: Alice
+        age: 25
+        role: admin
+    header_names:
+      - "User Name"
+      - "User Age"
+      - "User Role"
+  register: headers_result
+
+- name: Load expected headers table output
+  set_fact:
+    expected_headers_table: "{{ lookup('file', 'custom_headers.txt') }}"
+
+- name: Verify custom headers output
+  assert:
+    that:
+      - headers_result.pretty_table | trim == expected_headers_table | trim
+      - not headers_result.failed
+
+# Test column alignments
+- name: Test table with alignments
+  prettytable:
+    data:
+      - date: "2023-01-01"
+        description: "Office supplies"
+        amount: 123.45
+    column_alignments:
+      amount: right
+      description: left
+      date: center
+  register: aligned_result
+
+- name: Load expected aligned table output
+  set_fact:
+    expected_aligned_table: "{{ lookup('file', 'aligned_table.txt') }}"
+
+- name: Verify aligned table output
+  assert:
+    that:
+      - aligned_result.pretty_table | trim == expected_aligned_table | trim
+      - not aligned_result.failed
+
+# Test error conditions
+- name: Test missing data parameter (should fail)
+  prettytable: {}  # Empty dict to test missing required parameter
+  register: missing_data_result
+  ignore_errors: true
+
+- name: Verify error for missing data
+  vars:
+    expected_msg: 'missing required arguments: data'
+  assert:
+    that:
+      - missing_data_result.failed
+      - expected_msg in missing_data_result.msg
+
+- name: Test invalid data type (should fail)
+  prettytable:
+    data: "this is a string"
+  register: invalid_data_result
+  ignore_errors: true
+
+- name: Verify error for invalid data
+  vars:
+    expected_msg: "Expected a list of dictionaries, got a string"
+  assert:
+    that:
+      - invalid_data_result.failed
+      - expected_msg == invalid_data_result.msg
+
+- name: Test list with invalid items (should fail)
+  prettytable:
+    data:
+      - {"valid": "dict"}
+      - "invalid string item"
+  register: invalid_items_result
+  ignore_errors: true
+
+- name: Verify error for invalid items
+  vars:
+    expected_msg: "All items in the list must be dictionaries, got a string"
+  assert:
+    that:
+      - invalid_items_result.failed
+      - expected_msg == invalid_items_result.msg
+
+# Test empty data
+- name: Test empty data list
+  prettytable:
+    data: []
+  register: empty_result
+
+- name: Load expected empty table output
+  set_fact:
+    expected_empty_table: "{{ lookup('file', 'empty_table.txt') }}"
+
+- name: Verify empty table output
+  assert:
+    that:
+      - empty_result.pretty_table | trim == expected_empty_table | trim
+      - not empty_result.failed
+
+# Test invalid alignments
+- name: Test invalid alignment value
+  prettytable:
+    data:
+      - col1: value1
+        col2: value2
+    column_alignments:
+      col1: invalid
+  register: invalid_alignment_result
+
+- name: Verify invalid alignment handling
+  assert:
+    that:
+      - not invalid_alignment_result.failed  # Should not fail, just warn

From 9607eb03e734aa9fd34531b882c4a7c50b1c478b Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 17:34:37 +0400
Subject: [PATCH 3/8] Added BOTMETA details

---
 .github/BOTMETA.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index c92ce76034..02b0ecb4c9 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -15,6 +15,8 @@ files:
     labels: action
   $actions/iptables_state.py:
     maintainers: quidame
+  $actions/prettytable.py:
+    maintainers: tgadiev
   $actions/shutdown.py:
     maintainers: nitzmahone samdoran aminvakil
   $becomes/:
@@ -208,6 +210,8 @@ files:
     maintainers: resmo
   $filters/to_months.yml:
     maintainers: resmo
+  $filters/to_prettytable.py:
+    maintainers: tgadiev
   $filters/to_seconds.yml:
     maintainers: resmo
   $filters/to_time_unit.yml:
@@ -1133,6 +1137,8 @@ files:
     keywords: doas dragonfly freebsd iocage jail netbsd openbsd opnsense pfsense
     labels: bsd portinstall
     maintainers: $team_bsd berenddeboer
+  $modules/prettytable.py:
+    maintainers: tgadiev
   $modules/pritunl_:
     maintainers: Lowess
   $modules/profitbricks:

From dc205f6d292a04be129cfeed822dbb662caf8974 Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 18:03:40 +0400
Subject: [PATCH 4/8] Add COPYRIGHT details

---
 tests/integration/targets/prettytable/aliases |  4 ++
 .../prettytable/files/aligned_table.txt       |  4 ++
 .../targets/prettytable/files/basic_table.txt |  4 ++
 .../prettytable/files/custom_headers.txt      |  4 ++
 .../targets/prettytable/files/empty_table.txt |  4 ++
 .../prettytable/files/ordered_table.txt       |  4 ++
 .../targets/prettytable/meta/main.yml         |  3 ++
 .../targets/prettytable/tasks/main.yml        | 39 ++++++++++++++++---
 8 files changed, 61 insertions(+), 5 deletions(-)

diff --git a/tests/integration/targets/prettytable/aliases b/tests/integration/targets/prettytable/aliases
index 4434c7969e..f992fa439d 100644
--- a/tests/integration/targets/prettytable/aliases
+++ b/tests/integration/targets/prettytable/aliases
@@ -1 +1,5 @@
+# Copyright (c) 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
+
 azp/posix/1
\ No newline at end of file
diff --git a/tests/integration/targets/prettytable/files/aligned_table.txt b/tests/integration/targets/prettytable/files/aligned_table.txt
index c0394afe70..50f096485e 100644
--- a/tests/integration/targets/prettytable/files/aligned_table.txt
+++ b/tests/integration/targets/prettytable/files/aligned_table.txt
@@ -1,3 +1,7 @@
+# Copyright (c) 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
+
 +------------+-----------------+--------+
 |    date    | description     | amount |
 +------------+-----------------+--------+
diff --git a/tests/integration/targets/prettytable/files/basic_table.txt b/tests/integration/targets/prettytable/files/basic_table.txt
index 29f39077ec..c6e3cefed4 100644
--- a/tests/integration/targets/prettytable/files/basic_table.txt
+++ b/tests/integration/targets/prettytable/files/basic_table.txt
@@ -1,3 +1,7 @@
+# Copyright (c) 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
+
 +-------+-----+-------+
 |  name | age |  role |
 +-------+-----+-------+
diff --git a/tests/integration/targets/prettytable/files/custom_headers.txt b/tests/integration/targets/prettytable/files/custom_headers.txt
index 1630b327db..20d1c9a1d5 100644
--- a/tests/integration/targets/prettytable/files/custom_headers.txt
+++ b/tests/integration/targets/prettytable/files/custom_headers.txt
@@ -1,3 +1,7 @@
+# Copyright (c) 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
+
 +-----------+----------+-----------+
 | User Name | User Age | User Role |
 +-----------+----------+-----------+
diff --git a/tests/integration/targets/prettytable/files/empty_table.txt b/tests/integration/targets/prettytable/files/empty_table.txt
index 9b52315d4f..7fb2ceb9d7 100644
--- a/tests/integration/targets/prettytable/files/empty_table.txt
+++ b/tests/integration/targets/prettytable/files/empty_table.txt
@@ -1,2 +1,6 @@
+# Copyright (c) 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
+
 ++
 ++
diff --git a/tests/integration/targets/prettytable/files/ordered_table.txt b/tests/integration/targets/prettytable/files/ordered_table.txt
index de7fab56f6..1bb348736d 100644
--- a/tests/integration/targets/prettytable/files/ordered_table.txt
+++ b/tests/integration/targets/prettytable/files/ordered_table.txt
@@ -1,3 +1,7 @@
+# Copyright (c) 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
+
 +-------+-------+-----+
 |  role |  name | age |
 +-------+-------+-----+
diff --git a/tests/integration/targets/prettytable/meta/main.yml b/tests/integration/targets/prettytable/meta/main.yml
index ed97d539c0..e1e4a50798 100644
--- a/tests/integration/targets/prettytable/meta/main.yml
+++ b/tests/integration/targets/prettytable/meta/main.yml
@@ -1 +1,4 @@
 ---
+# Copyright (c) 2020, Pavlo Bashynskyi (@levonet) <levonet@gmail.com>
+# 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
diff --git a/tests/integration/targets/prettytable/tasks/main.yml b/tests/integration/targets/prettytable/tasks/main.yml
index 6ced925790..fc814519aa 100644
--- a/tests/integration/targets/prettytable/tasks/main.yml
+++ b/tests/integration/targets/prettytable/tasks/main.yml
@@ -1,4 +1,13 @@
 ---
+####################################################################
+# WARNING: These are designed specifically for Ansible tests       #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+# Copyright (c) 2020, Pavlo Bashynskyi (@levonet) <levonet@gmail.com>
+# 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
+
 - name: Install required libs
   pip:
     name: prettytable
@@ -20,7 +29,11 @@
 
 - name: Load expected basic table output
   set_fact:
-    expected_basic_table: "{{ lookup('file', 'basic_table.txt') }}"
+    expected_basic_table_raw: "{{ lookup('file', 'basic_table.txt') }}"
+
+- name: Remove copyright header from expected output
+  set_fact:
+    expected_basic_table: "{{ expected_basic_table_raw.split('\n')[4:] | join('\n') }}"
 
 - name: Verify basic table output
   assert:
@@ -46,7 +59,11 @@
 
 - name: Load expected ordered table output
   set_fact:
-    expected_ordered_table: "{{ lookup('file', 'ordered_table.txt') }}"
+    expected_ordered_table_raw: "{{ lookup('file', 'ordered_table.txt') }}"
+
+- name: Remove copyright header from expected output
+  set_fact:
+    expected_ordered_table: "{{ expected_ordered_table_raw.split('\n')[4:] | join('\n') }}"
 
 - name: Verify ordered table output
   assert:
@@ -69,7 +86,11 @@
 
 - name: Load expected headers table output
   set_fact:
-    expected_headers_table: "{{ lookup('file', 'custom_headers.txt') }}"
+    expected_headers_table_raw: "{{ lookup('file', 'custom_headers.txt') }}"
+
+- name: Remove copyright header from expected output
+  set_fact:
+    expected_headers_table: "{{ expected_headers_table_raw.split('\n')[4:] | join('\n') }}"
 
 - name: Verify custom headers output
   assert:
@@ -92,7 +113,11 @@
 
 - name: Load expected aligned table output
   set_fact:
-    expected_aligned_table: "{{ lookup('file', 'aligned_table.txt') }}"
+    expected_aligned_table_raw: "{{ lookup('file', 'aligned_table.txt') }}"
+
+- name: Remove copyright header from expected output
+  set_fact:
+    expected_aligned_table: "{{ expected_aligned_table_raw.split('\n')[4:] | join('\n') }}"
 
 - name: Verify aligned table output
   assert:
@@ -152,7 +177,11 @@
 
 - name: Load expected empty table output
   set_fact:
-    expected_empty_table: "{{ lookup('file', 'empty_table.txt') }}"
+    expected_empty_table_raw: "{{ lookup('file', 'empty_table.txt') }}"
+
+- name: Remove copyright header from expected output
+  set_fact:
+    expected_empty_table: "{{ expected_empty_table_raw.split('\n')[4:] | join('\n') }}"
 
 - name: Verify empty table output
   assert:

From 94e7200e42191390672a7a1e84b78f43cd2656d4 Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 19:24:31 +0400
Subject: [PATCH 5/8] Add 'to_prettytable' filter plugin and tests

---
 plugins/filter/to_prettytable.py              | 182 ++++++++++++++++++
 .../targets/filter_to_prettytable/aliases     |   5 +
 .../filter_to_prettytable/tasks/main.yml      | 130 +++++++++++++
 3 files changed, 317 insertions(+)
 create mode 100644 plugins/filter/to_prettytable.py
 create mode 100644 tests/integration/targets/filter_to_prettytable/aliases
 create mode 100644 tests/integration/targets/filter_to_prettytable/tasks/main.yml

diff --git a/plugins/filter/to_prettytable.py b/plugins/filter/to_prettytable.py
new file mode 100644
index 0000000000..ac5cc5a585
--- /dev/null
+++ b/plugins/filter/to_prettytable.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2025, Timur Gadiev <timur@example.com>
+# 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 = '''
+  name: to_prettytable
+  short_description: Format a list of dictionaries as an ASCII table
+  version_added: "8.0.0"
+  author: Timur Gadiev (@tgadiev)
+  description:
+    - This filter takes a list of dictionaries and formats it as an ASCII table using the I(prettytable) Python library.
+  requirements:
+    - prettytable
+  options:
+    _input:
+      description: A list of dictionaries to format.
+      type: list
+      elements: dictionary
+      required: true
+    column_order:
+      description: List of column names to specify the order of columns in the table.
+      type: list
+      elements: string
+    header_names:
+      description: List of custom header names to use instead of dictionary keys.
+      type: list
+      elements: string
+    column_alignments:
+      description: Dictionary of column alignments. Keys are column names, values are alignments.
+      type: dictionary
+      suboptions:
+        alignment:
+          description: Alignment for the column. Must be one of C(left), C(center), C(right), C(l), C(c), or C(r).
+          type: string
+          choices: [left, center, right, l, c, r]
+'''
+
+EXAMPLES = '''
+- name: Display a list of users as a table
+  vars:
+    users:
+      - name: Alice
+        age: 25
+        role: admin
+      - name: Bob
+        age: 30
+        role: user
+  debug:
+    msg: "{{ users | community.general.to_prettytable }}"
+
+- name: Display a table with custom column ordering
+  debug:
+    msg: "{{ users | community.general.to_prettytable('role', 'name', 'age') }}"
+
+- name: Display a table with custom headers
+  debug:
+    msg: "{{ users | community.general.to_prettytable(header_names=['User Name', 'User Age', 'User Role']) }}"
+
+- name: Display a table with custom alignments
+  debug:
+    msg: "{{ users | community.general.to_prettytable(column_alignments={'name': 'center', 'age': 'right', 'role': 'left'}) }}"
+
+- name: Combine multiple options
+  debug:
+    msg: "{{ users | community.general.to_prettytable(
+        column_order=['role', 'name', 'age'],
+        header_names=['Position', 'Full Name', 'Years'],
+        column_alignments={'name': 'center', 'age': 'right', 'role': 'left'}) }}"
+'''
+
+RETURN = '''
+  _value:
+    description: The formatted ASCII table.
+    type: string
+'''
+
+try:
+    import prettytable
+    HAS_PRETTYTABLE = True
+except ImportError:
+    HAS_PRETTYTABLE = False
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import string_types
+
+
+def to_prettytable(data, *args, **kwargs):
+    """Convert a list of dictionaries to an ASCII table.
+
+    Args:
+        data: List of dictionaries to format
+        *args: Optional list of column names to specify column order
+        **kwargs: Optional keyword arguments:
+            - column_order: List of column names to specify the order
+            - header_names: List of custom header names
+            - column_alignments: Dict of column alignments (left, center, right)
+
+    Returns:
+        String containing the ASCII table
+    """
+    if not HAS_PRETTYTABLE:
+        raise AnsibleFilterError(
+            'You need to install "prettytable" Python module to use this filter'
+        )
+
+    if not isinstance(data, list):
+        raise AnsibleFilterError(
+            "Expected a list of dictionaries, got a string"
+            if isinstance(data, string_types)
+            else f"Expected a list of dictionaries, got {type(data).__name__}"
+        )
+
+    # Handle empty data
+    if not data:
+        return "++\n++"
+
+    # Check that all items are dictionaries
+    if not all(isinstance(item, dict) for item in data):
+        invalid_item = next(item for item in data if not isinstance(item, dict))
+        raise AnsibleFilterError(
+            "All items in the list must be dictionaries, got a string"
+            if isinstance(invalid_item, string_types)
+            else f"All items in the list must be dictionaries, got {type(invalid_item).__name__}"
+        )
+
+    # Handle positional argument column order
+    column_order = kwargs.get('column_order', None)
+    if args and not column_order:
+        column_order = list(args)
+
+    # Create the table and configure it
+    table = prettytable.PrettyTable()
+
+    # Determine field names
+    field_names = column_order or list(data[0].keys())
+
+    # Set headers
+    header_names = kwargs.get('header_names', None)
+    table.field_names = header_names if header_names else field_names
+
+    # Configure alignments
+    _configure_alignments(table, field_names, kwargs.get('column_alignments', {}))
+
+    # Add rows
+    rows = [[item.get(col, "") for col in field_names] for item in data]
+    table.add_rows(rows)
+
+    return to_text(table)
+
+
+def _configure_alignments(table, field_names, column_alignments):
+    """Configure column alignments for the table.
+
+    Args:
+        table: The PrettyTable instance to configure
+        field_names: List of field names to align
+        column_alignments: Dict of column alignments
+    """
+    valid_alignments = {"left", "center", "right", "l", "c", "r"}
+
+    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 valid_alignments:
+                table.align[col_name] = alignment[0]
+
+
+class FilterModule(object):
+    """Ansible core jinja2 filters."""
+
+    def filters(self):
+        return {
+            'to_prettytable': to_prettytable
+        }
diff --git a/tests/integration/targets/filter_to_prettytable/aliases b/tests/integration/targets/filter_to_prettytable/aliases
new file mode 100644
index 0000000000..afda346c4e
--- /dev/null
+++ b/tests/integration/targets/filter_to_prettytable/aliases
@@ -0,0 +1,5 @@
+# Copyright (c) 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
+
+azp/posix/1
diff --git a/tests/integration/targets/filter_to_prettytable/tasks/main.yml b/tests/integration/targets/filter_to_prettytable/tasks/main.yml
new file mode 100644
index 0000000000..fd0f385267
--- /dev/null
+++ b/tests/integration/targets/filter_to_prettytable/tasks/main.yml
@@ -0,0 +1,130 @@
+---
+####################################################################
+# WARNING: These are designed specifically for Ansible tests       #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+# Copyright (c) 2025, Timur Gadiev (@tgadiev)
+# 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
+
+- name: Install required libs
+  pip:
+    name: prettytable
+    state: present
+  delegate_to: localhost
+  become: false
+
+- name: Set test data
+  set_fact:
+    test_data:
+      - name: Alice
+        age: 25
+        role: admin
+      - name: Bob
+        age: 30
+        role: user
+    data_for_align:
+      - date: 2023-01-01
+        description: Office supplies
+        amount: 123.45
+
+# Test basic functionality
+- name: Test basic table creation
+  set_fact:
+    basic_table: '{{ test_data | community.general.to_prettytable }}'
+
+- name: Verify basic table output
+  assert:
+    that:
+      - basic_table | trim is defined
+      - basic_table | trim is not none
+
+# Test column ordering
+- name: Test column ordering
+  set_fact:
+    ordered_table: "{{ test_data | community.general.to_prettytable(column_order=['role', 'name', 'age']) }}"
+
+- name: Verify ordered table output
+  assert:
+    that:
+      - ordered_table | trim is defined
+      - ordered_table | trim is not none
+      - ordered_table | trim != basic_table | trim
+
+# Test custom headers
+- name: Test custom headers
+  set_fact:
+    headers_table: "{{ test_data | community.general.to_prettytable(header_names=['User Name', 'User Age', 'User Role']) }}"
+
+- name: Verify custom headers output
+  assert:
+    that:
+      - headers_table | trim is defined  
+      - headers_table | trim is not none
+      - "headers_table | trim is search('User Name')"
+      - "headers_table | trim is search('User Age')"
+      - "headers_table | trim is search('User Role')"
+
+# Test alignments
+- name: Test column alignments
+  set_fact:
+    aligned_table: "{{ data_for_align | community.general.to_prettytable(column_alignments={'amount': 'right', 'description': 'left', 'date': 'center'}) }}"
+
+- name: Verify aligned table output
+  assert:
+    that:
+      - aligned_table | trim is defined
+      - aligned_table | trim is not none
+
+# Test combined options
+- name: Test combined options
+  set_fact:
+    combined_table: "{{ test_data | community.general.to_prettytable(
+        column_order=['role', 'name', 'age'],
+        header_names=['Position', 'Full Name', 'Years'],
+        column_alignments={'name': 'center', 'age': 'right', 'role': 'left'}) }}"
+
+- name: Verify combined table output
+  assert:
+    that:
+      - combined_table | trim is defined
+      - combined_table | trim is not none
+      - "combined_table | trim is search('Position')"
+      - "combined_table | trim is search('Full Name')"
+      - "combined_table | trim is search('Years')"
+
+# Test empty data
+- name: Test empty data list
+  set_fact:
+    empty_table: "{{ [] | community.general.to_prettytable }}"
+
+- name: Verify empty table output
+  assert:
+    that:
+      - empty_table | trim == "++\n++" | trim
+
+# Test error conditions
+- name: Test non-list input (expect failure)
+  block:
+    - set_fact:
+        invalid_table: "{{ 'not_a_list' | community.general.to_prettytable }}"
+      register: failure_result
+      ignore_errors: true
+    - name: Verify error message for non-list input
+      assert:
+        that:
+          - failure_result is failed
+          - "'Expected a list of dictionaries, got a string' in failure_result.msg"
+
+- name: Test list with non-dictionary items (expect failure)
+  block:
+    - set_fact:
+        invalid_table: "{{ ['not_a_dict', 'also_not_a_dict'] | community.general.to_prettytable }}"
+      register: failure_result
+      ignore_errors: true
+    - name: Verify error message for non-dictionary items
+      assert:
+        that:
+          - failure_result is failed
+          - "'All items in the list must be dictionaries' in failure_result.msg"

From 47073e747dce323453f25915ec20c35448c09d42 Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 19:49:46 +0400
Subject: [PATCH 6/8] fix: :bug: Fix add_rows method

---
 plugins/action/prettytable.py    | 5 +++--
 plugins/filter/to_prettytable.py | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/plugins/action/prettytable.py b/plugins/action/prettytable.py
index cb210e30bc..07d5e98bed 100644
--- a/plugins/action/prettytable.py
+++ b/plugins/action/prettytable.py
@@ -114,9 +114,10 @@ class ActionModule(ActionBase):
         # Configure alignments
         self._configure_alignments(table, field_names)
 
-        # Add rows
+        # Add rows - use add_row instead of add_rows for compatibility with older versions
         rows = [[item.get(col, "") for col in field_names] for item in data]
-        table.add_rows(rows)
+        for row in rows:
+            table.add_row(row)
 
         return table
 
diff --git a/plugins/filter/to_prettytable.py b/plugins/filter/to_prettytable.py
index ac5cc5a585..27599092a2 100644
--- a/plugins/filter/to_prettytable.py
+++ b/plugins/filter/to_prettytable.py
@@ -146,9 +146,10 @@ def to_prettytable(data, *args, **kwargs):
     # Configure alignments
     _configure_alignments(table, field_names, kwargs.get('column_alignments', {}))
 
-    # Add rows
+    # Add rows - use add_row instead of add_rows for compatibility with older versions
     rows = [[item.get(col, "") for col in field_names] for item in data]
-    table.add_rows(rows)
+    for row in rows:
+        table.add_row(row)
 
     return to_text(table)
 

From aed3475767a564b1c316513393f91cbaf17f0604 Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 20:52:24 +0400
Subject: [PATCH 7/8] Add changelog fragment

---
 changelogs/fragments/9954-prettytable_plugins.yml | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 changelogs/fragments/9954-prettytable_plugins.yml

diff --git a/changelogs/fragments/9954-prettytable_plugins.yml b/changelogs/fragments/9954-prettytable_plugins.yml
new file mode 100644
index 0000000000..0d5746dc9e
--- /dev/null
+++ b/changelogs/fragments/9954-prettytable_plugins.yml
@@ -0,0 +1,4 @@
+---
+minor_changes:
+  - prettytable - New action plugin and module to format data as ASCII tables using the PrettyTable library
+  - to_prettytable filter - New filter plugin to format data as ASCII tables in Jinja2 templates using the PrettyTable library

From 9eb454038624f2cab41173b5cbd7fed5e333788b Mon Sep 17 00:00:00 2001
From: Timur Gadiev <Timur_Gadiev@epam.com>
Date: Tue, 1 Apr 2025 20:53:56 +0400
Subject: [PATCH 8/8] Remove changelog fragments

---
 changelogs/fragments/9954-prettytable_plugins.yml | 4 ----
 1 file changed, 4 deletions(-)
 delete mode 100644 changelogs/fragments/9954-prettytable_plugins.yml

diff --git a/changelogs/fragments/9954-prettytable_plugins.yml b/changelogs/fragments/9954-prettytable_plugins.yml
deleted file mode 100644
index 0d5746dc9e..0000000000
--- a/changelogs/fragments/9954-prettytable_plugins.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-minor_changes:
-  - prettytable - New action plugin and module to format data as ASCII tables using the PrettyTable library
-  - to_prettytable filter - New filter plugin to format data as ASCII tables in Jinja2 templates using the PrettyTable library