fix: accept datetime ranges in parse_date_range
With period.dynamic: true and period.precision: datetime, running without --date crashed: compute_next_period() returns a datetime range (YYYY-MM-DDTHH:MM:SS), which parse_date_range() rejected with a fullmatch against YYYY-MM-DD only. parse_date_range now accepts YYYY-MM-DDTHH:MM:SS--YYYY-MM-DDTHH:MM:SS in addition to plain date ranges, returning datetime strings unchanged (pass-through, matching compute_next_period output). Mixed-precision ranges, invalid times and reversed ranges are still rejected with the existing error messages; date-range behavior is unchanged. Closes #59
This commit is contained in:
@@ -25,20 +25,27 @@ def parse_date_range(date_arg: str) -> tuple[str, str]:
|
|||||||
|
|
||||||
from_date, to_date = parts[0].strip(), parts[1].strip()
|
from_date, to_date = parts[0].strip(), parts[1].strip()
|
||||||
date_pattern = r"\d{4}-\d{2}-\d{2}"
|
date_pattern = r"\d{4}-\d{2}-\d{2}"
|
||||||
if not re.fullmatch(date_pattern, from_date) or not re.fullmatch(
|
datetime_pattern = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"
|
||||||
date_pattern, to_date
|
if re.fullmatch(date_pattern, from_date) and re.fullmatch(date_pattern, to_date):
|
||||||
|
fmt = "%Y-%m-%d"
|
||||||
|
elif re.fullmatch(datetime_pattern, from_date) and re.fullmatch(
|
||||||
|
datetime_pattern, to_date
|
||||||
):
|
):
|
||||||
|
fmt = "%Y-%m-%dT%H:%M:%S"
|
||||||
|
else:
|
||||||
raise ValueError("Date range must be in format YYYY-MM-DD--YYYY-MM-DD")
|
raise ValueError("Date range must be in format YYYY-MM-DD--YYYY-MM-DD")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start = datetime.strptime(from_date, "%Y-%m-%d").date()
|
start = datetime.strptime(from_date, fmt)
|
||||||
end = datetime.strptime(to_date, "%Y-%m-%d").date()
|
end = datetime.strptime(to_date, fmt)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ValueError("Date range contains invalid calendar date") from e
|
raise ValueError("Date range contains invalid calendar date") from e
|
||||||
|
|
||||||
if start > end:
|
if start > end:
|
||||||
raise ValueError("Date range start must be less than or equal to end")
|
raise ValueError("Date range start must be less than or equal to end")
|
||||||
|
|
||||||
|
if fmt == "%Y-%m-%d":
|
||||||
|
return start.date().isoformat(), end.date().isoformat()
|
||||||
return start.isoformat(), end.isoformat()
|
return start.isoformat(), end.isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ def _reset_config_overrides():
|
|||||||
[
|
[
|
||||||
("2026-01-01--2026-01-31", ("2026-01-01", "2026-01-31")),
|
("2026-01-01--2026-01-31", ("2026-01-01", "2026-01-31")),
|
||||||
(" 2026-01-01 -- 2026-01-31 ", ("2026-01-01", "2026-01-31")),
|
(" 2026-01-01 -- 2026-01-31 ", ("2026-01-01", "2026-01-31")),
|
||||||
|
(
|
||||||
|
"2026-06-01T00:00:00--2026-06-30T23:59:59",
|
||||||
|
("2026-06-01T00:00:00", "2026-06-30T23:59:59"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parse_date_range_valid(date_arg, expected):
|
def test_parse_date_range_valid(date_arg, expected):
|
||||||
@@ -43,6 +47,9 @@ def test_parse_date_range_valid(date_arg, expected):
|
|||||||
"2026-1-01--2026-01-31",
|
"2026-1-01--2026-01-31",
|
||||||
"2026-02-30--2026-03-01",
|
"2026-02-30--2026-03-01",
|
||||||
"2026-02-01--2026-01-31",
|
"2026-02-01--2026-01-31",
|
||||||
|
"2026-06-01T25:00:00--2026-06-30T23:59:59",
|
||||||
|
"2026-06-01--2026-06-30T23:59:59",
|
||||||
|
"2026-06-30T23:59:59--2026-06-01T00:00:00",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parse_date_range_invalid(date_arg):
|
def test_parse_date_range_invalid(date_arg):
|
||||||
@@ -1372,11 +1379,6 @@ class TestReportNoTimeIntegration:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
|
||||||
strict=True,
|
|
||||||
reason="bug #59: parse_date_range rejects datetime range produced by "
|
|
||||||
"dynamic period with precision=datetime when --date is omitted",
|
|
||||||
)
|
|
||||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||||
def test_cli_dynamic_datetime_period_without_date(mock_fetch, tmp_path):
|
def test_cli_dynamic_datetime_period_without_date(mock_fetch, tmp_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user