Skip to content

Commit daff844

Browse files
committed
Add provision of cron expressions for periodic task scheduling.
1 parent c08b0b3 commit daff844

File tree

5 files changed

+116
-5
lines changed

5 files changed

+116
-5
lines changed

README.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ The following options can be only specified in the task decorator:
389389
the initial task execution date when a worker is initialized, and to determine
390390
the next execution date when the task is about to get executed.
391391

392-
For most common scenarios, the ``periodic`` built-in function can be passed:
392+
For most common scenarios, the below mentioned built-in functions can be passed:
393393

394394
- ``periodic(seconds=0, minutes=0, hours=0, days=0, weeks=0, start_date=None,
395395
end_date=None)``
@@ -401,6 +401,17 @@ The following options can be only specified in the task decorator:
401401
every Sunday at 4am UTC, you could use
402402
``schedule=periodic(weeks=1, start_date=datetime.datetime(2000, 1, 2, 4))``.
403403

404+
- ``cron_expr(expr, start_date=None, end_date=None)``
405+
406+
``start_date``, to specify the periodic task start date. It defaults to
407+
``2000-01-01T00:00Z``, a Saturday, if not given.
408+
``end_date``, to specify the periodic task end date. The task repeats
409+
forever if ``end_date`` is not given.
410+
For example, to run a task every hour indefinitely,
411+
use ``schedule=cron_expr("0 * * * *")``. To run a task every Sunday at
412+
4am UTC, you could use ``schedule=cron_expr("0 4 * * 0")``.
413+
414+
404415
Custom retrying
405416
---------------
406417

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
click==8.1.3
22
redis==4.5.2
33
structlog==22.3.0
4+
croniter

tasktiger/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
TaskNotFound,
1010
)
1111
from .retry import exponential, fixed, linear
12-
from .schedule import periodic
12+
from .schedule import periodic, cron_expr
1313
from .task import Task
1414
from .tasktiger import TaskTiger, run_worker
1515
from .worker import Worker
@@ -31,6 +31,7 @@
3131
"exponential",
3232
# Schedules
3333
"periodic",
34+
"cron_expr"
3435
]
3536

3637

tasktiger/schedule.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import datetime
22
from typing import Callable, Optional, Tuple
33

4-
__all__ = ["periodic"]
4+
__all__ = ["periodic", "cron_expr"]
5+
6+
START_DATE = datetime.datetime(2000, 1, 1)
57

68

79
def _periodic(
@@ -54,5 +56,57 @@ def periodic(
5456
assert period > 0, "Must specify a positive period."
5557
if not start_date:
5658
# Saturday at midnight
57-
start_date = datetime.datetime(2000, 1, 1)
59+
start_date = START_DATE
5860
return (_periodic, (period, start_date, end_date))
61+
62+
63+
def _cron_expr(
64+
dt: datetime.datetime,
65+
expr: str,
66+
start_date: datetime.datetime,
67+
end_date: Optional[datetime.datetime] = None
68+
) -> Optional[datetime.datetime]:
69+
import pytz # type: ignore
70+
import croniter # type: ignore
71+
localize = pytz.utc.localize
72+
73+
if end_date and dt >= end_date:
74+
return None
75+
76+
if dt < start_date:
77+
return start_date
78+
79+
assert croniter.croniter.is_valid(expr), "Cron expression is not valid."
80+
81+
start_date = localize(start_date)
82+
dt = localize(dt)
83+
84+
next_utc = croniter.croniter(expr, dt).get_next(ret_type=datetime.datetime)
85+
next_utc = next_utc.replace(tzinfo=None)
86+
87+
# Make sure the time is still within bounds.
88+
if end_date and next_utc > end_date:
89+
return None
90+
91+
return next_utc
92+
93+
94+
def cron_expr(
95+
expr: str,
96+
start_date: Optional[datetime.datetime] = None,
97+
end_date: Optional[datetime.datetime] = None
98+
) -> Tuple[Callable[..., Optional[datetime.datetime]], Tuple]:
99+
"""
100+
Periodic task schedule via cron expression: Use to schedule a task to run periodically,
101+
starting from start_date (or None to be active immediately) until end_date
102+
(or None to repeat forever).
103+
104+
This function behaves similar to the cron jobs, which run with a minimum of 1 minute
105+
granularity. So specifying "* * * * *" expression will the run the task every
106+
minute.
107+
108+
For more details, see README.
109+
"""
110+
if not start_date:
111+
start_date = START_DATE
112+
return (_cron_expr, (expr, start_date, end_date))

tests/test_periodic.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import datetime
44
import time
55

6-
from tasktiger import Task, Worker, periodic
6+
from tasktiger import Task, Worker, periodic, cron_expr
77
from tasktiger._internal import (
88
QUEUED,
99
SCHEDULED,
@@ -64,6 +64,50 @@ def test_periodic_schedule(self):
6464
f = periodic(minutes=1, end_date=dt)
6565
assert f[0](datetime.datetime(2010, 1, 1, 0, 1), *f[1]) is None
6666

67+
def test_cron_schedule(self):
68+
"""
69+
Test the cron_expr() schedule function.
70+
"""
71+
dt = datetime.datetime(2010, 1, 1)
72+
73+
f = cron_expr("* * * * *")
74+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 1, 0, 1)
75+
76+
f = cron_expr("0 * * * *")
77+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 1, 1)
78+
79+
f = cron_expr("0 0 * * *")
80+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 2)
81+
82+
f = cron_expr("0 0 * * 6")
83+
# 2010-01-02 is a Saturday
84+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 2)
85+
86+
f = cron_expr("0 0 * * 0", start_date=datetime.datetime(2000, 1, 2))
87+
# 2000-01-02 is a Sunday and 2010-01-02 is a Saturday
88+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 3)
89+
90+
f = cron_expr("2 3 * * *", start_date=dt)
91+
assert f[0](dt, *f[1]) == datetime.datetime(2010, 1, 1, 3, 2)
92+
# Make sure we return the start_date if the current date is earlier.
93+
assert f[0](datetime.datetime(1990, 1, 1), *f[1]) == dt
94+
95+
f = cron_expr("* * * * *", end_date=dt)
96+
assert f[0](
97+
datetime.datetime(2009, 12, 31, 23, 58), *f[1]
98+
) == datetime.datetime(2009, 12, 31, 23, 59)
99+
100+
f = cron_expr("* * * * *", end_date=dt)
101+
assert f[0](
102+
datetime.datetime(2009, 12, 31, 23, 59), *f[1]
103+
) == datetime.datetime(2010, 1, 1, 0, 0)
104+
105+
f = cron_expr("* * * * *", end_date=dt)
106+
assert f[0](datetime.datetime(2010, 1, 1, 0, 0), *f[1]) is None
107+
108+
f = cron_expr("* * * * *", end_date=dt)
109+
assert f[0](datetime.datetime(2010, 1, 1, 0, 1), *f[1]) is None
110+
67111
def test_periodic_execution(self):
68112
"""
69113
Test periodic task execution.

0 commit comments

Comments
 (0)