Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ def json_dict_type(val: str | dict[str, Any]) -> dict[str, Any]:
return parsed


def iso_datetime_type(val: str | datetime.datetime) -> datetime.datetime:
"""Parse an ISO-8601 datetime argument."""
if isinstance(val, datetime.datetime):
return val
try:
return datetime.datetime.fromisoformat(val)
except ValueError as e:
raise argparse.ArgumentTypeError(f"invalid datetime value: {val!r}") from e


def _load_help_texts_yaml() -> dict[str, dict[str, str]]:
"""Load the help texts yaml for the auto-generated commands."""
help_texts_path = Path(__file__).parent / "help_texts.yaml"
Expand Down Expand Up @@ -566,7 +576,7 @@ def _python_type_from_string(type_name: str | type) -> type | Callable:
"dict": json_dict_type,
"tuple": tuple,
"set": set,
"datetime.datetime": datetime.datetime,
"datetime.datetime": iso_datetime_type,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test like this?

def test_command_factory_wires_iso_parser_to_datetime_params(self):
    """A generated datetime CLI arg parses an ISO date end to end."""
    command_factory = CommandFactory()
    dagrun_list_args = []
    for group in command_factory.group_commands:
        if group.name != "dagrun":
            continue
        for sub in group.subcommands:
            if sub.name == "list":
                dagrun_list_args = list(sub.args)
                break
    start_date_arg = next(a for a in dagrun_list_args if a.flags == ("--start-date",))
    assert start_date_arg.kwargs["type"]("2026-07-01") == datetime.datetime(2026, 7, 1)

"dict[str, typing.Any]": json_dict_type,
}
# Default to ``str`` to preserve previous behaviour for any unrecognised
Expand Down
45 changes: 45 additions & 0 deletions airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

import argparse
import datetime
from argparse import BooleanOptionalAction
from pathlib import Path
from textwrap import dedent
Expand All @@ -34,6 +35,7 @@
CommandFactory,
GroupCommand,
add_auth_token_to_all_commands,
iso_datetime_type,
json_dict_type,
merge_commands,
safe_call_command,
Expand Down Expand Up @@ -364,6 +366,31 @@ def test_json_dict_type_rejects_non_object_json(self, value):
with pytest.raises(argparse.ArgumentTypeError, match="expected JSON object"):
json_dict_type(value)

def test_iso_datetime_type_returns_datetime_input_unchanged(self):
"""A datetime.datetime input is returned as-is without re-parsing."""
import datetime

value = datetime.datetime(2026, 7, 1, tzinfo=datetime.timezone.utc)

assert iso_datetime_type(value) is value

@pytest.mark.parametrize(
("value", "expected"),
[
("2026-07-01", "2026-07-01T00:00:00"),
("2026-07-01T00:00:00", "2026-07-01T00:00:00"),
("2026-07-01T12:34:56+00:00", "2026-07-01T12:34:56+00:00"),
],
)
def test_iso_datetime_type_parses_iso_string(self, value, expected):
"""An ISO-8601 datetime string (date-only or full) is parsed into a datetime."""
assert iso_datetime_type(value).isoformat() == expected

def test_iso_datetime_type_rejects_invalid_value(self):
"""A non-parseable string raises an ArgumentTypeError."""
with pytest.raises(argparse.ArgumentTypeError, match="invalid datetime value"):
iso_datetime_type("not-a-date")

def test_command_factory_required_primitive_param_is_positional(self, tmp_path):
"""Required primitive parameters (no default, not Optional) become positional arguments.

Expand Down Expand Up @@ -848,3 +875,21 @@ def test_help_texts_used_for_auto_generated_commands(self, group_name, subcomman
)
return
pytest.fail(f"Auto-generated command not found: {group_name} {subcommand_name}")

def test_command_factory_wires_iso_parser_to_datetime_params(self):
"""A generated datetime CLI arg parses an ISO date end to end."""
command_factory = CommandFactory()
dagrun_list_args = []

for group in command_factory.group_commands:
if group.name != "dagrun":
continue

for sub in group.subcommands:
if sub.name == "list":
dagrun_list_args = list(sub.args)
break

start_date_arg = next(a for a in dagrun_list_args if a.flags == ("--start-date",))

assert start_date_arg.kwargs["type"]("2026-07-01") == datetime.datetime(2026, 7, 1)