croner 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- croner-1.0.0/.gitignore +9 -0
- croner-1.0.0/CHANGELOG.md +17 -0
- croner-1.0.0/LICENSE +22 -0
- croner-1.0.0/PKG-INFO +266 -0
- croner-1.0.0/README.md +236 -0
- croner-1.0.0/pyproject.toml +53 -0
- croner-1.0.0/src/croner/__init__.py +17 -0
- croner-1.0.0/src/croner/_cron.py +937 -0
- croner-1.0.0/src/croner/_describe.py +220 -0
- croner-1.0.0/src/croner/_errors.py +7 -0
- croner-1.0.0/src/croner/_pattern.py +495 -0
- croner-1.0.0/src/croner/_zone.py +147 -0
- croner-1.0.0/src/croner/py.typed +0 -0
- croner-1.0.0/tests/test_cron.py +305 -0
- croner-1.0.0/tests/test_dst.py +163 -0
- croner-1.0.0/tests/test_pattern.py +290 -0
croner-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
First release. A Python port of [croner](https://github.com/hexagon/croner) (JavaScript) and
|
|
6
|
+
[croner-rust](https://github.com/hexagon/croner-rust).
|
|
7
|
+
|
|
8
|
+
- Parse and evaluate cron patterns following OCPS 1.0 - 1.4: optional seconds and years, `L`, `LW`, `W`,
|
|
9
|
+
`#`, `+`, `?`, month and weekday names, nicknames, Quartz-style weekdays, field count modes and
|
|
10
|
+
sloppy ranges.
|
|
11
|
+
- `next_run()`, `next_runs()`, `previous_runs()`, `enumerate()`, `ms_to_next()` and `match()`, in any
|
|
12
|
+
IANA time zone, a fixed UTC offset or system local time, with the same DST handling as croner (JS)
|
|
13
|
+
and croner-rust.
|
|
14
|
+
- Scheduling in threads, with `max_runs`, `interval`, `start_at`, `stop_at`, `day_offset`, `protect`,
|
|
15
|
+
`catch`, `context`, `paused`, `unref`, one-off dates, named jobs and coroutine functions.
|
|
16
|
+
- `describe()` for human readable descriptions.
|
|
17
|
+
- No dependencies.
|
croner-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2026 Hexagon <github.com/Hexagon>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
croner-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: croner
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cron for Python. Parse and evaluate cron patterns, enumerate runs across time zones and DST, and schedule functions. A port of croner (JavaScript) and croner-rust, without dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Hexagon/croner-py
|
|
6
|
+
Project-URL: Repository, https://github.com/Hexagon/croner-py
|
|
7
|
+
Project-URL: Issues, https://github.com/Hexagon/croner-py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Hexagon/croner-py/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Hexagon <hexagon@56k.guru>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cron,crontab,job,ocps,parser,schedule,scheduler,timezone
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Provides-Extra: tzdata
|
|
28
|
+
Requires-Dist: tzdata; extra == 'tzdata'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Croner - Cron for Python
|
|
32
|
+
|
|
33
|
+
Trigger functions or evaluate cron expressions in Python. No dependencies. Most features. Python 3.9+.
|
|
34
|
+
|
|
35
|
+
This is the Python flavor of [croner](https://github.com/hexagon/croner) (JavaScript/TypeScript) and
|
|
36
|
+
[croner-rust](https://github.com/hexagon/croner-rust). All three parse the same patterns and schedule the
|
|
37
|
+
same runs, DST included.
|
|
38
|
+
|
|
39
|
+
* Schedule functions, or just evaluate patterns: next runs, previous runs, matching.
|
|
40
|
+
* Works in any time zone, or a fixed UTC offset, with well defined DST behavior.
|
|
41
|
+
* Supports the [Open Cron Pattern Standard](https://github.com/open-source-cron/ocps) (OCPS) 1.0 - 1.4,
|
|
42
|
+
with optional seconds and years, `L`, `W`, `#`, `+` and `?`.
|
|
43
|
+
* Over-run protection, error handling, pausing, max runs, intervals, start and stop dates, day offsets.
|
|
44
|
+
* Human readable descriptions of patterns.
|
|
45
|
+
* No dependencies, only the Python standard library. Fully typed.
|
|
46
|
+
|
|
47
|
+
Quick examples:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from croner import Cron
|
|
51
|
+
|
|
52
|
+
# Basic: run a function at the fifth second of every minute
|
|
53
|
+
job = Cron("5 * * * * *", lambda: print("This will run every fifth second"))
|
|
54
|
+
|
|
55
|
+
# Enumeration: what dates do the next 100 sundays occur on?
|
|
56
|
+
next_sundays = Cron("0 0 0 * * 7").next_runs(100)
|
|
57
|
+
|
|
58
|
+
# Days left to a specific date
|
|
59
|
+
ms_left = Cron("59 59 23 24 DEC *").ms_to_next()
|
|
60
|
+
print(f"{ms_left // 86_400_000} days left to next christmas eve")
|
|
61
|
+
|
|
62
|
+
# Run a function once, at a specific date and time
|
|
63
|
+
job = Cron("2027-01-23T00:00:00", lambda: print("Yay!"), timezone="Asia/Kolkata")
|
|
64
|
+
|
|
65
|
+
# As a decorator, with over-run protection
|
|
66
|
+
@Cron("*/5 * * * * *", protect=True)
|
|
67
|
+
def every_five_seconds(job):
|
|
68
|
+
print("Every five seconds, never overlapping. Next run:", job.next_run())
|
|
69
|
+
|
|
70
|
+
# Describe a pattern
|
|
71
|
+
print(Cron("0 12 * * MON-FRI").describe()) # At 12:00, on Monday, Tuesday, Wednesday, Thursday, and Friday.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install croner
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
On Windows, which has no system time zone database, also install `tzdata` to use the `timezone` option:
|
|
81
|
+
`pip install croner[tzdata]`.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
### Signature
|
|
86
|
+
|
|
87
|
+
`Cron` takes a pattern, an optional function, and keyword options:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# - pattern: Cron pattern, datetime (run once), or ISO 8601 string (run once)
|
|
91
|
+
# - fn: Optional function to schedule
|
|
92
|
+
# - options: Keyword options (optional)
|
|
93
|
+
job = Cron("* * * * * *", max_runs=1)
|
|
94
|
+
|
|
95
|
+
# If the function is omitted, it can be scheduled later
|
|
96
|
+
job.schedule(lambda job, context: None)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The function is called with the job and `options.context`, `fn(job, context)`. A function that takes fewer
|
|
100
|
+
arguments gets fewer: `fn(job)` or `fn()`. Coroutine functions (`async def`) are run to completion.
|
|
101
|
+
|
|
102
|
+
Each run happens in a thread of its own, so a slow run does not delay the schedule (use `protect` to
|
|
103
|
+
prevent overlapping runs). The scheduler keeps the process alive while a job is scheduled, like a
|
|
104
|
+
JavaScript timer, unless you pass `unref=True`.
|
|
105
|
+
|
|
106
|
+
#### Status
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
job.next_run(start=None) # The next run (datetime), or None.
|
|
110
|
+
job.next_runs(10, start=None) # The next 10 runs.
|
|
111
|
+
job.previous_runs(10, reference=None) # The 10 previous scheduled runs, most recent first.
|
|
112
|
+
job.enumerate(start=None) # A stateful iterator, for loops: for run in job.enumerate(): ...
|
|
113
|
+
job.ms_to_next(start=None) # Milliseconds to the next run, or None.
|
|
114
|
+
job.current_run() # When the current (or last) run started.
|
|
115
|
+
job.previous_run() # When the previous run started.
|
|
116
|
+
job.runs_left() # Runs left, None for unlimited.
|
|
117
|
+
|
|
118
|
+
job.match(date) # Whether a datetime or ISO 8601 string matches the pattern.
|
|
119
|
+
|
|
120
|
+
job.is_running() # Scheduled, and not paused or stopped.
|
|
121
|
+
job.is_stopped() # Stopped with stop().
|
|
122
|
+
job.is_busy() # The function is running right now.
|
|
123
|
+
|
|
124
|
+
job.get_pattern() # The original pattern, or None for one-off jobs.
|
|
125
|
+
job.get_once() # The date of a one-off job, or None.
|
|
126
|
+
job.describe() # Human readable description of the pattern.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Control functions
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
job.trigger() # Run the function now, in the calling thread.
|
|
133
|
+
job.pause() # Pause.
|
|
134
|
+
job.resume() # Resume.
|
|
135
|
+
job.stop() # Stop permanently. Also removes a named job from scheduled_jobs.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Dates and time zones
|
|
139
|
+
|
|
140
|
+
Every method that takes a date accepts a `datetime`, a `date` or an ISO 8601 string.
|
|
141
|
+
|
|
142
|
+
* Aware datetimes, and strings with `Z` or an offset, are exact instants.
|
|
143
|
+
* Naive datetimes, dates, and strings without an offset are wall clock times in the job's time zone
|
|
144
|
+
(the `timezone` or `utc_offset` option), or in system local time if neither is set.
|
|
145
|
+
|
|
146
|
+
Returned datetimes are aware, in the job's time zone (`zoneinfo.ZoneInfo`), so they print in local time
|
|
147
|
+
and compare correctly as instants.
|
|
148
|
+
|
|
149
|
+
> **Note**
|
|
150
|
+
> Python subtracts two aware datetimes that share a `tzinfo` as wall clock times, ignoring DST.
|
|
151
|
+
> Convert to UTC first, `a.astimezone(timezone.utc) - b.astimezone(timezone.utc)`, to get the real
|
|
152
|
+
> time between two runs across a DST change.
|
|
153
|
+
|
|
154
|
+
#### Options
|
|
155
|
+
|
|
156
|
+
| Key | Default | Type | Remarks |
|
|
157
|
+
|----------------------|---------|---------------------|---------|
|
|
158
|
+
| name | None | str | Keep a reference to the job in `croner.scheduled_jobs`. The reference is removed on `stop()`. |
|
|
159
|
+
| max_runs | None | int | Maximum number of runs, None for unlimited. |
|
|
160
|
+
| catch | False | bool \| callable | Catch errors raised by the function. `True` silently ignores them, a callable is called as `catch(error, job)`. |
|
|
161
|
+
| timezone | None | str \| tzinfo | Time zone in Europe/Stockholm format, or a `tzinfo`. |
|
|
162
|
+
| utc_offset | None | int | Schedule with a fixed UTC offset in minutes. This does not take DST into account, you probably want `timezone`. |
|
|
163
|
+
| start_at | None | datetime \| str | Earliest run. Inclusive, a run exactly at `start_at` is included (except when `interval` is set). |
|
|
164
|
+
| stop_at | None | datetime \| str | Stop before this date. Exclusive, a run exactly at `stop_at` is not included. |
|
|
165
|
+
| interval | 0 | int | Minimum number of seconds between runs. |
|
|
166
|
+
| paused | False | bool | Start paused. |
|
|
167
|
+
| context | None | Any | Passed as the second argument to the function. |
|
|
168
|
+
| dom_and_dow | False | bool | Combine day-of-month and day-of-week with AND (True) or OR (False). |
|
|
169
|
+
| legacy_mode | - | bool | **Deprecated:** use `dom_and_dow`. Inverse of `dom_and_dow`. |
|
|
170
|
+
| unref | False | bool | Run the scheduler in daemon threads, so the process can exit while the job is scheduled. |
|
|
171
|
+
| protect | False | bool \| callable | Over-run protection: skip runs while a previous run is still going. A callable is called as `protect(job)` for each skipped run. |
|
|
172
|
+
| alternative_weekdays | False | bool | Quartz-style weekday numbering (1 = Sunday, 2 = Monday, ..., 7 = Saturday). |
|
|
173
|
+
| allow_past | False | bool | Let a one-off job dated in the past run immediately. Otherwise only one dated within the last second does. |
|
|
174
|
+
| day_offset | 0 | int | Shift every run by a number of days, e.g. `-1` for the day before each match. All methods and `start_at` / `stop_at` refer to the shifted time. |
|
|
175
|
+
| mode | "auto" | str | How many fields the pattern has: `"auto"`, `"5-part"`, `"6-part"`, `"7-part"`, `"5-or-6-parts"` or `"6-or-7-parts"`. |
|
|
176
|
+
| sloppy_ranges | False | bool | Accept non-standard steps, like `/10`, `5/5` or `30/30`. By default only `*/step` and `min-max/step` are allowed. |
|
|
177
|
+
|
|
178
|
+
Invalid patterns and options raise `croner.CronError`, a subclass of `ValueError`.
|
|
179
|
+
|
|
180
|
+
#### Pattern
|
|
181
|
+
|
|
182
|
+
Croner uses [Vixie cron](https://en.wikipedia.org/wiki/Cron#CRON_expression) based expressions, with these extensions:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
┌──────────────── (optional) second (0 - 59)
|
|
186
|
+
│ ┌────────────── minute (0 - 59)
|
|
187
|
+
│ │ ┌──────────── hour (0 - 23)
|
|
188
|
+
│ │ │ ┌────────── day of month (1 - 31)
|
|
189
|
+
│ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
|
|
190
|
+
│ │ │ │ │ ┌────── day of week (0 - 6, SUN-SAT)
|
|
191
|
+
│ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
|
|
192
|
+
│ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
|
|
193
|
+
│ │ │ │ │ │ │
|
|
194
|
+
* * * * * * *
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
* **Optional second and year fields**: 6 fields add seconds first, 7 fields also add a year last.
|
|
198
|
+
* **L**: Last day of month, or last occurrence of a weekday. `L` in day-of-month is the last day of the
|
|
199
|
+
month, `LW` the last weekday (Mon-Fri) of the month, and `5#L`, `5L` or `FRI#L` the last Friday of the month.
|
|
200
|
+
* **W**: Nearest weekday. `15W` is the weekday closest to the 15th, never crossing into another month.
|
|
201
|
+
* **#**: Nth occurrence of a weekday. `5#2` is the second Friday, `MON#1` the first Monday of the month.
|
|
202
|
+
* **+**: Prefix the day-of-week field to combine it with day-of-month using AND:
|
|
203
|
+
`0 12 1 * +MON` only runs when the 1st is a Monday.
|
|
204
|
+
* **?**: An alias for `*`. Non-portable, avoid it in patterns meant for other systems.
|
|
205
|
+
* **DST**, the same as croner (JS), croner-rust and Vixie cron, deviating from OCPS 1.4 §4.3.1:
|
|
206
|
+
* **Fixed-time jobs**, with a single value in each of second, minute and hour (e.g. `0 30 2 * * *`):
|
|
207
|
+
a time that occurs twice (fall back) runs once, at the first occurrence. A time that doesn't
|
|
208
|
+
exist (spring forward) runs at the first valid time after the gap, on the same day.
|
|
209
|
+
* **Interval jobs**, with a wildcard, step, range or list in second, minute or hour
|
|
210
|
+
(e.g. `*/5 * * * *` or `0 30 1,2 * * *`): a time that occurs twice runs on both occurrences.
|
|
211
|
+
A time that doesn't exist is skipped.
|
|
212
|
+
* `next_run()`, `next_runs()`, `previous_runs()` and `enumerate()` move in real time. They never
|
|
213
|
+
return a time they have passed, and cover the first pass through a repeated hour before the second.
|
|
214
|
+
|
|
215
|
+
By default, day-of-month and day-of-week are combined with OR: `0 20 1 * MON` runs on the 1st of the month
|
|
216
|
+
and on Mondays. Use the `+` modifier or `dom_and_dow=True` for AND.
|
|
217
|
+
|
|
218
|
+
| Field | Required | Allowed values | Special characters | Remarks |
|
|
219
|
+
|--------------|----------|-----------------|--------------------|---------|
|
|
220
|
+
| Seconds | Optional | 0-59 | * , - / ? | Defaults to 0 |
|
|
221
|
+
| Minutes | Yes | 0-59 | * , - / ? | |
|
|
222
|
+
| Hours | Yes | 0-23 | * , - / ? | |
|
|
223
|
+
| Day of Month | Yes | 1-31 | * , - / ? L W | L = last day, W = nearest weekday |
|
|
224
|
+
| Month | Yes | 1-12 or JAN-DEC | * , - / ? | |
|
|
225
|
+
| Day of Week | Yes | 0-7 or SUN-SAT | * , - / ? L # + | 0 and 7 = Sunday, 1-7 = Sunday-Saturday with `alternative_weekdays` |
|
|
226
|
+
| Year | Optional | 1-9999 | * , - / | Defaults to * |
|
|
227
|
+
|
|
228
|
+
Weekday and month names are case-insensitive.
|
|
229
|
+
|
|
230
|
+
**Nicknames**:
|
|
231
|
+
|
|
232
|
+
| Nickname | Same as |
|
|
233
|
+
|------------------------|---------------|
|
|
234
|
+
| @yearly / @annually | `0 0 1 1 *` |
|
|
235
|
+
| @monthly | `0 0 1 * *` |
|
|
236
|
+
| @weekly | `0 0 * * 0` |
|
|
237
|
+
| @daily / @midnight | `0 0 * * *` |
|
|
238
|
+
| @hourly | `0 * * * *` |
|
|
239
|
+
| @reboot | Recognized, but raises `CronError`, as croner can't detect system startup. |
|
|
240
|
+
|
|
241
|
+
### Pattern objects
|
|
242
|
+
|
|
243
|
+
`croner.CronPattern` parses a pattern on its own. `job.pattern` is the parsed pattern of a job.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from croner import CronPattern
|
|
247
|
+
|
|
248
|
+
pattern = CronPattern("0 9-11 * * 1-5")
|
|
249
|
+
pattern.values("hour") # [9, 10, 11]
|
|
250
|
+
pattern.values("day_of_week") # [1, 2, 3, 4, 5]
|
|
251
|
+
pattern.is_fixed_time # False
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
pip install -e . pytest
|
|
258
|
+
python -m pytest
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`tools/compare_with_js.py` compares random patterns with croner (JavaScript), run from source with
|
|
262
|
+
Node.js, see the script for details.
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT License
|
croner-1.0.0/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Croner - Cron for Python
|
|
2
|
+
|
|
3
|
+
Trigger functions or evaluate cron expressions in Python. No dependencies. Most features. Python 3.9+.
|
|
4
|
+
|
|
5
|
+
This is the Python flavor of [croner](https://github.com/hexagon/croner) (JavaScript/TypeScript) and
|
|
6
|
+
[croner-rust](https://github.com/hexagon/croner-rust). All three parse the same patterns and schedule the
|
|
7
|
+
same runs, DST included.
|
|
8
|
+
|
|
9
|
+
* Schedule functions, or just evaluate patterns: next runs, previous runs, matching.
|
|
10
|
+
* Works in any time zone, or a fixed UTC offset, with well defined DST behavior.
|
|
11
|
+
* Supports the [Open Cron Pattern Standard](https://github.com/open-source-cron/ocps) (OCPS) 1.0 - 1.4,
|
|
12
|
+
with optional seconds and years, `L`, `W`, `#`, `+` and `?`.
|
|
13
|
+
* Over-run protection, error handling, pausing, max runs, intervals, start and stop dates, day offsets.
|
|
14
|
+
* Human readable descriptions of patterns.
|
|
15
|
+
* No dependencies, only the Python standard library. Fully typed.
|
|
16
|
+
|
|
17
|
+
Quick examples:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from croner import Cron
|
|
21
|
+
|
|
22
|
+
# Basic: run a function at the fifth second of every minute
|
|
23
|
+
job = Cron("5 * * * * *", lambda: print("This will run every fifth second"))
|
|
24
|
+
|
|
25
|
+
# Enumeration: what dates do the next 100 sundays occur on?
|
|
26
|
+
next_sundays = Cron("0 0 0 * * 7").next_runs(100)
|
|
27
|
+
|
|
28
|
+
# Days left to a specific date
|
|
29
|
+
ms_left = Cron("59 59 23 24 DEC *").ms_to_next()
|
|
30
|
+
print(f"{ms_left // 86_400_000} days left to next christmas eve")
|
|
31
|
+
|
|
32
|
+
# Run a function once, at a specific date and time
|
|
33
|
+
job = Cron("2027-01-23T00:00:00", lambda: print("Yay!"), timezone="Asia/Kolkata")
|
|
34
|
+
|
|
35
|
+
# As a decorator, with over-run protection
|
|
36
|
+
@Cron("*/5 * * * * *", protect=True)
|
|
37
|
+
def every_five_seconds(job):
|
|
38
|
+
print("Every five seconds, never overlapping. Next run:", job.next_run())
|
|
39
|
+
|
|
40
|
+
# Describe a pattern
|
|
41
|
+
print(Cron("0 12 * * MON-FRI").describe()) # At 12:00, on Monday, Tuesday, Wednesday, Thursday, and Friday.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install croner
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
On Windows, which has no system time zone database, also install `tzdata` to use the `timezone` option:
|
|
51
|
+
`pip install croner[tzdata]`.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
### Signature
|
|
56
|
+
|
|
57
|
+
`Cron` takes a pattern, an optional function, and keyword options:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# - pattern: Cron pattern, datetime (run once), or ISO 8601 string (run once)
|
|
61
|
+
# - fn: Optional function to schedule
|
|
62
|
+
# - options: Keyword options (optional)
|
|
63
|
+
job = Cron("* * * * * *", max_runs=1)
|
|
64
|
+
|
|
65
|
+
# If the function is omitted, it can be scheduled later
|
|
66
|
+
job.schedule(lambda job, context: None)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The function is called with the job and `options.context`, `fn(job, context)`. A function that takes fewer
|
|
70
|
+
arguments gets fewer: `fn(job)` or `fn()`. Coroutine functions (`async def`) are run to completion.
|
|
71
|
+
|
|
72
|
+
Each run happens in a thread of its own, so a slow run does not delay the schedule (use `protect` to
|
|
73
|
+
prevent overlapping runs). The scheduler keeps the process alive while a job is scheduled, like a
|
|
74
|
+
JavaScript timer, unless you pass `unref=True`.
|
|
75
|
+
|
|
76
|
+
#### Status
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
job.next_run(start=None) # The next run (datetime), or None.
|
|
80
|
+
job.next_runs(10, start=None) # The next 10 runs.
|
|
81
|
+
job.previous_runs(10, reference=None) # The 10 previous scheduled runs, most recent first.
|
|
82
|
+
job.enumerate(start=None) # A stateful iterator, for loops: for run in job.enumerate(): ...
|
|
83
|
+
job.ms_to_next(start=None) # Milliseconds to the next run, or None.
|
|
84
|
+
job.current_run() # When the current (or last) run started.
|
|
85
|
+
job.previous_run() # When the previous run started.
|
|
86
|
+
job.runs_left() # Runs left, None for unlimited.
|
|
87
|
+
|
|
88
|
+
job.match(date) # Whether a datetime or ISO 8601 string matches the pattern.
|
|
89
|
+
|
|
90
|
+
job.is_running() # Scheduled, and not paused or stopped.
|
|
91
|
+
job.is_stopped() # Stopped with stop().
|
|
92
|
+
job.is_busy() # The function is running right now.
|
|
93
|
+
|
|
94
|
+
job.get_pattern() # The original pattern, or None for one-off jobs.
|
|
95
|
+
job.get_once() # The date of a one-off job, or None.
|
|
96
|
+
job.describe() # Human readable description of the pattern.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Control functions
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
job.trigger() # Run the function now, in the calling thread.
|
|
103
|
+
job.pause() # Pause.
|
|
104
|
+
job.resume() # Resume.
|
|
105
|
+
job.stop() # Stop permanently. Also removes a named job from scheduled_jobs.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Dates and time zones
|
|
109
|
+
|
|
110
|
+
Every method that takes a date accepts a `datetime`, a `date` or an ISO 8601 string.
|
|
111
|
+
|
|
112
|
+
* Aware datetimes, and strings with `Z` or an offset, are exact instants.
|
|
113
|
+
* Naive datetimes, dates, and strings without an offset are wall clock times in the job's time zone
|
|
114
|
+
(the `timezone` or `utc_offset` option), or in system local time if neither is set.
|
|
115
|
+
|
|
116
|
+
Returned datetimes are aware, in the job's time zone (`zoneinfo.ZoneInfo`), so they print in local time
|
|
117
|
+
and compare correctly as instants.
|
|
118
|
+
|
|
119
|
+
> **Note**
|
|
120
|
+
> Python subtracts two aware datetimes that share a `tzinfo` as wall clock times, ignoring DST.
|
|
121
|
+
> Convert to UTC first, `a.astimezone(timezone.utc) - b.astimezone(timezone.utc)`, to get the real
|
|
122
|
+
> time between two runs across a DST change.
|
|
123
|
+
|
|
124
|
+
#### Options
|
|
125
|
+
|
|
126
|
+
| Key | Default | Type | Remarks |
|
|
127
|
+
|----------------------|---------|---------------------|---------|
|
|
128
|
+
| name | None | str | Keep a reference to the job in `croner.scheduled_jobs`. The reference is removed on `stop()`. |
|
|
129
|
+
| max_runs | None | int | Maximum number of runs, None for unlimited. |
|
|
130
|
+
| catch | False | bool \| callable | Catch errors raised by the function. `True` silently ignores them, a callable is called as `catch(error, job)`. |
|
|
131
|
+
| timezone | None | str \| tzinfo | Time zone in Europe/Stockholm format, or a `tzinfo`. |
|
|
132
|
+
| utc_offset | None | int | Schedule with a fixed UTC offset in minutes. This does not take DST into account, you probably want `timezone`. |
|
|
133
|
+
| start_at | None | datetime \| str | Earliest run. Inclusive, a run exactly at `start_at` is included (except when `interval` is set). |
|
|
134
|
+
| stop_at | None | datetime \| str | Stop before this date. Exclusive, a run exactly at `stop_at` is not included. |
|
|
135
|
+
| interval | 0 | int | Minimum number of seconds between runs. |
|
|
136
|
+
| paused | False | bool | Start paused. |
|
|
137
|
+
| context | None | Any | Passed as the second argument to the function. |
|
|
138
|
+
| dom_and_dow | False | bool | Combine day-of-month and day-of-week with AND (True) or OR (False). |
|
|
139
|
+
| legacy_mode | - | bool | **Deprecated:** use `dom_and_dow`. Inverse of `dom_and_dow`. |
|
|
140
|
+
| unref | False | bool | Run the scheduler in daemon threads, so the process can exit while the job is scheduled. |
|
|
141
|
+
| protect | False | bool \| callable | Over-run protection: skip runs while a previous run is still going. A callable is called as `protect(job)` for each skipped run. |
|
|
142
|
+
| alternative_weekdays | False | bool | Quartz-style weekday numbering (1 = Sunday, 2 = Monday, ..., 7 = Saturday). |
|
|
143
|
+
| allow_past | False | bool | Let a one-off job dated in the past run immediately. Otherwise only one dated within the last second does. |
|
|
144
|
+
| day_offset | 0 | int | Shift every run by a number of days, e.g. `-1` for the day before each match. All methods and `start_at` / `stop_at` refer to the shifted time. |
|
|
145
|
+
| mode | "auto" | str | How many fields the pattern has: `"auto"`, `"5-part"`, `"6-part"`, `"7-part"`, `"5-or-6-parts"` or `"6-or-7-parts"`. |
|
|
146
|
+
| sloppy_ranges | False | bool | Accept non-standard steps, like `/10`, `5/5` or `30/30`. By default only `*/step` and `min-max/step` are allowed. |
|
|
147
|
+
|
|
148
|
+
Invalid patterns and options raise `croner.CronError`, a subclass of `ValueError`.
|
|
149
|
+
|
|
150
|
+
#### Pattern
|
|
151
|
+
|
|
152
|
+
Croner uses [Vixie cron](https://en.wikipedia.org/wiki/Cron#CRON_expression) based expressions, with these extensions:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
┌──────────────── (optional) second (0 - 59)
|
|
156
|
+
│ ┌────────────── minute (0 - 59)
|
|
157
|
+
│ │ ┌──────────── hour (0 - 23)
|
|
158
|
+
│ │ │ ┌────────── day of month (1 - 31)
|
|
159
|
+
│ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
|
|
160
|
+
│ │ │ │ │ ┌────── day of week (0 - 6, SUN-SAT)
|
|
161
|
+
│ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
|
|
162
|
+
│ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
|
|
163
|
+
│ │ │ │ │ │ │
|
|
164
|
+
* * * * * * *
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
* **Optional second and year fields**: 6 fields add seconds first, 7 fields also add a year last.
|
|
168
|
+
* **L**: Last day of month, or last occurrence of a weekday. `L` in day-of-month is the last day of the
|
|
169
|
+
month, `LW` the last weekday (Mon-Fri) of the month, and `5#L`, `5L` or `FRI#L` the last Friday of the month.
|
|
170
|
+
* **W**: Nearest weekday. `15W` is the weekday closest to the 15th, never crossing into another month.
|
|
171
|
+
* **#**: Nth occurrence of a weekday. `5#2` is the second Friday, `MON#1` the first Monday of the month.
|
|
172
|
+
* **+**: Prefix the day-of-week field to combine it with day-of-month using AND:
|
|
173
|
+
`0 12 1 * +MON` only runs when the 1st is a Monday.
|
|
174
|
+
* **?**: An alias for `*`. Non-portable, avoid it in patterns meant for other systems.
|
|
175
|
+
* **DST**, the same as croner (JS), croner-rust and Vixie cron, deviating from OCPS 1.4 §4.3.1:
|
|
176
|
+
* **Fixed-time jobs**, with a single value in each of second, minute and hour (e.g. `0 30 2 * * *`):
|
|
177
|
+
a time that occurs twice (fall back) runs once, at the first occurrence. A time that doesn't
|
|
178
|
+
exist (spring forward) runs at the first valid time after the gap, on the same day.
|
|
179
|
+
* **Interval jobs**, with a wildcard, step, range or list in second, minute or hour
|
|
180
|
+
(e.g. `*/5 * * * *` or `0 30 1,2 * * *`): a time that occurs twice runs on both occurrences.
|
|
181
|
+
A time that doesn't exist is skipped.
|
|
182
|
+
* `next_run()`, `next_runs()`, `previous_runs()` and `enumerate()` move in real time. They never
|
|
183
|
+
return a time they have passed, and cover the first pass through a repeated hour before the second.
|
|
184
|
+
|
|
185
|
+
By default, day-of-month and day-of-week are combined with OR: `0 20 1 * MON` runs on the 1st of the month
|
|
186
|
+
and on Mondays. Use the `+` modifier or `dom_and_dow=True` for AND.
|
|
187
|
+
|
|
188
|
+
| Field | Required | Allowed values | Special characters | Remarks |
|
|
189
|
+
|--------------|----------|-----------------|--------------------|---------|
|
|
190
|
+
| Seconds | Optional | 0-59 | * , - / ? | Defaults to 0 |
|
|
191
|
+
| Minutes | Yes | 0-59 | * , - / ? | |
|
|
192
|
+
| Hours | Yes | 0-23 | * , - / ? | |
|
|
193
|
+
| Day of Month | Yes | 1-31 | * , - / ? L W | L = last day, W = nearest weekday |
|
|
194
|
+
| Month | Yes | 1-12 or JAN-DEC | * , - / ? | |
|
|
195
|
+
| Day of Week | Yes | 0-7 or SUN-SAT | * , - / ? L # + | 0 and 7 = Sunday, 1-7 = Sunday-Saturday with `alternative_weekdays` |
|
|
196
|
+
| Year | Optional | 1-9999 | * , - / | Defaults to * |
|
|
197
|
+
|
|
198
|
+
Weekday and month names are case-insensitive.
|
|
199
|
+
|
|
200
|
+
**Nicknames**:
|
|
201
|
+
|
|
202
|
+
| Nickname | Same as |
|
|
203
|
+
|------------------------|---------------|
|
|
204
|
+
| @yearly / @annually | `0 0 1 1 *` |
|
|
205
|
+
| @monthly | `0 0 1 * *` |
|
|
206
|
+
| @weekly | `0 0 * * 0` |
|
|
207
|
+
| @daily / @midnight | `0 0 * * *` |
|
|
208
|
+
| @hourly | `0 * * * *` |
|
|
209
|
+
| @reboot | Recognized, but raises `CronError`, as croner can't detect system startup. |
|
|
210
|
+
|
|
211
|
+
### Pattern objects
|
|
212
|
+
|
|
213
|
+
`croner.CronPattern` parses a pattern on its own. `job.pattern` is the parsed pattern of a job.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from croner import CronPattern
|
|
217
|
+
|
|
218
|
+
pattern = CronPattern("0 9-11 * * 1-5")
|
|
219
|
+
pattern.values("hour") # [9, 10, 11]
|
|
220
|
+
pattern.values("day_of_week") # [1, 2, 3, 4, 5]
|
|
221
|
+
pattern.is_fixed_time # False
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pip install -e . pytest
|
|
228
|
+
python -m pytest
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`tools/compare_with_js.py` compares random patterns with croner (JavaScript), run from source with
|
|
232
|
+
Node.js, see the script for details.
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT License
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "croner"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Cron for Python. Parse and evaluate cron patterns, enumerate runs across time zones and DST, and schedule functions. A port of croner (JavaScript) and croner-rust, without dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Hexagon", email = "hexagon@56k.guru" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["cron", "crontab", "scheduler", "schedule", "job", "timezone", "parser", "ocps"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# Windows has no system time zone database, zoneinfo then needs the tzdata package
|
|
34
|
+
tzdata = ["tzdata"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Hexagon/croner-py"
|
|
38
|
+
Repository = "https://github.com/Hexagon/croner-py"
|
|
39
|
+
Issues = "https://github.com/Hexagon/croner-py/issues"
|
|
40
|
+
Changelog = "https://github.com/Hexagon/croner-py/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "src/croner/__init__.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/croner"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["src/croner", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Croner - cron for Python.
|
|
2
|
+
|
|
3
|
+
A Python port of croner (JavaScript) and croner-rust: parse and evaluate cron
|
|
4
|
+
patterns, enumerate runs across time zones and DST, and schedule functions.
|
|
5
|
+
|
|
6
|
+
>>> from croner import Cron
|
|
7
|
+
>>> job = Cron("0 12 * * MON-FRI", timezone="Europe/Stockholm")
|
|
8
|
+
>>> job.next_run() # doctest: +SKIP
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._cron import Cron, CronIterator, scheduled_jobs
|
|
12
|
+
from ._errors import CronError
|
|
13
|
+
from ._pattern import CronPattern
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
|
|
17
|
+
__all__ = ["Cron", "CronError", "CronIterator", "CronPattern", "scheduled_jobs", "__version__"]
|