From f1acf4fa2f99567daf4b25e6f5065a5dd72a39f0 Mon Sep 17 00:00:00 2001 From: Jovial Joe Jayarson Date: Tue, 2 Apr 2024 12:33:16 +0530 Subject: [PATCH] feat: adds basic `cron` validator --- CHANGES.md | 21 ++++++++++ SECURITY.md | 2 +- docs/api/cron.md | 3 ++ docs/api/cron.rst | 5 +++ mkdocs.yaml | 1 + src/validators/__init__.py | 5 ++- src/validators/cron.py | 78 ++++++++++++++++++++++++++++++++++++++ tests/test_cron.py | 54 ++++++++++++++++++++++++++ 8 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 docs/api/cron.md create mode 100644 docs/api/cron.rst create mode 100644 src/validators/cron.py create mode 100644 tests/test_cron.py diff --git a/CHANGES.md b/CHANGES.md index 956f689f..63372a91 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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**_ @@ -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**_ diff --git a/SECURITY.md b/SECURITY.md index 930d0a8f..3eda6b54 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ---------- | ------------------ | -| `>=0.24.0` | :white_check_mark: | +| `>=0.25.0` | :white_check_mark: | ## Reporting a Vulnerability diff --git a/docs/api/cron.md b/docs/api/cron.md new file mode 100644 index 00000000..7523a393 --- /dev/null +++ b/docs/api/cron.md @@ -0,0 +1,3 @@ +# cron + +::: validators.cron.cron diff --git a/docs/api/cron.rst b/docs/api/cron.rst new file mode 100644 index 00000000..643fe7c3 --- /dev/null +++ b/docs/api/cron.rst @@ -0,0 +1,5 @@ +cron +---- + +.. module:: validators.cron +.. autofunction:: cron diff --git a/mkdocs.yaml b/mkdocs.yaml index 915054c7..895e5af0 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -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 diff --git a/src/validators/__init__.py b/src/validators/__init__.py index 31da3aa9..8a92047f 100644 --- a/src/validators/__init__.py +++ b/src/validators/__init__.py @@ -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 @@ -39,6 +40,8 @@ # ... "country_code", # ... + "cron", + # ... "domain", # ... "email", @@ -79,4 +82,4 @@ "validator", ) -__version__ = "0.24.0" +__version__ = "0.25.0" diff --git a/src/validators/cron.py b/src/validators/cron.py new file mode 100644 index 00000000..58976510 --- /dev/null +++ b/src/validators/cron.py @@ -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 diff --git a/tests/test_cron.py b/tests/test_cron.py new file mode 100644 index 00000000..e0b2a199 --- /dev/null +++ b/tests/test_cron.py @@ -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)