valid8r 0.6.2__py3-none-any.whl → 0.7.0__py3-none-any.whl
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/core/maybe.py +8 -0
- valid8r/core/parsers.py +341 -44
- valid8r/core/validators.py +378 -13
- valid8r/prompt/basic.py +51 -12
- {valid8r-0.6.2.dist-info → valid8r-0.7.0.dist-info}/METADATA +40 -14
- {valid8r-0.6.2.dist-info → valid8r-0.7.0.dist-info}/RECORD +9 -9
- {valid8r-0.6.2.dist-info → valid8r-0.7.0.dist-info}/WHEEL +0 -0
- {valid8r-0.6.2.dist-info → valid8r-0.7.0.dist-info}/entry_points.txt +0 -0
- {valid8r-0.6.2.dist-info → valid8r-0.7.0.dist-info}/licenses/LICENSE +0 -0
valid8r/core/maybe.py
CHANGED
|
@@ -108,6 +108,10 @@ class Success(Maybe[T]):
|
|
|
108
108
|
"""Get a string representation."""
|
|
109
109
|
return f'Success({self.value})'
|
|
110
110
|
|
|
111
|
+
def __repr__(self) -> str:
|
|
112
|
+
"""Get a repr representation for debugging and doctests."""
|
|
113
|
+
return f'Success({self.value!r})'
|
|
114
|
+
|
|
111
115
|
|
|
112
116
|
class Failure(Maybe[T]):
|
|
113
117
|
"""Represents a failed computation with an error message."""
|
|
@@ -160,3 +164,7 @@ class Failure(Maybe[T]):
|
|
|
160
164
|
def __str__(self) -> str:
|
|
161
165
|
"""Get a string representation."""
|
|
162
166
|
return f'Failure({self.error})'
|
|
167
|
+
|
|
168
|
+
def __repr__(self) -> str:
|
|
169
|
+
"""Get a repr representation for debugging and doctests."""
|
|
170
|
+
return f'Failure({self.error!r})'
|
valid8r/core/parsers.py
CHANGED
|
@@ -80,7 +80,30 @@ _PHONE_DIGIT_EXTRACTION_PATTERN = re.compile(r'\D')
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def parse_int(input_value: str, error_message: str | None = None) -> Maybe[int]:
|
|
83
|
-
"""Parse a string to an integer.
|
|
83
|
+
"""Parse a string to an integer.
|
|
84
|
+
|
|
85
|
+
Converts string representations of integers to Python int values.
|
|
86
|
+
Handles whitespace trimming and accepts whole numbers in float notation (e.g., "42.0").
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
input_value: String to parse (leading/trailing whitespace is stripped)
|
|
90
|
+
error_message: Optional custom error message for parsing failures
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Maybe[int]: Success(int) if parsing succeeds, Failure(str) with error message otherwise
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
>>> parse_int("42")
|
|
97
|
+
Success(42)
|
|
98
|
+
>>> parse_int(" -17 ")
|
|
99
|
+
Success(-17)
|
|
100
|
+
>>> parse_int("42.0")
|
|
101
|
+
Success(42)
|
|
102
|
+
>>> parse_int("42.5").is_failure()
|
|
103
|
+
True
|
|
104
|
+
>>> parse_int("not a number").is_failure()
|
|
105
|
+
True
|
|
106
|
+
"""
|
|
84
107
|
if not input_value:
|
|
85
108
|
return Maybe.failure('Input must not be empty')
|
|
86
109
|
|
|
@@ -102,7 +125,28 @@ def parse_int(input_value: str, error_message: str | None = None) -> Maybe[int]:
|
|
|
102
125
|
|
|
103
126
|
|
|
104
127
|
def parse_float(input_value: str, error_message: str | None = None) -> Maybe[float]:
|
|
105
|
-
"""Parse a string to a
|
|
128
|
+
"""Parse a string to a floating-point number.
|
|
129
|
+
|
|
130
|
+
Converts string representations of numbers to Python float values.
|
|
131
|
+
Handles whitespace trimming and scientific notation.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
input_value: String to parse (leading/trailing whitespace is stripped)
|
|
135
|
+
error_message: Optional custom error message for parsing failures
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Maybe[float]: Success(float) if parsing succeeds, Failure(str) with error message otherwise
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
>>> parse_float("3.14")
|
|
142
|
+
Success(3.14)
|
|
143
|
+
>>> parse_float(" -2.5 ")
|
|
144
|
+
Success(-2.5)
|
|
145
|
+
>>> parse_float("1e-3")
|
|
146
|
+
Success(0.001)
|
|
147
|
+
>>> parse_float("not a number").is_failure()
|
|
148
|
+
True
|
|
149
|
+
"""
|
|
106
150
|
if not input_value:
|
|
107
151
|
return Maybe.failure('Input must not be empty')
|
|
108
152
|
|
|
@@ -114,7 +158,33 @@ def parse_float(input_value: str, error_message: str | None = None) -> Maybe[flo
|
|
|
114
158
|
|
|
115
159
|
|
|
116
160
|
def parse_bool(input_value: str, error_message: str | None = None) -> Maybe[bool]:
|
|
117
|
-
"""Parse a string to a boolean.
|
|
161
|
+
"""Parse a string to a boolean value.
|
|
162
|
+
|
|
163
|
+
Accepts various common representations of true/false values.
|
|
164
|
+
Case-insensitive and handles whitespace.
|
|
165
|
+
|
|
166
|
+
Recognized true values: 'true', 't', 'yes', 'y', '1'
|
|
167
|
+
Recognized false values: 'false', 'f', 'no', 'n', '0'
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
input_value: String to parse (leading/trailing whitespace is stripped, case-insensitive)
|
|
171
|
+
error_message: Optional custom error message for parsing failures
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Maybe[bool]: Success(bool) if parsing succeeds, Failure(str) with error message otherwise
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
>>> parse_bool("true")
|
|
178
|
+
Success(True)
|
|
179
|
+
>>> parse_bool("YES")
|
|
180
|
+
Success(True)
|
|
181
|
+
>>> parse_bool("n")
|
|
182
|
+
Success(False)
|
|
183
|
+
>>> parse_bool(" 0 ")
|
|
184
|
+
Success(False)
|
|
185
|
+
>>> parse_bool("maybe").is_failure()
|
|
186
|
+
True
|
|
187
|
+
"""
|
|
118
188
|
if not input_value:
|
|
119
189
|
return Maybe.failure('Input must not be empty')
|
|
120
190
|
|
|
@@ -133,7 +203,27 @@ def parse_bool(input_value: str, error_message: str | None = None) -> Maybe[bool
|
|
|
133
203
|
|
|
134
204
|
|
|
135
205
|
def parse_date(input_value: str, date_format: str | None = None, error_message: str | None = None) -> Maybe[date]:
|
|
136
|
-
"""Parse a string to a date.
|
|
206
|
+
"""Parse a string to a date object.
|
|
207
|
+
|
|
208
|
+
Parses date strings using ISO 8601 format (YYYY-MM-DD) by default,
|
|
209
|
+
or a custom format if specified.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
input_value: String to parse (leading/trailing whitespace is stripped)
|
|
213
|
+
date_format: Optional strftime format string (e.g., '%Y-%m-%d', '%m/%d/%Y')
|
|
214
|
+
error_message: Optional custom error message for parsing failures
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Maybe[date]: Success(date) if parsing succeeds, Failure(str) with error message otherwise
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
>>> parse_date("2025-01-15")
|
|
221
|
+
Success(datetime.date(2025, 1, 15))
|
|
222
|
+
>>> parse_date("01/15/2025", date_format="%m/%d/%Y")
|
|
223
|
+
Success(datetime.date(2025, 1, 15))
|
|
224
|
+
>>> parse_date("invalid").is_failure()
|
|
225
|
+
True
|
|
226
|
+
"""
|
|
137
227
|
if not input_value:
|
|
138
228
|
return Maybe.failure('Input must not be empty')
|
|
139
229
|
|
|
@@ -157,7 +247,30 @@ def parse_date(input_value: str, date_format: str | None = None, error_message:
|
|
|
157
247
|
|
|
158
248
|
|
|
159
249
|
def parse_complex(input_value: str, error_message: str | None = None) -> Maybe[complex]:
|
|
160
|
-
"""Parse a string to a complex number.
|
|
250
|
+
"""Parse a string to a complex number.
|
|
251
|
+
|
|
252
|
+
Accepts various complex number representations including both 'j' and 'i' notation.
|
|
253
|
+
Handles parentheses and spaces in the input.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
input_value: String to parse (whitespace is stripped, both 'i' and 'j' accepted)
|
|
257
|
+
error_message: Optional custom error message for parsing failures
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Maybe[complex]: Success(complex) if parsing succeeds, Failure(str) with error message otherwise
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
>>> parse_complex("3+4j")
|
|
264
|
+
Success((3+4j))
|
|
265
|
+
>>> parse_complex("3 + 4i")
|
|
266
|
+
Success((3+4j))
|
|
267
|
+
>>> parse_complex("(2-3j)")
|
|
268
|
+
Success((2-3j))
|
|
269
|
+
>>> parse_complex("5j")
|
|
270
|
+
Success(5j)
|
|
271
|
+
>>> parse_complex("invalid").is_failure()
|
|
272
|
+
True
|
|
273
|
+
"""
|
|
161
274
|
if not input_value:
|
|
162
275
|
return Maybe.failure('Input must not be empty')
|
|
163
276
|
|
|
@@ -187,7 +300,10 @@ def parse_complex(input_value: str, error_message: str | None = None) -> Maybe[c
|
|
|
187
300
|
|
|
188
301
|
|
|
189
302
|
def parse_decimal(input_value: str, error_message: str | None = None) -> Maybe[Decimal]:
|
|
190
|
-
"""Parse a string to a Decimal.
|
|
303
|
+
"""Parse a string to a Decimal for precise decimal arithmetic.
|
|
304
|
+
|
|
305
|
+
Uses Python's Decimal type for arbitrary-precision decimal arithmetic,
|
|
306
|
+
avoiding floating-point rounding errors. Ideal for financial calculations.
|
|
191
307
|
|
|
192
308
|
Args:
|
|
193
309
|
input_value: String representation of a decimal number
|
|
@@ -196,6 +312,15 @@ def parse_decimal(input_value: str, error_message: str | None = None) -> Maybe[D
|
|
|
196
312
|
Returns:
|
|
197
313
|
Maybe[Decimal]: Success with Decimal value or Failure with an error message
|
|
198
314
|
|
|
315
|
+
Examples:
|
|
316
|
+
>>> parse_decimal("3.14159")
|
|
317
|
+
Success(Decimal('3.14159'))
|
|
318
|
+
>>> parse_decimal(" 0.1 ")
|
|
319
|
+
Success(Decimal('0.1'))
|
|
320
|
+
>>> parse_decimal("-99.99")
|
|
321
|
+
Success(Decimal('-99.99'))
|
|
322
|
+
>>> parse_decimal("not a number").is_failure()
|
|
323
|
+
True
|
|
199
324
|
"""
|
|
200
325
|
if not input_value:
|
|
201
326
|
return Maybe.failure('Input must not be empty')
|
|
@@ -229,7 +354,34 @@ def _find_enum_by_name(enum_class: type[E], value: str) -> E | None:
|
|
|
229
354
|
|
|
230
355
|
|
|
231
356
|
def parse_enum(input_value: str, enum_class: type[E], error_message: str | None = None) -> Maybe[object]:
|
|
232
|
-
"""Parse a string to an enum
|
|
357
|
+
"""Parse a string to an enum member.
|
|
358
|
+
|
|
359
|
+
Matches input against enum member values and names (case-insensitive for names).
|
|
360
|
+
Handles whitespace trimming and supports enums with empty string values.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
input_value: String to parse (whitespace is stripped for non-exact matches)
|
|
364
|
+
enum_class: The Enum class to parse into
|
|
365
|
+
error_message: Optional custom error message for parsing failures
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Maybe[object]: Success with enum member if valid, Failure(str) with error message otherwise
|
|
369
|
+
|
|
370
|
+
Examples:
|
|
371
|
+
>>> from enum import Enum
|
|
372
|
+
>>> class Color(Enum):
|
|
373
|
+
... RED = 'red'
|
|
374
|
+
... GREEN = 'green'
|
|
375
|
+
... BLUE = 'blue'
|
|
376
|
+
>>> parse_enum("red", Color)
|
|
377
|
+
Success(<Color.RED: 'red'>)
|
|
378
|
+
>>> parse_enum("RED", Color)
|
|
379
|
+
Success(<Color.RED: 'red'>)
|
|
380
|
+
>>> parse_enum(" green ", Color)
|
|
381
|
+
Success(<Color.GREEN: 'green'>)
|
|
382
|
+
>>> parse_enum("yellow", Color).is_failure()
|
|
383
|
+
True
|
|
384
|
+
"""
|
|
233
385
|
if not isinstance(enum_class, type) or not issubclass(enum_class, Enum):
|
|
234
386
|
return Maybe.failure(error_message or 'Invalid enum class provided')
|
|
235
387
|
|
|
@@ -269,15 +421,27 @@ def parse_list(
|
|
|
269
421
|
) -> Maybe[list[T]]:
|
|
270
422
|
"""Parse a string to a list using the specified element parser and separator.
|
|
271
423
|
|
|
424
|
+
Splits the input string by the separator and parses each element using the element parser.
|
|
425
|
+
If no element parser is provided, elements are returned as trimmed strings.
|
|
426
|
+
|
|
272
427
|
Args:
|
|
273
428
|
input_value: The string to parse
|
|
274
|
-
element_parser: A function that parses individual elements
|
|
275
|
-
separator: The string that separates elements
|
|
429
|
+
element_parser: A function that parses individual elements (default: strips whitespace)
|
|
430
|
+
separator: The string that separates elements (default: ',')
|
|
276
431
|
error_message: Custom error message for parsing failures
|
|
277
432
|
|
|
278
433
|
Returns:
|
|
279
|
-
|
|
434
|
+
Maybe[list[T]]: Success with parsed list or Failure with error message
|
|
280
435
|
|
|
436
|
+
Examples:
|
|
437
|
+
>>> parse_list("a,b,c")
|
|
438
|
+
Success(['a', 'b', 'c'])
|
|
439
|
+
>>> parse_list("1, 2, 3", element_parser=parse_int)
|
|
440
|
+
Success([1, 2, 3])
|
|
441
|
+
>>> parse_list("apple|banana|cherry", separator="|")
|
|
442
|
+
Success(['apple', 'banana', 'cherry'])
|
|
443
|
+
>>> parse_list("1,2,invalid", element_parser=parse_int).is_failure()
|
|
444
|
+
True
|
|
281
445
|
"""
|
|
282
446
|
if not input_value:
|
|
283
447
|
return Maybe.failure('Input must not be empty')
|
|
@@ -358,7 +522,32 @@ def parse_dict( # noqa: PLR0913
|
|
|
358
522
|
key_value_separator: str = ':',
|
|
359
523
|
error_message: str | None = None,
|
|
360
524
|
) -> Maybe[dict[K, V]]:
|
|
361
|
-
"""Parse a string to a dictionary using the specified parsers and separators.
|
|
525
|
+
"""Parse a string to a dictionary using the specified parsers and separators.
|
|
526
|
+
|
|
527
|
+
Splits the input string by pair_separator, then splits each pair by key_value_separator.
|
|
528
|
+
Parses keys and values using the provided parsers (defaults to trimmed strings).
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
input_value: The string to parse
|
|
532
|
+
key_parser: A function that parses keys (default: strips whitespace)
|
|
533
|
+
value_parser: A function that parses values (default: strips whitespace)
|
|
534
|
+
pair_separator: The string that separates key-value pairs (default: ',')
|
|
535
|
+
key_value_separator: The string that separates keys from values (default: ':')
|
|
536
|
+
error_message: Custom error message for parsing failures
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Maybe[dict[K, V]]: Success with parsed dictionary or Failure with error message
|
|
540
|
+
|
|
541
|
+
Examples:
|
|
542
|
+
>>> parse_dict("a:1,b:2,c:3")
|
|
543
|
+
Success({'a': '1', 'b': '2', 'c': '3'})
|
|
544
|
+
>>> parse_dict("x:10, y:20", value_parser=parse_int)
|
|
545
|
+
Success({'x': 10, 'y': 20})
|
|
546
|
+
>>> parse_dict("name=Alice|age=30", pair_separator="|", key_value_separator="=")
|
|
547
|
+
Success({'name': 'Alice', 'age': '30'})
|
|
548
|
+
>>> parse_dict("a:1,b:invalid", value_parser=parse_int).is_failure()
|
|
549
|
+
True
|
|
550
|
+
"""
|
|
362
551
|
if not input_value:
|
|
363
552
|
return Maybe.failure('Input must not be empty')
|
|
364
553
|
|
|
@@ -402,15 +591,33 @@ def parse_set(
|
|
|
402
591
|
) -> Maybe[set[T]]:
|
|
403
592
|
"""Parse a string to a set using the specified element parser and separator.
|
|
404
593
|
|
|
594
|
+
Splits the input string by the separator and parses each element using the element parser.
|
|
595
|
+
Automatically removes duplicate values. If no element parser is provided, elements are
|
|
596
|
+
returned as trimmed strings.
|
|
597
|
+
|
|
405
598
|
Args:
|
|
406
599
|
input_value: The string to parse
|
|
407
|
-
element_parser: A function that parses individual elements
|
|
408
|
-
separator: The string that separates elements
|
|
600
|
+
element_parser: A function that parses individual elements (default: strips whitespace)
|
|
601
|
+
separator: The string that separates elements (default: ',')
|
|
409
602
|
error_message: Custom error message for parsing failures
|
|
410
603
|
|
|
411
604
|
Returns:
|
|
412
|
-
|
|
605
|
+
Maybe[set[T]]: Success with parsed set or Failure with error message
|
|
413
606
|
|
|
607
|
+
Examples:
|
|
608
|
+
>>> result = parse_set("a,b,c")
|
|
609
|
+
>>> result.is_success()
|
|
610
|
+
True
|
|
611
|
+
>>> sorted(result.value_or(set()))
|
|
612
|
+
['a', 'b', 'c']
|
|
613
|
+
>>> result = parse_set("1, 2, 3, 2, 1", element_parser=parse_int)
|
|
614
|
+
>>> sorted(result.value_or(set()))
|
|
615
|
+
[1, 2, 3]
|
|
616
|
+
>>> result = parse_set("red|blue|green|red", separator="|")
|
|
617
|
+
>>> sorted(result.value_or(set()))
|
|
618
|
+
['blue', 'green', 'red']
|
|
619
|
+
>>> parse_set("1,2,invalid", element_parser=parse_int).is_failure()
|
|
620
|
+
True
|
|
414
621
|
"""
|
|
415
622
|
if separator is None:
|
|
416
623
|
separator = ','
|
|
@@ -433,7 +640,10 @@ def parse_int_with_validation(
|
|
|
433
640
|
max_value: int | None = None,
|
|
434
641
|
error_message: str | None = None,
|
|
435
642
|
) -> Maybe[int]:
|
|
436
|
-
"""Parse a string to an integer with validation.
|
|
643
|
+
"""Parse a string to an integer with range validation.
|
|
644
|
+
|
|
645
|
+
Combines parsing and validation in a single step. First parses the string to an integer,
|
|
646
|
+
then validates it falls within the specified range.
|
|
437
647
|
|
|
438
648
|
Args:
|
|
439
649
|
input_value: The string to parse
|
|
@@ -442,8 +652,17 @@ def parse_int_with_validation(
|
|
|
442
652
|
error_message: Custom error message for parsing failures
|
|
443
653
|
|
|
444
654
|
Returns:
|
|
445
|
-
|
|
655
|
+
Maybe[int]: Success with validated integer or Failure with error message
|
|
446
656
|
|
|
657
|
+
Examples:
|
|
658
|
+
>>> parse_int_with_validation("42", min_value=0, max_value=100)
|
|
659
|
+
Success(42)
|
|
660
|
+
>>> parse_int_with_validation("5", min_value=10).is_failure()
|
|
661
|
+
True
|
|
662
|
+
>>> parse_int_with_validation("150", max_value=100).is_failure()
|
|
663
|
+
True
|
|
664
|
+
>>> parse_int_with_validation("50", min_value=0, max_value=100)
|
|
665
|
+
Success(50)
|
|
447
666
|
"""
|
|
448
667
|
result = parse_int(input_value, error_message)
|
|
449
668
|
if result.is_failure():
|
|
@@ -469,7 +688,10 @@ def parse_list_with_validation( # noqa: PLR0913
|
|
|
469
688
|
max_length: int | None = None,
|
|
470
689
|
error_message: str | None = None,
|
|
471
690
|
) -> Maybe[list[T]]:
|
|
472
|
-
"""Parse a string to a list with validation.
|
|
691
|
+
"""Parse a string to a list with length validation.
|
|
692
|
+
|
|
693
|
+
Combines parsing and validation in a single step. First parses the string to a list,
|
|
694
|
+
then validates it has an acceptable number of elements.
|
|
473
695
|
|
|
474
696
|
Args:
|
|
475
697
|
input_value: The string to parse
|
|
@@ -480,8 +702,17 @@ def parse_list_with_validation( # noqa: PLR0913
|
|
|
480
702
|
error_message: Custom error message for parsing failures
|
|
481
703
|
|
|
482
704
|
Returns:
|
|
483
|
-
|
|
705
|
+
Maybe[list[T]]: Success with validated list or Failure with error message
|
|
484
706
|
|
|
707
|
+
Examples:
|
|
708
|
+
>>> parse_list_with_validation("a,b,c", min_length=2, max_length=5)
|
|
709
|
+
Success(['a', 'b', 'c'])
|
|
710
|
+
>>> parse_list_with_validation("1,2", element_parser=parse_int, min_length=3).is_failure()
|
|
711
|
+
True
|
|
712
|
+
>>> parse_list_with_validation("1,2,3,4,5,6", max_length=5).is_failure()
|
|
713
|
+
True
|
|
714
|
+
>>> parse_list_with_validation("10,20,30", element_parser=parse_int, min_length=1)
|
|
715
|
+
Success([10, 20, 30])
|
|
485
716
|
"""
|
|
486
717
|
result = parse_list(input_value, element_parser, separator, error_message)
|
|
487
718
|
if result.is_failure():
|
|
@@ -508,7 +739,10 @@ def parse_dict_with_validation( # noqa: PLR0913
|
|
|
508
739
|
required_keys: list[str] | None = None,
|
|
509
740
|
error_message: str | None = None,
|
|
510
741
|
) -> Maybe[dict[K, V]]:
|
|
511
|
-
"""Parse a string to a dictionary with validation.
|
|
742
|
+
"""Parse a string to a dictionary with required keys validation.
|
|
743
|
+
|
|
744
|
+
Combines parsing and validation in a single step. First parses the string to a dictionary,
|
|
745
|
+
then validates that all required keys are present.
|
|
512
746
|
|
|
513
747
|
Args:
|
|
514
748
|
input_value: The string to parse
|
|
@@ -520,8 +754,16 @@ def parse_dict_with_validation( # noqa: PLR0913
|
|
|
520
754
|
error_message: Custom error message for parsing failures
|
|
521
755
|
|
|
522
756
|
Returns:
|
|
523
|
-
|
|
757
|
+
Maybe[dict[K, V]]: Success with validated dictionary or Failure with error message
|
|
524
758
|
|
|
759
|
+
Examples:
|
|
760
|
+
>>> parse_dict_with_validation("name:Alice,age:30", required_keys=["name", "age"])
|
|
761
|
+
Success({'name': 'Alice', 'age': '30'})
|
|
762
|
+
>>> parse_dict_with_validation("name:Bob", required_keys=["name", "age"]).is_failure()
|
|
763
|
+
True
|
|
764
|
+
>>> result = parse_dict_with_validation("x:10,y:20", value_parser=parse_int, required_keys=["x"])
|
|
765
|
+
>>> result.value_or({})
|
|
766
|
+
{'x': 10, 'y': 20}
|
|
525
767
|
"""
|
|
526
768
|
result = parse_dict(input_value, key_parser, value_parser, pair_separator, key_value_separator, error_message)
|
|
527
769
|
if result.is_failure():
|
|
@@ -705,13 +947,24 @@ def parse_uuid(text: str, version: int | None = None, strict: bool = True) -> Ma
|
|
|
705
947
|
def parse_ipv4(text: str) -> Maybe[IPv4Address]:
|
|
706
948
|
"""Parse an IPv4 address string.
|
|
707
949
|
|
|
708
|
-
|
|
709
|
-
|
|
950
|
+
Validates and parses IPv4 addresses in dotted-decimal notation.
|
|
951
|
+
Trims surrounding whitespace.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
text: String containing an IPv4 address (whitespace is stripped)
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
Maybe[IPv4Address]: Success(IPv4Address) if valid, Failure(str) with error message otherwise
|
|
710
958
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
959
|
+
Examples:
|
|
960
|
+
>>> parse_ipv4("192.168.1.1")
|
|
961
|
+
Success(IPv4Address('192.168.1.1'))
|
|
962
|
+
>>> parse_ipv4(" 10.0.0.1 ")
|
|
963
|
+
Success(IPv4Address('10.0.0.1'))
|
|
964
|
+
>>> parse_ipv4("256.1.1.1").is_failure()
|
|
965
|
+
True
|
|
966
|
+
>>> parse_ipv4("not an ip").is_failure()
|
|
967
|
+
True
|
|
715
968
|
"""
|
|
716
969
|
if not isinstance(text, str):
|
|
717
970
|
return Maybe.failure('Input must be a string')
|
|
@@ -734,13 +987,24 @@ def parse_ipv4(text: str) -> Maybe[IPv4Address]:
|
|
|
734
987
|
def parse_ipv6(text: str) -> Maybe[IPv6Address]:
|
|
735
988
|
"""Parse an IPv6 address string.
|
|
736
989
|
|
|
737
|
-
|
|
738
|
-
|
|
990
|
+
Validates and parses IPv6 addresses in standard notation.
|
|
991
|
+
Rejects scope IDs (e.g., %eth0). Trims surrounding whitespace.
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
text: String containing an IPv6 address (whitespace is stripped)
|
|
995
|
+
|
|
996
|
+
Returns:
|
|
997
|
+
Maybe[IPv6Address]: Success(IPv6Address) if valid, Failure(str) with error message otherwise
|
|
739
998
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
999
|
+
Examples:
|
|
1000
|
+
>>> parse_ipv6("::1")
|
|
1001
|
+
Success(IPv6Address('::1'))
|
|
1002
|
+
>>> parse_ipv6("2001:0db8:85a3::8a2e:0370:7334")
|
|
1003
|
+
Success(IPv6Address('2001:db8:85a3::8a2e:370:7334'))
|
|
1004
|
+
>>> parse_ipv6(" fe80::1 ")
|
|
1005
|
+
Success(IPv6Address('fe80::1'))
|
|
1006
|
+
>>> parse_ipv6("192.168.1.1").is_failure()
|
|
1007
|
+
True
|
|
744
1008
|
"""
|
|
745
1009
|
if not isinstance(text, str):
|
|
746
1010
|
return Maybe.failure('Input must be a string')
|
|
@@ -767,12 +1031,27 @@ def parse_ipv6(text: str) -> Maybe[IPv6Address]:
|
|
|
767
1031
|
def parse_ip(text: str) -> Maybe[IPv4Address | IPv6Address]:
|
|
768
1032
|
"""Parse a string as either an IPv4 or IPv6 address.
|
|
769
1033
|
|
|
770
|
-
|
|
1034
|
+
Automatically detects and parses either IPv4 or IPv6 addresses.
|
|
1035
|
+
Trims surrounding whitespace.
|
|
771
1036
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1037
|
+
Args:
|
|
1038
|
+
text: String containing an IP address (IPv4 or IPv6, whitespace is stripped)
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
Maybe[IPv4Address | IPv6Address]: Success with IPv4Address or IPv6Address if valid,
|
|
1042
|
+
Failure(str) with error message otherwise
|
|
1043
|
+
|
|
1044
|
+
Examples:
|
|
1045
|
+
>>> result = parse_ip("192.168.1.1")
|
|
1046
|
+
>>> result.is_success()
|
|
1047
|
+
True
|
|
1048
|
+
>>> result = parse_ip("::1")
|
|
1049
|
+
>>> result.is_success()
|
|
1050
|
+
True
|
|
1051
|
+
>>> parse_ip(" 10.0.0.1 ")
|
|
1052
|
+
Success(IPv4Address('10.0.0.1'))
|
|
1053
|
+
>>> parse_ip("not an ip").is_failure()
|
|
1054
|
+
True
|
|
776
1055
|
"""
|
|
777
1056
|
if not isinstance(text, str):
|
|
778
1057
|
return Maybe.failure('Input must be a string')
|
|
@@ -799,14 +1078,32 @@ def parse_ip(text: str) -> Maybe[IPv4Address | IPv6Address]:
|
|
|
799
1078
|
def parse_cidr(text: str, *, strict: bool = True) -> Maybe[IPv4Network | IPv6Network]:
|
|
800
1079
|
"""Parse a CIDR network string (IPv4 or IPv6).
|
|
801
1080
|
|
|
802
|
-
|
|
803
|
-
|
|
1081
|
+
Validates and parses network addresses in CIDR notation (e.g., 192.168.1.0/24).
|
|
1082
|
+
By default, validates that host bits are not set (strict mode).
|
|
1083
|
+
With strict=False, host bits are masked to the network address.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
text: String containing a CIDR network (whitespace is stripped)
|
|
1087
|
+
strict: If True, reject networks with host bits set; if False, mask them (default: True)
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
Maybe[IPv4Network | IPv6Network]: Success with IPv4Network or IPv6Network if valid,
|
|
1091
|
+
Failure(str) with error message otherwise
|
|
804
1092
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1093
|
+
Examples:
|
|
1094
|
+
>>> parse_cidr("192.168.1.0/24")
|
|
1095
|
+
Success(IPv4Network('192.168.1.0/24'))
|
|
1096
|
+
>>> parse_cidr("10.0.0.0/8")
|
|
1097
|
+
Success(IPv4Network('10.0.0.0/8'))
|
|
1098
|
+
>>> parse_cidr("2001:db8::/32")
|
|
1099
|
+
Success(IPv6Network('2001:db8::/32'))
|
|
1100
|
+
>>> # Strict mode rejects host bits
|
|
1101
|
+
>>> parse_cidr("192.168.1.5/24").is_failure()
|
|
1102
|
+
True
|
|
1103
|
+
>>> # Non-strict mode masks host bits
|
|
1104
|
+
>>> result = parse_cidr("192.168.1.5/24", strict=False)
|
|
1105
|
+
>>> str(result.value_or(None))
|
|
1106
|
+
'192.168.1.0/24'
|
|
810
1107
|
"""
|
|
811
1108
|
if not isinstance(text, str):
|
|
812
1109
|
return Maybe.failure('Input must be a string')
|
valid8r/core/validators.py
CHANGED
|
@@ -7,6 +7,7 @@ that either contains the validated value or an error message.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import re
|
|
10
11
|
from typing import (
|
|
11
12
|
TYPE_CHECKING,
|
|
12
13
|
Generic,
|
|
@@ -102,11 +103,25 @@ def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
|
|
|
102
103
|
"""Create a validator that ensures a value is at least the minimum.
|
|
103
104
|
|
|
104
105
|
Args:
|
|
105
|
-
min_value: The minimum allowed value
|
|
106
|
+
min_value: The minimum allowed value (inclusive)
|
|
106
107
|
error_message: Optional custom error message
|
|
107
108
|
|
|
108
109
|
Returns:
|
|
109
|
-
A validator function
|
|
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'
|
|
110
125
|
|
|
111
126
|
"""
|
|
112
127
|
|
|
@@ -122,11 +137,25 @@ def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
|
122
137
|
"""Create a validator that ensures a value is at most the maximum.
|
|
123
138
|
|
|
124
139
|
Args:
|
|
125
|
-
max_value: The maximum allowed value
|
|
140
|
+
max_value: The maximum allowed value (inclusive)
|
|
126
141
|
error_message: Optional custom error message
|
|
127
142
|
|
|
128
143
|
Returns:
|
|
129
|
-
A validator function
|
|
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'
|
|
130
159
|
|
|
131
160
|
"""
|
|
132
161
|
|
|
@@ -142,12 +171,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
|
|
|
142
171
|
"""Create a validator that ensures a value is between minimum and maximum (inclusive).
|
|
143
172
|
|
|
144
173
|
Args:
|
|
145
|
-
min_value: The minimum allowed value
|
|
146
|
-
max_value: The maximum allowed value
|
|
174
|
+
min_value: The minimum allowed value (inclusive)
|
|
175
|
+
max_value: The maximum allowed value (inclusive)
|
|
147
176
|
error_message: Optional custom error message
|
|
148
177
|
|
|
149
178
|
Returns:
|
|
150
|
-
A validator function
|
|
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'
|
|
151
198
|
|
|
152
199
|
"""
|
|
153
200
|
|
|
@@ -162,12 +209,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
|
|
|
162
209
|
def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
|
|
163
210
|
"""Create a validator using a custom predicate function.
|
|
164
211
|
|
|
212
|
+
Allows creating custom validators for any validation logic by providing
|
|
213
|
+
a predicate function that returns True for valid values.
|
|
214
|
+
|
|
165
215
|
Args:
|
|
166
|
-
pred: A function that takes a value and returns
|
|
167
|
-
error_message: Error message when validation fails
|
|
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
|
|
168
218
|
|
|
169
219
|
Returns:
|
|
170
|
-
A validator function
|
|
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'"
|
|
171
236
|
|
|
172
237
|
"""
|
|
173
238
|
|
|
@@ -183,12 +248,30 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
|
|
|
183
248
|
"""Create a validator that ensures a string's length is within bounds.
|
|
184
249
|
|
|
185
250
|
Args:
|
|
186
|
-
min_length: Minimum length of the string
|
|
187
|
-
max_length: Maximum length of the string
|
|
251
|
+
min_length: Minimum length of the string (inclusive)
|
|
252
|
+
max_length: Maximum length of the string (inclusive)
|
|
188
253
|
error_message: Optional custom error message
|
|
189
254
|
|
|
190
255
|
Returns:
|
|
191
|
-
A validator function
|
|
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'
|
|
192
275
|
|
|
193
276
|
"""
|
|
194
277
|
|
|
@@ -198,3 +281,285 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
|
|
|
198
281
|
return Maybe.failure(error_message or f'String length must be between {min_length} and {max_length}')
|
|
199
282
|
|
|
200
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)
|
valid8r/prompt/basic.py
CHANGED
|
@@ -85,29 +85,68 @@ def ask( # noqa: PLR0913
|
|
|
85
85
|
retry: bool | int = False,
|
|
86
86
|
_test_mode: bool = False,
|
|
87
87
|
) -> Maybe[T]:
|
|
88
|
-
"""Prompt the user for input with validation.
|
|
88
|
+
"""Prompt the user for input with parsing and validation.
|
|
89
|
+
|
|
90
|
+
Displays a prompt to the user, parses their input using the provided parser,
|
|
91
|
+
validates the result, and optionally retries on failure. Returns a Maybe monad
|
|
92
|
+
containing either the validated input or an error message.
|
|
89
93
|
|
|
90
94
|
Args:
|
|
91
|
-
prompt_text: The prompt to display to the user
|
|
92
|
-
parser: Function to convert string to desired type
|
|
93
|
-
validator: Function to validate the parsed value
|
|
94
|
-
error_message: Custom error message
|
|
95
|
-
default: Default value to use if input
|
|
96
|
-
retry:
|
|
97
|
-
_test_mode:
|
|
95
|
+
prompt_text: The prompt message to display to the user
|
|
96
|
+
parser: Function to convert string input to desired type (default: returns string as-is)
|
|
97
|
+
validator: Function to validate the parsed value (default: accepts any value)
|
|
98
|
+
error_message: Custom error message to display on validation failure
|
|
99
|
+
default: Default value to use if user provides empty input (displays in prompt)
|
|
100
|
+
retry: Enable retry on failure - True for unlimited, integer for max attempts, False to disable
|
|
101
|
+
_test_mode: Internal testing parameter (do not use)
|
|
98
102
|
|
|
99
103
|
Returns:
|
|
100
|
-
|
|
104
|
+
Maybe[T]: Success with validated input, or Failure with error message
|
|
101
105
|
|
|
102
106
|
Examples:
|
|
103
|
-
>>> # This would prompt the user and validate their input
|
|
104
107
|
>>> from valid8r.core import parsers, validators
|
|
105
|
-
>>>
|
|
108
|
+
>>> from valid8r.prompt import ask
|
|
109
|
+
>>>
|
|
110
|
+
>>> # Basic integer input with validation
|
|
111
|
+
>>> result = ask(
|
|
106
112
|
... "Enter your age: ",
|
|
107
113
|
... parser=parsers.parse_int,
|
|
108
|
-
... validator=validators.
|
|
114
|
+
... validator=validators.between(0, 120),
|
|
109
115
|
... retry=True
|
|
110
116
|
... )
|
|
117
|
+
>>> # User enters "25" -> Success(25)
|
|
118
|
+
>>> # User enters "invalid" -> prompts again with error message
|
|
119
|
+
>>>
|
|
120
|
+
>>> # Input with default value
|
|
121
|
+
>>> result = ask(
|
|
122
|
+
... "Enter port: ",
|
|
123
|
+
... parser=parsers.parse_int,
|
|
124
|
+
... default=8080
|
|
125
|
+
... )
|
|
126
|
+
>>> # User presses Enter -> Success(8080)
|
|
127
|
+
>>> # User enters "3000" -> Success(3000)
|
|
128
|
+
>>>
|
|
129
|
+
>>> # Limited retries with custom error
|
|
130
|
+
>>> result = ask(
|
|
131
|
+
... "Email: ",
|
|
132
|
+
... parser=parsers.parse_email,
|
|
133
|
+
... error_message="Invalid email format",
|
|
134
|
+
... retry=3
|
|
135
|
+
... )
|
|
136
|
+
>>> # User has 3 attempts to enter valid email
|
|
137
|
+
>>>
|
|
138
|
+
>>> # Boolean input with retry
|
|
139
|
+
>>> result = ask(
|
|
140
|
+
... "Continue? (yes/no): ",
|
|
141
|
+
... parser=parsers.parse_bool,
|
|
142
|
+
... retry=True
|
|
143
|
+
... )
|
|
144
|
+
>>> # User enters "yes" -> Success(True)
|
|
145
|
+
>>> # User enters "maybe" -> error, retry prompt
|
|
146
|
+
|
|
147
|
+
Note:
|
|
148
|
+
The returned Maybe must be unwrapped to access the value.
|
|
149
|
+
Use pattern matching or .value_or() to extract the result.
|
|
111
150
|
|
|
112
151
|
"""
|
|
113
152
|
# Create a config object from the parameters
|
|
@@ -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
|
|
@@ -168,21 +192,21 @@ from valid8r.core.maybe import Success, Failure
|
|
|
168
192
|
from valid8r.core import parsers
|
|
169
193
|
|
|
170
194
|
# Phone number parsing with NANP validation (PhoneNumber)
|
|
171
|
-
match parsers.parse_phone("+1 (
|
|
195
|
+
match parsers.parse_phone("+1 (415) 555-2671"):
|
|
172
196
|
case Success(phone):
|
|
173
197
|
print(f"Country: {phone.country_code}") # 1
|
|
174
|
-
print(f"Area: {phone.area_code}") #
|
|
175
|
-
print(f"Exchange: {phone.exchange}") #
|
|
176
|
-
print(f"Subscriber: {phone.subscriber}") #
|
|
198
|
+
print(f"Area: {phone.area_code}") # 415
|
|
199
|
+
print(f"Exchange: {phone.exchange}") # 555
|
|
200
|
+
print(f"Subscriber: {phone.subscriber}") # 2671
|
|
177
201
|
|
|
178
202
|
# Format for display using properties
|
|
179
|
-
print(f"E.164: {phone.e164}") # +
|
|
180
|
-
print(f"National: {phone.national}") # (
|
|
203
|
+
print(f"E.164: {phone.e164}") # +14155552671
|
|
204
|
+
print(f"National: {phone.national}") # (415) 555-2671
|
|
181
205
|
case Failure(err):
|
|
182
206
|
print("Error:", err)
|
|
183
207
|
|
|
184
208
|
# Also accepts various formats
|
|
185
|
-
for number in ["
|
|
209
|
+
for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
|
|
186
210
|
result = parsers.parse_phone(number)
|
|
187
211
|
assert result.is_success()
|
|
188
212
|
```
|
|
@@ -212,24 +236,26 @@ assert assert_maybe_failure(result, "at least 0")
|
|
|
212
236
|
|
|
213
237
|
# Test prompts with mock input
|
|
214
238
|
with MockInputContext(["yes", "42", "invalid", "25"]):
|
|
215
|
-
# First prompt
|
|
239
|
+
# First prompt - returns Maybe, unwrap with value_or()
|
|
216
240
|
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
217
241
|
assert result.value_or(False) == True
|
|
218
242
|
|
|
219
|
-
# Second prompt
|
|
220
|
-
|
|
243
|
+
# Second prompt - unwrap the Maybe result
|
|
244
|
+
result = prompt.ask(
|
|
221
245
|
"Age? ",
|
|
222
246
|
parser=parsers.parse_int,
|
|
223
247
|
validator=validate_age
|
|
224
248
|
)
|
|
249
|
+
age = result.value_or(None)
|
|
225
250
|
assert age == 42
|
|
226
251
|
|
|
227
|
-
# Third prompt will fail, fourth succeeds
|
|
228
|
-
|
|
252
|
+
# Third prompt will fail, fourth succeeds - unwrap result
|
|
253
|
+
result = prompt.ask(
|
|
229
254
|
"Age again? ",
|
|
230
255
|
parser=parsers.parse_int,
|
|
231
|
-
|
|
256
|
+
retry=1 # Retry once after failure
|
|
232
257
|
)
|
|
258
|
+
age = result.value_or(None)
|
|
233
259
|
assert age == 25
|
|
234
260
|
```
|
|
235
261
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
valid8r/__init__.py,sha256=2fzSl6XtKX44sdqzf0GBTn4oEaCvhmyGkdsPDMJjZz8,447
|
|
2
2
|
valid8r/core/__init__.py,sha256=ASOdzqCtpZHbHjjYMZkb78Z-nKxtD26ruTY0bd43ImA,520
|
|
3
3
|
valid8r/core/combinators.py,sha256=KvRiDEqoZgH58cBYPO6SW9pdtkyijk0lS8aGSB5DbO4,2349
|
|
4
|
-
valid8r/core/maybe.py,sha256=
|
|
5
|
-
valid8r/core/parsers.py,sha256=
|
|
6
|
-
valid8r/core/validators.py,sha256=
|
|
4
|
+
valid8r/core/maybe.py,sha256=ifz15tDMwRaQLsYvU3pWGBZBxJ2JyyOW-g5YpOw_-3w,4643
|
|
5
|
+
valid8r/core/parsers.py,sha256=WYAgwCIh8HyrXr0AoZeqa0c43-_iVIK_S3wb2lha73c,67456
|
|
6
|
+
valid8r/core/validators.py,sha256=e3B_fi7ch5m0Zczg87r8AhrgEdBTcbz-aygrKCvi0dg,18537
|
|
7
7
|
valid8r/prompt/__init__.py,sha256=XYB3NEp-tmqT6fGmETVEeXd7Urj0M4ijlwdRAjj-rG8,175
|
|
8
|
-
valid8r/prompt/basic.py,sha256=
|
|
8
|
+
valid8r/prompt/basic.py,sha256=fFARuy5nGTE7xM3dB1jpRC3OPNmp4WwaymFMz7BSgdo,7635
|
|
9
9
|
valid8r/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
valid8r/testing/__init__.py,sha256=8mk54zt0Ai2dK0a3GMOTfDPsVQWXaS6uvQJDrkRV9hs,779
|
|
11
11
|
valid8r/testing/assertions.py,sha256=9KGz1JooCoyikyxMX7VuXB9VYAtj-4H_LPYFGdvS-ps,1820
|
|
12
12
|
valid8r/testing/generators.py,sha256=kAV6NRO9x1gPy0BfGs07ETVxjpTIxOZyV9wH2BA1nHA,8791
|
|
13
13
|
valid8r/testing/mock_input.py,sha256=9GRT7h0PCh9Dea-OcQ5Uls7YqhsTdqMWuX6I6ZlW1aw,2334
|
|
14
|
-
valid8r-0.
|
|
15
|
-
valid8r-0.
|
|
16
|
-
valid8r-0.
|
|
17
|
-
valid8r-0.
|
|
18
|
-
valid8r-0.
|
|
14
|
+
valid8r-0.7.0.dist-info/METADATA,sha256=X7tG_qUhsJ3LE0M28nIDMjERAiTOp_xcHnSen9RbXgU,10458
|
|
15
|
+
valid8r-0.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
16
|
+
valid8r-0.7.0.dist-info/entry_points.txt,sha256=H_24A4zUgnKIXAIRosJliIcntyqMfmcgKh5_Prl7W18,79
|
|
17
|
+
valid8r-0.7.0.dist-info/licenses/LICENSE,sha256=JpEmJvRYOTIUt0UjgvpDrd3U94Wnbt_Grr5z-xU2jtk,1066
|
|
18
|
+
valid8r-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|