valify 0.1.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.
- valify/__init__.py +62 -0
- valify/exceptions.py +50 -0
- valify/schema.py +126 -0
- valify/validators.py +324 -0
- valify-0.1.0.dist-info/METADATA +134 -0
- valify-0.1.0.dist-info/RECORD +9 -0
- valify-0.1.0.dist-info/WHEEL +5 -0
- valify-0.1.0.dist-info/licenses/LICENSE +0 -0
- valify-0.1.0.dist-info/top_level.txt +1 -0
valify/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
valify
|
|
3
|
+
~~~~~~
|
|
4
|
+
|
|
5
|
+
A composable, expressive data validation library for Python.
|
|
6
|
+
|
|
7
|
+
Basic usage::
|
|
8
|
+
|
|
9
|
+
from valify import Schema, StringValidator, IntValidator
|
|
10
|
+
|
|
11
|
+
schema = Schema({
|
|
12
|
+
"name": StringValidator(min_length=2),
|
|
13
|
+
"age": IntValidator(min_value=0, max_value=120),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
result = schema.validate({"name": "Alice", "age": 30})
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
ValifyError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
SchemaError,
|
|
23
|
+
RequiredFieldError
|
|
24
|
+
)
|
|
25
|
+
from .validators import (
|
|
26
|
+
Validator,
|
|
27
|
+
StringValidator,
|
|
28
|
+
IntValidator,
|
|
29
|
+
FloatValidator,
|
|
30
|
+
BoolValidator,
|
|
31
|
+
EmailValidator
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from .schema import Schema
|
|
35
|
+
|
|
36
|
+
# __all__ defines the public API — what gets exported when someone writes
|
|
37
|
+
# `from valify import *`. More importantly, it tells IDEs and documentation
|
|
38
|
+
# tools exactly what your library exposes publicly.
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
|
|
42
|
+
# Exceptions
|
|
43
|
+
"ValifyError",
|
|
44
|
+
"ValidationError",
|
|
45
|
+
"RequiredFieldError",
|
|
46
|
+
"SchemaError",
|
|
47
|
+
|
|
48
|
+
# Validators
|
|
49
|
+
"Validator",
|
|
50
|
+
"StringValidator",
|
|
51
|
+
"IntValidator",
|
|
52
|
+
"FloatValidator",
|
|
53
|
+
"BoolValidator",
|
|
54
|
+
"EmailValidator",
|
|
55
|
+
|
|
56
|
+
# Schema
|
|
57
|
+
"Schema",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
__version__ = "0.1.0"
|
|
61
|
+
__author__ = "Darshan Bamankar"
|
|
62
|
+
|
valify/exceptions.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
valify.exceptions
|
|
3
|
+
~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Custom exception hierarchy for the valify library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
class ValifyError(Exception):
|
|
9
|
+
""" Base class for all valify exceptions """
|
|
10
|
+
|
|
11
|
+
class ValidationError(ValifyError):
|
|
12
|
+
"""
|
|
13
|
+
Raised when a value fails a validation rule.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
message : str
|
|
18
|
+
Human-readable description of what failed.
|
|
19
|
+
field : str
|
|
20
|
+
The field name that failed. None if used outside a schema.
|
|
21
|
+
value : object
|
|
22
|
+
The actual value that was rejected.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message, *, field = None, value = None):
|
|
26
|
+
|
|
27
|
+
self.message = message
|
|
28
|
+
self.field = field
|
|
29
|
+
self.value = value
|
|
30
|
+
|
|
31
|
+
full_message = f"[{field}] {message}" if field else message
|
|
32
|
+
super().__init__(full_message)
|
|
33
|
+
|
|
34
|
+
class RequiredFieldError(ValidationError):
|
|
35
|
+
""" Raised when a required field is missing from validation data. """
|
|
36
|
+
|
|
37
|
+
def __init__(self, field):
|
|
38
|
+
super().__init__(
|
|
39
|
+
message="Required field is missing",
|
|
40
|
+
field=field,
|
|
41
|
+
value=None,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
class SchemaError(ValifyError):
|
|
45
|
+
""" Raised when the schema definition itself is inavlid. """
|
|
46
|
+
|
|
47
|
+
def __init__(self, message):
|
|
48
|
+
self.message = message
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
|
valify/schema.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
valify.schema
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
The Schema class for validating dictionaries against a set of validators.
|
|
6
|
+
|
|
7
|
+
A Schema maps field names to Validator instances. When validate() is called,
|
|
8
|
+
each field in the data is run through its corresponding validator, and all
|
|
9
|
+
errors are collected before raising — so you get all errors at once, not
|
|
10
|
+
just the first one.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .exceptions import ValidationError, RequiredFieldError, SchemaError
|
|
14
|
+
from .validators import Validator
|
|
15
|
+
|
|
16
|
+
class Schema:
|
|
17
|
+
""" Validates a dictionary against a set of validators.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
fields : dict
|
|
22
|
+
A mapping of field names to Validator instances.
|
|
23
|
+
strict : bool
|
|
24
|
+
If True, raise ValidationError for keys in the data that are
|
|
25
|
+
not defined in the schema. Defaults to False.
|
|
26
|
+
|
|
27
|
+
Example
|
|
28
|
+
-------
|
|
29
|
+
schema = Schema({
|
|
30
|
+
"name": StringValidator(min_length=2),
|
|
31
|
+
"age": IntValidator(min_value=0, max_value=120),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
result = schema.validate({"name": "Alice", "age": 30})
|
|
35
|
+
print(result) # {"name": "Alice", "age": 30}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, fields, * , strict=False):
|
|
39
|
+
|
|
40
|
+
# Validate the schema definition itself before storing it.
|
|
41
|
+
# This catches developer mistakes early, at schema creation time,
|
|
42
|
+
# rather than mysteriously failing later at validation time.
|
|
43
|
+
|
|
44
|
+
if not isinstance(fields,dict):
|
|
45
|
+
raise SchemaError("Fields must be a dictionary")
|
|
46
|
+
|
|
47
|
+
for key,val in fields.items():
|
|
48
|
+
if not isinstance(key,str):
|
|
49
|
+
raise SchemaError(
|
|
50
|
+
f"Field names must be string, got {type(key).__name__!r}."
|
|
51
|
+
)
|
|
52
|
+
if not isinstance(val,Validator):
|
|
53
|
+
raise SchemaError(
|
|
54
|
+
f"Field {key!r} must be a Validator instance, "
|
|
55
|
+
f"got {type(val).__name__!r}."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.fields=fields
|
|
59
|
+
self.strict=strict
|
|
60
|
+
|
|
61
|
+
def validate(self,data):
|
|
62
|
+
""" Validate a dictionary of data against the schema.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
data : dict
|
|
67
|
+
The raw data to validate.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
dict
|
|
72
|
+
A new dictionary containing only the validated (and possibly
|
|
73
|
+
coerced) values.
|
|
74
|
+
|
|
75
|
+
Raises
|
|
76
|
+
------
|
|
77
|
+
ValidationError
|
|
78
|
+
If any field fails validation. Contains all errors at once.
|
|
79
|
+
RequiredFieldError
|
|
80
|
+
If a required field is missing from data.
|
|
81
|
+
SchemaError
|
|
82
|
+
If data is not a dictionary.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if not isinstance(data,dict):
|
|
86
|
+
raise SchemaError(
|
|
87
|
+
f"Expected a dictionary, got {type(data).__name__!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
errors = {}
|
|
91
|
+
result = {}
|
|
92
|
+
|
|
93
|
+
if self.strict:
|
|
94
|
+
for key in data:
|
|
95
|
+
if key not in self.fields:
|
|
96
|
+
errors[key] = f"Unknown field."
|
|
97
|
+
|
|
98
|
+
for field_name,validator in self.fields.items():
|
|
99
|
+
if field_name not in data:
|
|
100
|
+
errors[field_name] = "Required field missing."
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
result[field_name]=validator.validate(data[field_name])
|
|
105
|
+
except ValidationError as e:
|
|
106
|
+
errors[field_name]=e.message
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if errors:
|
|
110
|
+
error_lines = "\n".join(
|
|
111
|
+
f" {field}: {msg}" for field,msg in errors.items()
|
|
112
|
+
)
|
|
113
|
+
raise ValidationError(
|
|
114
|
+
f"Validation Failed:\n{error_lines}",
|
|
115
|
+
)
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
field_reprs = ", ".join(
|
|
122
|
+
f"{k!r}: {v!r}" for k, v in self.fields.items()
|
|
123
|
+
)
|
|
124
|
+
return f"Schema({{{field_reprs}}}, strict={self.strict!r})"
|
|
125
|
+
|
|
126
|
+
|
valify/validators.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from .exceptions import ValidationError
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
valify.validators
|
|
6
|
+
~~~~~~~~~~~~~~~~~
|
|
7
|
+
|
|
8
|
+
Built-in validators for common Python types and patterns.
|
|
9
|
+
|
|
10
|
+
Each validator is a class that accepts configuration on instantiation
|
|
11
|
+
and exposes a single `validate(value)` method that either returns the
|
|
12
|
+
(possibly coerced) value or raises ValidationError.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Validator:
|
|
17
|
+
""" Base class for all valify validators.
|
|
18
|
+
|
|
19
|
+
All built-in and custom validators inherit from this class.
|
|
20
|
+
Subclasses must implement the `validate` method.
|
|
21
|
+
|
|
22
|
+
Example
|
|
23
|
+
-------
|
|
24
|
+
Creating a custom validator::
|
|
25
|
+
|
|
26
|
+
class PositiveInt(Validator):
|
|
27
|
+
def validate(self, value):
|
|
28
|
+
if not isinstance(value, int) or value <= 0:
|
|
29
|
+
raise ValidationError(
|
|
30
|
+
"Value must be a positive integer.",
|
|
31
|
+
value=value,
|
|
32
|
+
)
|
|
33
|
+
return value
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def validate(self, value):
|
|
37
|
+
""" Validates a value and returns it, possibly coerced
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
value : object
|
|
42
|
+
The Validated (and possibly coerced) value.
|
|
43
|
+
|
|
44
|
+
Raises
|
|
45
|
+
------
|
|
46
|
+
ValidationError
|
|
47
|
+
If the value fails validation.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError(
|
|
51
|
+
f"{type(self).__name__} must implement validate()"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __repr__(self):
|
|
55
|
+
return f"{type(self).__name__}()"
|
|
56
|
+
|
|
57
|
+
class StringValidator(Validator):
|
|
58
|
+
""" Validates that a value is a string, with optional lenght constraints.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
min_length : int or None
|
|
63
|
+
Minimum allowed length. None means no minimum.
|
|
64
|
+
max_length : int or None
|
|
65
|
+
Maximum allowed length. None means no maximum.
|
|
66
|
+
strip : bool
|
|
67
|
+
If True, strip leading/trailing whitespace before validating.
|
|
68
|
+
Defaults to True.
|
|
69
|
+
|
|
70
|
+
Example
|
|
71
|
+
-------
|
|
72
|
+
v = StringValidator(min_length=2, max_length=50)
|
|
73
|
+
v.validate("Alice") # returns "Alice"
|
|
74
|
+
v.validate("A") # raises ValidationError
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
def __init__(self, *, min_length=None, max_length=None, strip=True):
|
|
78
|
+
|
|
79
|
+
self.min_length = min_length
|
|
80
|
+
self.max_length = max_length
|
|
81
|
+
self.strip = strip
|
|
82
|
+
|
|
83
|
+
def validate(self, value):
|
|
84
|
+
if not isinstance(value, str):
|
|
85
|
+
raise ValidationError(
|
|
86
|
+
f"Expected a string, got {type(value).__name__}",
|
|
87
|
+
value=value
|
|
88
|
+
)
|
|
89
|
+
if self.strip:
|
|
90
|
+
value = value.strip()
|
|
91
|
+
|
|
92
|
+
if self.min_length is not None and len(value) < self.min_length:
|
|
93
|
+
raise ValidationError(
|
|
94
|
+
f"Must be at least {self.min_length} characters long.",
|
|
95
|
+
value=value,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.max_length is not None and len(value) > self.max_length:
|
|
99
|
+
raise ValidationError(
|
|
100
|
+
f"Must be at most {self.max_length} characters long.",
|
|
101
|
+
value=value,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
def __repr__(self):
|
|
107
|
+
return (
|
|
108
|
+
f"StringValidator("
|
|
109
|
+
f"min_length={self.min_length!r}, "
|
|
110
|
+
f"max_length={self.max_length!r}, "
|
|
111
|
+
f"strip={self.strip!r})"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
class IntValidator(Validator):
|
|
115
|
+
"""Validates that a value is an integer, with optional range constraints.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
min_value : int or None
|
|
120
|
+
Minimum allowed value. None means no minimum.
|
|
121
|
+
max_value : int or None
|
|
122
|
+
Maximum allowed value. None means no maximum.
|
|
123
|
+
coerce : bool
|
|
124
|
+
If True, attempt to convert strings to int before validating.
|
|
125
|
+
Defaults to False.
|
|
126
|
+
|
|
127
|
+
Example
|
|
128
|
+
-------
|
|
129
|
+
v = IntValidator(min_value=0, max_value=120)
|
|
130
|
+
v.validate(25) # returns 25
|
|
131
|
+
v.validate(-1) # raises ValidationError
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, *, min_value=None, max_value=None, coerce=False):
|
|
135
|
+
self.min_value=min_value
|
|
136
|
+
self.max_value=max_value
|
|
137
|
+
self.coerce=coerce
|
|
138
|
+
|
|
139
|
+
def validate(self, value):
|
|
140
|
+
if self.coerce and not isinstance(value,int):
|
|
141
|
+
try:
|
|
142
|
+
value = int(value)
|
|
143
|
+
except(ValueError, TypeError):
|
|
144
|
+
raise ValidationError(
|
|
145
|
+
f"Could not convert {value!r} to int",
|
|
146
|
+
value=value
|
|
147
|
+
)
|
|
148
|
+
if not isinstance(value, int) or isinstance(value,bool):
|
|
149
|
+
raise ValidationError(
|
|
150
|
+
f"Expected an integer, got {type(value).__name__}.",
|
|
151
|
+
value=value
|
|
152
|
+
)
|
|
153
|
+
if self.min_value is not None and value < self.min_value:
|
|
154
|
+
raise ValidationError(
|
|
155
|
+
f"Must be at least {self.min_value}.",
|
|
156
|
+
value=value,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if self.max_value is not None and value > self.max_value:
|
|
160
|
+
raise ValidationError(
|
|
161
|
+
f"Must be at most {self.max_value}.",
|
|
162
|
+
value=value,
|
|
163
|
+
)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def __repr__(self):
|
|
167
|
+
return (
|
|
168
|
+
f"IntValidator("
|
|
169
|
+
f"min_value={self.min_value!r}, "
|
|
170
|
+
f"max_value={self.max_value!r}, "
|
|
171
|
+
f"coerce={self.coerce!r})"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
class FloatValidator(Validator):
|
|
175
|
+
""" Validates that a value is float, with optional range values.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
min_value : float or None
|
|
180
|
+
Minimum allowed value. None means no minimum.
|
|
181
|
+
max_value : float or None
|
|
182
|
+
Maximum allowed value. None means no maximum.
|
|
183
|
+
coerce : bool
|
|
184
|
+
If True, attempt to convert strings and ints to float.
|
|
185
|
+
Defaults to False.
|
|
186
|
+
|
|
187
|
+
Example
|
|
188
|
+
-------
|
|
189
|
+
v = FloatValidator(min_value=0.0, max_value=1.0)
|
|
190
|
+
v.validate(0.5) # returns 0.5
|
|
191
|
+
v.validate(1.5) # raises ValidationError
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, *, min_value=None, max_value=None, coerce=False):
|
|
195
|
+
self.min_value=min_value
|
|
196
|
+
self.max_value=max_value
|
|
197
|
+
self.coerce=coerce
|
|
198
|
+
|
|
199
|
+
def validate(self,value):
|
|
200
|
+
if self.coerce and not isinstance(value,float):
|
|
201
|
+
try:
|
|
202
|
+
value=float(value)
|
|
203
|
+
except(ValueError, TypeError):
|
|
204
|
+
raise ValidationError(
|
|
205
|
+
f"Could not convert {value!r} to float",
|
|
206
|
+
value=value
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if not isinstance(value, (int,float)) or isinstance(value,bool):
|
|
210
|
+
raise ValidationError(
|
|
211
|
+
f"Expected a float, got {type(value).__name__}.",
|
|
212
|
+
value=value
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
value= float(value)
|
|
216
|
+
|
|
217
|
+
if self.min_value is not None and value<self.min_value:
|
|
218
|
+
raise ValidationError(
|
|
219
|
+
f"Must be at least {self.min_value}",
|
|
220
|
+
value=value,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if self.max_value is not None and value>self.max_value:
|
|
224
|
+
raise ValidationError(
|
|
225
|
+
f"Must be at most {self.max_value}",
|
|
226
|
+
value=value
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return value
|
|
230
|
+
|
|
231
|
+
def __repr__(self):
|
|
232
|
+
return (
|
|
233
|
+
f"FloatValidator("
|
|
234
|
+
f"min_value={self.min_value!r}, "
|
|
235
|
+
f"max_value={self.max_value!r}, "
|
|
236
|
+
f"coerce={self.coerce!r})"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
class BoolValidator(Validator):
|
|
240
|
+
""" Validates that a value is boolean.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
coerce : bool
|
|
245
|
+
If True, accept truthy strings like 'true', 'false', '1', '0'.
|
|
246
|
+
Defaults to False.
|
|
247
|
+
|
|
248
|
+
Example
|
|
249
|
+
-------
|
|
250
|
+
v = BoolValidator()
|
|
251
|
+
v.validate(True) # returns True
|
|
252
|
+
v.validate("true") # raises ValidationError unless coerce=True
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
# Accepted string values while coercing -
|
|
257
|
+
_TRUTH_VALUES = {"true","1","yes"}
|
|
258
|
+
_FALSE_VALUES = {"false","0","no"}
|
|
259
|
+
|
|
260
|
+
def __init__(self,*,coerce=False):
|
|
261
|
+
self.coerce= coerce
|
|
262
|
+
|
|
263
|
+
def validate(self, value):
|
|
264
|
+
if self.coerce and isinstance(value,str):
|
|
265
|
+
lowered = value.lower()
|
|
266
|
+
if lowered in self._TRUTH_VALUES:
|
|
267
|
+
return True
|
|
268
|
+
if lowered in self._FALSE_VALUES:
|
|
269
|
+
return False
|
|
270
|
+
raise ValidationError(
|
|
271
|
+
f"Cannot coerce {value!r} to boolean",
|
|
272
|
+
value=value,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if not isinstance(value,bool):
|
|
276
|
+
raise ValidationError(
|
|
277
|
+
F"Expected a boolean, got {type(value).__name__}.",
|
|
278
|
+
value=value,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return value
|
|
282
|
+
|
|
283
|
+
def __repr__(self):
|
|
284
|
+
return f"BoolValidator(coerce={self.coerce!r})"
|
|
285
|
+
|
|
286
|
+
class EmailValidator(Validator):
|
|
287
|
+
""" Validates that a value is valid email address.
|
|
288
|
+
|
|
289
|
+
This validator checks format only — it does not send a confirmation
|
|
290
|
+
email or verify the address exists. This is intentional: full email
|
|
291
|
+
verification requires network access, which a validator should never do.
|
|
292
|
+
|
|
293
|
+
Example
|
|
294
|
+
-------
|
|
295
|
+
v = EmailValidator()
|
|
296
|
+
v.validate("alice@example.com") # returns "alice@example.com"
|
|
297
|
+
v.validate("not-an-email") # raises ValidationError
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
# Email regex -
|
|
301
|
+
_EMAIL_REGEX = re.compile(
|
|
302
|
+
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def validate(self,value):
|
|
306
|
+
if not isinstance(value,str):
|
|
307
|
+
raise ValidationError(
|
|
308
|
+
f"Expected a string, got {type(value).__name__}",
|
|
309
|
+
value=value,
|
|
310
|
+
)
|
|
311
|
+
value = value.strip().lower()
|
|
312
|
+
|
|
313
|
+
if not self._EMAIL_REGEX.match(value):
|
|
314
|
+
raise ValidationError(
|
|
315
|
+
f"{value!r} is not a valid email address",
|
|
316
|
+
value=value,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return value
|
|
320
|
+
|
|
321
|
+
def __repr__(self):
|
|
322
|
+
return "EmailValidator()"
|
|
323
|
+
|
|
324
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A composable, expressive data validation library for python
|
|
5
|
+
Author-email: Darshan Bamankar <darshanbamankar7@gmail.com>
|
|
6
|
+
Keywords: validation,schema,data
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# valify
|
|
18
|
+
|
|
19
|
+
A composable, expressive data validation library for Python.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install valify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from valify import Schema, StringValidator, IntValidator, EmailValidator
|
|
31
|
+
|
|
32
|
+
schema = Schema({
|
|
33
|
+
"name": StringValidator(min_length=2, max_length=50),
|
|
34
|
+
"age": IntValidator(min_value=0, max_value=120),
|
|
35
|
+
"email": EmailValidator(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
# Valid data — returns cleaned, validated dictionary
|
|
39
|
+
result = schema.validate({
|
|
40
|
+
"name": "Alice",
|
|
41
|
+
"age": 30,
|
|
42
|
+
"email": "alice@example.com",
|
|
43
|
+
})
|
|
44
|
+
print(result)
|
|
45
|
+
# {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
|
|
46
|
+
|
|
47
|
+
# Invalid data — raises ValidationError with ALL errors at once
|
|
48
|
+
schema.validate({
|
|
49
|
+
"name": "A",
|
|
50
|
+
"age": -5,
|
|
51
|
+
"email": "not-an-email",
|
|
52
|
+
})
|
|
53
|
+
# ValidationError: Validation failed:
|
|
54
|
+
# name: Must be at least 2 characters long.
|
|
55
|
+
# age: Must be at least 0.
|
|
56
|
+
# email: 'not-an-email' is not a valid email address.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Validators
|
|
60
|
+
|
|
61
|
+
| Validator | What it checks |
|
|
62
|
+
|-----------|---------------|
|
|
63
|
+
| `StringValidator` | Strings, with optional min/max length |
|
|
64
|
+
| `IntValidator` | Integers, with optional min/max value |
|
|
65
|
+
| `FloatValidator` | Floats, with optional min/max value |
|
|
66
|
+
| `BoolValidator` | Booleans, with optional string coercion |
|
|
67
|
+
| `EmailValidator` | Email address format |
|
|
68
|
+
|
|
69
|
+
## Validators in Detail
|
|
70
|
+
|
|
71
|
+
### StringValidator
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from valify import StringValidator
|
|
75
|
+
|
|
76
|
+
v = StringValidator(
|
|
77
|
+
min_length=2, # minimum character length
|
|
78
|
+
max_length=50, # maximum character length
|
|
79
|
+
strip=True, # strip whitespace before validating (default: True)
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### IntValidator
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from valify import IntValidator
|
|
87
|
+
|
|
88
|
+
v = IntValidator(
|
|
89
|
+
min_value=0, # minimum allowed value
|
|
90
|
+
max_value=120, # maximum allowed value
|
|
91
|
+
coerce=False, # if True, converts "42" -> 42 (default: False)
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### EmailValidator
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from valify import EmailValidator
|
|
99
|
+
|
|
100
|
+
v = EmailValidator()
|
|
101
|
+
v.validate("alice@example.com") # returns "alice@example.com"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Using Validators Standalone
|
|
105
|
+
|
|
106
|
+
Validators work without a Schema too:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from valify import IntValidator
|
|
110
|
+
from valify.exceptions import ValidationError
|
|
111
|
+
|
|
112
|
+
v = IntValidator(min_value=0)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
v.validate(-1)
|
|
116
|
+
except ValidationError as e:
|
|
117
|
+
print(e.message) # Must be at least 0.
|
|
118
|
+
print(e.value) # -1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from valify.exceptions import (
|
|
125
|
+
ValifyError, # base — catches everything
|
|
126
|
+
ValidationError, # a value failed validation
|
|
127
|
+
RequiredFieldError, # a required field was missing
|
|
128
|
+
SchemaError, # the schema definition is invalid
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
valify/__init__.py,sha256=-Iyb_hy58FcbwMuN7UsWctgTwDpLHEgYEj0LB4SPLAg,1264
|
|
2
|
+
valify/exceptions.py,sha256=dehW1gztmUInK5-DO369LK4B3RJvDoYLLypL5Wmj6_k,1355
|
|
3
|
+
valify/schema.py,sha256=wPLNlNB474-B6Y3PaunY4vhobkrmoCj0dZpyEYuQiXg,4004
|
|
4
|
+
valify/validators.py,sha256=KIpJ5HfMSIZWolNwEHlJ9yRIECxCs5jrWjGNCLp5KDw,10116
|
|
5
|
+
valify-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
valify-0.1.0.dist-info/METADATA,sha256=p8IekDMQ0Qa9qthoBoZ33M0fo8IgU8XlbD-YWMQKbfA,3332
|
|
7
|
+
valify-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
valify-0.1.0.dist-info/top_level.txt,sha256=Ab4qZC-SoZbiGgMW5aXD9WjMBvu_oBEqgEmjwhu2h7o,7
|
|
9
|
+
valify-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
valify
|