Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds basic cron validator #348

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ Note to self: Breaking changes must increment either

-->

## 0.25.0 (2024-04-02)

_**Breaking**_

> No breaking changes were introduced in this version.

_**Features**_

- feat: adds basic `cron` validator by @yozachar in [#348](https://github.com/python-validators/validators/pull/348)

_**Maintenance**_

- maint: adds quick start docs by @yozachar in [#344](https://github.com/python-validators/validators/pull/344)
- fix: `domain` validation is now more consistent across rfcs by @yozachar in [#347](https://github.com/python-validators/validators/pull/347)

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

---

## 0.24.0 (2024-03-24)

_**Breaking**_
Expand All @@ -27,6 +46,8 @@ _**Maintenance**_

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

---

## 0.23.2 (2024-03-20)

_**Breaking**_
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Supported |
| ---------- | ------------------ |
| `>=0.24.0` | :white_check_mark: |
| `>=0.25.0` | :white_check_mark: |

## Reporting a Vulnerability

Expand Down
3 changes: 3 additions & 0 deletions docs/api/cron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# cron

::: validators.cron.cron
5 changes: 5 additions & 0 deletions docs/api/cron.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cron
----

.. module:: validators.cron
.. autofunction:: cron
1 change: 1 addition & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ nav:
- api/btc_address.md
- api/card.md
- api/country_code.md
- api/cron.md
- api/domain.md
- api/email.md
- api/hashes.md
Expand Down
5 changes: 4 additions & 1 deletion src/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .btc_address import btc_address
from .card import amex, card_number, diners, discover, jcb, mastercard, unionpay, visa
from .country_code import country_code
from .cron import cron
from .domain import domain
from .email import email
from .hashes import md5, sha1, sha224, sha256, sha512
Expand Down Expand Up @@ -39,6 +40,8 @@
# ...
"country_code",
# ...
"cron",
# ...
"domain",
# ...
"email",
Expand Down Expand Up @@ -79,4 +82,4 @@
"validator",
)

__version__ = "0.24.0"
__version__ = "0.25.0"
78 changes: 78 additions & 0 deletions src/validators/cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Cron."""

# local
from .utils import validator


def _validate_cron_component(component: str, min_val: int, max_val: int):
if component == "*":
return True

if component.isdecimal():
return min_val <= int(component) <= max_val

if "/" in component:
parts = component.split("/")
if len(parts) != 2 or not parts[1].isdecimal() or int(parts[1]) < 1:
return False
if parts[0] == "*":
return True
return parts[0].isdecimal() and min_val <= int(parts[0]) <= max_val

if "-" in component:
parts = component.split("-")
if len(parts) != 2 or not parts[0].isdecimal() or not parts[1].isdecimal():
return False
start, end = int(parts[0]), int(parts[1])
return min_val <= start <= max_val and min_val <= end <= max_val and start <= end

if "," in component:
for item in component.split(","):
if not _validate_cron_component(item, min_val, max_val):
return False
return True
# return all(
# _validate_cron_component(item, min_val, max_val) for item in component.split(",")
# ) # throws type error. why?

return False


@validator
def cron(value: str, /):
"""Return whether or not given value is a valid cron string.

Examples:
>>> cron('*/5 * * * *')
# Output: True
>>> cron('30-20 * * * *')
# Output: ValidationError(func=cron, ...)

Args:
value:
Cron string to validate.

Returns:
(Literal[True]): If `value` is a valid cron string.
(ValidationError): If `value` is an invalid cron string.
"""
if not value:
return False

try:
minutes, hours, days, months, weekdays = value.strip().split()
except ValueError as err:
raise ValueError("Badly formatted cron string") from err

if not _validate_cron_component(minutes, 0, 59):
return False
if not _validate_cron_component(hours, 0, 23):
return False
if not _validate_cron_component(days, 1, 31):
return False
if not _validate_cron_component(months, 1, 12):
return False
if not _validate_cron_component(weekdays, 0, 6):
return False

return True
54 changes: 54 additions & 0 deletions tests/test_cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Test Cron."""

# external
import pytest

# local
from validators import ValidationError, cron


@pytest.mark.parametrize(
"value",
[
"* * * * *",
"*/5 * * * *",
"0 0 * * *",
"30 3 * * 1-5",
"15 5 * * 1,3,5",
"0 12 1 */2 *",
"0 */3 * * *",
"0 0 1 1 *",
"0 12 * 1-6 1-5",
"0 3-6 * * *",
"*/15 0,6,12,18 * * *",
"0 12 * * 0",
"*/61 * * * *",
# "5-10/2 * * * *", # this is valid, but not supported yet
],
)
def test_returns_true_on_valid_cron(value: str):
"""Test returns true on valid cron string."""
assert cron(value)


@pytest.mark.parametrize(
"value",
[
"* * * * * *",
"* * * *",
"*/5 25 * * *",
"*/5 * *-1 * *",
"32-30 * * * *",
"0 12 32 * *",
"0 12 * * 8",
"0 */0 * * *",
"30-20 * * * *",
"10-* * * * *",
"*/15 0,6,12,24 * * *",
"& * * & * *",
"* - * * - *",
],
)
def test_returns_failed_validation_on_invalid_cron(value: str):
"""Test returns failed validation on invalid cron string."""
assert isinstance(cron(value), ValidationError)