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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ build/
8
+ dist/
9
+ *.egg-info/
@@ -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__"]