valid8r 0.6.3__tar.gz → 0.7.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.
Potentially problematic release.
This version of valid8r might be problematic. Click here for more details.
- {valid8r-0.6.3 → valid8r-0.7.0}/PKG-INFO +25 -1
- {valid8r-0.6.3 → valid8r-0.7.0}/README.md +24 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/pyproject.toml +1 -1
- valid8r-0.7.0/valid8r/core/validators.py +565 -0
- valid8r-0.6.3/valid8r/core/validators.py +0 -282
- {valid8r-0.6.3 → valid8r-0.7.0}/LICENSE +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/__init__.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/core/__init__.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/core/combinators.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/core/maybe.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/core/parsers.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/prompt/__init__.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/prompt/basic.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/py.typed +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/testing/__init__.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/testing/assertions.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/testing/generators.py +0 -0
- {valid8r-0.6.3 → valid8r-0.7.0}/valid8r/testing/mock_input.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: valid8r
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Clean, flexible input validation for Python applications
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -78,6 +78,30 @@ A clean, flexible input validation library for Python applications.
|
|
|
78
78
|
- **Enums**: `parse_enum` (type-safe enum parsing)
|
|
79
79
|
- **Custom**: `create_parser`, `make_parser`, `validated_parser` (parser factories)
|
|
80
80
|
|
|
81
|
+
## Available Validators
|
|
82
|
+
|
|
83
|
+
### Numeric Validators
|
|
84
|
+
- **`minimum(min_value)`** - Ensures value is at least the minimum (inclusive)
|
|
85
|
+
- **`maximum(max_value)`** - Ensures value is at most the maximum (inclusive)
|
|
86
|
+
- **`between(min_value, max_value)`** - Ensures value is within range (inclusive)
|
|
87
|
+
|
|
88
|
+
### String Validators
|
|
89
|
+
- **`non_empty_string()`** - Rejects empty strings and whitespace-only strings
|
|
90
|
+
- **`matches_regex(pattern)`** - Validates string matches regex pattern (string or compiled)
|
|
91
|
+
- **`length(min_length, max_length)`** - Validates string length within bounds
|
|
92
|
+
|
|
93
|
+
### Collection Validators
|
|
94
|
+
- **`in_set(allowed_values)`** - Ensures value is in set of allowed values
|
|
95
|
+
- **`unique_items()`** - Ensures all items in a list are unique
|
|
96
|
+
- **`subset_of(allowed_set)`** - Validates set is subset of allowed values
|
|
97
|
+
- **`superset_of(required_set)`** - Validates set is superset of required values
|
|
98
|
+
- **`is_sorted(reverse=False)`** - Ensures list is sorted (ascending or descending)
|
|
99
|
+
|
|
100
|
+
### Custom Validators
|
|
101
|
+
- **`predicate(func, error_message)`** - Create custom validator from any predicate function
|
|
102
|
+
|
|
103
|
+
**Note**: All validators support custom error messages and can be combined using `&` (and), `|` (or), and `~` (not) operators.
|
|
104
|
+
|
|
81
105
|
## Installation
|
|
82
106
|
|
|
83
107
|
**Requirements**: Python 3.11 or higher
|
|
@@ -47,6 +47,30 @@ A clean, flexible input validation library for Python applications.
|
|
|
47
47
|
- **Enums**: `parse_enum` (type-safe enum parsing)
|
|
48
48
|
- **Custom**: `create_parser`, `make_parser`, `validated_parser` (parser factories)
|
|
49
49
|
|
|
50
|
+
## Available Validators
|
|
51
|
+
|
|
52
|
+
### Numeric Validators
|
|
53
|
+
- **`minimum(min_value)`** - Ensures value is at least the minimum (inclusive)
|
|
54
|
+
- **`maximum(max_value)`** - Ensures value is at most the maximum (inclusive)
|
|
55
|
+
- **`between(min_value, max_value)`** - Ensures value is within range (inclusive)
|
|
56
|
+
|
|
57
|
+
### String Validators
|
|
58
|
+
- **`non_empty_string()`** - Rejects empty strings and whitespace-only strings
|
|
59
|
+
- **`matches_regex(pattern)`** - Validates string matches regex pattern (string or compiled)
|
|
60
|
+
- **`length(min_length, max_length)`** - Validates string length within bounds
|
|
61
|
+
|
|
62
|
+
### Collection Validators
|
|
63
|
+
- **`in_set(allowed_values)`** - Ensures value is in set of allowed values
|
|
64
|
+
- **`unique_items()`** - Ensures all items in a list are unique
|
|
65
|
+
- **`subset_of(allowed_set)`** - Validates set is subset of allowed values
|
|
66
|
+
- **`superset_of(required_set)`** - Validates set is superset of required values
|
|
67
|
+
- **`is_sorted(reverse=False)`** - Ensures list is sorted (ascending or descending)
|
|
68
|
+
|
|
69
|
+
### Custom Validators
|
|
70
|
+
- **`predicate(func, error_message)`** - Create custom validator from any predicate function
|
|
71
|
+
|
|
72
|
+
**Note**: All validators support custom error messages and can be combined using `&` (and), `|` (or), and `~` (not) operators.
|
|
73
|
+
|
|
50
74
|
## Installation
|
|
51
75
|
|
|
52
76
|
**Requirements**: Python 3.11 or higher
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""Core validators for validating values against specific criteria.
|
|
2
|
+
|
|
3
|
+
This module provides a collection of validator functions for common validation scenarios.
|
|
4
|
+
All validators follow the same pattern - they take a value and return a Maybe object
|
|
5
|
+
that either contains the validated value or an error message.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Generic,
|
|
14
|
+
Protocol,
|
|
15
|
+
TypeVar,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from valid8r.core.combinators import (
|
|
19
|
+
and_then,
|
|
20
|
+
not_validator,
|
|
21
|
+
or_else,
|
|
22
|
+
)
|
|
23
|
+
from valid8r.core.maybe import Maybe
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SupportsComparison(Protocol): # noqa: D101
|
|
30
|
+
def __le__(self, other: object, /) -> bool: ... # noqa: D105
|
|
31
|
+
def __lt__(self, other: object, /) -> bool: ... # noqa: D105
|
|
32
|
+
def __ge__(self, other: object, /) -> bool: ... # noqa: D105
|
|
33
|
+
def __gt__(self, other: object, /) -> bool: ... # noqa: D105
|
|
34
|
+
def __eq__(self, other: object, /) -> bool: ... # noqa: D105
|
|
35
|
+
def __ne__(self, other: object, /) -> bool: ... # noqa: D105
|
|
36
|
+
def __hash__(self, /) -> int: ... # noqa: D105
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
T = TypeVar('T')
|
|
40
|
+
U = TypeVar('U')
|
|
41
|
+
N = TypeVar('N', bound=SupportsComparison)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Validator(Generic[T]):
|
|
45
|
+
"""A wrapper class for validator functions that supports operator overloading."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, func: Callable[[T], Maybe[T]]) -> None:
|
|
48
|
+
"""Initialize a validator with a validation function.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
func: A function that takes a value and returns a Maybe
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
self.func = func
|
|
55
|
+
|
|
56
|
+
def __call__(self, value: T) -> Maybe[T]:
|
|
57
|
+
"""Apply the validator to a value.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: The value to validate
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A Maybe containing either the validated value or an error
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
return self.func(value)
|
|
67
|
+
|
|
68
|
+
def __and__(self, other: Validator[T]) -> Validator[T]:
|
|
69
|
+
"""Combine with another validator using logical AND.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
other: Another validator to combine with
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
A new validator that passes only if both validators pass
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
return Validator(lambda value: and_then(self.func, other.func)(value))
|
|
79
|
+
|
|
80
|
+
def __or__(self, other: Validator[T]) -> Validator[T]:
|
|
81
|
+
"""Combine with another validator using logical OR.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
other: Another validator to combine with
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A new validator that passes if either validator passes
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
return Validator(lambda value: or_else(self.func, other.func)(value))
|
|
91
|
+
|
|
92
|
+
def __invert__(self) -> Validator[T]:
|
|
93
|
+
"""Negate this validator.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A new validator that passes if this validator fails
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
return Validator(lambda value: not_validator(self.func, 'Negated validation failed')(value))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
|
|
103
|
+
"""Create a validator that ensures a value is at least the minimum.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
min_value: The minimum allowed value (inclusive)
|
|
107
|
+
error_message: Optional custom error message
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Validator[N]: A validator function that accepts values >= min_value
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> from valid8r.core.validators import minimum
|
|
114
|
+
>>> validator = minimum(0)
|
|
115
|
+
>>> validator(5)
|
|
116
|
+
Success(5)
|
|
117
|
+
>>> validator(0)
|
|
118
|
+
Success(0)
|
|
119
|
+
>>> validator(-1).is_failure()
|
|
120
|
+
True
|
|
121
|
+
>>> # With custom error message
|
|
122
|
+
>>> validator = minimum(18, error_message="Must be an adult")
|
|
123
|
+
>>> validator(17).error_or("")
|
|
124
|
+
'Must be an adult'
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def validator(value: N) -> Maybe[N]:
|
|
129
|
+
if value >= min_value:
|
|
130
|
+
return Maybe.success(value)
|
|
131
|
+
return Maybe.failure(error_message or f'Value must be at least {min_value}')
|
|
132
|
+
|
|
133
|
+
return Validator(validator)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
137
|
+
"""Create a validator that ensures a value is at most the maximum.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
max_value: The maximum allowed value (inclusive)
|
|
141
|
+
error_message: Optional custom error message
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Validator[N]: A validator function that accepts values <= max_value
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
>>> from valid8r.core.validators import maximum
|
|
148
|
+
>>> validator = maximum(100)
|
|
149
|
+
>>> validator(50)
|
|
150
|
+
Success(50)
|
|
151
|
+
>>> validator(100)
|
|
152
|
+
Success(100)
|
|
153
|
+
>>> validator(101).is_failure()
|
|
154
|
+
True
|
|
155
|
+
>>> # With custom error message
|
|
156
|
+
>>> validator = maximum(120, error_message="Age too high")
|
|
157
|
+
>>> validator(150).error_or("")
|
|
158
|
+
'Age too high'
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def validator(value: N) -> Maybe[N]:
|
|
163
|
+
if value <= max_value:
|
|
164
|
+
return Maybe.success(value)
|
|
165
|
+
return Maybe.failure(error_message or f'Value must be at most {max_value}')
|
|
166
|
+
|
|
167
|
+
return Validator(validator)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def between(min_value: N, max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
171
|
+
"""Create a validator that ensures a value is between minimum and maximum (inclusive).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
min_value: The minimum allowed value (inclusive)
|
|
175
|
+
max_value: The maximum allowed value (inclusive)
|
|
176
|
+
error_message: Optional custom error message
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Validator[N]: A validator function that accepts values where min_value <= value <= max_value
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> from valid8r.core.validators import between
|
|
183
|
+
>>> validator = between(0, 100)
|
|
184
|
+
>>> validator(50)
|
|
185
|
+
Success(50)
|
|
186
|
+
>>> validator(0)
|
|
187
|
+
Success(0)
|
|
188
|
+
>>> validator(100)
|
|
189
|
+
Success(100)
|
|
190
|
+
>>> validator(-1).is_failure()
|
|
191
|
+
True
|
|
192
|
+
>>> validator(101).is_failure()
|
|
193
|
+
True
|
|
194
|
+
>>> # With custom error message
|
|
195
|
+
>>> validator = between(1, 10, error_message="Rating must be 1-10")
|
|
196
|
+
>>> validator(11).error_or("")
|
|
197
|
+
'Rating must be 1-10'
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def validator(value: N) -> Maybe[N]:
|
|
202
|
+
if min_value <= value <= max_value:
|
|
203
|
+
return Maybe.success(value)
|
|
204
|
+
return Maybe.failure(error_message or f'Value must be between {min_value} and {max_value}')
|
|
205
|
+
|
|
206
|
+
return Validator(validator)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
|
|
210
|
+
"""Create a validator using a custom predicate function.
|
|
211
|
+
|
|
212
|
+
Allows creating custom validators for any validation logic by providing
|
|
213
|
+
a predicate function that returns True for valid values.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
pred: A function that takes a value and returns True if valid, False otherwise
|
|
217
|
+
error_message: Error message to return when validation fails
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Validator[T]: A validator function that applies the predicate
|
|
221
|
+
|
|
222
|
+
Examples:
|
|
223
|
+
>>> from valid8r.core.validators import predicate
|
|
224
|
+
>>> # Validate even numbers
|
|
225
|
+
>>> is_even = predicate(lambda x: x % 2 == 0, "Must be even")
|
|
226
|
+
>>> is_even(4)
|
|
227
|
+
Success(4)
|
|
228
|
+
>>> is_even(3).is_failure()
|
|
229
|
+
True
|
|
230
|
+
>>> # Validate string patterns
|
|
231
|
+
>>> starts_with_a = predicate(lambda s: s.startswith('a'), "Must start with 'a'")
|
|
232
|
+
>>> starts_with_a("apple")
|
|
233
|
+
Success('apple')
|
|
234
|
+
>>> starts_with_a("banana").error_or("")
|
|
235
|
+
"Must start with 'a'"
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def validator(value: T) -> Maybe[T]:
|
|
240
|
+
if pred(value):
|
|
241
|
+
return Maybe.success(value)
|
|
242
|
+
return Maybe.failure(error_message)
|
|
243
|
+
|
|
244
|
+
return Validator(validator)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def length(min_length: int, max_length: int, error_message: str | None = None) -> Validator[str]:
|
|
248
|
+
"""Create a validator that ensures a string's length is within bounds.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
min_length: Minimum length of the string (inclusive)
|
|
252
|
+
max_length: Maximum length of the string (inclusive)
|
|
253
|
+
error_message: Optional custom error message
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Validator[str]: A validator function that checks string length
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
>>> from valid8r.core.validators import length
|
|
260
|
+
>>> validator = length(3, 10)
|
|
261
|
+
>>> validator("hello")
|
|
262
|
+
Success('hello')
|
|
263
|
+
>>> validator("abc")
|
|
264
|
+
Success('abc')
|
|
265
|
+
>>> validator("abcdefghij")
|
|
266
|
+
Success('abcdefghij')
|
|
267
|
+
>>> validator("ab").is_failure()
|
|
268
|
+
True
|
|
269
|
+
>>> validator("abcdefghijk").is_failure()
|
|
270
|
+
True
|
|
271
|
+
>>> # With custom error message
|
|
272
|
+
>>> validator = length(8, 20, error_message="Password must be 8-20 characters")
|
|
273
|
+
>>> validator("short").error_or("")
|
|
274
|
+
'Password must be 8-20 characters'
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def validator(value: str) -> Maybe[str]:
|
|
279
|
+
if min_length <= len(value) <= max_length:
|
|
280
|
+
return Maybe.success(value)
|
|
281
|
+
return Maybe.failure(error_message or f'String length must be between {min_length} and {max_length}')
|
|
282
|
+
|
|
283
|
+
return Validator(validator)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def matches_regex(pattern: str | re.Pattern[str], error_message: str | None = None) -> Validator[str]:
|
|
287
|
+
r"""Create a validator that ensures a string matches a regular expression pattern.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
pattern: Regular expression pattern (string or compiled Pattern object)
|
|
291
|
+
error_message: Optional custom error message
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Validator[str]: A validator function that checks pattern matching
|
|
295
|
+
|
|
296
|
+
Examples:
|
|
297
|
+
>>> from valid8r.core.validators import matches_regex
|
|
298
|
+
>>> import re
|
|
299
|
+
>>> # String pattern
|
|
300
|
+
>>> validator = matches_regex(r'^\\d{3}-\\d{2}-\\d{4}$')
|
|
301
|
+
>>> validator('123-45-6789')
|
|
302
|
+
Success('123-45-6789')
|
|
303
|
+
>>> validator('invalid').is_failure()
|
|
304
|
+
True
|
|
305
|
+
>>> # Compiled regex pattern
|
|
306
|
+
>>> pattern = re.compile(r'^[A-Z][a-z]+$')
|
|
307
|
+
>>> validator = matches_regex(pattern)
|
|
308
|
+
>>> validator('Hello')
|
|
309
|
+
Success('Hello')
|
|
310
|
+
>>> validator('hello').is_failure()
|
|
311
|
+
True
|
|
312
|
+
>>> # With custom error message
|
|
313
|
+
>>> validator = matches_regex(r'^\\d{5}$', error_message='Must be a 5-digit ZIP code')
|
|
314
|
+
>>> validator('1234').error_or('')
|
|
315
|
+
'Must be a 5-digit ZIP code'
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
compiled_pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
|
|
319
|
+
|
|
320
|
+
def validator(value: str) -> Maybe[str]:
|
|
321
|
+
if compiled_pattern.match(value):
|
|
322
|
+
return Maybe.success(value)
|
|
323
|
+
return Maybe.failure(error_message or f'Value must match pattern {compiled_pattern.pattern}')
|
|
324
|
+
|
|
325
|
+
return Validator(validator)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def in_set(allowed_values: set[T], error_message: str | None = None) -> Validator[T]:
|
|
329
|
+
"""Create a validator that ensures a value is in a set of allowed values.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
allowed_values: Set of allowed values
|
|
333
|
+
error_message: Optional custom error message
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Validator[T]: A validator function that checks membership
|
|
337
|
+
|
|
338
|
+
Examples:
|
|
339
|
+
>>> from valid8r.core.validators import in_set
|
|
340
|
+
>>> # String values
|
|
341
|
+
>>> validator = in_set({'red', 'green', 'blue'})
|
|
342
|
+
>>> validator('red')
|
|
343
|
+
Success('red')
|
|
344
|
+
>>> validator('yellow').is_failure()
|
|
345
|
+
True
|
|
346
|
+
>>> # Numeric values
|
|
347
|
+
>>> validator = in_set({1, 2, 3, 4, 5})
|
|
348
|
+
>>> validator(3)
|
|
349
|
+
Success(3)
|
|
350
|
+
>>> validator(10).is_failure()
|
|
351
|
+
True
|
|
352
|
+
>>> # With custom error message
|
|
353
|
+
>>> validator = in_set({'small', 'medium', 'large'}, error_message='Size must be S, M, or L')
|
|
354
|
+
>>> validator('extra-large').error_or('')
|
|
355
|
+
'Size must be S, M, or L'
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def validator(value: T) -> Maybe[T]:
|
|
360
|
+
if value in allowed_values:
|
|
361
|
+
return Maybe.success(value)
|
|
362
|
+
return Maybe.failure(error_message or f'Value must be one of {allowed_values}')
|
|
363
|
+
|
|
364
|
+
return Validator(validator)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def non_empty_string(error_message: str | None = None) -> Validator[str]:
|
|
368
|
+
"""Create a validator that ensures a string is not empty.
|
|
369
|
+
|
|
370
|
+
Validates that a string contains at least one non-whitespace character.
|
|
371
|
+
Both empty strings and whitespace-only strings are rejected.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
error_message: Optional custom error message
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Validator[str]: A validator function that checks for non-empty strings
|
|
378
|
+
|
|
379
|
+
Examples:
|
|
380
|
+
>>> from valid8r.core.validators import non_empty_string
|
|
381
|
+
>>> validator = non_empty_string()
|
|
382
|
+
>>> validator('hello')
|
|
383
|
+
Success('hello')
|
|
384
|
+
>>> validator(' hello ')
|
|
385
|
+
Success(' hello ')
|
|
386
|
+
>>> validator('').is_failure()
|
|
387
|
+
True
|
|
388
|
+
>>> validator(' ').is_failure()
|
|
389
|
+
True
|
|
390
|
+
>>> # With custom error message
|
|
391
|
+
>>> validator = non_empty_string(error_message='Name is required')
|
|
392
|
+
>>> validator('').error_or('')
|
|
393
|
+
'Name is required'
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def validator(value: str) -> Maybe[str]:
|
|
398
|
+
if value.strip():
|
|
399
|
+
return Maybe.success(value)
|
|
400
|
+
return Maybe.failure(error_message or 'String must not be empty')
|
|
401
|
+
|
|
402
|
+
return Validator(validator)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def unique_items(error_message: str | None = None) -> Validator[list[T]]:
|
|
406
|
+
"""Create a validator that ensures all items in a list are unique.
|
|
407
|
+
|
|
408
|
+
Validates that a list contains no duplicate elements by comparing
|
|
409
|
+
the list length to the set length.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
error_message: Optional custom error message
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Validator[list[T]]: A validator function that checks for unique items
|
|
416
|
+
|
|
417
|
+
Examples:
|
|
418
|
+
>>> from valid8r.core.validators import unique_items
|
|
419
|
+
>>> validator = unique_items()
|
|
420
|
+
>>> validator([1, 2, 3, 4, 5])
|
|
421
|
+
Success([1, 2, 3, 4, 5])
|
|
422
|
+
>>> validator([1, 2, 2, 3]).is_failure()
|
|
423
|
+
True
|
|
424
|
+
>>> # Works with strings
|
|
425
|
+
>>> validator(['a', 'b', 'c'])
|
|
426
|
+
Success(['a', 'b', 'c'])
|
|
427
|
+
>>> validator(['a', 'b', 'a']).is_failure()
|
|
428
|
+
True
|
|
429
|
+
>>> # With custom error message
|
|
430
|
+
>>> validator = unique_items(error_message='Duplicate items found')
|
|
431
|
+
>>> validator([1, 1, 2]).error_or('')
|
|
432
|
+
'Duplicate items found'
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def validator(value: list[T]) -> Maybe[list[T]]:
|
|
437
|
+
if len(value) == len(set(value)):
|
|
438
|
+
return Maybe.success(value)
|
|
439
|
+
return Maybe.failure(error_message or 'All items must be unique')
|
|
440
|
+
|
|
441
|
+
return Validator(validator)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def subset_of(allowed_set: set[T], error_message: str | None = None) -> Validator[set[T]]:
|
|
445
|
+
"""Create a validator that ensures a set is a subset of allowed values.
|
|
446
|
+
|
|
447
|
+
Validates that all elements in the input set are contained within
|
|
448
|
+
the allowed set. An empty set is always a valid subset.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
allowed_set: The set of allowed values
|
|
452
|
+
error_message: Optional custom error message
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Validator[set[T]]: A validator function that checks subset relationship
|
|
456
|
+
|
|
457
|
+
Examples:
|
|
458
|
+
>>> from valid8r.core.validators import subset_of
|
|
459
|
+
>>> validator = subset_of({1, 2, 3, 4, 5})
|
|
460
|
+
>>> validator({1, 2, 3})
|
|
461
|
+
Success({1, 2, 3})
|
|
462
|
+
>>> validator({1, 2, 3, 4, 5, 6}).is_failure()
|
|
463
|
+
True
|
|
464
|
+
>>> # Empty set is valid subset
|
|
465
|
+
>>> validator(set())
|
|
466
|
+
Success(set())
|
|
467
|
+
>>> # With custom error message
|
|
468
|
+
>>> validator = subset_of({'a', 'b', 'c'}, error_message='Invalid characters')
|
|
469
|
+
>>> validator({'a', 'd'}).error_or('')
|
|
470
|
+
'Invalid characters'
|
|
471
|
+
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def validator(value: set[T]) -> Maybe[set[T]]:
|
|
475
|
+
if value.issubset(allowed_set):
|
|
476
|
+
return Maybe.success(value)
|
|
477
|
+
return Maybe.failure(error_message or f'Value must be a subset of {allowed_set}')
|
|
478
|
+
|
|
479
|
+
return Validator(validator)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def superset_of(required_set: set[T], error_message: str | None = None) -> Validator[set[T]]:
|
|
483
|
+
"""Create a validator that ensures a set is a superset of required values.
|
|
484
|
+
|
|
485
|
+
Validates that the input set contains all elements from the required set.
|
|
486
|
+
The input set may contain additional elements beyond those required.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
required_set: The set of required values
|
|
490
|
+
error_message: Optional custom error message
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Validator[set[T]]: A validator function that checks superset relationship
|
|
494
|
+
|
|
495
|
+
Examples:
|
|
496
|
+
>>> from valid8r.core.validators import superset_of
|
|
497
|
+
>>> validator = superset_of({1, 2, 3})
|
|
498
|
+
>>> validator({1, 2, 3, 4, 5})
|
|
499
|
+
Success({1, 2, 3, 4, 5})
|
|
500
|
+
>>> validator({1, 2}).is_failure()
|
|
501
|
+
True
|
|
502
|
+
>>> # Exact match is valid
|
|
503
|
+
>>> validator({1, 2, 3})
|
|
504
|
+
Success({1, 2, 3})
|
|
505
|
+
>>> # With custom error message
|
|
506
|
+
>>> validator = superset_of({'read', 'write'}, error_message='Missing required permissions')
|
|
507
|
+
>>> validator({'read'}).error_or('')
|
|
508
|
+
'Missing required permissions'
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
def validator(value: set[T]) -> Maybe[set[T]]:
|
|
513
|
+
if value.issuperset(required_set):
|
|
514
|
+
return Maybe.success(value)
|
|
515
|
+
return Maybe.failure(error_message or f'Value must be a superset of {required_set}')
|
|
516
|
+
|
|
517
|
+
return Validator(validator)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def is_sorted(*, reverse: bool = False, error_message: str | None = None) -> Validator[list[N]]:
|
|
521
|
+
"""Create a validator that ensures a list is sorted.
|
|
522
|
+
|
|
523
|
+
Validates that a list is sorted in either ascending or descending order.
|
|
524
|
+
Uses keyword-only parameters to avoid boolean trap anti-pattern.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
reverse: If True, checks for descending order; otherwise ascending (default)
|
|
528
|
+
error_message: Optional custom error message
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Validator[list[N]]: A validator function that checks if list is sorted
|
|
532
|
+
|
|
533
|
+
Examples:
|
|
534
|
+
>>> from valid8r.core.validators import is_sorted
|
|
535
|
+
>>> # Ascending order (default)
|
|
536
|
+
>>> validator = is_sorted()
|
|
537
|
+
>>> validator([1, 2, 3, 4, 5])
|
|
538
|
+
Success([1, 2, 3, 4, 5])
|
|
539
|
+
>>> validator([3, 1, 4, 2]).is_failure()
|
|
540
|
+
True
|
|
541
|
+
>>> # Descending order
|
|
542
|
+
>>> validator = is_sorted(reverse=True)
|
|
543
|
+
>>> validator([5, 4, 3, 2, 1])
|
|
544
|
+
Success([5, 4, 3, 2, 1])
|
|
545
|
+
>>> validator([1, 2, 3]).is_failure()
|
|
546
|
+
True
|
|
547
|
+
>>> # Works with strings
|
|
548
|
+
>>> validator = is_sorted()
|
|
549
|
+
>>> validator(['a', 'b', 'c'])
|
|
550
|
+
Success(['a', 'b', 'c'])
|
|
551
|
+
>>> # With custom error message
|
|
552
|
+
>>> validator = is_sorted(error_message='List must be in order')
|
|
553
|
+
>>> validator([3, 1, 2]).error_or('')
|
|
554
|
+
'List must be in order'
|
|
555
|
+
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
def validator(value: list[N]) -> Maybe[list[N]]:
|
|
559
|
+
sorted_value = sorted(value, reverse=reverse)
|
|
560
|
+
if value == sorted_value:
|
|
561
|
+
return Maybe.success(value)
|
|
562
|
+
direction = 'descending' if reverse else 'ascending'
|
|
563
|
+
return Maybe.failure(error_message or f'List must be sorted in {direction} order')
|
|
564
|
+
|
|
565
|
+
return Validator(validator)
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
"""Core validators for validating values against specific criteria.
|
|
2
|
-
|
|
3
|
-
This module provides a collection of validator functions for common validation scenarios.
|
|
4
|
-
All validators follow the same pattern - they take a value and return a Maybe object
|
|
5
|
-
that either contains the validated value or an error message.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from typing import (
|
|
11
|
-
TYPE_CHECKING,
|
|
12
|
-
Generic,
|
|
13
|
-
Protocol,
|
|
14
|
-
TypeVar,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
from valid8r.core.combinators import (
|
|
18
|
-
and_then,
|
|
19
|
-
not_validator,
|
|
20
|
-
or_else,
|
|
21
|
-
)
|
|
22
|
-
from valid8r.core.maybe import Maybe
|
|
23
|
-
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
from collections.abc import Callable
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class SupportsComparison(Protocol): # noqa: D101
|
|
29
|
-
def __le__(self, other: object, /) -> bool: ... # noqa: D105
|
|
30
|
-
def __lt__(self, other: object, /) -> bool: ... # noqa: D105
|
|
31
|
-
def __ge__(self, other: object, /) -> bool: ... # noqa: D105
|
|
32
|
-
def __gt__(self, other: object, /) -> bool: ... # noqa: D105
|
|
33
|
-
def __eq__(self, other: object, /) -> bool: ... # noqa: D105
|
|
34
|
-
def __ne__(self, other: object, /) -> bool: ... # noqa: D105
|
|
35
|
-
def __hash__(self, /) -> int: ... # noqa: D105
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
T = TypeVar('T')
|
|
39
|
-
U = TypeVar('U')
|
|
40
|
-
N = TypeVar('N', bound=SupportsComparison)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class Validator(Generic[T]):
|
|
44
|
-
"""A wrapper class for validator functions that supports operator overloading."""
|
|
45
|
-
|
|
46
|
-
def __init__(self, func: Callable[[T], Maybe[T]]) -> None:
|
|
47
|
-
"""Initialize a validator with a validation function.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
func: A function that takes a value and returns a Maybe
|
|
51
|
-
|
|
52
|
-
"""
|
|
53
|
-
self.func = func
|
|
54
|
-
|
|
55
|
-
def __call__(self, value: T) -> Maybe[T]:
|
|
56
|
-
"""Apply the validator to a value.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
value: The value to validate
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
A Maybe containing either the validated value or an error
|
|
63
|
-
|
|
64
|
-
"""
|
|
65
|
-
return self.func(value)
|
|
66
|
-
|
|
67
|
-
def __and__(self, other: Validator[T]) -> Validator[T]:
|
|
68
|
-
"""Combine with another validator using logical AND.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
other: Another validator to combine with
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
A new validator that passes only if both validators pass
|
|
75
|
-
|
|
76
|
-
"""
|
|
77
|
-
return Validator(lambda value: and_then(self.func, other.func)(value))
|
|
78
|
-
|
|
79
|
-
def __or__(self, other: Validator[T]) -> Validator[T]:
|
|
80
|
-
"""Combine with another validator using logical OR.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
other: Another validator to combine with
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
A new validator that passes if either validator passes
|
|
87
|
-
|
|
88
|
-
"""
|
|
89
|
-
return Validator(lambda value: or_else(self.func, other.func)(value))
|
|
90
|
-
|
|
91
|
-
def __invert__(self) -> Validator[T]:
|
|
92
|
-
"""Negate this validator.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
A new validator that passes if this validator fails
|
|
96
|
-
|
|
97
|
-
"""
|
|
98
|
-
return Validator(lambda value: not_validator(self.func, 'Negated validation failed')(value))
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
|
|
102
|
-
"""Create a validator that ensures a value is at least the minimum.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
min_value: The minimum allowed value (inclusive)
|
|
106
|
-
error_message: Optional custom error message
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
Validator[N]: A validator function that accepts values >= min_value
|
|
110
|
-
|
|
111
|
-
Examples:
|
|
112
|
-
>>> from valid8r.core.validators import minimum
|
|
113
|
-
>>> validator = minimum(0)
|
|
114
|
-
>>> validator(5)
|
|
115
|
-
Success(5)
|
|
116
|
-
>>> validator(0)
|
|
117
|
-
Success(0)
|
|
118
|
-
>>> validator(-1).is_failure()
|
|
119
|
-
True
|
|
120
|
-
>>> # With custom error message
|
|
121
|
-
>>> validator = minimum(18, error_message="Must be an adult")
|
|
122
|
-
>>> validator(17).error_or("")
|
|
123
|
-
'Must be an adult'
|
|
124
|
-
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
def validator(value: N) -> Maybe[N]:
|
|
128
|
-
if value >= min_value:
|
|
129
|
-
return Maybe.success(value)
|
|
130
|
-
return Maybe.failure(error_message or f'Value must be at least {min_value}')
|
|
131
|
-
|
|
132
|
-
return Validator(validator)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
136
|
-
"""Create a validator that ensures a value is at most the maximum.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
max_value: The maximum allowed value (inclusive)
|
|
140
|
-
error_message: Optional custom error message
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Validator[N]: A validator function that accepts values <= max_value
|
|
144
|
-
|
|
145
|
-
Examples:
|
|
146
|
-
>>> from valid8r.core.validators import maximum
|
|
147
|
-
>>> validator = maximum(100)
|
|
148
|
-
>>> validator(50)
|
|
149
|
-
Success(50)
|
|
150
|
-
>>> validator(100)
|
|
151
|
-
Success(100)
|
|
152
|
-
>>> validator(101).is_failure()
|
|
153
|
-
True
|
|
154
|
-
>>> # With custom error message
|
|
155
|
-
>>> validator = maximum(120, error_message="Age too high")
|
|
156
|
-
>>> validator(150).error_or("")
|
|
157
|
-
'Age too high'
|
|
158
|
-
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
def validator(value: N) -> Maybe[N]:
|
|
162
|
-
if value <= max_value:
|
|
163
|
-
return Maybe.success(value)
|
|
164
|
-
return Maybe.failure(error_message or f'Value must be at most {max_value}')
|
|
165
|
-
|
|
166
|
-
return Validator(validator)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def between(min_value: N, max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
170
|
-
"""Create a validator that ensures a value is between minimum and maximum (inclusive).
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
min_value: The minimum allowed value (inclusive)
|
|
174
|
-
max_value: The maximum allowed value (inclusive)
|
|
175
|
-
error_message: Optional custom error message
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
Validator[N]: A validator function that accepts values where min_value <= value <= max_value
|
|
179
|
-
|
|
180
|
-
Examples:
|
|
181
|
-
>>> from valid8r.core.validators import between
|
|
182
|
-
>>> validator = between(0, 100)
|
|
183
|
-
>>> validator(50)
|
|
184
|
-
Success(50)
|
|
185
|
-
>>> validator(0)
|
|
186
|
-
Success(0)
|
|
187
|
-
>>> validator(100)
|
|
188
|
-
Success(100)
|
|
189
|
-
>>> validator(-1).is_failure()
|
|
190
|
-
True
|
|
191
|
-
>>> validator(101).is_failure()
|
|
192
|
-
True
|
|
193
|
-
>>> # With custom error message
|
|
194
|
-
>>> validator = between(1, 10, error_message="Rating must be 1-10")
|
|
195
|
-
>>> validator(11).error_or("")
|
|
196
|
-
'Rating must be 1-10'
|
|
197
|
-
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
def validator(value: N) -> Maybe[N]:
|
|
201
|
-
if min_value <= value <= max_value:
|
|
202
|
-
return Maybe.success(value)
|
|
203
|
-
return Maybe.failure(error_message or f'Value must be between {min_value} and {max_value}')
|
|
204
|
-
|
|
205
|
-
return Validator(validator)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
|
|
209
|
-
"""Create a validator using a custom predicate function.
|
|
210
|
-
|
|
211
|
-
Allows creating custom validators for any validation logic by providing
|
|
212
|
-
a predicate function that returns True for valid values.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
pred: A function that takes a value and returns True if valid, False otherwise
|
|
216
|
-
error_message: Error message to return when validation fails
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
Validator[T]: A validator function that applies the predicate
|
|
220
|
-
|
|
221
|
-
Examples:
|
|
222
|
-
>>> from valid8r.core.validators import predicate
|
|
223
|
-
>>> # Validate even numbers
|
|
224
|
-
>>> is_even = predicate(lambda x: x % 2 == 0, "Must be even")
|
|
225
|
-
>>> is_even(4)
|
|
226
|
-
Success(4)
|
|
227
|
-
>>> is_even(3).is_failure()
|
|
228
|
-
True
|
|
229
|
-
>>> # Validate string patterns
|
|
230
|
-
>>> starts_with_a = predicate(lambda s: s.startswith('a'), "Must start with 'a'")
|
|
231
|
-
>>> starts_with_a("apple")
|
|
232
|
-
Success('apple')
|
|
233
|
-
>>> starts_with_a("banana").error_or("")
|
|
234
|
-
"Must start with 'a'"
|
|
235
|
-
|
|
236
|
-
"""
|
|
237
|
-
|
|
238
|
-
def validator(value: T) -> Maybe[T]:
|
|
239
|
-
if pred(value):
|
|
240
|
-
return Maybe.success(value)
|
|
241
|
-
return Maybe.failure(error_message)
|
|
242
|
-
|
|
243
|
-
return Validator(validator)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def length(min_length: int, max_length: int, error_message: str | None = None) -> Validator[str]:
|
|
247
|
-
"""Create a validator that ensures a string's length is within bounds.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
min_length: Minimum length of the string (inclusive)
|
|
251
|
-
max_length: Maximum length of the string (inclusive)
|
|
252
|
-
error_message: Optional custom error message
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
Validator[str]: A validator function that checks string length
|
|
256
|
-
|
|
257
|
-
Examples:
|
|
258
|
-
>>> from valid8r.core.validators import length
|
|
259
|
-
>>> validator = length(3, 10)
|
|
260
|
-
>>> validator("hello")
|
|
261
|
-
Success('hello')
|
|
262
|
-
>>> validator("abc")
|
|
263
|
-
Success('abc')
|
|
264
|
-
>>> validator("abcdefghij")
|
|
265
|
-
Success('abcdefghij')
|
|
266
|
-
>>> validator("ab").is_failure()
|
|
267
|
-
True
|
|
268
|
-
>>> validator("abcdefghijk").is_failure()
|
|
269
|
-
True
|
|
270
|
-
>>> # With custom error message
|
|
271
|
-
>>> validator = length(8, 20, error_message="Password must be 8-20 characters")
|
|
272
|
-
>>> validator("short").error_or("")
|
|
273
|
-
'Password must be 8-20 characters'
|
|
274
|
-
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
def validator(value: str) -> Maybe[str]:
|
|
278
|
-
if min_length <= len(value) <= max_length:
|
|
279
|
-
return Maybe.success(value)
|
|
280
|
-
return Maybe.failure(error_message or f'String length must be between {min_length} and {max_length}')
|
|
281
|
-
|
|
282
|
-
return Validator(validator)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|