Skip to content

Commit 500720f

Browse files
committed
fix: round-trip timedelta cron offset
1 parent 1bcb01a commit 500720f

4 files changed

Lines changed: 70 additions & 6 deletions

File tree

‎taskiq/scheduler/scheduled_task/cron_spec.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from datetime import timedelta
2-
31
from pydantic import BaseModel
42

3+
from taskiq.scheduler.scheduled_task.validators import CronOffset
4+
55

66
class CronSpec(BaseModel):
77
"""Cron specification for running tasks."""
@@ -12,7 +12,7 @@ class CronSpec(BaseModel):
1212
months: str | int | None = "*"
1313
weekdays: str | int | None = "*"
1414

15-
offset: str | timedelta | None = None
15+
offset: CronOffset | None = None
1616

1717
def to_cron(self) -> str: # pragma: no cover
1818
"""Converts cron spec to cron string."""

‎taskiq/scheduler/scheduled_task/task.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
from pydantic import BaseModel, Field, model_validator
77

8-
from taskiq.scheduler.scheduled_task.validators import validate_interval_value
8+
from taskiq.scheduler.scheduled_task.validators import (
9+
CronOffset,
10+
validate_interval_value,
11+
)
912

1013
if sys.version_info >= (3, 11):
1114
from typing import Self
@@ -23,7 +26,7 @@ class ScheduledTask(BaseModel):
2326
task_id: str | None = None
2427
schedule_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
2528
cron: str | None = None
26-
cron_offset: str | timedelta | None = None
29+
cron_offset: CronOffset | None = None
2730
time: datetime | None = None
2831
interval: int | timedelta | None = None
2932

‎taskiq/scheduler/scheduled_task/validators.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,28 @@
11
from datetime import timedelta
2+
from typing import Annotated, Any
3+
4+
from pydantic import Discriminator, Tag, ValidationError
5+
6+
from taskiq.compat import parse_obj_as
7+
8+
9+
def discriminate_offset(value: Any) -> str:
10+
"""Pick the union branch a raw cron offset value belongs to."""
11+
if isinstance(value, timedelta):
12+
return "timedelta"
13+
if isinstance(value, str):
14+
try:
15+
parse_obj_as(timedelta, value)
16+
except ValidationError:
17+
return "str"
18+
return "timedelta"
19+
return "str"
20+
21+
22+
CronOffset = Annotated[
23+
Annotated[timedelta, Tag("timedelta")] | Annotated[str, Tag("str")],
24+
Discriminator(discriminate_offset),
25+
]
226

327

428
def validate_interval_value(

‎tests/scheduler/test_scheduled_task.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
1+
from datetime import timedelta
2+
from typing import Any
3+
14
import pytest
25

3-
from taskiq.scheduler.scheduled_task import ScheduledTask
6+
from taskiq.scheduler.scheduled_task import CronSpec, ScheduledTask
7+
8+
9+
@pytest.mark.parametrize(
10+
("offset", "offset_type"),
11+
[
12+
(timedelta(hours=4), timedelta),
13+
("US/Eastern", str),
14+
],
15+
)
16+
def test_cron_spec_offset_roundtrip(offset: Any, offset_type: type) -> None:
17+
restored = CronSpec.model_validate(CronSpec(offset=offset).model_dump(mode="json"))
18+
assert restored.offset == offset
19+
assert type(restored.offset) is offset_type
20+
21+
22+
@pytest.mark.parametrize(
23+
("offset", "offset_type"),
24+
[
25+
(timedelta(hours=2), timedelta),
26+
("US/Eastern", str),
27+
],
28+
)
29+
def test_scheduled_task_cron_offset_roundtrip(offset: Any, offset_type: type) -> None:
30+
task = ScheduledTask(
31+
task_name="a",
32+
labels={},
33+
args=[],
34+
kwargs={},
35+
cron="* * * * *",
36+
cron_offset=offset,
37+
)
38+
restored = ScheduledTask.model_validate(task.model_dump(mode="json"))
39+
assert restored.cron_offset == offset
40+
assert type(restored.cron_offset) is offset_type
441

542

643
def test_scheduled_task_parameters() -> None:

0 commit comments

Comments
 (0)