valid8r 0.6.1__tar.gz → 0.6.3__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.1 → valid8r-0.6.3}/PKG-INFO +16 -14
- {valid8r-0.6.1 → valid8r-0.6.3}/README.md +15 -13
- {valid8r-0.6.1 → valid8r-0.6.3}/pyproject.toml +1 -1
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/core/maybe.py +8 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/core/parsers.py +341 -44
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/core/validators.py +95 -13
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/prompt/basic.py +51 -12
- {valid8r-0.6.1 → valid8r-0.6.3}/LICENSE +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/__init__.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/core/__init__.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/core/combinators.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/prompt/__init__.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/py.typed +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/testing/__init__.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/testing/assertions.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/testing/generators.py +0 -0
- {valid8r-0.6.1 → valid8r-0.6.3}/valid8r/testing/mock_input.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: valid8r
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Clean, flexible input validation for Python applications
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -168,21 +168,21 @@ from valid8r.core.maybe import Success, Failure
|
|
|
168
168
|
from valid8r.core import parsers
|
|
169
169
|
|
|
170
170
|
# Phone number parsing with NANP validation (PhoneNumber)
|
|
171
|
-
match parsers.parse_phone("+1 (
|
|
171
|
+
match parsers.parse_phone("+1 (415) 555-2671"):
|
|
172
172
|
case Success(phone):
|
|
173
173
|
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}") #
|
|
174
|
+
print(f"Area: {phone.area_code}") # 415
|
|
175
|
+
print(f"Exchange: {phone.exchange}") # 555
|
|
176
|
+
print(f"Subscriber: {phone.subscriber}") # 2671
|
|
177
177
|
|
|
178
178
|
# Format for display using properties
|
|
179
|
-
print(f"E.164: {phone.e164}") # +
|
|
180
|
-
print(f"National: {phone.national}") # (
|
|
179
|
+
print(f"E.164: {phone.e164}") # +14155552671
|
|
180
|
+
print(f"National: {phone.national}") # (415) 555-2671
|
|
181
181
|
case Failure(err):
|
|
182
182
|
print("Error:", err)
|
|
183
183
|
|
|
184
184
|
# Also accepts various formats
|
|
185
|
-
for number in ["
|
|
185
|
+
for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
|
|
186
186
|
result = parsers.parse_phone(number)
|
|
187
187
|
assert result.is_success()
|
|
188
188
|
```
|
|
@@ -212,24 +212,26 @@ assert assert_maybe_failure(result, "at least 0")
|
|
|
212
212
|
|
|
213
213
|
# Test prompts with mock input
|
|
214
214
|
with MockInputContext(["yes", "42", "invalid", "25"]):
|
|
215
|
-
# First prompt
|
|
215
|
+
# First prompt - returns Maybe, unwrap with value_or()
|
|
216
216
|
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
217
217
|
assert result.value_or(False) == True
|
|
218
218
|
|
|
219
|
-
# Second prompt
|
|
220
|
-
|
|
219
|
+
# Second prompt - unwrap the Maybe result
|
|
220
|
+
result = prompt.ask(
|
|
221
221
|
"Age? ",
|
|
222
222
|
parser=parsers.parse_int,
|
|
223
223
|
validator=validate_age
|
|
224
224
|
)
|
|
225
|
+
age = result.value_or(None)
|
|
225
226
|
assert age == 42
|
|
226
227
|
|
|
227
|
-
# Third prompt will fail, fourth succeeds
|
|
228
|
-
|
|
228
|
+
# Third prompt will fail, fourth succeeds - unwrap result
|
|
229
|
+
result = prompt.ask(
|
|
229
230
|
"Age again? ",
|
|
230
231
|
parser=parsers.parse_int,
|
|
231
|
-
|
|
232
|
+
retry=1 # Retry once after failure
|
|
232
233
|
)
|
|
234
|
+
age = result.value_or(None)
|
|
233
235
|
assert age == 25
|
|
234
236
|
```
|
|
235
237
|
|
|
@@ -137,21 +137,21 @@ from valid8r.core.maybe import Success, Failure
|
|
|
137
137
|
from valid8r.core import parsers
|
|
138
138
|
|
|
139
139
|
# Phone number parsing with NANP validation (PhoneNumber)
|
|
140
|
-
match parsers.parse_phone("+1 (
|
|
140
|
+
match parsers.parse_phone("+1 (415) 555-2671"):
|
|
141
141
|
case Success(phone):
|
|
142
142
|
print(f"Country: {phone.country_code}") # 1
|
|
143
|
-
print(f"Area: {phone.area_code}") #
|
|
144
|
-
print(f"Exchange: {phone.exchange}") #
|
|
145
|
-
print(f"Subscriber: {phone.subscriber}") #
|
|
143
|
+
print(f"Area: {phone.area_code}") # 415
|
|
144
|
+
print(f"Exchange: {phone.exchange}") # 555
|
|
145
|
+
print(f"Subscriber: {phone.subscriber}") # 2671
|
|
146
146
|
|
|
147
147
|
# Format for display using properties
|
|
148
|
-
print(f"E.164: {phone.e164}") # +
|
|
149
|
-
print(f"National: {phone.national}") # (
|
|
148
|
+
print(f"E.164: {phone.e164}") # +14155552671
|
|
149
|
+
print(f"National: {phone.national}") # (415) 555-2671
|
|
150
150
|
case Failure(err):
|
|
151
151
|
print("Error:", err)
|
|
152
152
|
|
|
153
153
|
# Also accepts various formats
|
|
154
|
-
for number in ["
|
|
154
|
+
for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
|
|
155
155
|
result = parsers.parse_phone(number)
|
|
156
156
|
assert result.is_success()
|
|
157
157
|
```
|
|
@@ -181,24 +181,26 @@ assert assert_maybe_failure(result, "at least 0")
|
|
|
181
181
|
|
|
182
182
|
# Test prompts with mock input
|
|
183
183
|
with MockInputContext(["yes", "42", "invalid", "25"]):
|
|
184
|
-
# First prompt
|
|
184
|
+
# First prompt - returns Maybe, unwrap with value_or()
|
|
185
185
|
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
186
186
|
assert result.value_or(False) == True
|
|
187
187
|
|
|
188
|
-
# Second prompt
|
|
189
|
-
|
|
188
|
+
# Second prompt - unwrap the Maybe result
|
|
189
|
+
result = prompt.ask(
|
|
190
190
|
"Age? ",
|
|
191
191
|
parser=parsers.parse_int,
|
|
192
192
|
validator=validate_age
|
|
193
193
|
)
|
|
194
|
+
age = result.value_or(None)
|
|
194
195
|
assert age == 42
|
|
195
196
|
|
|
196
|
-
# Third prompt will fail, fourth succeeds
|
|
197
|
-
|
|
197
|
+
# Third prompt will fail, fourth succeeds - unwrap result
|
|
198
|
+
result = prompt.ask(
|
|
198
199
|
"Age again? ",
|
|
199
200
|
parser=parsers.parse_int,
|
|
200
|
-
|
|
201
|
+
retry=1 # Retry once after failure
|
|
201
202
|
)
|
|
203
|
+
age = result.value_or(None)
|
|
202
204
|
assert age == 25
|
|
203
205
|
```
|
|
204
206
|
|
|
@@ -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})'
|
|
@@ -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')
|
|
@@ -102,11 +102,25 @@ def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
|
|
|
102
102
|
"""Create a validator that ensures a value is at least the minimum.
|
|
103
103
|
|
|
104
104
|
Args:
|
|
105
|
-
min_value: The minimum allowed value
|
|
105
|
+
min_value: The minimum allowed value (inclusive)
|
|
106
106
|
error_message: Optional custom error message
|
|
107
107
|
|
|
108
108
|
Returns:
|
|
109
|
-
A validator function
|
|
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'
|
|
110
124
|
|
|
111
125
|
"""
|
|
112
126
|
|
|
@@ -122,11 +136,25 @@ def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
|
|
|
122
136
|
"""Create a validator that ensures a value is at most the maximum.
|
|
123
137
|
|
|
124
138
|
Args:
|
|
125
|
-
max_value: The maximum allowed value
|
|
139
|
+
max_value: The maximum allowed value (inclusive)
|
|
126
140
|
error_message: Optional custom error message
|
|
127
141
|
|
|
128
142
|
Returns:
|
|
129
|
-
A validator function
|
|
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'
|
|
130
158
|
|
|
131
159
|
"""
|
|
132
160
|
|
|
@@ -142,12 +170,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
|
|
|
142
170
|
"""Create a validator that ensures a value is between minimum and maximum (inclusive).
|
|
143
171
|
|
|
144
172
|
Args:
|
|
145
|
-
min_value: The minimum allowed value
|
|
146
|
-
max_value: The maximum allowed value
|
|
173
|
+
min_value: The minimum allowed value (inclusive)
|
|
174
|
+
max_value: The maximum allowed value (inclusive)
|
|
147
175
|
error_message: Optional custom error message
|
|
148
176
|
|
|
149
177
|
Returns:
|
|
150
|
-
A validator function
|
|
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'
|
|
151
197
|
|
|
152
198
|
"""
|
|
153
199
|
|
|
@@ -162,12 +208,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
|
|
|
162
208
|
def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
|
|
163
209
|
"""Create a validator using a custom predicate function.
|
|
164
210
|
|
|
211
|
+
Allows creating custom validators for any validation logic by providing
|
|
212
|
+
a predicate function that returns True for valid values.
|
|
213
|
+
|
|
165
214
|
Args:
|
|
166
|
-
pred: A function that takes a value and returns
|
|
167
|
-
error_message: Error message when validation fails
|
|
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
|
|
168
217
|
|
|
169
218
|
Returns:
|
|
170
|
-
A validator function
|
|
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'"
|
|
171
235
|
|
|
172
236
|
"""
|
|
173
237
|
|
|
@@ -183,12 +247,30 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
|
|
|
183
247
|
"""Create a validator that ensures a string's length is within bounds.
|
|
184
248
|
|
|
185
249
|
Args:
|
|
186
|
-
min_length: Minimum length of the string
|
|
187
|
-
max_length: Maximum length of the string
|
|
250
|
+
min_length: Minimum length of the string (inclusive)
|
|
251
|
+
max_length: Maximum length of the string (inclusive)
|
|
188
252
|
error_message: Optional custom error message
|
|
189
253
|
|
|
190
254
|
Returns:
|
|
191
|
-
A validator function
|
|
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'
|
|
192
274
|
|
|
193
275
|
"""
|
|
194
276
|
|
|
@@ -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
|
|
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
|