fix(cron): accept named months/weekdays in cron schedules

`parse_schedule()` detected cron expressions with a digit-only field pattern
(`^[\d\*\-,/]+$`), so any field using named months or weekdays — `MON`, `JAN`,
and common ranges/lists like `MON-FRI` or `MON,WED,FRI` — failed detection and
fell through to a confusing "Invalid schedule" error, even though croniter
supports them and they're standard cron.

Allow letters in the field pattern so these route to croniter for validation.
Truly-invalid expressions (`0 9 * * FUNDAY`, `99 9 * * MON`) are still rejected
there with a clear "Invalid cron expression" message; duration/interval/ISO
parsing is unchanged.

Adds tests for named weekdays/months (incl. ranges and lists) and that an
invalid named field is still rejected.
This commit is contained in:
devorun
2026-06-25 17:00:03 +03:00
committed by Teknium
parent ecdc03c616
commit 77fd6db4c6
2 changed files with 24 additions and 1 deletions
+5 -1
View File
@@ -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")
+19
View File
@@ -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"