diff --git a/cron/jobs.py b/cron/jobs.py index 94794c90ed..2d9862e6f3 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -960,9 +960,13 @@ def parse_schedule(schedule: str) -> Dict[str, Any]: # Check for cron expression (5 or 6 space-separated fields) # Cron fields: minute hour day month weekday [year] + # Allow letters so named months/weekdays (JAN-DEC, MON-SUN, incl. ranges + # and lists like MON-FRI or MON,WED,FRI) are routed to croniter, which + # supports them. The previous digit-only pattern silently rejected these + # valid expressions as "Invalid schedule". parts = schedule.split() if len(parts) >= 5 and all( - re.match(r'^[\d\*\-,/]+$', p) for p in parts[:5] + re.match(r'^[A-Za-z\d\*\-,/]+$', p) for p in parts[:5] ): if not _ensure_croniter(): raise ValueError("Cron expressions require 'croniter' package. Install with: pip install croniter") diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index 54522e231c..d72d1d76df 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -180,6 +180,25 @@ class TestParseSchedule: assert result["kind"] == "cron" assert result["expr"] == "0 9 * * *" + def test_cron_named_weekdays_and_months(self): + # Named months/weekdays (and ranges/lists) are valid cron and must + # route to croniter, not be rejected as "Invalid schedule". + pytest.importorskip("croniter") + for expr in ( + "0 9 * * MON", + "*/15 9-17 * * MON-FRI", + "0 9 1 JAN *", + "0 9 * * MON,WED,FRI", + ): + result = parse_schedule(expr) + assert result["kind"] == "cron", expr + assert result["expr"] == expr + + def test_invalid_named_cron_still_rejected(self): + pytest.importorskip("croniter") + with pytest.raises(ValueError): + parse_schedule("0 9 * * FUNDAY") + def test_iso_timestamp(self): result = parse_schedule("2030-01-15T14:00:00") assert result["kind"] == "once"