# CronSim
[![Tests](https://github.com/cuu508/cronsim/actions/workflows/pytest.yml/badge.svg)](https://github.com/cuu508/cronsim/actions/workflows/pytest.yml)
Cron Sim(ulator), a cron expression parser and evaluator. Works with Python 3.8+.
CronSim is written for and being used in
[Healthchecks](https://github.com/healthchecks/healthchecks/)
(a cron job monitoring service).
Development priorities:
* Correctness. CronSim tries to match Debian's cron as closely as possible,
including its quirky behaviour during DST transitions.
* Readability. Prefer simple over clever.
* Minimalism. Don't implement features that Healthchecks will not use
(for example, the seconds field in cron expressions).
## Installation
```
pip install cronsim
```
## Usage
```python
from datetime import datetime
from cronsim import CronSim
it = CronSim("0 0 * 2 MON#5", datetime(2020, 1, 1))
for x in range(0, 5):
print(next(it))
```
Produces:
```
2044-02-29 00:00:00
2072-02-29 00:00:00
2112-02-29 00:00:00
2140-02-29 00:00:00
2168-02-29 00:00:00
```
To iterate backwards in time, add `reverse=True` in the constructor:
```python
from datetime import datetime
from cronsim import CronSim
it = CronSim("0 0 * 2 MON#5", datetime(2020, 1, 1), reverse=True)
print(next(it))
```
Produces:
```
2016-02-29 00:00:00
```
If CronSim receives an invalid cron expression, it raises `cronsim.CronSimError`:
```python
from datetime import datetime
from cronsim import CronSim
CronSim("123 * * * *", datetime(2020, 1, 1))
```
Produces:
```
cronsim.cronsim.CronSimError: Bad minute
```
If CronSim cannot find a matching datetime in the next 50 years from the start
date or from the previous match, it stops iteration by raising `StopIteration`:
```python
from datetime import datetime
from cronsim import CronSim
# Every minute of 1st and 21st of month,
# if it is also the *last Monday* of the month:
it = CronSim("* * */20 * 1L", datetime(2020, 1, 1))
print(next(it))
```
Produces:
```
StopIteration
```
## CronSim Works With zoneinfo
CronSim starting from version 2.0 is designed to work with timezones provided by
the zoneinfo module.
A previous version, CronSim 1.0, was designed for pytz and relied on its
following non-standard features:
* the non-standard `is_dst` flag in the `localize()` method
* the `pytz.AmbiguousTimeError` and `pytz.NonExistentTimeError` exceptions
* the `normalize()` method
## Supported Cron Expression Features
CronSim aims to match [Debian's cron implementation](https://salsa.debian.org/debian/cron/-/tree/master/)
(which itself is based on Paul Vixie's cron, with modifications). If CronSim evaluates
an expression differently from Debian's cron, that's a bug.
CronSim is open to adding support for non-standard syntax features, as long as
they don't conflict or interfere with the standard syntax.
## DST Transitions
CronSim handles Daylight Saving Time transitions the same as
Debian's cron. Debian has special handling for jobs with a granularity
greater than one hour:
```
Local time changes of less than three hours, such as those caused by
the start or end of Daylight Saving Time, are handled specially. This
only applies to jobs that run at a specific time and jobs that are run
with a granularity greater than one hour. Jobs that run more fre-
quently are scheduled normally.
If time has moved forward, those jobs that would have run in the inter-
val that has been skipped will be run immediately. Conversely, if time
has moved backward, care is taken to avoid running jobs twice.
```
See test cases in `test_cronsim.py`, `TestDstTransitions` class
for examples of this special handling.
## Cron Expression Feature Matrix
| Feature | Debian | Quartz | croniter | cronsim |
| ------------------------------------ | :----: | :----: | :------: | :-----: |
| Seconds in the 6th field | No | Yes | Yes | No |
| "L" as the day-of-month | No | Yes | Yes | Yes |
| "LW" as the day-of-month | No | Yes | No | Yes |
| "L" in the day-of-week field | No | Yes | No | Yes |
| Nth weekday of month | No | Yes | Yes | Yes |
**Seconds in the 6th field**: an optional sixth field specifying seconds.
Supports the same syntax features as the minutes field. Quartz Scheduler
expects seconds in the first field, croniter expects seconds in the last field.
Quartz example: `*/15 * * * * *` (every 15 seconds).
**"L" as the day-of-month**: support for the "L" special character in the
day-of-month field. Interpreted as "the last day of the month".
Example: `0 0 L * *` (at the midnight of the last day of every month).
**"LW" as the day-of-month**: support for the "LW" special value in the
day-of-month field. Interpreted as "the last weekday (Mon-Fri) of the month".
Example: `0 0 LW * *` (at the midnight of the last weekday of every month).
**"L" in the day-of-week field**: support for the "{weekday}L" syntax.
For example, "5L" means "the last Friday of the month".
Example: `0 0 * * 6L` (at the midnight of the last Saturday of every month).
**Nth weekday of month**: support for "{weekday}#{nth}" syntax.
For example, "MON#1" means "first Monday of the month", "MON#2" means "second Monday
of the month".
Example: `0 0 * * MON#1` (at midnight of the first monday of every month).
## The `explain()` Method
Starting from version 2.4, the CronSim objects have an `explain()` method
which generates a text description of the supplied cron expression.
```python
from datetime import datetime
from cronsim import CronSim
expr = CronSim("*/5 9-17 * * *", datetime.now())
print(expr.explain())
```
Outputs:
```
Every fifth minute from 09:00 through 17:59
```
The text descriptions are available in English only. The text descriptions
use the 24-hour time format ("23:00" instead of "11:00 PM").
For examples of generated descriptions see `tests/test_explain.py`.
Raw data
{
"_id": null,
"home_page": "https://github.com/cuu508/cronsim",
"name": "cronsim",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "cron, cronjob, crontab, schedule",
"author": "P\u0113teris Caune",
"author_email": "cuu508@monkeyseemonkeydo.lv",
"download_url": "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz",
"platform": "any",
"description": "# CronSim\n\n[![Tests](https://github.com/cuu508/cronsim/actions/workflows/pytest.yml/badge.svg)](https://github.com/cuu508/cronsim/actions/workflows/pytest.yml)\n\nCron Sim(ulator), a cron expression parser and evaluator. Works with Python 3.8+.\nCronSim is written for and being used in\n[Healthchecks](https://github.com/healthchecks/healthchecks/)\n(a cron job monitoring service).\n\nDevelopment priorities:\n\n* Correctness. CronSim tries to match Debian's cron as closely as possible,\n including its quirky behaviour during DST transitions.\n* Readability. Prefer simple over clever.\n* Minimalism. Don't implement features that Healthchecks will not use\n (for example, the seconds field in cron expressions).\n\n## Installation\n\n```\npip install cronsim\n```\n\n## Usage\n\n```python\nfrom datetime import datetime\nfrom cronsim import CronSim\n\nit = CronSim(\"0 0 * 2 MON#5\", datetime(2020, 1, 1))\nfor x in range(0, 5):\n print(next(it))\n```\n\nProduces:\n\n```\n2044-02-29 00:00:00\n2072-02-29 00:00:00\n2112-02-29 00:00:00\n2140-02-29 00:00:00\n2168-02-29 00:00:00\n```\n\nTo iterate backwards in time, add `reverse=True` in the constructor:\n\n```python\nfrom datetime import datetime\nfrom cronsim import CronSim\n\nit = CronSim(\"0 0 * 2 MON#5\", datetime(2020, 1, 1), reverse=True)\nprint(next(it))\n```\n\nProduces:\n\n```\n2016-02-29 00:00:00\n```\n\nIf CronSim receives an invalid cron expression, it raises `cronsim.CronSimError`:\n\n```python\nfrom datetime import datetime\nfrom cronsim import CronSim\n\nCronSim(\"123 * * * *\", datetime(2020, 1, 1))\n```\n\nProduces:\n\n```\ncronsim.cronsim.CronSimError: Bad minute\n```\n\nIf CronSim cannot find a matching datetime in the next 50 years from the start\ndate or from the previous match, it stops iteration by raising `StopIteration`:\n\n```python\nfrom datetime import datetime\nfrom cronsim import CronSim\n\n# Every minute of 1st and 21st of month,\n# if it is also the *last Monday* of the month:\nit = CronSim(\"* * */20 * 1L\", datetime(2020, 1, 1))\nprint(next(it))\n```\n\nProduces:\n\n```\nStopIteration\n```\n\n## CronSim Works With zoneinfo\n\nCronSim starting from version 2.0 is designed to work with timezones provided by\nthe zoneinfo module.\n\nA previous version, CronSim 1.0, was designed for pytz and relied on its\nfollowing non-standard features:\n\n* the non-standard `is_dst` flag in the `localize()` method\n* the `pytz.AmbiguousTimeError` and `pytz.NonExistentTimeError` exceptions\n* the `normalize()` method\n\n## Supported Cron Expression Features\n\nCronSim aims to match [Debian's cron implementation](https://salsa.debian.org/debian/cron/-/tree/master/)\n(which itself is based on Paul Vixie's cron, with modifications). If CronSim evaluates\nan expression differently from Debian's cron, that's a bug.\n\nCronSim is open to adding support for non-standard syntax features, as long as\nthey don't conflict or interfere with the standard syntax.\n\n## DST Transitions\n\nCronSim handles Daylight Saving Time transitions the same as\nDebian's cron. Debian has special handling for jobs with a granularity\ngreater than one hour:\n\n```\nLocal time changes of less than three hours, such as those caused by\nthe start or end of Daylight Saving Time, are handled specially. This\nonly applies to jobs that run at a specific time and jobs that are run\nwith a granularity greater than one hour. Jobs that run more fre-\nquently are scheduled normally.\n\nIf time has moved forward, those jobs that would have run in the inter-\nval that has been skipped will be run immediately. Conversely, if time\nhas moved backward, care is taken to avoid running jobs twice.\n```\n\nSee test cases in `test_cronsim.py`, `TestDstTransitions` class\nfor examples of this special handling.\n\n## Cron Expression Feature Matrix\n\n| Feature | Debian | Quartz | croniter | cronsim |\n| ------------------------------------ | :----: | :----: | :------: | :-----: |\n| Seconds in the 6th field | No | Yes | Yes | No |\n| \"L\" as the day-of-month | No | Yes | Yes | Yes |\n| \"LW\" as the day-of-month | No | Yes | No | Yes |\n| \"L\" in the day-of-week field | No | Yes | No | Yes |\n| Nth weekday of month | No | Yes | Yes | Yes |\n\n\n**Seconds in the 6th field**: an optional sixth field specifying seconds.\nSupports the same syntax features as the minutes field. Quartz Scheduler\nexpects seconds in the first field, croniter expects seconds in the last field.\n\nQuartz example: `*/15 * * * * *` (every 15 seconds).\n\n**\"L\" as the day-of-month**: support for the \"L\" special character in the\nday-of-month field. Interpreted as \"the last day of the month\".\n\nExample: `0 0 L * *` (at the midnight of the last day of every month).\n\n**\"LW\" as the day-of-month**: support for the \"LW\" special value in the\nday-of-month field. Interpreted as \"the last weekday (Mon-Fri) of the month\".\n\nExample: `0 0 LW * *` (at the midnight of the last weekday of every month).\n\n**\"L\" in the day-of-week field**: support for the \"{weekday}L\" syntax.\nFor example, \"5L\" means \"the last Friday of the month\".\n\nExample: `0 0 * * 6L` (at the midnight of the last Saturday of every month).\n\n**Nth weekday of month**: support for \"{weekday}#{nth}\" syntax.\nFor example, \"MON#1\" means \"first Monday of the month\", \"MON#2\" means \"second Monday\nof the month\".\n\nExample: `0 0 * * MON#1` (at midnight of the first monday of every month).\n\n## The `explain()` Method\n\nStarting from version 2.4, the CronSim objects have an `explain()` method\nwhich generates a text description of the supplied cron expression.\n\n```python\nfrom datetime import datetime\nfrom cronsim import CronSim\n\nexpr = CronSim(\"*/5 9-17 * * *\", datetime.now())\nprint(expr.explain())\n```\n\nOutputs:\n\n```\nEvery fifth minute from 09:00 through 17:59\n```\n\nThe text descriptions are available in English only. The text descriptions\nuse the 24-hour time format (\"23:00\" instead of \"11:00 PM\").\n\nFor examples of generated descriptions see `tests/test_explain.py`.\n",
"bugtrack_url": null,
"license": "BSD",
"summary": "Cron expression parser and evaluator",
"version": "2.6",
"project_urls": {
"Changelog": "https://github.com/cuu508/cronsim/blob/main/CHANGELOG.md",
"Homepage": "https://github.com/cuu508/cronsim"
},
"split_keywords": [
"cron",
" cronjob",
" crontab",
" schedule"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "5bd8cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b",
"md5": "25b48d931c19e6314e2b26ac81d2ef94",
"sha256": "5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d"
},
"downloads": -1,
"filename": "cronsim-2.6.tar.gz",
"has_sig": false,
"md5_digest": "25b48d931c19e6314e2b26ac81d2ef94",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 20315,
"upload_time": "2024-11-02T14:34:02",
"upload_time_iso_8601": "2024-11-02T14:34:02.475788Z",
"url": "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-11-02 14:34:02",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "cuu508",
"github_project": "cronsim",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "cronsim"
}