quartz-cron-checker 0.1.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.
- quartz_cron_checker-0.1.0/LICENSE +21 -0
- quartz_cron_checker-0.1.0/PKG-INFO +57 -0
- quartz_cron_checker-0.1.0/README.md +47 -0
- quartz_cron_checker-0.1.0/pyproject.toml +19 -0
- quartz_cron_checker-0.1.0/setup.cfg +4 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/__init__.py +25 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/checker.py +271 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/constants.py +40 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/exceptions.py +107 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/field_configs.py +187 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/parsers.py +97 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker/validators.py +165 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker.egg-info/PKG-INFO +57 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker.egg-info/SOURCES.txt +15 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker.egg-info/dependency_links.txt +1 -0
- quartz_cron_checker-0.1.0/src/quartz_cron_checker.egg-info/top_level.txt +1 -0
- quartz_cron_checker-0.1.0/tests/test_cron_string_checker.py +271 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jessica Smith
|
|
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.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quartz-cron-checker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple Python library to check if a Quartz cron expression is valid.
|
|
5
|
+
Author-email: Jessica Smith <j.smith.git1@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# quartz-cron-checker
|
|
12
|
+
|
|
13
|
+
**quartz-cron-checker** is a lightweight, strict validator for [Quartz-style cron expressions](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) in Python.
|
|
14
|
+
|
|
15
|
+
It ensures all parts of the cron string conform to the Quartz cron spec—including support for special characters, named values, and mutual exclusivity of day-of-month and day-of-week.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- ✅ Full support for 6- or 7-field Quartz cron expressions (`second minute hour day-of-month month day-of-week [year]`)
|
|
20
|
+
- ✅ Literal handling (`*`, `?`, `L`, `W`, `#`)
|
|
21
|
+
- ✅ Named months (`JAN`, `FEB`, etc.) and days (`MON`, `TUE`, etc.)
|
|
22
|
+
- ✅ Detailed error messages and exceptions
|
|
23
|
+
- ✅ Clean object-oriented API
|
|
24
|
+
- ✅ Extensible field-level validation
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install quartz-cron-checker
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Calling `validate` will raise an exception if the cron string is invalid. The method will return `True` otherwise.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from quartz_cron_checker import QuartzCronChecker
|
|
38
|
+
|
|
39
|
+
# Parse and validate a cron string
|
|
40
|
+
cron = QuartzCronChecker.from_cron_string("0 0 12 ? * MON-FRI")
|
|
41
|
+
cron.validate() # Raises if invalid
|
|
42
|
+
|
|
43
|
+
print(str(cron)) # "0 0 12 ? * MON-FRI"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also validate either a whole cron string or individual fields without creating an object:
|
|
47
|
+
```python
|
|
48
|
+
from quartz_cron_checker import validate_cron_string, validate_year, validate_second
|
|
49
|
+
|
|
50
|
+
# Validate a full cron string
|
|
51
|
+
validate_cron_string("0 0 12 ? * MON-FRI") # Raises if invalid
|
|
52
|
+
|
|
53
|
+
# Validate individual fields
|
|
54
|
+
validate_second("0") # Raises if invalid
|
|
55
|
+
|
|
56
|
+
validate_year("2023") # Raises if invalid
|
|
57
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# quartz-cron-checker
|
|
2
|
+
|
|
3
|
+
**quartz-cron-checker** is a lightweight, strict validator for [Quartz-style cron expressions](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) in Python.
|
|
4
|
+
|
|
5
|
+
It ensures all parts of the cron string conform to the Quartz cron spec—including support for special characters, named values, and mutual exclusivity of day-of-month and day-of-week.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ✅ Full support for 6- or 7-field Quartz cron expressions (`second minute hour day-of-month month day-of-week [year]`)
|
|
10
|
+
- ✅ Literal handling (`*`, `?`, `L`, `W`, `#`)
|
|
11
|
+
- ✅ Named months (`JAN`, `FEB`, etc.) and days (`MON`, `TUE`, etc.)
|
|
12
|
+
- ✅ Detailed error messages and exceptions
|
|
13
|
+
- ✅ Clean object-oriented API
|
|
14
|
+
- ✅ Extensible field-level validation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install quartz-cron-checker
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Calling `validate` will raise an exception if the cron string is invalid. The method will return `True` otherwise.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from quartz_cron_checker import QuartzCronChecker
|
|
28
|
+
|
|
29
|
+
# Parse and validate a cron string
|
|
30
|
+
cron = QuartzCronChecker.from_cron_string("0 0 12 ? * MON-FRI")
|
|
31
|
+
cron.validate() # Raises if invalid
|
|
32
|
+
|
|
33
|
+
print(str(cron)) # "0 0 12 ? * MON-FRI"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You can also validate either a whole cron string or individual fields without creating an object:
|
|
37
|
+
```python
|
|
38
|
+
from quartz_cron_checker import validate_cron_string, validate_year, validate_second
|
|
39
|
+
|
|
40
|
+
# Validate a full cron string
|
|
41
|
+
validate_cron_string("0 0 12 ? * MON-FRI") # Raises if invalid
|
|
42
|
+
|
|
43
|
+
# Validate individual fields
|
|
44
|
+
validate_second("0") # Raises if invalid
|
|
45
|
+
|
|
46
|
+
validate_year("2023") # Raises if invalid
|
|
47
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quartz-cron-checker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Simple Python library to check if a Quartz cron expression is valid."
|
|
5
|
+
authors = [{ name = "Jessica Smith", email = "j.smith.git1@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[dependency-groups]
|
|
11
|
+
test = ["hypothesis>=6.131.30", "pytest>=8.3.5"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[tool.uv]
|
|
15
|
+
default-groups = ["test"]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["setuptools >= 80"]
|
|
19
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .checker import (
|
|
2
|
+
QuartzCronChecker,
|
|
3
|
+
validate_cron_string,
|
|
4
|
+
validate_day_of_month,
|
|
5
|
+
validate_day_of_month_or_week,
|
|
6
|
+
validate_day_of_week,
|
|
7
|
+
validate_hour,
|
|
8
|
+
validate_minute,
|
|
9
|
+
validate_month,
|
|
10
|
+
validate_second,
|
|
11
|
+
validate_year,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"QuartzCronChecker",
|
|
16
|
+
"validate_cron_string",
|
|
17
|
+
"validate_day_of_month",
|
|
18
|
+
"validate_day_of_month_or_week",
|
|
19
|
+
"validate_day_of_week",
|
|
20
|
+
"validate_hour",
|
|
21
|
+
"validate_minute",
|
|
22
|
+
"validate_month",
|
|
23
|
+
"validate_second",
|
|
24
|
+
"validate_year",
|
|
25
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from quartz_cron_checker.exceptions import InvalidCronStructureError
|
|
5
|
+
from quartz_cron_checker.validators import validate_day_of_month_or_week
|
|
6
|
+
|
|
7
|
+
from .field_configs import (
|
|
8
|
+
DAY_OF_MONTH_CONFIG,
|
|
9
|
+
DAY_OF_WEEK_CONFIG,
|
|
10
|
+
HOUR_CONFIG,
|
|
11
|
+
MINUTE_CONFIG,
|
|
12
|
+
MONTH_CONFIG,
|
|
13
|
+
SECOND_CONFIG,
|
|
14
|
+
YEAR_CONFIG,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CRON_TEMPLATE_NO_YEAR = "{second} {minute} {hour} {day_of_month} {month} {day_of_week}"
|
|
18
|
+
CRON_TEMPLATE = CRON_TEMPLATE_NO_YEAR + " {year}"
|
|
19
|
+
REQUIRED_PARTS = ["second", "minute", "hour", "day_of_month", "month", "day_of_week"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class QuartzCronChecker:
|
|
24
|
+
second: str
|
|
25
|
+
minute: str
|
|
26
|
+
hour: str
|
|
27
|
+
day_of_month: str
|
|
28
|
+
month: str
|
|
29
|
+
day_of_week: str
|
|
30
|
+
year: str | None = None
|
|
31
|
+
cron_string: str | None = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
self.second = self.second.replace(" ", "")
|
|
35
|
+
self.minute = self.minute.replace(" ", "")
|
|
36
|
+
self.hour = self.hour.replace(" ", "")
|
|
37
|
+
self.day_of_month = self.day_of_month.replace(" ", "")
|
|
38
|
+
self.month = self.month.replace(" ", "")
|
|
39
|
+
self.day_of_week = self.day_of_week.replace(" ", "")
|
|
40
|
+
self.year = self.year.replace(" ", "") if self.year else None
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"<QuartzCronChecker {self!s}>"
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
parts = {
|
|
47
|
+
"second": self.second,
|
|
48
|
+
"minute": self.minute,
|
|
49
|
+
"hour": self.hour,
|
|
50
|
+
"day_of_month": self.day_of_month,
|
|
51
|
+
"month": self.month,
|
|
52
|
+
"day_of_week": self.day_of_week,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if self.year is not None:
|
|
56
|
+
parts["year"] = self.year
|
|
57
|
+
return CRON_TEMPLATE.format(**parts)
|
|
58
|
+
|
|
59
|
+
return CRON_TEMPLATE_NO_YEAR.format(**parts)
|
|
60
|
+
|
|
61
|
+
def validate(self) -> Literal[True]:
|
|
62
|
+
"""Validate the cron string.
|
|
63
|
+
|
|
64
|
+
This method performs the following checks:
|
|
65
|
+
1. Ensuring required parts are present.
|
|
66
|
+
2. Checking mutual exclusivity of day_of_month and day_of_week.
|
|
67
|
+
3. Validating each part using its corresponding field config.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
InvalidCronStructureError: If the cron string is missing required parts or has invalid structure.
|
|
71
|
+
InvalidCronPartError: If any part of the cron string is invalid.
|
|
72
|
+
ValueOutOfBoundsError: If any part of the cron string is out of bounds.
|
|
73
|
+
IncrementOutOfBoundsError: If an increment value is out of bounds.
|
|
74
|
+
RangeOutOfBoundsError: If a range is out of bounds.
|
|
75
|
+
RangeIncrementOutOfBoundsError: If a range with increment is out of bounds.
|
|
76
|
+
SpecificsOutOfBoundsError: If specific values are out of bounds.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Literal[True]: Returns True if the cron string is valid.
|
|
80
|
+
"""
|
|
81
|
+
if not all(getattr(self, part) for part in REQUIRED_PARTS):
|
|
82
|
+
missing_parts = [part for part in REQUIRED_PARTS if not getattr(self, part)]
|
|
83
|
+
raise InvalidCronStructureError(f"Missing required parts in cron string: {', '.join(missing_parts)}")
|
|
84
|
+
|
|
85
|
+
validate_day_of_month_or_week(self.day_of_month, self.day_of_week)
|
|
86
|
+
|
|
87
|
+
for config, value in [
|
|
88
|
+
(SECOND_CONFIG, self.second),
|
|
89
|
+
(MINUTE_CONFIG, self.minute),
|
|
90
|
+
(HOUR_CONFIG, self.hour),
|
|
91
|
+
(DAY_OF_MONTH_CONFIG, self.day_of_month),
|
|
92
|
+
(MONTH_CONFIG, self.month),
|
|
93
|
+
(DAY_OF_WEEK_CONFIG, self.day_of_week),
|
|
94
|
+
]:
|
|
95
|
+
config.validate(value)
|
|
96
|
+
|
|
97
|
+
if self.year:
|
|
98
|
+
YEAR_CONFIG.validate(self.year)
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_cron_string(cls, cron_str: str) -> "QuartzCronChecker":
|
|
104
|
+
"""Convert a cron string to a QuartzCronChecker object.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
cron_str (str): The cron string to convert.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
QuartzCronChecker: A QuartzCronChecker object with the converted values.
|
|
111
|
+
"""
|
|
112
|
+
cron_parts = cron_str.strip().split(" ")
|
|
113
|
+
if len(cron_parts) == 6:
|
|
114
|
+
# this slightly weird looking syntax makes the type checker happy
|
|
115
|
+
# we add a None for the year part, since it's not present in 6-part cron strings
|
|
116
|
+
return cls(*[*cron_parts, None], cron_string=cron_str)
|
|
117
|
+
if len(cron_parts) == 7:
|
|
118
|
+
return cls(*cron_parts, cron_string=cron_str)
|
|
119
|
+
|
|
120
|
+
raise InvalidCronStructureError(
|
|
121
|
+
f"Invalid cron string: {cron_str}. Expected 6 or 7 parts, got {len(cron_parts)}."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def validate_cron_string(cron_str: str) -> Literal[True]:
|
|
126
|
+
"""Validate a cron string.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
cron_str (str): The cron string to validate.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Literal[True]: Returns True if the cron string is valid.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
InvalidCronStructureError: If the cron string is invalid.
|
|
136
|
+
InvalidCronPartError: If any part of the cron string is invalid.
|
|
137
|
+
ValueOutOfBoundsError: If any part of the cron string is out of bounds.
|
|
138
|
+
IncrementOutOfBoundsError: If an increment value is out of bounds.
|
|
139
|
+
RangeOutOfBoundsError: If a range is out of bounds.
|
|
140
|
+
RangeIncrementOutOfBoundsError: If a range with increment is out of bounds.
|
|
141
|
+
SpecificsOutOfBoundsError: If specific values are out of bounds.
|
|
142
|
+
"""
|
|
143
|
+
cron = QuartzCronChecker.from_cron_string(cron_str)
|
|
144
|
+
cron.validate()
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def validate_second(part: str) -> Literal[True]:
|
|
149
|
+
"""Validate the 'second' part of a cron string.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
part (str): The 'second' part of the cron string to validate.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Literal[True]: Returns True if validation is successful.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
InvalidCronPartError: If the 'second' part is invalid - exact error depends on the specific validation failure.
|
|
159
|
+
"""
|
|
160
|
+
return SECOND_CONFIG.validate(part)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_minute(part: str) -> Literal[True]:
|
|
164
|
+
"""Validate the 'minute' part of a cron string.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
part (str): The 'minute' part of the cron string to validate.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Literal[True]: Returns True if validation is successful.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
174
|
+
"""
|
|
175
|
+
return MINUTE_CONFIG.validate(part)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_hour(part: str) -> Literal[True]:
|
|
179
|
+
"""Validate the 'hour' part of a cron string.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
part (str): The 'hour' part of the cron string to validate.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Literal[True]: Returns True if validation is successful.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
189
|
+
"""
|
|
190
|
+
return HOUR_CONFIG.validate(part)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def validate_day_of_month(part: str) -> Literal[True]:
|
|
194
|
+
"""Validate the 'day_of_month' part of a cron string.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
part (str): The 'day_of_month' part of the cron string to validate.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Literal[True]: Returns True if validation is successful.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
204
|
+
"""
|
|
205
|
+
return DAY_OF_MONTH_CONFIG.validate(part)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def validate_month(part: str) -> Literal[True]:
|
|
209
|
+
"""Validate the 'month' part of a cron string.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
part (str): The 'month' part of the cron string to validate.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Literal[True]: Returns True if validation is successful.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
219
|
+
"""
|
|
220
|
+
return MONTH_CONFIG.validate(part)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def validate_day_of_week(part: str) -> Literal[True]:
|
|
224
|
+
"""Validate the 'day_of_week' part of a cron string.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
part (str): The 'day_of_week' part of the cron string to validate.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Literal[True]: Returns True if validation is successful.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
234
|
+
"""
|
|
235
|
+
return DAY_OF_WEEK_CONFIG.validate(part)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def validate_year(part: str) -> Literal[True]:
|
|
239
|
+
"""Validate the 'year' part of a cron string.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
part (str): The 'year' part of the cron string to validate.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Literal[True]: Returns True if validation is successful.
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
InvalidCronPartError: If the part is invalid - exact error depends on the specific validation failure.
|
|
249
|
+
"""
|
|
250
|
+
return YEAR_CONFIG.validate(part)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def validate_cron_string(cron_str: str) -> Literal[True]:
|
|
254
|
+
"""Validate a cron string.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
cron_str (str): The cron string to validate.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Literal[True]: Returns True if the cron string is valid.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
InvalidCronStructureError: If the cron string is invalid.
|
|
264
|
+
InvalidCronPartError: If any part of the cron string is invalid.
|
|
265
|
+
ValueOutOfBoundsError: If any part of the cron string is out of bounds.
|
|
266
|
+
IncrementOutOfBoundsError: If an increment value is out of bounds.
|
|
267
|
+
RangeOutOfBoundsError: If a range is out of bounds.
|
|
268
|
+
RangeIncrementOutOfBoundsError: If a range with increment is out of bounds.
|
|
269
|
+
SpecificsOutOfBoundsError: If specific values are out of bounds.
|
|
270
|
+
"""
|
|
271
|
+
return QuartzCronChecker.validate_cron_string(cron_str)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
SINGLE_LITERAL_PATTERN = re.compile(r"^\d+$")
|
|
5
|
+
WILDCARD_PATTERN = re.compile(r"^\*$")
|
|
6
|
+
INCREMENT_PATTERN = re.compile(r"^(\*|\d+)(/\d+)$")
|
|
7
|
+
RANGE_PATTERN = re.compile(r"^\d+-\d+$")
|
|
8
|
+
RANGE_WITH_INCREMENT_PATTERN = re.compile(r"^\d+-\d+/\d+$")
|
|
9
|
+
LIST_OF_DIGITS_PATTERN = re.compile(r"^\d+(,\d+)+$")
|
|
10
|
+
LIST_OF_STRINGS_PATTERN = re.compile(r"^[A-Z]{3}(,[A-Z]{3})+$", re.IGNORECASE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# DOW patterns
|
|
14
|
+
DOW_LIST = list(map(str.upper, calendar.day_abbr))
|
|
15
|
+
DOW_OR_PATTERN = f"({'|'.join(DOW_LIST)})"
|
|
16
|
+
|
|
17
|
+
DOW_OCCURRENCE_PATTERN = re.compile(r"\d+#\d+")
|
|
18
|
+
LAST_DOW_IN_MONTH_PATTERN = re.compile(r"\d+L", re.IGNORECASE)
|
|
19
|
+
DOW_RANGE_PATTERN = re.compile(f"^{DOW_OR_PATTERN}-{DOW_OR_PATTERN}$", re.IGNORECASE)
|
|
20
|
+
SPECIFIC_DOW_PATTERN = re.compile(f"^{DOW_OR_PATTERN}(,{DOW_OR_PATTERN})*$", re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
# Month patterns
|
|
23
|
+
MONTH_LIST = list(map(str.upper, calendar.month_abbr))[1:]
|
|
24
|
+
|
|
25
|
+
MONTH_OR_PATTERN = f"({'|'.join(MONTH_LIST)})"
|
|
26
|
+
MONTH_RANGE_PATTERN = re.compile(f"^{MONTH_OR_PATTERN}-{MONTH_OR_PATTERN}$", re.IGNORECASE)
|
|
27
|
+
SPECIFIC_MONTH_PATTERN = re.compile(f"^{MONTH_OR_PATTERN}(,{MONTH_OR_PATTERN})*$", re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
# Day of month patterns
|
|
30
|
+
LAST_DAY_OF_MONTH = "L"
|
|
31
|
+
LAST_WEEKDAY_OF_MONTH = "LW"
|
|
32
|
+
LAST_N_DAYS_OF_MONTH_PATTERN = re.compile(r"^L-\d+$", re.IGNORECASE)
|
|
33
|
+
NEAREST_WEEKDAY_DAY_OF_MONTH_PATTERN = re.compile(r"^\d+W$", re.IGNORECASE)
|
|
34
|
+
|
|
35
|
+
DEFAULT_NUMERIC_PATTERNS = (
|
|
36
|
+
RANGE_PATTERN, # e.g. 1-31
|
|
37
|
+
INCREMENT_PATTERN, # e.g. 1/2
|
|
38
|
+
RANGE_WITH_INCREMENT_PATTERN, # e.g. 1-31/2
|
|
39
|
+
LIST_OF_DIGITS_PATTERN, # e.g. 1,2,3
|
|
40
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CronValidationError(ValueError):
|
|
6
|
+
"""Base class for all cron validation errors."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidCronStructureError(CronValidationError):
|
|
10
|
+
"""Raised when the overall structure of a cron string is invalid.
|
|
11
|
+
|
|
12
|
+
This includes cases like:
|
|
13
|
+
- wrong number of parts
|
|
14
|
+
- mutually exclusive fields both set to values
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidCronPartError(CronValidationError):
|
|
22
|
+
part_name: str
|
|
23
|
+
part_value: str | None
|
|
24
|
+
|
|
25
|
+
def __init__(self, part_name: str, part_value: str | None, message: str) -> None:
|
|
26
|
+
self.part_name = part_name
|
|
27
|
+
self.part_value = part_value
|
|
28
|
+
super().__init__(f"[{part_name}={part_value}] {message}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValueOutOfBoundsError(InvalidCronPartError):
|
|
32
|
+
def __init__(self, part_name: str, part_value: str, min_value: int, max_value: int) -> None:
|
|
33
|
+
msg = f"Value {part_value} is out of bounds ({min_value}-{max_value})"
|
|
34
|
+
super().__init__(part_name, part_value, msg)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IncrementOutOfBoundsError(InvalidCronPartError):
|
|
38
|
+
def __init__(self, part_name: str, part_value: str, increment_max: int) -> None:
|
|
39
|
+
msg = f"Increment is out of bounds (1-{increment_max})"
|
|
40
|
+
super().__init__(part_name, part_value, msg)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RangeOutOfBoundsError(InvalidCronPartError):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
part_name: str,
|
|
47
|
+
part_value: str,
|
|
48
|
+
start: int,
|
|
49
|
+
end: int,
|
|
50
|
+
min_value: int,
|
|
51
|
+
max_value: int,
|
|
52
|
+
) -> None:
|
|
53
|
+
msg = f"Range {start}-{end} is out of bounds ({min_value}-{max_value})"
|
|
54
|
+
super().__init__(part_name, part_value, msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RangeIncrementOutOfBoundsError(InvalidCronPartError):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
part_name: str,
|
|
61
|
+
part_value: str,
|
|
62
|
+
start: int,
|
|
63
|
+
end: int,
|
|
64
|
+
increment: int,
|
|
65
|
+
min_value: int,
|
|
66
|
+
max_value: int,
|
|
67
|
+
increment_max: int,
|
|
68
|
+
) -> None:
|
|
69
|
+
msg = (
|
|
70
|
+
f"Range with increment {start}-{end}/{increment} is out of bounds "
|
|
71
|
+
f"({min_value}-{max_value}, 1-{increment_max})"
|
|
72
|
+
)
|
|
73
|
+
super().__init__(part_name, part_value, msg)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SpecificsOutOfBoundsError(InvalidCronPartError):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
part_name: str,
|
|
80
|
+
part_value: str,
|
|
81
|
+
values: Sequence[str],
|
|
82
|
+
min_value: int,
|
|
83
|
+
max_value: int,
|
|
84
|
+
) -> None:
|
|
85
|
+
joined = ", ".join(values)
|
|
86
|
+
msg = f"Specific values {joined} are out of bounds ({min_value}-{max_value})"
|
|
87
|
+
super().__init__(part_name, part_value, msg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PatternOrLiteralMatchError(InvalidCronPartError):
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
part_name: str,
|
|
94
|
+
part_value: str,
|
|
95
|
+
patterns: Sequence[re.Pattern],
|
|
96
|
+
literals: set[str],
|
|
97
|
+
) -> None:
|
|
98
|
+
pattern_str = ", ".join(p.pattern for p in patterns)
|
|
99
|
+
literal_str = ", ".join(literals)
|
|
100
|
+
msg = f"Part '{part_value}' does not match any allowed pattern ({pattern_str}) or literal ({literal_str})"
|
|
101
|
+
super().__init__(part_name, part_value, msg)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PartCannotBeNoneError(InvalidCronPartError):
|
|
105
|
+
def __init__(self, part_name: str) -> None:
|
|
106
|
+
msg = "Part cannot be None"
|
|
107
|
+
super().__init__(part_name, None, msg)
|