Skip to content

Commit 649457e

Browse files
authored
Merge pull request #2 from dapper91/dev
weekday iteration bug fixed
2 parents b52172a + e1590e6 commit 649457e

File tree

7 files changed

+109
-50
lines changed

7 files changed

+109
-50
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
- released
77

88
jobs:
9-
test:
9+
release:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v2

.github/workflows/test.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: release
1+
name: test
22

33
on:
44
pull_request:
@@ -26,4 +26,11 @@ jobs:
2626
- name: Run linter
2727
run: pipenv run flake8 .
2828
- name: Run tests
29-
run: PYTHONPATH="$(pwd):$PYTHONPATH" pipenv run py.test --cov=crontools tests
29+
run: PYTHONPATH="$(pwd):$PYTHONPATH" pipenv run py.test --cov=crontools --cov-report=xml tests
30+
- name: Upload coverage to Codecov
31+
uses: codecov/codecov-action@v1
32+
with:
33+
token: ${{ secrets.CODECOV_TOKEN }}
34+
files: ./coverage.xml
35+
flags: unittests
36+
fail_ci_if_error: true

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Changelog
22
=========
33

4+
0.1.1 (2021-03-16)
5+
------------------
6+
7+
- Weekday iteration bug fixed
8+
49

510
0.1.0 (2021-03-15)
611
------------------

README.rst

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22
crontools
33
=========
44

5+
.. image:: https://github.com/dapper91/crontools/actions/workflows/test.yml/badge.svg
6+
:target: https://github.com/dapper91/crontools/actions/workflows/test.yml
7+
:alt: Build status
8+
.. image:: https://img.shields.io/pypi/l/crontools.svg
9+
:target: https://pypi.org/project/crontools
10+
:alt: License
11+
.. image:: https://img.shields.io/pypi/pyversions/crontools.svg
12+
:target: https://pypi.org/project/crontools
13+
:alt: Supported Python versions
14+
.. image:: https://codecov.io/gh/dapper91/crontools/branch/master/graph/badge.svg
15+
:target: https://codecov.io/gh/dapper91/crontools
16+
:alt: Code coverage
17+
18+
19+
``crontools`` is a library that allows you to parse crontab expression and iterate over scheduled fire times.
20+
21+
522
Features:
623

724
- crontab expression parser
@@ -22,28 +39,49 @@ You can install crontools with pip:
2239
Quickstart
2340
----------
2441

25-
On the server side everything is also pretty straightforward:
42+
43+
Get next cron fire time:
44+
________________________
2645

2746
.. code-block:: python
2847
29-
import datetime as dt
30-
import crontools as ct
48+
>>> import datetime as dt
49+
>>> import crontools as ct
50+
>>>
51+
>>> tz = dt.timezone.utc
52+
>>> now = dt.datetime.fromisoformat('2020-02-29 23:59:59.999+00:00')
53+
>>> ct = ct.Crontab.parse(
54+
... '* * * * * * *',
55+
... seconds_ext=True,
56+
... years_ext=True,
57+
... tz=tz,
58+
... )
59+
>>>
60+
>>> print(f"Next fire time: {ct.next_fire_time(now=now)}")
61+
Next fire time: 2020-03-01 00:00:00+00:00
3162
32-
tz = dt.timezone.utc
33-
now = dt.datetime.fromisoformat('2021-02-01 00:00:00+00:00')
34-
ct = ct.Crontab.parse(
35-
'30 30 12-16/2 1,2 JAN SAT,SUN *',
36-
seconds_ext=True,
37-
years_ext=True,
38-
tz=tz,
39-
)
4063
41-
cron_iter = ct.iter(start_from=now)
42-
for n, fire_datetime in zip(range(1, 31), cron_iter):
43-
print("{n:2}: {dt}".format(n=n, dt=fire_datetime))
64+
Iteration over cron fire times starting from now:
65+
_________________________________________________
4466

45-
output:
67+
.. code-block:: python
4668
69+
>>> import crontools as ct
70+
>>>
71+
>>> tz = dt.timezone.utc
72+
>>> now = dt.datetime.fromisoformat('2021-02-01 00:00:00+00:00')
73+
>>> ct = ct.Crontab.parse(
74+
... '30 30 12-16/2 1,2 JAN SAT,SUN *',
75+
... seconds_ext=True,
76+
... years_ext=True,
77+
... tz=tz,
78+
... )
79+
>>>
80+
>>> cron_iter = ct.iter(start_from=now)
81+
>>> for n, fire_datetime in zip(range(1, 31), cron_iter):
82+
... print("{n:2}: {dt}".format(n=n, dt=fire_datetime))
83+
...
84+
...
4785
1: 2022-01-01 12:30:30+00:00
4886
2: 2022-01-01 14:30:30+00:00
4987
3: 2022-01-01 16:30:30+00:00

crontools/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__description__ = 'Python cron tools'
33
__url__ = 'https://github.com/dapper91/crontools'
44

5-
__version__ = '0.1.0'
5+
__version__ = '0.1.1'
66

77
__author__ = 'Dmitry Pershin'
88
__email__ = '[email protected]'

crontools/crontab.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,7 @@ def __init_subclass__(cls, title: str, min_value: int, max_value: int, aliases:
5353
cls.aliases = aliases
5454

5555
def __iter__(self) -> Iterator[int]:
56-
if self.is_default:
57-
begin = self.min_value
58-
end = self.max_value
59-
else:
60-
begin = self.begin
61-
end = self.begin if self.end is None else self.end
62-
63-
step = 1 if self.step is None else self.step
64-
65-
return iter(range(begin, end + 1, step))
56+
return self.iter()
6657

6758
def __str__(self) -> str:
6859
if self.begin is None:
@@ -139,11 +130,19 @@ def iter(self, start_from: Optional[int] = None) -> Iterator[int]:
139130
:return: range values iterator
140131
"""
141132

142-
range_iter = iter(self)
143-
if start_from:
144-
range_iter = it.dropwhile(lambda value: value < start_from, range_iter)
133+
if self.is_default:
134+
begin = self.min_value
135+
end = self.max_value
136+
else:
137+
begin = self.begin
138+
end = self.begin if self.end is None else self.end
139+
140+
step = 1 if self.step is None else self.step
141+
142+
if start_from is not None:
143+
begin = max(begin, start_from)
145144

146-
return range_iter
145+
return iter(range(begin, end + 1, step))
147146

148147

149148
@dc.dataclass(frozen=True)
@@ -236,7 +235,7 @@ def __str__(self) -> str:
236235
return ",".join(map(str, self.ranges))
237236

238237
def __iter__(self) -> Iterator[int]:
239-
return unique(heapq.merge(*self.ranges))
238+
return self.iter()
240239

241240
def iter(self, start_from: Optional[int] = None) -> Iterator[int]:
242241
"""
@@ -246,11 +245,7 @@ def iter(self, start_from: Optional[int] = None) -> Iterator[int]:
246245
:return: iterator over all field values
247246
"""
248247

249-
range_iter = iter(self)
250-
if start_from:
251-
range_iter = it.dropwhile(lambda value: value < start_from, range_iter)
252-
253-
return range_iter
248+
return unique(heapq.merge(*(rng.iter(start_from=start_from) for rng in self.ranges)))
254249

255250
@classmethod
256251
def __init_subclass__(cls, range_type: Type[Range]):
@@ -339,22 +334,22 @@ def _weekday_iter(self, year: int, month: int, start_day: int = 1) -> Iterator[i
339334
curr_weekday = calendar.weekday(year, month, curr_day) + 1
340335
weekday_iter = self._weekday_field.iter(start_from=curr_weekday)
341336

342-
for _ in range(4):
343-
for weekday in it.cycle(it.chain(weekday_iter, (None,))):
344-
if weekday is None:
345-
curr_day += (8 - curr_weekday)
346-
curr_weekday = calendar.weekday(year, month, curr_day) + 1
347-
weekday_iter = self._weekday_field.iter()
348-
break
349-
else:
350-
curr_day += (weekday - curr_weekday)
351-
curr_weekday = calendar.weekday(year, month, curr_day) + 1
352-
337+
for _ in range(5):
338+
for weekday in weekday_iter:
339+
curr_day += (weekday - curr_weekday)
340+
curr_weekday += (weekday - curr_weekday)
353341
if curr_day > calendar.monthrange(year, month)[1]:
354342
return
355343

356344
yield curr_day
357345

346+
curr_day += (8 - curr_weekday)
347+
curr_weekday = 1
348+
if curr_day > calendar.monthrange(year, month)[1]:
349+
return
350+
351+
weekday_iter = self._weekday_field.iter()
352+
358353

359354
@dc.dataclass(frozen=True)
360355
class Crontab:

tests/test_crontab.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,20 @@ def test_cron_iterator_start_from(expr, start_from, result, years_ext, seconds_e
460460
True,
461461
True,
462462
),
463+
(
464+
'0 0 * * MON',
465+
dt.datetime(year=2021, month=2, day=28, hour=0, minute=0, second=0, microsecond=0, tzinfo=dt.timezone.utc),
466+
dt.datetime(year=2021, month=3, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=dt.timezone.utc),
467+
False,
468+
False,
469+
),
470+
(
471+
'0 0 * * WED,THU',
472+
dt.datetime(year=2021, month=3, day=31, hour=0, minute=0, second=0, microsecond=1, tzinfo=dt.timezone.utc),
473+
dt.datetime(year=2021, month=4, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=dt.timezone.utc),
474+
False,
475+
False,
476+
),
463477
],
464478
)
465479
@freezegun.freeze_time('2020-01-01 00:00:00.000Z+00:00')

0 commit comments

Comments
 (0)