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 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
File without changes
@@ -0,0 +1 @@
1
+ valify