Skip to content

Commit e3d30f8

Browse files
authored
Merge pull request #348 from yozachar/workshop
feat: adds basic `cron` validator
2 parents 1f49c5f + f1acf4f commit e3d30f8

File tree

8 files changed

+167
-2
lines changed

8 files changed

+167
-2
lines changed

CHANGES.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ Note to self: Breaking changes must increment either
99
1010
-->
1111

12+
## 0.25.0 (2024-04-02)
13+
14+
_**Breaking**_
15+
16+
> No breaking changes were introduced in this version.
17+
18+
_**Features**_
19+
20+
- feat: adds basic `cron` validator by @yozachar in [#348](https://github.com/python-validators/validators/pull/348)
21+
22+
_**Maintenance**_
23+
24+
- maint: adds quick start docs by @yozachar in [#344](https://github.com/python-validators/validators/pull/344)
25+
- fix: `domain` validation is now more consistent across rfcs by @yozachar in [#347](https://github.com/python-validators/validators/pull/347)
26+
27+
**Full Changelog**: [`0.24.2...0.25.0`](https://github.com/python-validators/validators/compare/0.23.2...0.24.0)
28+
29+
---
30+
1231
## 0.24.0 (2024-03-24)
1332

1433
_**Breaking**_
@@ -27,6 +46,8 @@ _**Maintenance**_
2746

2847
**Full Changelog**: [`0.23.2...0.24.0`](https://github.com/python-validators/validators/compare/0.23.2...0.24.0)
2948

49+
---
50+
3051
## 0.23.2 (2024-03-20)
3152

3253
_**Breaking**_

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| Version | Supported |
66
| ---------- | ------------------ |
7-
| `>=0.24.0` | :white_check_mark: |
7+
| `>=0.25.0` | :white_check_mark: |
88

99
## Reporting a Vulnerability
1010

docs/api/cron.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# cron
2+
3+
::: validators.cron.cron

docs/api/cron.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cron
2+
----
3+
4+
.. module:: validators.cron
5+
.. autofunction:: cron

mkdocs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ nav:
7373
- api/btc_address.md
7474
- api/card.md
7575
- api/country_code.md
76+
- api/cron.md
7677
- api/domain.md
7778
- api/email.md
7879
- api/hashes.md

src/validators/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .btc_address import btc_address
66
from .card import amex, card_number, diners, discover, jcb, mastercard, unionpay, visa
77
from .country_code import country_code
8+
from .cron import cron
89
from .domain import domain
910
from .email import email
1011
from .hashes import md5, sha1, sha224, sha256, sha512
@@ -39,6 +40,8 @@
3940
# ...
4041
"country_code",
4142
# ...
43+
"cron",
44+
# ...
4245
"domain",
4346
# ...
4447
"email",
@@ -79,4 +82,4 @@
7982
"validator",
8083
)
8184

82-
__version__ = "0.24.0"
85+
__version__ = "0.25.0"

src/validators/cron.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Cron."""
2+
3+
# local
4+
from .utils import validator
5+
6+
7+
def _validate_cron_component(component: str, min_val: int, max_val: int):
8+
if component == "*":
9+
return True
10+
11+
if component.isdecimal():
12+
return min_val <= int(component) <= max_val
13+
14+
if "/" in component:
15+
parts = component.split("/")
16+
if len(parts) != 2 or not parts[1].isdecimal() or int(parts[1]) < 1:
17+
return False
18+
if parts[0] == "*":
19+
return True
20+
return parts[0].isdecimal() and min_val <= int(parts[0]) <= max_val
21+
22+
if "-" in component:
23+
parts = component.split("-")
24+
if len(parts) != 2 or not parts[0].isdecimal() or not parts[1].isdecimal():
25+
return False
26+
start, end = int(parts[0]), int(parts[1])
27+
return min_val <= start <= max_val and min_val <= end <= max_val and start <= end
28+
29+
if "," in component:
30+
for item in component.split(","):
31+
if not _validate_cron_component(item, min_val, max_val):
32+
return False
33+
return True
34+
# return all(
35+
# _validate_cron_component(item, min_val, max_val) for item in component.split(",")
36+
# ) # throws type error. why?
37+
38+
return False
39+
40+
41+
@validator
42+
def cron(value: str, /):
43+
"""Return whether or not given value is a valid cron string.
44+
45+
Examples:
46+
>>> cron('*/5 * * * *')
47+
# Output: True
48+
>>> cron('30-20 * * * *')
49+
# Output: ValidationError(func=cron, ...)
50+
51+
Args:
52+
value:
53+
Cron string to validate.
54+
55+
Returns:
56+
(Literal[True]): If `value` is a valid cron string.
57+
(ValidationError): If `value` is an invalid cron string.
58+
"""
59+
if not value:
60+
return False
61+
62+
try:
63+
minutes, hours, days, months, weekdays = value.strip().split()
64+
except ValueError as err:
65+
raise ValueError("Badly formatted cron string") from err
66+
67+
if not _validate_cron_component(minutes, 0, 59):
68+
return False
69+
if not _validate_cron_component(hours, 0, 23):
70+
return False
71+
if not _validate_cron_component(days, 1, 31):
72+
return False
73+
if not _validate_cron_component(months, 1, 12):
74+
return False
75+
if not _validate_cron_component(weekdays, 0, 6):
76+
return False
77+
78+
return True

tests/test_cron.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Test Cron."""
2+
3+
# external
4+
import pytest
5+
6+
# local
7+
from validators import ValidationError, cron
8+
9+
10+
@pytest.mark.parametrize(
11+
"value",
12+
[
13+
"* * * * *",
14+
"*/5 * * * *",
15+
"0 0 * * *",
16+
"30 3 * * 1-5",
17+
"15 5 * * 1,3,5",
18+
"0 12 1 */2 *",
19+
"0 */3 * * *",
20+
"0 0 1 1 *",
21+
"0 12 * 1-6 1-5",
22+
"0 3-6 * * *",
23+
"*/15 0,6,12,18 * * *",
24+
"0 12 * * 0",
25+
"*/61 * * * *",
26+
# "5-10/2 * * * *", # this is valid, but not supported yet
27+
],
28+
)
29+
def test_returns_true_on_valid_cron(value: str):
30+
"""Test returns true on valid cron string."""
31+
assert cron(value)
32+
33+
34+
@pytest.mark.parametrize(
35+
"value",
36+
[
37+
"* * * * * *",
38+
"* * * *",
39+
"*/5 25 * * *",
40+
"*/5 * *-1 * *",
41+
"32-30 * * * *",
42+
"0 12 32 * *",
43+
"0 12 * * 8",
44+
"0 */0 * * *",
45+
"30-20 * * * *",
46+
"10-* * * * *",
47+
"*/15 0,6,12,24 * * *",
48+
"& * * & * *",
49+
"* - * * - *",
50+
],
51+
)
52+
def test_returns_failed_validation_on_invalid_cron(value: str):
53+
"""Test returns failed validation on invalid cron string."""
54+
assert isinstance(cron(value), ValidationError)

0 commit comments

Comments
 (0)