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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)