From 8f9294000b01fd5efac38f3d08a377c67e29c090 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Mar 2025 11:11:09 +0100 Subject: [PATCH] Adding upstream version 2.4.0. Signed-off-by: Daniel Baumann --- .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