diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index bbf5e59f8e9d9..bef86369a05e7 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -205,6 +205,14 @@ def json_dict_type(val: str | dict[str, Any]) -> dict[str, Any]: return parsed +def iso_date_type(val: str) -> datetime.date: + """Parse ISO 8601 date argument.""" + try: + return datetime.date.fromisoformat(val) + except ValueError as e: + raise argparse.ArgumentTypeError(f"invalid ISO 8601 date: {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" @@ -636,7 +644,7 @@ def _python_type_from_string(type_name: str | type) -> type | Callable: "dict": json_dict_type, "tuple": tuple, "set": set, - "datetime.date": datetime.date, + "datetime.date": iso_date_type, "datetime.datetime": datetime.datetime, "dict[str, typing.Any]": json_dict_type, } diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py index fcbd3d748b2f2..ec63fb0129828 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py @@ -18,6 +18,7 @@ from __future__ import annotations import argparse +import datetime from argparse import BooleanOptionalAction from pathlib import Path from textwrap import dedent @@ -34,6 +35,7 @@ CommandFactory, GroupCommand, add_auth_token_to_all_commands, + iso_date_type, json_dict_type, merge_commands, safe_call_command, @@ -364,6 +366,40 @@ 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_command_factory_parses_date_datamodel_fields(self): + """Date datamodel fields should parse ISO 8601 date CLI values.""" + command_factory = CommandFactory() + dagrun_list_args = [] + for generated_group_command in command_factory.group_commands: + if generated_group_command.name != "dagrun": + continue + for sub_command in generated_group_command.subcommands: + if sub_command.name == "list": + dagrun_list_args = list(sub_command.args) + break + + partition_date_arg = next(arg for arg in dagrun_list_args if arg.flags == ("--partition-date-gte",)) + parsed_partition_date = partition_date_arg.kwargs["type"]("2026-07-01") + + assert parsed_partition_date == datetime.date(2026, 7, 1) + + @pytest.mark.parametrize( + ("value", "expected"), + [("2026-07-01", datetime.date(2026, 7, 1)), ("20260701", datetime.date(2026, 7, 1))], + ) + def test_iso_date_type_parses_iso_date(self, value, expected): + """An ISO 8601 date string is parsed into a date.""" + assert iso_date_type(value) == expected + + @pytest.mark.parametrize( + "value", + ["", "2026-7-1", "01/07/2026", "2026-07-01T00:00:00", "yesterday"], + ) + def test_iso_date_type_rejects_invalid_date(self, value): + """A value that is not an ISO 8601 date raises an ArgumentTypeError.""" + with pytest.raises(argparse.ArgumentTypeError, match="invalid ISO 8601 date"): + iso_date_type(value) + def test_command_factory_required_primitive_param_is_positional(self, tmp_path): """Required primitive parameters (no default, not Optional) become positional arguments.