1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
|
Introduction
============
.. contents::
croniter provides iteration for the datetime object with a cron like format.
::
_ _
___ _ __ ___ _ __ (_) |_ ___ _ __
/ __| '__/ _ \| '_ \| | __/ _ \ '__|
| (__| | | (_) | | | | | || __/ |
\___|_| \___/|_| |_|_|\__\___|_|
Website: https://github.com/kiorky/croniter
Build Badge
===========
.. image:: https://github.com/kiorky/croniter/actions/workflows/cicd.yml/badge.svg
:target: https://github.com/kiorky/croniter/actions/workflows/cicd.yml
Usage
============
A simple example::
>>> from croniter import croniter
>>> from datetime import datetime
>>> base = datetime(2010, 1, 25, 4, 46)
>>> iter = croniter('*/5 * * * *', base) # every 5 minutes
>>> print(iter.get_next(datetime)) # 2010-01-25 04:50:00
>>> print(iter.get_next(datetime)) # 2010-01-25 04:55:00
>>> print(iter.get_next(datetime)) # 2010-01-25 05:00:00
>>>
>>> iter = croniter('2 4 * * mon,fri', base) # 04:02 on every Monday and Friday
>>> print(iter.get_next(datetime)) # 2010-01-26 04:02:00
>>> print(iter.get_next(datetime)) # 2010-01-30 04:02:00
>>> print(iter.get_next(datetime)) # 2010-02-02 04:02:00
>>>
>>> iter = croniter('2 4 1 * wed', base) # 04:02 on every Wednesday OR on 1st day of month
>>> print(iter.get_next(datetime)) # 2010-01-27 04:02:00
>>> print(iter.get_next(datetime)) # 2010-02-01 04:02:00
>>> print(iter.get_next(datetime)) # 2010-02-03 04:02:00
>>>
>>> iter = croniter('2 4 1 * wed', base, day_or=False) # 04:02 on every 1st day of the month if it is a Wednesday
>>> print(iter.get_next(datetime)) # 2010-09-01 04:02:00
>>> print(iter.get_next(datetime)) # 2010-12-01 04:02:00
>>> print(iter.get_next(datetime)) # 2011-06-01 04:02:00
>>>
>>> iter = croniter('0 0 * * sat#1,sun#2', base) # 1st Saturday, and 2nd Sunday of the month
>>> print(iter.get_next(datetime)) # 2010-02-06 00:00:00
>>>
>>> iter = croniter('0 0 * * 5#3,L5', base) # 3rd and last Friday of the month
>>> print(iter.get_next(datetime)) # 2010-01-29 00:00:00
>>> print(iter.get_next(datetime)) # 2010-02-19 00:00:00
All you need to know is how to use the constructor and the ``get_next``
method, the signature of these methods are listed below::
>>> def __init__(self, cron_format, start_time=time.time(), day_or=True)
croniter iterates along with ``cron_format`` from ``start_time``.
``cron_format`` is **min hour day month day_of_week**, you can refer to
http://en.wikipedia.org/wiki/Cron for more details. The ``day_or``
switch is used to control how croniter handles **day** and **day_of_week**
entries. Default option is the cron behaviour, which connects those
values using **OR**. If the switch is set to False, the values are connected
using **AND**. This behaves like fcron and enables you to e.g. define a job that
executes each 2nd Friday of a month by setting the days of month and the
weekday.
::
>>> def get_next(self, ret_type=float)
get_next calculates the next value according to the cron expression and
returns an object of type ``ret_type``. ``ret_type`` should be a ``float`` or a
``datetime`` object.
Supported added for ``get_prev`` method. (>= 0.2.0)::
>>> base = datetime(2010, 8, 25)
>>> itr = croniter('0 0 1 * *', base)
>>> print(itr.get_prev(datetime)) # 2010-08-01 00:00:00
>>> print(itr.get_prev(datetime)) # 2010-07-01 00:00:00
>>> print(itr.get_prev(datetime)) # 2010-06-01 00:00:00
You can validate your crons using ``is_valid`` class method. (>= 0.3.18)::
>>> croniter.is_valid('0 0 1 * *') # True
>>> croniter.is_valid('0 wrong_value 1 * *') # False
About DST
=========
Be sure to init your croniter instance with a TZ aware datetime for this to work!
Example using pytz::
>>> import pytz
>>> tz = pytz.timezone("Europe/Paris")
>>> local_date = tz.localize(datetime(2017, 3, 26))
>>> val = croniter('0 0 * * *', local_date).get_next(datetime)
Example using python_dateutil::
>>> import dateutil.tz
>>> tz = dateutil.tz.gettz('Asia/Tokyo')
>>> local_date = datetime(2017, 3, 26, tzinfo=tz)
>>> val = croniter('0 0 * * *', local_date).get_next(datetime)
Example using python built in module::
>>> from datetime import datetime, timezone
>>> local_date = datetime(2017, 3, 26, tzinfo=timezone.utc)
>>> val = croniter('0 0 * * *', local_date).get_next(datetime)
About second repeats
=====================
Croniter is able to do second repetition crontabs form::
>>> croniter('* * * * * 1', local_date).get_next(datetime)
>>> base = datetime(2012, 4, 6, 13, 26, 10)
>>> itr = croniter('* * * * * 15,25', base)
>>> itr.get_next(datetime) # 4/6 13:26:15
>>> itr.get_next(datetime) # 4/6 13:26:25
>>> itr.get_next(datetime) # 4/6 13:27:15
You can also note that this expression will repeat every second from the start datetime.::
>>> croniter('* * * * * *', local_date).get_next(datetime)
You can also use seconds as first slot::
>>> croniter('* * * * * *', local_date, second_at_beginning=True).get_next(datetime)
Support for start_time shifts
==============================
See https://github.com/kiorky/croniter/pull/76,
You can set start_time=, then expand_from_start_time=True for your generations to be computed from start_time instead of calendar days::
>>> from pprint import pprint
>>> iter = croniter('0 0 */7 * *', start_time=datetime(2024, 7, 11), expand_from_start_time=True);pprint([iter.get_next(datetime) for a in range(10)])
[datetime.datetime(2024, 7, 18, 0, 0),
datetime.datetime(2024, 7, 25, 0, 0),
datetime.datetime(2024, 8, 4, 0, 0),
datetime.datetime(2024, 8, 11, 0, 0),
datetime.datetime(2024, 8, 18, 0, 0),
datetime.datetime(2024, 8, 25, 0, 0),
datetime.datetime(2024, 9, 4, 0, 0),
datetime.datetime(2024, 9, 11, 0, 0),
datetime.datetime(2024, 9, 18, 0, 0),
datetime.datetime(2024, 9, 25, 0, 0)]
>>> # INSTEAD OF THE DEFAULT BEHAVIOR:
>>> iter = croniter('0 0 */7 * *', start_time=datetime(2024, 7, 11), expand_from_start_time=False);pprint([iter.get_next(datetime) for a in range(10)])
[datetime.datetime(2024, 7, 15, 0, 0),
datetime.datetime(2024, 7, 22, 0, 0),
datetime.datetime(2024, 7, 29, 0, 0),
datetime.datetime(2024, 8, 1, 0, 0),
datetime.datetime(2024, 8, 8, 0, 0),
datetime.datetime(2024, 8, 15, 0, 0),
datetime.datetime(2024, 8, 22, 0, 0),
datetime.datetime(2024, 8, 29, 0, 0),
datetime.datetime(2024, 9, 1, 0, 0),
datetime.datetime(2024, 9, 8, 0, 0)]
Testing if a date matches a crontab
===================================
Test for a match with (>=0.3.32)::
>>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0))
True
>>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 2, 0, 0))
False
>>>
>>> croniter.match("2 4 1 * wed", datetime(2019, 1, 1, 4, 2, 0, 0)) # 04:02 on every Wednesday OR on 1st day of month
True
>>> croniter.match("2 4 1 * wed", datetime(2019, 1, 1, 4, 2, 0, 0), day_or=False) # 04:02 on every 1st day of the month if it is a Wednesday
False
Testing if a crontab matches in datetime range
==============================================
Test for a match_range with (>=2.0.3)::
>>> croniter.match_range("0 0 * * *", datetime(2019, 1, 13, 0, 59, 0, 0), datetime(2019, 1, 14, 0, 1, 0, 0))
True
>>> croniter.match_range("0 0 * * *", datetime(2019, 1, 13, 0, 1, 0, 0), datetime(2019, 1, 13, 0, 59, 0, 0))
False
>>> croniter.match_range("2 4 1 * wed", datetime(2019, 1, 1, 3, 2, 0, 0), datetime(2019, 1, 1, 5, 1, 0, 0))
# 04:02 on every Wednesday OR on 1st day of month
True
>>> croniter.match_range("2 4 1 * wed", datetime(2019, 1, 1, 3, 2, 0, 0), datetime(2019, 1, 1, 5, 2, 0, 0), day_or=False)
# 04:02 on every 1st day of the month if it is a Wednesday
False
Gaps between date matches
=========================
For performance reasons, croniter limits the amount of CPU cycles spent attempting to find the next match.
Starting in v0.3.35, this behavior is configurable via the ``max_years_between_matches`` parameter, and the default window has been increased from 1 year to 50 years.
The defaults should be fine for many use cases.
Applications that evaluate multiple cron expressions or handle cron expressions from untrusted sources or end-users should use this parameter.
Iterating over sparse cron expressions can result in increased CPU consumption or a raised ``CroniterBadDateError`` exception which indicates that croniter has given up attempting to find the next (or previous) match.
Explicitly specifying ``max_years_between_matches`` provides a way to limit CPU utilization and simplifies the iterable interface by eliminating the need for ``CroniterBadDateError``.
The difference in the iterable interface is based on the reasoning that whenever ``max_years_between_matches`` is explicitly agreed upon, there is no need for croniter to signal that it has given up; simply stopping the iteration is preferable.
This example matches 4 AM Friday, January 1st.
Since January 1st isn't often a Friday, there may be a few years between each occurrence.
Setting the limit to 15 years ensures all matches::
>>> it = croniter("0 4 1 1 fri", datetime(2000,1,1), day_or=False, max_years_between_matches=15).all_next(datetime)
>>> for i in range(5):
... print(next(it))
...
2010-01-01 04:00:00
2016-01-01 04:00:00
2021-01-01 04:00:00
2027-01-01 04:00:00
2038-01-01 04:00:00
However, when only concerned with dates within the next 5 years, simply set ``max_years_between_matches=5`` in the above example.
This will result in no matches found, but no additional cycles will be wasted on unwanted matches far in the future.
Iterating over a range using cron
=================================
Find matches within a range using the ``croniter_range()`` function. This is much like the builtin ``range(start,stop,step)`` function, but for dates. The `step` argument is a cron expression.
Added in (>=0.3.34)
List the first Saturday of every month in 2019::
>>> from croniter import croniter_range
>>> for dt in croniter_range(datetime(2019, 1, 1), datetime(2019, 12, 31), "0 0 * * sat#1"):
>>> print(dt)
Hashed expressions
==================
croniter supports Jenkins-style hashed expressions, using the "H" definition keyword and the required hash_id keyword argument.
Hashed expressions remain consistent, given the same hash_id, but different hash_ids will evaluate completely different to each other.
This allows, for example, for an even distribution of differently-named jobs without needing to manually spread them out.
>>> itr = croniter("H H * * *", hash_id="hello")
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 10, 11, 10)
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 11, 11, 10)
>>> itr = croniter("H H * * *", hash_id="hello")
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 10, 11, 10)
>>> itr = croniter("H H * * *", hash_id="bonjour")
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 10, 20, 52)
Random expressions
==================
Random "R" definition keywords are supported, and remain consistent only within their croniter() instance.
>>> itr = croniter("R R * * *")
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 10, 22, 56)
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 11, 22, 56)
>>> itr = croniter("R R * * *")
>>> itr.get_next(datetime)
datetime.datetime(2021, 4, 11, 4, 19)
Keyword expressions
===================
Vixie cron-style "@" keyword expressions are supported.
What they evaluate to depends on whether you supply hash_id: no hash_id corresponds to Vixie cron definitions (exact times, minute resolution), while with hash_id corresponds to Jenkins definitions (hashed within the period, second resolution).
============ ============ ================
Keyword No hash_id With hash_id
============ ============ ================
@midnight 0 0 * * * H H(0-2) * * * H
@hourly 0 * * * * H * * * * H
@daily 0 0 * * * H H * * * H
@weekly 0 0 * * 0 H H * * H H
@monthly 0 0 1 * * H H H * * H
@yearly 0 0 1 1 * H H H H * H
@annually 0 0 1 1 * H H H H * H
============ ============ ================
Upgrading
==========
To 2.0.0
---------
- Install or upgrade pytz by using version specified requirements/base.txt if you have it installed `<=2021.1`.
Develop this package
====================
::
git clone https://github.com/kiorky/croniter.git
cd croniter
virtualenv --no-site-packages venv
. venv/bin/activate
pip install --upgrade -r requirements/test.txt
py.test src
Make a new release
====================
We use zest.fullreleaser, a great release infrastructure.
Do and follow these instructions
::
. venv/bin/activate
pip install --upgrade -r requirements/release.txt
./release.sh
Contributors
===============
Thanks to all who have contributed to this project!
If you have contributed and your name is not listed below please let me know.
- mrmachine
- Hinnack
- shazow
- kiorky
- jlsandell
- mag009
- djmitche
- GreatCombinator
- chris-baynes
- ipartola
- yuzawa-san
- lowell80 (Kintyre)
- scop
- zed2015
- Ryan Finnie (rfinnie)
- salitaba
|