enforce-rules 1.0.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,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Loepker-James
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: enforce-rules
3
+ Version: 1.0.0
4
+ Summary: Runtime rule validation for Python using dictionary-based constraints.
5
+ Author: Jake Loepker, Microsoft Copilot (AI Assistant)
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2026, Loepker-James
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ Project-URL: Homepage, https://github.com/Loepker-James/enforce-rules
36
+ Project-URL: Issues, https://github.com/Loepker-James/enforce-rules/issues
37
+ Project-URL: Copilot, https://copilot.microsoft.com
38
+ Classifier: Development Status :: 5 - Production/Stable
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: License :: OSI Approved :: BSD License
41
+ Classifier: Programming Language :: Python
42
+ Classifier: Programming Language :: Python :: 3
43
+ Classifier: Programming Language :: Python :: 3.10
44
+ Classifier: Programming Language :: Python :: 3.11
45
+ Classifier: Programming Language :: Python :: 3.12
46
+ Classifier: Programming Language :: Python :: 3.13
47
+ Classifier: Topic :: Software Development
48
+ Classifier: Topic :: Software Development :: Libraries
49
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
50
+ Classifier: Topic :: Utilities
51
+ Classifier: Typing :: Typed
52
+ Requires-Python: >=3.10
53
+ Description-Content-Type: text/markdown
54
+ License-File: LICENSE
55
+ Dynamic: license-file
56
+
57
+ Project Enforce Rules
58
+ Project Enforce Rules (or PER for short) expands the Python type system to allow more constraints.
59
+
60
+ Version #
61
+ ----------------
62
+ MAJOR: 1
63
+ MINOR: 0
64
+ PATCH: 0
65
+
66
+ If you need to catch up, you can see the full version history in the [CHANGELOG](CHANGELOG.md).
67
+
68
+ It exists to let you use features Python doesn't already provide in the typing system — things you probably want, like min, max, length, all_same, and many more.
69
+
70
+ PER now supports runtime validation anywhere using:
71
+
72
+ ```python
73
+ validate(value: object, rules: Dict[str, typing.Any])
74
+ ```
75
+
76
+ This enforces rule dictionaries and returns the original value if valid.
77
+ If invalid, it raises a descriptive error.
78
+
79
+ Why I Made It
80
+ I wanted type‑hint features Python didn’t give me. I thought dictionaries would work until I learned... well...
81
+ Python didn’t enforce them.
82
+
83
+ So with the help of Microsoft Copilot and my dad, we designed a module that enforces this type of stuff.
84
+ Then I realized everyone in the Python community could use this, so it became a project. Hence, creating PER.
85
+
86
+ PER uses rule dictionaries and validate() to enforce constraints at runtime.
87
+
88
+ Install it with pip:
89
+
90
+ ```bash
91
+ pip install enforce-rules
92
+ ```
93
+ Then use it like:
94
+
95
+ ```python
96
+ from enforce_rules import validate
97
+ ```
98
+ Features
99
+ 1. Runtime enforcement of rule dictionaries
100
+ 2. Dictionary‑based rule definitions
101
+ 3. No extra objects required
102
+ 4. Works anywhere in your code
103
+ 5. Extensible via must_be_true
104
+
105
+ How It Works
106
+ PER validates values using:
107
+
108
+ ```python
109
+ validate(value, rules)
110
+ ```
111
+ If the value violates a rule, PER raises an error.
112
+ If the value passes, PER returns the original value unchanged.
113
+
114
+ This means validated values behave exactly like normal Python values.
115
+
116
+ Keywords and Usage
117
+ Below are all supported keywords.
118
+
119
+ length
120
+ The length of the object must be exactly this.
121
+
122
+ ```python
123
+ lst = validate([1, 2, 3, 4, 5], {"length": 5})
124
+ ```
125
+
126
+ min_length
127
+ Minimum length (inclusive).
128
+
129
+ ```python
130
+ lst = validate(['a', 'b', 'c', 'd', 'e'], {"min_length": 3})
131
+ ```
132
+ max_length
133
+ Maximum length (inclusive).
134
+
135
+ ```python
136
+ lst = validate([1, 2, 3, 4, 5, 6], {"max_length": 7})
137
+ ```
138
+ min
139
+ Minimum numeric value (inclusive).
140
+
141
+ ```python
142
+ number = validate(10, {"min": 0})
143
+ ```
144
+ max
145
+ Maximum numeric value (inclusive).
146
+
147
+ ```python
148
+ number = validate(10, {"max": 20})
149
+ ```
150
+ allowed_values
151
+ Similar to Literal; value must be one of the allowed values.
152
+
153
+ ```python
154
+ val = validate("a", {"allowed_values": ("a", "b", "c", "d")})
155
+ ```
156
+ invariant
157
+ Value must be truthy.
158
+
159
+ ```python
160
+ val = validate((0 == 0), {"invariant": True})
161
+ ```
162
+ all_same
163
+ All values in the collection must be the same.
164
+
165
+ ```python
166
+ numbers = validate([1, 1, 1], {"all_same": True})
167
+ ```
168
+ all_unique
169
+ All values in the collection must be unique.
170
+
171
+ ```python
172
+ numbers = validate([1, 2, 3], {"all_unique": True})
173
+ ```
174
+ non_empty
175
+ Collection must not be empty.
176
+
177
+ ```python
178
+ my_strings = validate(['a', 'b', 'c'], {"non_empty": True})
179
+ ```
180
+ no_nulls
181
+ Collection must not contain None.
182
+
183
+ ```python
184
+ my_things = validate([1, 2, 3, "a", "b", "c"], {"no_nulls": True})
185
+ ```
186
+
187
+ sorted
188
+ List must be sorted (increasing or decreasing).
189
+
190
+ ```python
191
+ numbers = validate([1, 5, 9], {"sorted": True})
192
+ ```
193
+
194
+ increasing
195
+ List must be strictly increasing.
196
+
197
+ ```python
198
+ numbers = validate([1, 5, 9], {"increasing": True})
199
+ ```
200
+ decreasing
201
+ List must be strictly decreasing.
202
+
203
+ ```python
204
+ numbers = validate([9, 5, 1], {"decreasing": True})
205
+ ```
206
+ sum_min
207
+ Minimum sum of the collection (inclusive).
208
+
209
+ ```python
210
+ numbers = validate([10, 20, 30], {"sum_min": 50})
211
+ ```
212
+ sum_max
213
+ Maximum sum of the collection (inclusive).
214
+
215
+ ```python
216
+ numbers = validate([10, 20, 30], {"sum_max": 70})
217
+ ```
218
+ element_min
219
+ Minimum value for any element (inclusive).
220
+
221
+ ```python
222
+ numbers = validate([10, 20, 30], {"element_min": 5})
223
+ ```
224
+ element_max
225
+ Maximum value for any element (inclusive).
226
+
227
+ ```python
228
+ numbers = validate([10, 20, 30], {"element_max": 40})
229
+ ```
230
+ regex
231
+ String must match the regex.
232
+
233
+ ```python
234
+ cat_or_dog = validate("cat", {"regex": "cat|dog"})
235
+ ```
236
+
237
+
238
+ must_be_true
239
+ Custom rule: a function that returns True for allowed values.
240
+
241
+ ```python
242
+ def is_even(x: int) -> bool:
243
+ return x % 2 == 0
244
+ even_number = validate(8, {"must_be_true": is_even})
245
+ ```
246
+ This calls:
247
+
248
+ ```python
249
+ is_even(8)
250
+ ```
251
+ If enough people use a must_be_true lambda, it may become an added keyword in a later version
252
+
253
+ Contributing
254
+ Contributions are welcome. Please open an issue or pull request.
255
+ When contributing, ensure backwards compatibility (you cannot remove keywords and/or features).
256
+
257
+ Please note, when using my module, that you will manually have to validate each time should you choose to mutate a variable.
258
+
259
+
260
+ Versioning Policy
261
+ -----------------
262
+
263
+ This project guarantees full backwards compatibility. Existing rule files, keyword meanings, validator behaviors, and metadata formats will continue to work exactly as before. No update will ever break existing configurations.
264
+
265
+ Version Numbering
266
+ -----------------
267
+ This project uses a non-breaking semantic versioning model:
268
+
269
+ MAJOR.MINOR.PATCH
270
+
271
+ MAJOR = large new feature families
272
+ MINOR = small additive keywords or enhancements
273
+ PATCH = bug fixes or internal improvements
274
+
275
+ Major bumps do not imply breaking changes. They only indicate that a significant new capability has been added.
276
+
277
+ Major bumps always reset MINOR and PATCH to 0. For example:
278
+ 1.7.3 -> 2.0.0
279
+ 1.12.0 -> 2.0.0
280
+ 1.0.0 -> 2.0.0
281
+
282
+ Minor bumps always reset PATCH to 0. For example:
283
+ 1.7.3 -> 1.8.0
284
+ 1.12.9 -> 1.13.0
285
+ 1.0.4 -> 1.1.0
286
+
287
+ Minor Version Bumps
288
+ -------------------
289
+ Minor bumps occur when adding small keywords. Examples include:
290
+
291
+ min_inclusive
292
+ max_inclusive
293
+ trim_whitespace
294
+ pattern_flags
295
+
296
+ These additions do not change the meaning of existing keywords, do not require users to modify rule files, and do not alter validator behavior. They are classified as minor updates.
297
+
298
+ Minor bumps always reset PATCH to 0
299
+
300
+ Major Version Bumps
301
+ -------------------
302
+ Major bumps occur only when adding large new keyword families or entire new capability domains. Examples include:
303
+
304
+ a full date-validation system
305
+ a full numeric-range system
306
+ a full conditional-rules system
307
+ a full schema-level rule system
308
+
309
+ These are major features, even though they remain fully backwards-compatible. Major bumps communicate that the release adds a significant new capability.
310
+
311
+ Major bumps always reset MINOR and PATCH to 0.
312
+
313
+ Patch Version Bumps
314
+ -------------------
315
+ Patch bumps occur when fixing bugs, improving internal logic, optimizing performance, correcting documentation, or adjusting error messages without changing meaning. Patch updates never add new keywords.
316
+
317
+ Summary
318
+ -------
319
+ MAJOR: large new feature families, backwards-compatible, resets MINOR and PATCH to 0
320
+ MINOR: small additive keywords, backwards-compatible
321
+ PATCH: fixes only, backwards-compatible
322
+
323
+
324
+
325
+ Credits
326
+ Microsoft Copilot — for helping me code it. It did a TON of the coding, and to be honest, I couldn't have made this project without it. It did ~~some~~ a lot of readme, in addition to it writing most of CONTRIBUTING.md
327
+ Dad — for helping me along the journey.
328
+
329
+ License Type: BSD 3-clause
@@ -0,0 +1,273 @@
1
+ Project Enforce Rules
2
+ Project Enforce Rules (or PER for short) expands the Python type system to allow more constraints.
3
+
4
+ Version #
5
+ ----------------
6
+ MAJOR: 1
7
+ MINOR: 0
8
+ PATCH: 0
9
+
10
+ If you need to catch up, you can see the full version history in the [CHANGELOG](CHANGELOG.md).
11
+
12
+ It exists to let you use features Python doesn't already provide in the typing system — things you probably want, like min, max, length, all_same, and many more.
13
+
14
+ PER now supports runtime validation anywhere using:
15
+
16
+ ```python
17
+ validate(value: object, rules: Dict[str, typing.Any])
18
+ ```
19
+
20
+ This enforces rule dictionaries and returns the original value if valid.
21
+ If invalid, it raises a descriptive error.
22
+
23
+ Why I Made It
24
+ I wanted type‑hint features Python didn’t give me. I thought dictionaries would work until I learned... well...
25
+ Python didn’t enforce them.
26
+
27
+ So with the help of Microsoft Copilot and my dad, we designed a module that enforces this type of stuff.
28
+ Then I realized everyone in the Python community could use this, so it became a project. Hence, creating PER.
29
+
30
+ PER uses rule dictionaries and validate() to enforce constraints at runtime.
31
+
32
+ Install it with pip:
33
+
34
+ ```bash
35
+ pip install enforce-rules
36
+ ```
37
+ Then use it like:
38
+
39
+ ```python
40
+ from enforce_rules import validate
41
+ ```
42
+ Features
43
+ 1. Runtime enforcement of rule dictionaries
44
+ 2. Dictionary‑based rule definitions
45
+ 3. No extra objects required
46
+ 4. Works anywhere in your code
47
+ 5. Extensible via must_be_true
48
+
49
+ How It Works
50
+ PER validates values using:
51
+
52
+ ```python
53
+ validate(value, rules)
54
+ ```
55
+ If the value violates a rule, PER raises an error.
56
+ If the value passes, PER returns the original value unchanged.
57
+
58
+ This means validated values behave exactly like normal Python values.
59
+
60
+ Keywords and Usage
61
+ Below are all supported keywords.
62
+
63
+ length
64
+ The length of the object must be exactly this.
65
+
66
+ ```python
67
+ lst = validate([1, 2, 3, 4, 5], {"length": 5})
68
+ ```
69
+
70
+ min_length
71
+ Minimum length (inclusive).
72
+
73
+ ```python
74
+ lst = validate(['a', 'b', 'c', 'd', 'e'], {"min_length": 3})
75
+ ```
76
+ max_length
77
+ Maximum length (inclusive).
78
+
79
+ ```python
80
+ lst = validate([1, 2, 3, 4, 5, 6], {"max_length": 7})
81
+ ```
82
+ min
83
+ Minimum numeric value (inclusive).
84
+
85
+ ```python
86
+ number = validate(10, {"min": 0})
87
+ ```
88
+ max
89
+ Maximum numeric value (inclusive).
90
+
91
+ ```python
92
+ number = validate(10, {"max": 20})
93
+ ```
94
+ allowed_values
95
+ Similar to Literal; value must be one of the allowed values.
96
+
97
+ ```python
98
+ val = validate("a", {"allowed_values": ("a", "b", "c", "d")})
99
+ ```
100
+ invariant
101
+ Value must be truthy.
102
+
103
+ ```python
104
+ val = validate((0 == 0), {"invariant": True})
105
+ ```
106
+ all_same
107
+ All values in the collection must be the same.
108
+
109
+ ```python
110
+ numbers = validate([1, 1, 1], {"all_same": True})
111
+ ```
112
+ all_unique
113
+ All values in the collection must be unique.
114
+
115
+ ```python
116
+ numbers = validate([1, 2, 3], {"all_unique": True})
117
+ ```
118
+ non_empty
119
+ Collection must not be empty.
120
+
121
+ ```python
122
+ my_strings = validate(['a', 'b', 'c'], {"non_empty": True})
123
+ ```
124
+ no_nulls
125
+ Collection must not contain None.
126
+
127
+ ```python
128
+ my_things = validate([1, 2, 3, "a", "b", "c"], {"no_nulls": True})
129
+ ```
130
+
131
+ sorted
132
+ List must be sorted (increasing or decreasing).
133
+
134
+ ```python
135
+ numbers = validate([1, 5, 9], {"sorted": True})
136
+ ```
137
+
138
+ increasing
139
+ List must be strictly increasing.
140
+
141
+ ```python
142
+ numbers = validate([1, 5, 9], {"increasing": True})
143
+ ```
144
+ decreasing
145
+ List must be strictly decreasing.
146
+
147
+ ```python
148
+ numbers = validate([9, 5, 1], {"decreasing": True})
149
+ ```
150
+ sum_min
151
+ Minimum sum of the collection (inclusive).
152
+
153
+ ```python
154
+ numbers = validate([10, 20, 30], {"sum_min": 50})
155
+ ```
156
+ sum_max
157
+ Maximum sum of the collection (inclusive).
158
+
159
+ ```python
160
+ numbers = validate([10, 20, 30], {"sum_max": 70})
161
+ ```
162
+ element_min
163
+ Minimum value for any element (inclusive).
164
+
165
+ ```python
166
+ numbers = validate([10, 20, 30], {"element_min": 5})
167
+ ```
168
+ element_max
169
+ Maximum value for any element (inclusive).
170
+
171
+ ```python
172
+ numbers = validate([10, 20, 30], {"element_max": 40})
173
+ ```
174
+ regex
175
+ String must match the regex.
176
+
177
+ ```python
178
+ cat_or_dog = validate("cat", {"regex": "cat|dog"})
179
+ ```
180
+
181
+
182
+ must_be_true
183
+ Custom rule: a function that returns True for allowed values.
184
+
185
+ ```python
186
+ def is_even(x: int) -> bool:
187
+ return x % 2 == 0
188
+ even_number = validate(8, {"must_be_true": is_even})
189
+ ```
190
+ This calls:
191
+
192
+ ```python
193
+ is_even(8)
194
+ ```
195
+ If enough people use a must_be_true lambda, it may become an added keyword in a later version
196
+
197
+ Contributing
198
+ Contributions are welcome. Please open an issue or pull request.
199
+ When contributing, ensure backwards compatibility (you cannot remove keywords and/or features).
200
+
201
+ Please note, when using my module, that you will manually have to validate each time should you choose to mutate a variable.
202
+
203
+
204
+ Versioning Policy
205
+ -----------------
206
+
207
+ This project guarantees full backwards compatibility. Existing rule files, keyword meanings, validator behaviors, and metadata formats will continue to work exactly as before. No update will ever break existing configurations.
208
+
209
+ Version Numbering
210
+ -----------------
211
+ This project uses a non-breaking semantic versioning model:
212
+
213
+ MAJOR.MINOR.PATCH
214
+
215
+ MAJOR = large new feature families
216
+ MINOR = small additive keywords or enhancements
217
+ PATCH = bug fixes or internal improvements
218
+
219
+ Major bumps do not imply breaking changes. They only indicate that a significant new capability has been added.
220
+
221
+ Major bumps always reset MINOR and PATCH to 0. For example:
222
+ 1.7.3 -> 2.0.0
223
+ 1.12.0 -> 2.0.0
224
+ 1.0.0 -> 2.0.0
225
+
226
+ Minor bumps always reset PATCH to 0. For example:
227
+ 1.7.3 -> 1.8.0
228
+ 1.12.9 -> 1.13.0
229
+ 1.0.4 -> 1.1.0
230
+
231
+ Minor Version Bumps
232
+ -------------------
233
+ Minor bumps occur when adding small keywords. Examples include:
234
+
235
+ min_inclusive
236
+ max_inclusive
237
+ trim_whitespace
238
+ pattern_flags
239
+
240
+ These additions do not change the meaning of existing keywords, do not require users to modify rule files, and do not alter validator behavior. They are classified as minor updates.
241
+
242
+ Minor bumps always reset PATCH to 0
243
+
244
+ Major Version Bumps
245
+ -------------------
246
+ Major bumps occur only when adding large new keyword families or entire new capability domains. Examples include:
247
+
248
+ a full date-validation system
249
+ a full numeric-range system
250
+ a full conditional-rules system
251
+ a full schema-level rule system
252
+
253
+ These are major features, even though they remain fully backwards-compatible. Major bumps communicate that the release adds a significant new capability.
254
+
255
+ Major bumps always reset MINOR and PATCH to 0.
256
+
257
+ Patch Version Bumps
258
+ -------------------
259
+ Patch bumps occur when fixing bugs, improving internal logic, optimizing performance, correcting documentation, or adjusting error messages without changing meaning. Patch updates never add new keywords.
260
+
261
+ Summary
262
+ -------
263
+ MAJOR: large new feature families, backwards-compatible, resets MINOR and PATCH to 0
264
+ MINOR: small additive keywords, backwards-compatible
265
+ PATCH: fixes only, backwards-compatible
266
+
267
+
268
+
269
+ Credits
270
+ Microsoft Copilot — for helping me code it. It did a TON of the coding, and to be honest, I couldn't have made this project without it. It did ~~some~~ a lot of readme, in addition to it writing most of CONTRIBUTING.md
271
+ Dad — for helping me along the journey.
272
+
273
+ License Type: BSD 3-clause
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "enforce-rules"
3
+ version = "1.0.0"
4
+ description = "Runtime rule validation for Python using dictionary-based constraints."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "Jake Loepker" },
10
+ { name = "Microsoft Copilot (AI Assistant)" }
11
+ ]
12
+
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: BSD License",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development",
24
+ "Topic :: Software Development :: Libraries",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Utilities",
27
+ "Typing :: Typed"
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Loepker-James/enforce-rules"
32
+ Issues = "https://github.com/Loepker-James/enforce-rules/issues"
33
+ Copilot = "https://copilot.microsoft.com"
34
+
35
+ [build-system]
36
+ requires = ["setuptools>=61.0", "wheel"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from . import enforce_rules
@@ -0,0 +1,171 @@
1
+ from typing import Any, Dict, Iterable, Callable, Sequence
2
+ import re
3
+
4
+
5
+ # -----------------------------
6
+ # VALIDATOR FUNCTIONS (DEFINED FIRST)
7
+ # -----------------------------
8
+
9
+ def _validate_length(value: Sequence[Any], expected: int) -> None:
10
+ if len(value) != expected:
11
+ raise ValueError(f"Expected length {expected}, got {len(value)}")
12
+
13
+
14
+ def _validate_min_length(value: Sequence[Any], expected: int) -> None:
15
+ if len(value) < expected:
16
+ raise ValueError(f"Expected min length {expected}, got {len(value)}")
17
+
18
+
19
+ def _validate_max_length(value: Sequence[Any], expected: int) -> None:
20
+ if len(value) > expected:
21
+ raise ValueError(f"Expected max length {expected}, got {len(value)}")
22
+
23
+
24
+ def _validate_min(value: float | int, expected: float | int) -> None:
25
+ if value < expected:
26
+ raise ValueError(f"Expected value >= {expected}, got {value}")
27
+
28
+
29
+ def _validate_max(value: float | int, expected: float | int) -> None:
30
+ if value > expected:
31
+ raise ValueError(f"Expected value <= {expected}, got {value}")
32
+
33
+
34
+ def _validate_allowed_values(value: Any, allowed: Iterable[Any]) -> None:
35
+ if value not in allowed:
36
+ raise ValueError(f"Value {value} not in allowed values {allowed}")
37
+
38
+
39
+ def _validate_invariant(value: Any, expected: bool) -> None:
40
+ if expected and not value:
41
+ raise ValueError("Invariant rule failed: value must be truthy")
42
+
43
+
44
+ def _validate_all_same(value: Sequence[Any], expected: bool) -> None:
45
+ if expected and len(set(value)) != 1:
46
+ raise ValueError("All elements must be the same")
47
+
48
+
49
+ def _validate_all_unique(value: Sequence[Any], expected: bool) -> None:
50
+ if expected and len(set(value)) != len(value):
51
+ raise ValueError("All elements must be unique")
52
+
53
+
54
+ def _validate_non_empty(value: Sequence[Any], expected: bool) -> None:
55
+ if expected and len(value) == 0:
56
+ raise ValueError("Collection must not be empty")
57
+
58
+
59
+ def _validate_no_nulls(value: Iterable[Any], expected: bool) -> None:
60
+ if expected and any(v is None for v in value):
61
+ raise ValueError("Collection must not contain None")
62
+
63
+
64
+ def _validate_sorted(value: Sequence[Any], expected: bool) -> None:
65
+ if expected:
66
+ if value != sorted(value) and value != sorted(value, reverse=True):
67
+ raise ValueError("List must be sorted increasing or decreasing")
68
+
69
+
70
+ def _validate_increasing(value: Sequence[Any], expected: bool) -> None:
71
+ if expected:
72
+ for a, b in zip(value, value[1:]):
73
+ if not (b > a):
74
+ raise ValueError("List must be strictly increasing")
75
+
76
+
77
+ def _validate_decreasing(value: Sequence[Any], expected: bool) -> None:
78
+ if expected:
79
+ for a, b in zip(value, value[1:]):
80
+ if not (b < a):
81
+ raise ValueError("List must be strictly decreasing")
82
+
83
+
84
+ def _validate_sum_min(value: Iterable[float | int], expected: float | int) -> None:
85
+ total = sum(value)
86
+ if total < expected:
87
+ raise ValueError(f"Sum must be >= {expected}, got {total}")
88
+
89
+
90
+ def _validate_sum_max(value: Iterable[float | int], expected: float | int) -> None:
91
+ total = sum(value)
92
+ if total > expected:
93
+ raise ValueError(f"Sum must be <= {expected}, got {total}")
94
+
95
+
96
+ def _validate_element_min(value: Iterable[float | int], expected: float | int) -> None:
97
+ if min(value) < expected:
98
+ raise ValueError(f"Elements must be >= {expected}")
99
+
100
+
101
+ def _validate_element_max(value: Iterable[float | int], expected: float | int) -> None:
102
+ if max(value) > expected:
103
+ raise ValueError(f"Elements must be <= {expected}")
104
+
105
+
106
+ def _validate_regex(value: str, pattern: str, flags: Any) -> None:
107
+ compiled = re.compile(pattern, flags or 0)
108
+ if not compiled.fullmatch(value):
109
+ raise ValueError(f"Value '{value}' does not match regex '{pattern}'")
110
+
111
+
112
+ def _validate_must_be_true(value: Any, func: Callable[[Any], bool]) -> None:
113
+ if not func(value):
114
+ raise ValueError("must_be_true rule failed")
115
+
116
+
117
+ # -----------------------------
118
+ # VALIDATE() — DEFINED LAST
119
+ # -----------------------------
120
+
121
+ def validate(value: Any, rules: Dict[str, Any]) -> Any:
122
+ """
123
+ Validate a value against a dictionary of rules.
124
+ Returns the original value if valid, otherwise raises ValueError.
125
+ """
126
+ for key, rule in rules.items():
127
+ match key:
128
+ case "length":
129
+ _validate_length(value, int(rule))
130
+ case "min_length":
131
+ _validate_min_length(value, int(rule))
132
+ case "max_length":
133
+ _validate_max_length(value, int(rule))
134
+ case "min":
135
+ _validate_min(value, rule)
136
+ case "max":
137
+ _validate_max(value, rule)
138
+ case "allowed_values":
139
+ _validate_allowed_values(value, rule)
140
+ case "invariant":
141
+ _validate_invariant(value, bool(rule))
142
+ case "all_same":
143
+ _validate_all_same(value, bool(rule))
144
+ case "all_unique":
145
+ _validate_all_unique(value, bool(rule))
146
+ case "non_empty":
147
+ _validate_non_empty(value, bool(rule))
148
+ case "no_nulls":
149
+ _validate_no_nulls(value, bool(rule))
150
+ case "sorted":
151
+ _validate_sorted(value, bool(rule))
152
+ case "increasing":
153
+ _validate_increasing(value, bool(rule))
154
+ case "decreasing":
155
+ _validate_decreasing(value, bool(rule))
156
+ case "sum_min":
157
+ _validate_sum_min(value, rule)
158
+ case "sum_max":
159
+ _validate_sum_max(value, rule)
160
+ case "element_min":
161
+ _validate_element_min(value, rule)
162
+ case "element_max":
163
+ _validate_element_max(value, rule)
164
+ case "regex":
165
+ _validate_regex(value, str(rule), rules.get("regex_flags"))
166
+ case "must_be_true":
167
+ _validate_must_be_true(value, rule)
168
+ case _:
169
+ raise ValueError(f"Unknown rule: {key}")
170
+
171
+ return value
@@ -0,0 +1,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: enforce-rules
3
+ Version: 1.0.0
4
+ Summary: Runtime rule validation for Python using dictionary-based constraints.
5
+ Author: Jake Loepker, Microsoft Copilot (AI Assistant)
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2026, Loepker-James
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ Project-URL: Homepage, https://github.com/Loepker-James/enforce-rules
36
+ Project-URL: Issues, https://github.com/Loepker-James/enforce-rules/issues
37
+ Project-URL: Copilot, https://copilot.microsoft.com
38
+ Classifier: Development Status :: 5 - Production/Stable
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: License :: OSI Approved :: BSD License
41
+ Classifier: Programming Language :: Python
42
+ Classifier: Programming Language :: Python :: 3
43
+ Classifier: Programming Language :: Python :: 3.10
44
+ Classifier: Programming Language :: Python :: 3.11
45
+ Classifier: Programming Language :: Python :: 3.12
46
+ Classifier: Programming Language :: Python :: 3.13
47
+ Classifier: Topic :: Software Development
48
+ Classifier: Topic :: Software Development :: Libraries
49
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
50
+ Classifier: Topic :: Utilities
51
+ Classifier: Typing :: Typed
52
+ Requires-Python: >=3.10
53
+ Description-Content-Type: text/markdown
54
+ License-File: LICENSE
55
+ Dynamic: license-file
56
+
57
+ Project Enforce Rules
58
+ Project Enforce Rules (or PER for short) expands the Python type system to allow more constraints.
59
+
60
+ Version #
61
+ ----------------
62
+ MAJOR: 1
63
+ MINOR: 0
64
+ PATCH: 0
65
+
66
+ If you need to catch up, you can see the full version history in the [CHANGELOG](CHANGELOG.md).
67
+
68
+ It exists to let you use features Python doesn't already provide in the typing system — things you probably want, like min, max, length, all_same, and many more.
69
+
70
+ PER now supports runtime validation anywhere using:
71
+
72
+ ```python
73
+ validate(value: object, rules: Dict[str, typing.Any])
74
+ ```
75
+
76
+ This enforces rule dictionaries and returns the original value if valid.
77
+ If invalid, it raises a descriptive error.
78
+
79
+ Why I Made It
80
+ I wanted type‑hint features Python didn’t give me. I thought dictionaries would work until I learned... well...
81
+ Python didn’t enforce them.
82
+
83
+ So with the help of Microsoft Copilot and my dad, we designed a module that enforces this type of stuff.
84
+ Then I realized everyone in the Python community could use this, so it became a project. Hence, creating PER.
85
+
86
+ PER uses rule dictionaries and validate() to enforce constraints at runtime.
87
+
88
+ Install it with pip:
89
+
90
+ ```bash
91
+ pip install enforce-rules
92
+ ```
93
+ Then use it like:
94
+
95
+ ```python
96
+ from enforce_rules import validate
97
+ ```
98
+ Features
99
+ 1. Runtime enforcement of rule dictionaries
100
+ 2. Dictionary‑based rule definitions
101
+ 3. No extra objects required
102
+ 4. Works anywhere in your code
103
+ 5. Extensible via must_be_true
104
+
105
+ How It Works
106
+ PER validates values using:
107
+
108
+ ```python
109
+ validate(value, rules)
110
+ ```
111
+ If the value violates a rule, PER raises an error.
112
+ If the value passes, PER returns the original value unchanged.
113
+
114
+ This means validated values behave exactly like normal Python values.
115
+
116
+ Keywords and Usage
117
+ Below are all supported keywords.
118
+
119
+ length
120
+ The length of the object must be exactly this.
121
+
122
+ ```python
123
+ lst = validate([1, 2, 3, 4, 5], {"length": 5})
124
+ ```
125
+
126
+ min_length
127
+ Minimum length (inclusive).
128
+
129
+ ```python
130
+ lst = validate(['a', 'b', 'c', 'd', 'e'], {"min_length": 3})
131
+ ```
132
+ max_length
133
+ Maximum length (inclusive).
134
+
135
+ ```python
136
+ lst = validate([1, 2, 3, 4, 5, 6], {"max_length": 7})
137
+ ```
138
+ min
139
+ Minimum numeric value (inclusive).
140
+
141
+ ```python
142
+ number = validate(10, {"min": 0})
143
+ ```
144
+ max
145
+ Maximum numeric value (inclusive).
146
+
147
+ ```python
148
+ number = validate(10, {"max": 20})
149
+ ```
150
+ allowed_values
151
+ Similar to Literal; value must be one of the allowed values.
152
+
153
+ ```python
154
+ val = validate("a", {"allowed_values": ("a", "b", "c", "d")})
155
+ ```
156
+ invariant
157
+ Value must be truthy.
158
+
159
+ ```python
160
+ val = validate((0 == 0), {"invariant": True})
161
+ ```
162
+ all_same
163
+ All values in the collection must be the same.
164
+
165
+ ```python
166
+ numbers = validate([1, 1, 1], {"all_same": True})
167
+ ```
168
+ all_unique
169
+ All values in the collection must be unique.
170
+
171
+ ```python
172
+ numbers = validate([1, 2, 3], {"all_unique": True})
173
+ ```
174
+ non_empty
175
+ Collection must not be empty.
176
+
177
+ ```python
178
+ my_strings = validate(['a', 'b', 'c'], {"non_empty": True})
179
+ ```
180
+ no_nulls
181
+ Collection must not contain None.
182
+
183
+ ```python
184
+ my_things = validate([1, 2, 3, "a", "b", "c"], {"no_nulls": True})
185
+ ```
186
+
187
+ sorted
188
+ List must be sorted (increasing or decreasing).
189
+
190
+ ```python
191
+ numbers = validate([1, 5, 9], {"sorted": True})
192
+ ```
193
+
194
+ increasing
195
+ List must be strictly increasing.
196
+
197
+ ```python
198
+ numbers = validate([1, 5, 9], {"increasing": True})
199
+ ```
200
+ decreasing
201
+ List must be strictly decreasing.
202
+
203
+ ```python
204
+ numbers = validate([9, 5, 1], {"decreasing": True})
205
+ ```
206
+ sum_min
207
+ Minimum sum of the collection (inclusive).
208
+
209
+ ```python
210
+ numbers = validate([10, 20, 30], {"sum_min": 50})
211
+ ```
212
+ sum_max
213
+ Maximum sum of the collection (inclusive).
214
+
215
+ ```python
216
+ numbers = validate([10, 20, 30], {"sum_max": 70})
217
+ ```
218
+ element_min
219
+ Minimum value for any element (inclusive).
220
+
221
+ ```python
222
+ numbers = validate([10, 20, 30], {"element_min": 5})
223
+ ```
224
+ element_max
225
+ Maximum value for any element (inclusive).
226
+
227
+ ```python
228
+ numbers = validate([10, 20, 30], {"element_max": 40})
229
+ ```
230
+ regex
231
+ String must match the regex.
232
+
233
+ ```python
234
+ cat_or_dog = validate("cat", {"regex": "cat|dog"})
235
+ ```
236
+
237
+
238
+ must_be_true
239
+ Custom rule: a function that returns True for allowed values.
240
+
241
+ ```python
242
+ def is_even(x: int) -> bool:
243
+ return x % 2 == 0
244
+ even_number = validate(8, {"must_be_true": is_even})
245
+ ```
246
+ This calls:
247
+
248
+ ```python
249
+ is_even(8)
250
+ ```
251
+ If enough people use a must_be_true lambda, it may become an added keyword in a later version
252
+
253
+ Contributing
254
+ Contributions are welcome. Please open an issue or pull request.
255
+ When contributing, ensure backwards compatibility (you cannot remove keywords and/or features).
256
+
257
+ Please note, when using my module, that you will manually have to validate each time should you choose to mutate a variable.
258
+
259
+
260
+ Versioning Policy
261
+ -----------------
262
+
263
+ This project guarantees full backwards compatibility. Existing rule files, keyword meanings, validator behaviors, and metadata formats will continue to work exactly as before. No update will ever break existing configurations.
264
+
265
+ Version Numbering
266
+ -----------------
267
+ This project uses a non-breaking semantic versioning model:
268
+
269
+ MAJOR.MINOR.PATCH
270
+
271
+ MAJOR = large new feature families
272
+ MINOR = small additive keywords or enhancements
273
+ PATCH = bug fixes or internal improvements
274
+
275
+ Major bumps do not imply breaking changes. They only indicate that a significant new capability has been added.
276
+
277
+ Major bumps always reset MINOR and PATCH to 0. For example:
278
+ 1.7.3 -> 2.0.0
279
+ 1.12.0 -> 2.0.0
280
+ 1.0.0 -> 2.0.0
281
+
282
+ Minor bumps always reset PATCH to 0. For example:
283
+ 1.7.3 -> 1.8.0
284
+ 1.12.9 -> 1.13.0
285
+ 1.0.4 -> 1.1.0
286
+
287
+ Minor Version Bumps
288
+ -------------------
289
+ Minor bumps occur when adding small keywords. Examples include:
290
+
291
+ min_inclusive
292
+ max_inclusive
293
+ trim_whitespace
294
+ pattern_flags
295
+
296
+ These additions do not change the meaning of existing keywords, do not require users to modify rule files, and do not alter validator behavior. They are classified as minor updates.
297
+
298
+ Minor bumps always reset PATCH to 0
299
+
300
+ Major Version Bumps
301
+ -------------------
302
+ Major bumps occur only when adding large new keyword families or entire new capability domains. Examples include:
303
+
304
+ a full date-validation system
305
+ a full numeric-range system
306
+ a full conditional-rules system
307
+ a full schema-level rule system
308
+
309
+ These are major features, even though they remain fully backwards-compatible. Major bumps communicate that the release adds a significant new capability.
310
+
311
+ Major bumps always reset MINOR and PATCH to 0.
312
+
313
+ Patch Version Bumps
314
+ -------------------
315
+ Patch bumps occur when fixing bugs, improving internal logic, optimizing performance, correcting documentation, or adjusting error messages without changing meaning. Patch updates never add new keywords.
316
+
317
+ Summary
318
+ -------
319
+ MAJOR: large new feature families, backwards-compatible, resets MINOR and PATCH to 0
320
+ MINOR: small additive keywords, backwards-compatible
321
+ PATCH: fixes only, backwards-compatible
322
+
323
+
324
+
325
+ Credits
326
+ Microsoft Copilot — for helping me code it. It did a TON of the coding, and to be honest, I couldn't have made this project without it. It did ~~some~~ a lot of readme, in addition to it writing most of CONTRIBUTING.md
327
+ Dad — for helping me along the journey.
328
+
329
+ License Type: BSD 3-clause
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/enforce_rules/__init__.py
5
+ src/enforce_rules/enforce_rules.py
6
+ src/enforce_rules.egg-info/PKG-INFO
7
+ src/enforce_rules.egg-info/SOURCES.txt
8
+ src/enforce_rules.egg-info/dependency_links.txt
9
+ src/enforce_rules.egg-info/top_level.txt
10
+ tests/test_enforce_rules.py
@@ -0,0 +1 @@
1
+ enforce_rules
@@ -0,0 +1,190 @@
1
+ import unittest
2
+ from typing import Any, Dict
3
+ from enforce_rules import validate
4
+
5
+
6
+ class TestValidate(unittest.TestCase):
7
+
8
+ # -----------------------------
9
+ # LENGTH RULES
10
+ # -----------------------------
11
+ def test_length_valid(self):
12
+ self.assertEqual(validate([1, 2, 3], {"length": 3}), [1, 2, 3])
13
+
14
+ def test_length_invalid(self):
15
+ with self.assertRaises(ValueError):
16
+ validate([1, 2], {"length": 3})
17
+
18
+ def test_min_length_valid(self):
19
+ self.assertEqual(validate([1, 2, 3], {"min_length": 2}), [1, 2, 3])
20
+
21
+ def test_min_length_invalid(self):
22
+ with self.assertRaises(ValueError):
23
+ validate([1], {"min_length": 2})
24
+
25
+ def test_max_length_valid(self):
26
+ self.assertEqual(validate([1, 2], {"max_length": 3}), [1, 2])
27
+
28
+ def test_max_length_invalid(self):
29
+ with self.assertRaises(ValueError):
30
+ validate([1, 2, 3, 4], {"max_length": 3})
31
+
32
+ # -----------------------------
33
+ # MIN / MAX
34
+ # -----------------------------
35
+ def test_min_valid(self):
36
+ self.assertEqual(validate(5, {"min": 3}), 5)
37
+
38
+ def test_min_invalid(self):
39
+ with self.assertRaises(ValueError):
40
+ validate(2, {"min": 3})
41
+
42
+ def test_max_valid(self):
43
+ self.assertEqual(validate(5, {"max": 10}), 5)
44
+
45
+ def test_max_invalid(self):
46
+ with self.assertRaises(ValueError):
47
+ validate(20, {"max": 10})
48
+
49
+ # -----------------------------
50
+ # ALLOWED VALUES
51
+ # -----------------------------
52
+ def test_allowed_values_valid(self):
53
+ self.assertEqual(validate("b", {"allowed_values": ("a", "b", "c")}), "b")
54
+
55
+ def test_allowed_values_invalid(self):
56
+ with self.assertRaises(ValueError):
57
+ validate("x", {"allowed_values": ("a", "b", "c")})
58
+
59
+ # -----------------------------
60
+ # INVARIANT
61
+ # -----------------------------
62
+ def test_invariant_valid(self):
63
+ self.assertTrue(validate(True, {"invariant": True}))
64
+
65
+ def test_invariant_invalid(self):
66
+ with self.assertRaises(ValueError):
67
+ validate(False, {"invariant": True})
68
+
69
+ # -----------------------------
70
+ # ALL SAME / ALL UNIQUE
71
+ # -----------------------------
72
+ def test_all_same_valid(self):
73
+ self.assertEqual(validate([1, 1, 1], {"all_same": True}), [1, 1, 1])
74
+
75
+ def test_all_same_invalid(self):
76
+ with self.assertRaises(ValueError):
77
+ validate([1, 2, 1], {"all_same": True})
78
+
79
+ def test_all_unique_valid(self):
80
+ self.assertEqual(validate([1, 2, 3], {"all_unique": True}), [1, 2, 3])
81
+
82
+ def test_all_unique_invalid(self):
83
+ with self.assertRaises(ValueError):
84
+ validate([1, 2, 2], {"all_unique": True})
85
+
86
+ # -----------------------------
87
+ # NON EMPTY / NO NULLS
88
+ # -----------------------------
89
+ def test_non_empty_valid(self):
90
+ self.assertEqual(validate([1], {"non_empty": True}), [1])
91
+
92
+ def test_non_empty_invalid(self):
93
+ with self.assertRaises(ValueError):
94
+ validate([], {"non_empty": True})
95
+
96
+ def test_no_nulls_valid(self):
97
+ self.assertEqual(validate([1, 2, 3], {"no_nulls": True}), [1, 2, 3])
98
+
99
+ def test_no_nulls_invalid(self):
100
+ with self.assertRaises(ValueError):
101
+ validate([1, None, 3], {"no_nulls": True})
102
+
103
+ # -----------------------------
104
+ # SORTED / INCREASING / DECREASING
105
+ # -----------------------------
106
+ def test_sorted_valid(self):
107
+ self.assertEqual(validate([1, 2, 3], {"sorted": True}), [1, 2, 3])
108
+
109
+ def test_sorted_reverse_valid(self):
110
+ self.assertEqual(validate([3, 2, 1], {"sorted": True}), [3, 2, 1])
111
+
112
+ def test_sorted_invalid(self):
113
+ with self.assertRaises(ValueError):
114
+ validate([1, 3, 2], {"sorted": True})
115
+
116
+ def test_increasing_valid(self):
117
+ self.assertEqual(validate([1, 2, 3], {"increasing": True}), [1, 2, 3])
118
+
119
+ def test_increasing_invalid(self):
120
+ with self.assertRaises(ValueError):
121
+ validate([1, 3, 2], {"increasing": True})
122
+
123
+ def test_decreasing_valid(self):
124
+ self.assertEqual(validate([3, 2, 1], {"decreasing": True}), [3, 2, 1])
125
+
126
+ def test_decreasing_invalid(self):
127
+ with self.assertRaises(ValueError):
128
+ validate([3, 1, 2], {"decreasing": True})
129
+
130
+ # -----------------------------
131
+ # SUM MIN / SUM MAX
132
+ # -----------------------------
133
+ def test_sum_min_valid(self):
134
+ self.assertEqual(validate([10, 20, 30], {"sum_min": 50}), [10, 20, 30])
135
+
136
+ def test_sum_min_invalid(self):
137
+ with self.assertRaises(ValueError):
138
+ validate([10, 20], {"sum_min": 50})
139
+
140
+ def test_sum_max_valid(self):
141
+ self.assertEqual(validate([10, 20, 30], {"sum_max": 70}), [10, 20, 30])
142
+
143
+ def test_sum_max_invalid(self):
144
+ with self.assertRaises(ValueError):
145
+ validate([50, 50], {"sum_max": 70})
146
+
147
+ # -----------------------------
148
+ # ELEMENT MIN / ELEMENT MAX
149
+ # -----------------------------
150
+ def test_element_min_valid(self):
151
+ self.assertEqual(validate([10, 20, 30], {"element_min": 5}), [10, 20, 30])
152
+
153
+ def test_element_min_invalid(self):
154
+ with self.assertRaises(ValueError):
155
+ validate([1, 20, 30], {"element_min": 5})
156
+
157
+ def test_element_max_valid(self):
158
+ self.assertEqual(validate([10, 20, 30], {"element_max": 40}), [10, 20, 30])
159
+
160
+ def test_element_max_invalid(self):
161
+ with self.assertRaises(ValueError):
162
+ validate([10, 20, 100], {"element_max": 40})
163
+
164
+ # -----------------------------
165
+ # REGEX
166
+ # -----------------------------
167
+ def test_regex_valid(self):
168
+ self.assertEqual(validate("cat", {"regex": "cat|dog"}), "cat")
169
+
170
+ def test_regex_invalid(self):
171
+ with self.assertRaises(ValueError):
172
+ validate("bird", {"regex": "cat|dog"})
173
+
174
+ # -----------------------------
175
+ # MUST BE TRUE
176
+ # -----------------------------
177
+ def test_must_be_true_valid(self):
178
+ def is_even(x: int) -> bool:
179
+ return x % 2 == 0
180
+ self.assertEqual(validate(8, {"must_be_true": is_even}), 8)
181
+
182
+ def test_must_be_true_invalid(self):
183
+ def is_even(x: int) -> bool:
184
+ return x % 2 == 0
185
+ with self.assertRaises(ValueError):
186
+ validate(7, {"must_be_true": is_even})
187
+
188
+
189
+ if __name__ == "__main__":
190
+ unittest.main()