valid8r 1.6.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.
- valid8r/__init__.py +39 -0
- valid8r/_version.py +34 -0
- valid8r/core/__init__.py +28 -0
- valid8r/core/combinators.py +89 -0
- valid8r/core/maybe.py +170 -0
- valid8r/core/parsers.py +2115 -0
- valid8r/core/validators.py +982 -0
- valid8r/integrations/__init__.py +57 -0
- valid8r/integrations/click.py +143 -0
- valid8r/integrations/env.py +220 -0
- valid8r/integrations/pydantic.py +196 -0
- valid8r/prompt/__init__.py +8 -0
- valid8r/prompt/basic.py +229 -0
- valid8r/py.typed +0 -0
- valid8r/testing/__init__.py +32 -0
- valid8r/testing/assertions.py +67 -0
- valid8r/testing/generators.py +283 -0
- valid8r/testing/mock_input.py +84 -0
- valid8r-1.6.0.dist-info/METADATA +504 -0
- valid8r-1.6.0.dist-info/RECORD +23 -0
- valid8r-1.6.0.dist-info/WHEEL +4 -0
- valid8r-1.6.0.dist-info/entry_points.txt +3 -0
- valid8r-1.6.0.dist-info/licenses/LICENSE +21 -0
valid8r/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# valid8r/__init__.py
|
|
2
|
+
"""Valid8r: A clean, flexible input validation library for Python."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
# Import version from generated file
|
|
7
|
+
try:
|
|
8
|
+
from valid8r._version import __version__
|
|
9
|
+
except ImportError:
|
|
10
|
+
__version__ = '1.6.0'
|
|
11
|
+
|
|
12
|
+
# Public API re-exports for concise imports
|
|
13
|
+
# Modules
|
|
14
|
+
from . import prompt
|
|
15
|
+
from .core import (
|
|
16
|
+
combinators,
|
|
17
|
+
parsers,
|
|
18
|
+
validators,
|
|
19
|
+
)
|
|
20
|
+
from .core.maybe import Maybe
|
|
21
|
+
from .core.parsers import (
|
|
22
|
+
EmailAddress,
|
|
23
|
+
PhoneNumber,
|
|
24
|
+
UrlParts,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Types
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
'EmailAddress',
|
|
31
|
+
'Maybe',
|
|
32
|
+
'PhoneNumber',
|
|
33
|
+
'UrlParts',
|
|
34
|
+
'__version__',
|
|
35
|
+
'combinators',
|
|
36
|
+
'parsers',
|
|
37
|
+
'prompt',
|
|
38
|
+
'validators',
|
|
39
|
+
]
|
valid8r/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.6.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 6, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
valid8r/core/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# valid8r/core/__init__.py
|
|
2
|
+
"""Core validation components."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from valid8r.core.parsers import (
|
|
7
|
+
EmailAddress,
|
|
8
|
+
UrlParts,
|
|
9
|
+
parse_cidr,
|
|
10
|
+
parse_email,
|
|
11
|
+
parse_ip,
|
|
12
|
+
parse_ipv4,
|
|
13
|
+
parse_ipv6,
|
|
14
|
+
parse_url,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'EmailAddress',
|
|
19
|
+
'UrlParts',
|
|
20
|
+
'parse_cidr',
|
|
21
|
+
'parse_email',
|
|
22
|
+
'parse_ip',
|
|
23
|
+
# existing exports may be defined elsewhere; explicitly expose IP helpers
|
|
24
|
+
'parse_ipv4',
|
|
25
|
+
'parse_ipv6',
|
|
26
|
+
# URL/Email helpers
|
|
27
|
+
'parse_url',
|
|
28
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Combinators for creating complex validation rules.
|
|
2
|
+
|
|
3
|
+
This module provides functions to combine validators using logical operations like AND, OR, and NOT.
|
|
4
|
+
These combinators allow for creation of complex validation chains.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from valid8r.core.maybe import (
|
|
15
|
+
Maybe,
|
|
16
|
+
Success,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
T = TypeVar('T')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def and_then(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]:
|
|
26
|
+
"""Combine two validators with logical AND (both must succeed).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
first: The first validator function
|
|
30
|
+
second: The second validator function
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A new validator function that passes only if both validators pass
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def combined_validator(value: T) -> Maybe[T]:
|
|
38
|
+
result = first(value)
|
|
39
|
+
match result:
|
|
40
|
+
case Success(value):
|
|
41
|
+
return second(value)
|
|
42
|
+
case _:
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
return combined_validator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def or_else(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]:
|
|
49
|
+
"""Combine two validators with logical OR (either can succeed).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
first: The first validator function
|
|
53
|
+
second: The second validator function
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A new validator function that passes if either validator passes
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def combined_validator(value: T) -> Maybe[T]:
|
|
61
|
+
result = first(value)
|
|
62
|
+
match result:
|
|
63
|
+
case Success(value):
|
|
64
|
+
return result
|
|
65
|
+
case _:
|
|
66
|
+
return second(value)
|
|
67
|
+
|
|
68
|
+
return combined_validator
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def not_validator(validator: Callable[[T], Maybe[T]], error_message: str) -> Callable[[T], Maybe[T]]:
|
|
72
|
+
"""Negate a validator (success becomes failure and vice versa).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
validator: The validator function to negate
|
|
76
|
+
error_message: Error message for when the negated validator fails
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A new validator function that passes if the original validator fails
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def negated_validator(value: T) -> Maybe[T]:
|
|
84
|
+
result = validator(value)
|
|
85
|
+
if result.is_failure():
|
|
86
|
+
return Maybe.success(value)
|
|
87
|
+
return Maybe.failure(error_message)
|
|
88
|
+
|
|
89
|
+
return negated_validator
|
valid8r/core/maybe.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Maybe monad for clean error handling using Success and Failure types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import (
|
|
6
|
+
ABC,
|
|
7
|
+
abstractmethod,
|
|
8
|
+
)
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Generic,
|
|
12
|
+
TypeVar,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
T = TypeVar('T')
|
|
20
|
+
U = TypeVar('U')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Maybe(ABC, Generic[T]):
|
|
24
|
+
"""Base class for the Maybe monad."""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def success(value: T) -> Success[T]:
|
|
28
|
+
"""Create a Success containing a value."""
|
|
29
|
+
return Success(value)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def failure(error: str) -> Failure[T]:
|
|
33
|
+
"""Create a Failure containing an error message."""
|
|
34
|
+
return Failure(error)
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def is_success(self) -> bool:
|
|
38
|
+
"""Check if the Maybe is a Success."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def is_failure(self) -> bool:
|
|
42
|
+
"""Check if the Maybe is a Failure."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def bind(self, f: Callable[[T], Maybe[U]]) -> Maybe[U]:
|
|
46
|
+
"""Chain operations that might fail."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def map(self, f: Callable[[T], U]) -> Maybe[U]:
|
|
50
|
+
"""Transform the value if present."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def value_or(self, default: T) -> T:
|
|
54
|
+
"""Return the contained value or the provided default if this is a Failure."""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def error_or(self, default: str) -> str:
|
|
58
|
+
"""Return the error message or the provided default if this is a Success."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_error(self) -> str | None:
|
|
62
|
+
"""Get the error message if present, otherwise None."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Success(Maybe[T]):
|
|
66
|
+
"""Represents a successful computation with a value."""
|
|
67
|
+
|
|
68
|
+
__match_args__ = ('value',)
|
|
69
|
+
|
|
70
|
+
def __init__(self, value: T) -> None:
|
|
71
|
+
"""Initialize a Success with a value.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value: The successful result value
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
self.value = value
|
|
78
|
+
|
|
79
|
+
def is_success(self) -> bool:
|
|
80
|
+
"""Check if the Maybe is a Success."""
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def is_failure(self) -> bool:
|
|
84
|
+
"""Check if the Maybe is a Failure."""
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def bind(self, f: Callable[[T], Maybe[U]]) -> Maybe[U]:
|
|
88
|
+
"""Chain operations that might fail."""
|
|
89
|
+
return f(self.value)
|
|
90
|
+
|
|
91
|
+
def map(self, f: Callable[[T], U]) -> Maybe[U]:
|
|
92
|
+
"""Transform the value."""
|
|
93
|
+
return Success(f(self.value))
|
|
94
|
+
|
|
95
|
+
def value_or(self, _default: T) -> T:
|
|
96
|
+
"""Return the contained value (default is ignored for Success)."""
|
|
97
|
+
return self.value
|
|
98
|
+
|
|
99
|
+
def error_or(self, default: str) -> str:
|
|
100
|
+
"""Return the provided default since Success has no error."""
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
def get_error(self) -> str | None:
|
|
104
|
+
"""Get None since Success has no error."""
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def __str__(self) -> str:
|
|
108
|
+
"""Get a string representation."""
|
|
109
|
+
return f'Success({self.value})'
|
|
110
|
+
|
|
111
|
+
def __repr__(self) -> str:
|
|
112
|
+
"""Get a repr representation for debugging and doctests."""
|
|
113
|
+
return f'Success({self.value!r})'
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Failure(Maybe[T]):
|
|
117
|
+
"""Represents a failed computation with an error message."""
|
|
118
|
+
|
|
119
|
+
__match_args__ = ('error',)
|
|
120
|
+
|
|
121
|
+
def __init__(self, error: str) -> None:
|
|
122
|
+
"""Initialize a Failure with an error message.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
error: The error message explaining the failure
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
self.error = error
|
|
129
|
+
|
|
130
|
+
def is_success(self) -> bool:
|
|
131
|
+
"""Check if the Maybe is a Success."""
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def is_failure(self) -> bool:
|
|
135
|
+
"""Check if the Maybe is a Failure."""
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def bind(self, _f: Callable[[T], Maybe[U]]) -> Maybe[U]:
|
|
139
|
+
"""Chain operations that might fail.
|
|
140
|
+
|
|
141
|
+
Function is unused in Failure case as we always propagate the error.
|
|
142
|
+
"""
|
|
143
|
+
return Failure(self.error)
|
|
144
|
+
|
|
145
|
+
def map(self, _f: Callable[[T], U]) -> Maybe[U]:
|
|
146
|
+
"""Transform the value if present.
|
|
147
|
+
|
|
148
|
+
Function is unused in Failure case as we always propagate the error.
|
|
149
|
+
"""
|
|
150
|
+
return Failure(self.error)
|
|
151
|
+
|
|
152
|
+
def value_or(self, default: T) -> T:
|
|
153
|
+
"""Return the provided default for Failure."""
|
|
154
|
+
return default
|
|
155
|
+
|
|
156
|
+
def error_or(self, default: str) -> str:
|
|
157
|
+
"""Return the error message for Failure (or provided default if empty)."""
|
|
158
|
+
return self.error or default
|
|
159
|
+
|
|
160
|
+
def get_error(self) -> str | None:
|
|
161
|
+
"""Get the error message."""
|
|
162
|
+
return self.error
|
|
163
|
+
|
|
164
|
+
def __str__(self) -> str:
|
|
165
|
+
"""Get a string representation."""
|
|
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})'
|