From 5f801f685c28ac40c716ce4c04b348666cc73dee Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel@debian.org>
Date: Wed, 12 Mar 2025 11:11:12 +0100
Subject: [PATCH 1/2] Merging upstream version 2.4.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
---
 .github/workflows/codeql.yml                | 41 +++++++++++++++++
 .gitignore                                  |  1 +
 AUTHORS                                     |  1 +
 CHANGELOG                                   |  6 +++
 cli_helpers/__init__.py                     |  2 +-
 cli_helpers/tabular_output/preprocessors.py | 50 +++++++++++++++++++--
 tests/tabular_output/test_preprocessors.py  | 23 ++++++++++
 7 files changed, 120 insertions(+), 4 deletions(-)
 create mode 100644 .github/workflows/codeql.yml

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..057ff98
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,41 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+  schedule:
+    - cron: "36 4 * * 1"
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ python ]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v2
+        with:
+          languages: ${{ matrix.language }}
+          queries: +security-and-quality
+
+      - name: Autobuild
+        uses: github/codeql-action/autobuild@v2
+
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v2
+        with:
+          category: "/language:${{ matrix.language }}"
diff --git a/.gitignore b/.gitignore
index 213f266..9986fe3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ __pycache__
 /cli_helpers_dev
 .idea/
 .cache/
+.vscode/
\ No newline at end of file
diff --git a/AUTHORS b/AUTHORS
index 40f2b90..536b972 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -25,6 +25,7 @@ This project receives help from these awesome contributors:
 - Mel Dafert
 - Andrii Kohut
 - Roland Walker
+- Doug Harris
 
 Thanks
 ------
diff --git a/CHANGELOG b/CHANGELOG
index 7b7bd02..b17256b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,11 @@
 # Changelog
 
+## Version 2.4.0
+
+(released on 2025-03-10)
+
+- Added format_timestamps preprocessor for per-column date/time formatting.
+
 ## Version 2.3.1
 
 - Don't escape newlines in `ascii` tables, and add `ascii_escaped` table format.
diff --git a/cli_helpers/__init__.py b/cli_helpers/__init__.py
index 3a5935a..3d67cd6 100644
--- a/cli_helpers/__init__.py
+++ b/cli_helpers/__init__.py
@@ -1 +1 @@
-__version__ = "2.3.1"
+__version__ = "2.4.0"
diff --git a/cli_helpers/tabular_output/preprocessors.py b/cli_helpers/tabular_output/preprocessors.py
index 8342d67..a47fec0 100644
--- a/cli_helpers/tabular_output/preprocessors.py
+++ b/cli_helpers/tabular_output/preprocessors.py
@@ -2,6 +2,7 @@
 """These preprocessor functions are used to process data prior to output."""
 
 import string
+from datetime import datetime
 
 from cli_helpers import utils
 from cli_helpers.compat import text_type, int_types, float_types, HAS_PYGMENTS, Token
@@ -125,9 +126,11 @@ def escape_newlines(data, headers, **_):
     return (
         (
             [
-                v.replace("\r", r"\r").replace("\n", r"\n")
-                if isinstance(v, text_type)
-                else v
+                (
+                    v.replace("\r", r"\r").replace("\n", r"\n")
+                    if isinstance(v, text_type)
+                    else v
+                )
                 for v in row
             ]
             for row in data
@@ -351,3 +354,44 @@ def format_numbers(
         [_format_number(v, column_types[i]) for i, v in enumerate(row)] for row in data
     )
     return data, headers
+
+
+def format_timestamps(data, headers, column_date_formats=None, **_):
+    """Format timestamps according to user preference.
+
+    This allows for per-column formatting for date, time, or datetime like data.
+
+    Add a `column_date_formats` section to your config file with separate lines for each column
+    that you'd like to specify a format using `name=format`. Use standard Python strftime
+    formatting strings
+
+    Example: `signup_date = "%Y-%m-%d"`
+
+    :param iterable data: An :term:`iterable` (e.g. list) of rows.
+    :param iterable headers: The column headers.
+    :param str column_date_format: The format strings to use for specific columns.
+    :return: The processed data and headers.
+    :rtype: tuple
+
+    """
+    if column_date_formats is None:
+        return iter(data), headers
+
+    def _format_timestamp(value, name, column_date_formats):
+        if name not in column_date_formats:
+            return value
+        try:
+            dt = datetime.fromisoformat(value)
+            return dt.strftime(column_date_formats[name])
+        except (ValueError, TypeError):
+            # not a date
+            return value
+
+    data = (
+        [
+            _format_timestamp(v, headers[i], column_date_formats)
+            for i, v in enumerate(row)
+        ]
+        for row in data
+    )
+    return data, headers
diff --git a/tests/tabular_output/test_preprocessors.py b/tests/tabular_output/test_preprocessors.py
index e428bfa..5ebd06d 100644
--- a/tests/tabular_output/test_preprocessors.py
+++ b/tests/tabular_output/test_preprocessors.py
@@ -16,6 +16,7 @@ from cli_helpers.tabular_output.preprocessors import (
     override_tab_value,
     style_output,
     format_numbers,
+    format_timestamps,
 )
 
 if HAS_PYGMENTS:
@@ -348,3 +349,25 @@ def test_enforce_iterable():
             assert False, "{} doesn't return iterable".format(name)
         if isinstance(preprocessed[1], types.GeneratorType):
             assert False, "{} returns headers as iterator".format(name)
+
+
+def test_format_timestamps():
+    data = (
+        ("name1", "2024-12-13T18:32:22", "2024-12-13T19:32:22", "2024-12-13T20:32:22"),
+        ("name2", "2025-02-13T02:32:22", "2025-02-13T02:32:22", "2025-02-13T02:32:22"),
+        ("name3", None, "not-actually-timestamp", "2025-02-13T02:32:22"),
+    )
+    headers = ["name", "date_col", "datetime_col", "unchanged_col"]
+    column_date_formats = {
+        "date_col": "%Y-%m-%d",
+        "datetime_col": "%I:%M:%S %m/%d/%y",
+    }
+    result_data, result_headers = format_timestamps(data, headers, column_date_formats)
+
+    expected = [
+        ["name1", "2024-12-13", "07:32:22 12/13/24", "2024-12-13T20:32:22"],
+        ["name2", "2025-02-13", "02:32:22 02/13/25", "2025-02-13T02:32:22"],
+        ["name3", None, "not-actually-timestamp", "2025-02-13T02:32:22"],
+    ]
+    assert expected == list(result_data)
+    assert headers == result_headers

From df4e08e33ac8fa8e236d0d6d7bd3d9b284e4f305 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel@debian.org>
Date: Wed, 12 Mar 2025 11:11:29 +0100
Subject: [PATCH 2/2] Releasing debian version 2.4.0-1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
---
 debian/changelog | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/debian/changelog b/debian/changelog
index 2e3d1ef..70bef62 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+cli-helpers (2.4.0-1) sid; urgency=medium
+
+  * Updating to standards version 4.7.1.
+  * Updating to standards version 4.7.2.
+  * Merging upstream version 2.4.0.
+
+ -- Daniel Baumann <daniel@debian.org>  Wed, 12 Mar 2025 11:11:25 +0100
+
 cli-helpers (2.3.1-3) sid; urgency=medium
 
   * Updating source url in copyright.