From 77fd6db4c619b512c4b3919053ddcab2752a302a Mon Sep 17 00:00:00 2001 From: devorun <130918800+devorun@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:00:03 +0300 Subject: [PATCH] fix(cron): accept named months/weekdays in cron schedules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- cron/jobs.py | 6 +++++- tests/cron/test_jobs.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) 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"