validatedata 0.2.6__tar.gz → 0.4.0__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.
- {validatedata-0.2.6/validatedata.egg-info → validatedata-0.4.0}/PKG-INFO +307 -18
- {validatedata-0.2.6 → validatedata-0.4.0}/README.md +305 -17
- {validatedata-0.2.6 → validatedata-0.4.0}/pyproject.toml +3 -1
- validatedata-0.4.0/tests/test_async.py +194 -0
- validatedata-0.4.0/tests/test_check_rule.py +107 -0
- validatedata-0.4.0/tests/test_examples.py +391 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_functions.py +3 -1
- validatedata-0.4.0/tests/test_nested_shorthand.py +339 -0
- validatedata-0.4.0/tests/test_shorthand.py +525 -0
- validatedata-0.4.0/validatedata/__init__.py +14 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata/messages.py +3 -1
- validatedata-0.4.0/validatedata/validatedata.py +672 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata/validator.py +68 -45
- {validatedata-0.2.6 → validatedata-0.4.0/validatedata.egg-info}/PKG-INFO +307 -18
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/SOURCES.txt +4 -0
- validatedata-0.2.6/tests/test_examples.py +0 -58
- validatedata-0.2.6/validatedata/__init__.py +0 -3
- validatedata-0.2.6/validatedata/validatedata.py +0 -285
- {validatedata-0.2.6 → validatedata-0.4.0}/LICENSE +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/setup.cfg +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_new_features.py +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_new_types.py +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_types.py +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/dependency_links.txt +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/requires.txt +0 -0
- {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: validatedata
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An easier way to validate data in python
|
|
5
5
|
Author-email: Edward Kigozi <edwardinbytes@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Edward-K1/validatedata
|
|
8
8
|
Project-URL: Repository, https://github.com/Edward-K1/validatedata.git
|
|
9
9
|
Project-URL: Issues, https://github.com/Edward-K1/validatedata/issues
|
|
10
|
+
Project-URL: Documentation, https://validatedata.readthedocs.io
|
|
10
11
|
Keywords: validate,data,validation
|
|
11
12
|
Classifier: Programming Language :: Python :: 3
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -37,19 +38,24 @@ pip install phonenumbers
|
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
---
|
|
41
|
+
📖 **[Read the full documentation](https://validatedata.readthedocs.io)**
|
|
40
42
|
|
|
41
43
|
## Quick Start
|
|
42
44
|
|
|
43
45
|
```python
|
|
44
46
|
from validatedata import validate_data
|
|
45
47
|
|
|
48
|
+
# with shorthand
|
|
49
|
+
rule={
|
|
50
|
+
'username': 'str|min:3|max:32',
|
|
51
|
+
'email': 'email',
|
|
52
|
+
'age': 'int|min:18',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
46
56
|
result = validate_data(
|
|
47
57
|
data={'username': 'alice', 'email': 'alice@example.com', 'age': 25},
|
|
48
|
-
rule=
|
|
49
|
-
'username': {'type': 'str', 'range': (3, 32)},
|
|
50
|
-
'email': {'type': 'email'},
|
|
51
|
-
'age': {'type': 'int', 'range': (18, 'any')}
|
|
52
|
-
}}
|
|
58
|
+
rule=rule
|
|
53
59
|
)
|
|
54
60
|
|
|
55
61
|
if result.ok:
|
|
@@ -58,6 +64,18 @@ else:
|
|
|
58
64
|
print(result.errors)
|
|
59
65
|
```
|
|
60
66
|
|
|
67
|
+
> With the `keys` wrapper
|
|
68
|
+
>
|
|
69
|
+
> ```python
|
|
70
|
+
> rule = {'keys': {
|
|
71
|
+
> 'username': 'str|min:3|max:32',
|
|
72
|
+
> 'email': 'email',
|
|
73
|
+
> 'age': 'int|min:18',
|
|
74
|
+
> }}
|
|
75
|
+
> ```
|
|
76
|
+
>
|
|
77
|
+
> The `keys` form is recommended when you need to pair field rules with top-level options (such as `strict_keys` in a future release).
|
|
78
|
+
|
|
61
79
|
---
|
|
62
80
|
|
|
63
81
|
## Three Ways to Validate
|
|
@@ -114,6 +132,19 @@ user.signup('alice_99', 'alice@example.com', 'Secure@123') # works
|
|
|
114
132
|
user.signup('alice_99', 'not-an-email', 'weak') # raises ValidationError
|
|
115
133
|
```
|
|
116
134
|
|
|
135
|
+
Async functions are supported. The decorator behaves identically:
|
|
136
|
+
```python
|
|
137
|
+
from validatedata import validate
|
|
138
|
+
|
|
139
|
+
@validate(signup_rules, raise_exceptions=True)
|
|
140
|
+
async def signup(self, username, email, password):
|
|
141
|
+
await db.save(username, email, password)
|
|
142
|
+
return 'Account Created'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`validate_types` works the same way with async functions.
|
|
146
|
+
|
|
147
|
+
|
|
117
148
|
Class methods:
|
|
118
149
|
|
|
119
150
|
```python
|
|
@@ -272,34 +303,238 @@ rules = [{
|
|
|
272
303
|
|
|
273
304
|
## Shorthand Rule Strings
|
|
274
305
|
|
|
275
|
-
|
|
306
|
+
Rules can be expressed as compact strings instead of dicts. There are two syntaxes: the original **colon syntax** for simple cases, and the newer **pipe syntax** for anything more expressive. Both work side by side in the same rule list.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### Colon syntax (original)
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
'str' # string
|
|
314
|
+
'str:20' # string of exactly 20 characters
|
|
315
|
+
'int:10' # int of exactly 10 digits
|
|
316
|
+
'email' # email address
|
|
317
|
+
'email:msg:invalid email address' # with custom error message
|
|
318
|
+
'int:1:to:100' # int in range 1 to 100
|
|
319
|
+
'regex:[A-Z]{3}' # must match regex
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### Pipe syntax
|
|
325
|
+
|
|
326
|
+
The pipe syntax uses `|` to chain modifiers onto a type. It supports the full set of validation rules, optional transforms, and custom messages — all in one readable string.
|
|
327
|
+
|
|
328
|
+
**General shape:**
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
type [| transform ...] [| modifier ...] [| msg:message]
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Transforms must come before validators. `msg:` must always be last.
|
|
335
|
+
|
|
336
|
+
#### Type
|
|
337
|
+
|
|
338
|
+
Any supported type name is valid as the first token:
|
|
276
339
|
|
|
277
340
|
```python
|
|
278
|
-
'str'
|
|
279
|
-
'
|
|
280
|
-
'
|
|
281
|
-
'
|
|
282
|
-
'
|
|
283
|
-
|
|
284
|
-
'regex:[A-Z]{3}' # must match regex
|
|
341
|
+
'str|...'
|
|
342
|
+
'int|...'
|
|
343
|
+
'email|...'
|
|
344
|
+
'url|...'
|
|
345
|
+
'uuid|...'
|
|
346
|
+
# ...any type from the Types section
|
|
285
347
|
```
|
|
286
348
|
|
|
287
|
-
|
|
349
|
+
#### Flags
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
'int|strict' # no type coercion — value must already be the right type
|
|
353
|
+
'email|nullable' # None is accepted as a valid value
|
|
354
|
+
'int|strict|nullable' # both
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Range
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
'int|min:18' # >= 18, no upper limit
|
|
361
|
+
'int|max:100' # no lower limit, <= 100
|
|
362
|
+
'int|min:0|max:100' # between 0 and 100 inclusive
|
|
363
|
+
'int|between:0,100' # shorthand for the above
|
|
364
|
+
'str|min:3|max:32' # string length between 3 and 32
|
|
365
|
+
'float|min:0.5|max:9.9' # float range
|
|
366
|
+
'list|min:1|max:10' # list must have between 1 and 10 items
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`min` and `max` can be used independently for open bounds. `between` is a convenience alias for `min` + `max` together — they cannot be combined.
|
|
370
|
+
|
|
371
|
+
> **Note:** `validatedata` does not impose a maximum size on lists or tuples. If you are validating untrusted input in a web API or other public-facing context, always set an explicit upper bound to prevent memory exhaustion from unexpectedly large payloads.
|
|
372
|
+
|
|
373
|
+
#### Enums and exclusions
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
'str|in:admin,user,guest' # value must be one of these
|
|
377
|
+
'str|not_in:root,superuser' # value must not be any of these
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### String constraints
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
'str|starts_with:https' # must start with this prefix
|
|
384
|
+
'str|ends_with:.pdf' # must end with this suffix
|
|
385
|
+
'str|contains:@' # must contain this substring
|
|
386
|
+
'list|unique' # no duplicate values
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Values can safely contain `|` — the parser only splits on `|` when followed by a recognised keyword:
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
'str|starts_with:image/png|min:3' # 'image/png' is treated as one value
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### Format
|
|
396
|
+
|
|
397
|
+
For types that support format variants:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
'color|format:hex' # #fff or #ffffff
|
|
401
|
+
'color|format:rgb' # rgb(255, 0, 0)
|
|
402
|
+
'color|format:hsl' # hsl(0, 100%, 50%)
|
|
403
|
+
'color|format:named' # red, cornflowerblue, etc.
|
|
404
|
+
'phone|format:national' # (415) 555-2671 — requires: pip install phonenumbers
|
|
405
|
+
'phone|format:e164' # +14155552671 — built-in
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Transforms
|
|
409
|
+
|
|
410
|
+
Named transforms are applied to the value **before** validation runs. They are the only modifiers that must come before validators.
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
'str|strip|min:3|max:32' # strip whitespace, then check length
|
|
414
|
+
'str|lower|in:admin,user,guest' # lowercase, then check options
|
|
415
|
+
'str|strip|lower|min:3|max:32' # chain as many as needed
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Available named transforms: `strip`, `lstrip`, `rstrip`, `lower`, `upper`, `title`.
|
|
419
|
+
|
|
420
|
+
To get the transformed value back, pass `mutate=True` to `validate_data` or `@validate`:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
result = validate_data([' Alice '], ['str|strip|lower'], mutate=True)
|
|
424
|
+
result.data # ['alice']
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Regex
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
'str|re:[A-Z]{3}' # must match pattern
|
|
431
|
+
'str|min:8|re:(?=.*[A-Z])(?=.*\d).+' # combined with other modifiers
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The pattern is everything after `re:` up to the next recognised modifier or end of string. Patterns can safely contain `:` and `|`:
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
'str|re:https?://\S+' # colons in pattern are safe
|
|
438
|
+
'str|re:(?=.*[A-Z]|.*\d).+' # pipes in pattern are safe
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### Custom error message
|
|
442
|
+
|
|
443
|
+
`msg:` must be the last modifier. The message text can contain any characters including `|`:
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
'str|min:3|max:32|msg:must be 3 to 32 characters'
|
|
447
|
+
'int|min:18|msg:you must be 18 or older'
|
|
448
|
+
'str|re:[A-Z]+|msg:uppercase letters only'
|
|
449
|
+
'int|min:0|msg:must be positive | or zero' # | inside message is fine
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### Mixing syntaxes
|
|
455
|
+
|
|
456
|
+
Colon shorthand, pipe shorthand, and dict rules can all coexist in the same rule list:
|
|
288
457
|
|
|
289
458
|
```python
|
|
290
459
|
rules = [
|
|
291
|
-
{'type': 'str', 'expression': r'^[
|
|
292
|
-
'email
|
|
293
|
-
|
|
460
|
+
{'type': 'str', 'expression': r'^[\w-]{3,32}$', 'expression-message': 'invalid username'},
|
|
461
|
+
'email|nullable|msg:invalid email',
|
|
462
|
+
'str|min:8|re:(?=.*[A-Z])(?=.*\d).+|msg:password too weak',
|
|
294
463
|
]
|
|
295
464
|
```
|
|
296
465
|
|
|
297
466
|
---
|
|
298
467
|
|
|
468
|
+
### Reference table
|
|
469
|
+
|
|
470
|
+
| Modifier | Example | Description |
|
|
471
|
+
|---|---|---|
|
|
472
|
+
| `strict` | `int\|strict` | No type coercion |
|
|
473
|
+
| `nullable` | `email\|nullable` | Allow `None` |
|
|
474
|
+
| `unique` | `list\|unique` | No duplicate values |
|
|
475
|
+
| `min:N` | `int\|min:18` | Minimum value or length |
|
|
476
|
+
| `max:N` | `int\|max:100` | Maximum value or length |
|
|
477
|
+
| `between:N,M` | `int\|between:0,100` | Range shorthand |
|
|
478
|
+
| `in:a,b,c` | `str\|in:admin,user` | Allowed values |
|
|
479
|
+
| `not_in:a,b` | `str\|not_in:root` | Excluded values |
|
|
480
|
+
| `starts_with:x` | `str\|starts_with:https` | Required prefix |
|
|
481
|
+
| `ends_with:x` | `str\|ends_with:.pdf` | Required suffix |
|
|
482
|
+
| `contains:x` | `str\|contains:@` | Required substring |
|
|
483
|
+
| `format:x` | `color\|format:hex` | Format variant |
|
|
484
|
+
| `strip` | `str\|strip\|min:3` | Remove surrounding whitespace |
|
|
485
|
+
| `lstrip` | `str\|lstrip\|min:3` | Remove leading whitespace |
|
|
486
|
+
| `rstrip` | `str\|rstrip\|min:3` | Remove trailing whitespace |
|
|
487
|
+
| `lower` | `str\|lower\|in:yes,no` | Lowercase before validating |
|
|
488
|
+
| `upper` | `str\|upper\|starts_with:ADM` | Uppercase before validating |
|
|
489
|
+
| `title` | `str\|title\|min:3` | Title case before validating |
|
|
490
|
+
| `re:pattern` | `str\|re:[A-Z]{3}` | Regex pattern |
|
|
491
|
+
| `msg:text` | `str\|min:3\|msg:too short` | Custom error message — must be last |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
### Real-world examples
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
# user signup fields
|
|
499
|
+
rules = [
|
|
500
|
+
'str|strip|min:3|max:32|msg:username must be 3 to 32 characters',
|
|
501
|
+
'email|nullable|msg:invalid email address',
|
|
502
|
+
'str|min:8|re:(?=.*[A-Z])(?=.*\d).+|msg:password must contain uppercase and a number',
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
# role with enum
|
|
506
|
+
'str|in:admin,editor,viewer|msg:invalid role'
|
|
507
|
+
|
|
508
|
+
# optional hex colour
|
|
509
|
+
'color|format:hex|nullable'
|
|
510
|
+
|
|
511
|
+
# URL that must use HTTPS
|
|
512
|
+
'url|starts_with:https|msg:must be a secure URL'
|
|
513
|
+
|
|
514
|
+
# slugified identifier
|
|
515
|
+
'slug|min:3|max:64|msg:invalid slug'
|
|
516
|
+
|
|
517
|
+
# age gate
|
|
518
|
+
'int|strict|min:18|max:120|msg:invalid age'
|
|
519
|
+
|
|
520
|
+
# phone — any format, optional
|
|
521
|
+
'phone|nullable'
|
|
522
|
+
|
|
523
|
+
# deduplicated tag list
|
|
524
|
+
'list|unique|min:1|max:10'
|
|
525
|
+
|
|
526
|
+
# transform then validate
|
|
527
|
+
'str|strip|lower|in:yes,no,maybe|msg:invalid response'
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
299
532
|
## Range Rule
|
|
300
533
|
|
|
301
534
|
The `'any'` keyword is used as an open bound:
|
|
302
535
|
|
|
536
|
+
> **Note:** `validatedata` does not impose a maximum size on lists or tuples. If you are validating untrusted input in a web API or other public-facing context, always set an explicit upper bound to prevent memory exhaustion from unexpectedly large payloads.
|
|
537
|
+
|
|
303
538
|
```python
|
|
304
539
|
{'type': 'int', 'range': (1, 'any')} # >= 1, no upper limit
|
|
305
540
|
{'type': 'int', 'range': ('any', 100)} # no lower limit, <= 100
|
|
@@ -556,6 +791,38 @@ result = validate_data(
|
|
|
556
791
|
result.errors # ['company.address.postcode: value is not of required length']
|
|
557
792
|
```
|
|
558
793
|
|
|
794
|
+
**Mirror-structure shorthand (0.4.0+):**
|
|
795
|
+
|
|
796
|
+
Instead of wrapping every nested dict in `{'type': 'dict', 'fields': {...}}`, you can write a rule that mirrors the shape of your data:
|
|
797
|
+
```python
|
|
798
|
+
data = {
|
|
799
|
+
'app': {
|
|
800
|
+
'name': 'QuickScript',
|
|
801
|
+
'version': '1.0.0',
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
# before — explicit form
|
|
806
|
+
rule = {'keys': {
|
|
807
|
+
'app': {
|
|
808
|
+
'type': 'dict',
|
|
809
|
+
'fields': {
|
|
810
|
+
'name': {'type': 'str', 'range': (3, 'any')},
|
|
811
|
+
'version': {'type': 'semver'},
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}}
|
|
815
|
+
|
|
816
|
+
# after — rule mirrors the data
|
|
817
|
+
rule = {
|
|
818
|
+
'app': {
|
|
819
|
+
'name': 'str|min:3',
|
|
820
|
+
'version': 'semver',
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Error paths are identical in both forms. Nesting can go up to **100 levels** deep — exceeding this raises a `ValueError`. See the [mirror-rules guide](https://validatedata.readthedocs.io/en/latest/mirror-rules.html) for the full reference.
|
|
559
826
|
**List of typed items:**
|
|
560
827
|
|
|
561
828
|
```python
|
|
@@ -687,6 +954,28 @@ if not result.ok:
|
|
|
687
954
|
|
|
688
955
|
---
|
|
689
956
|
|
|
957
|
+
## Contributing
|
|
958
|
+
|
|
959
|
+
Contributions are welcome!
|
|
960
|
+
|
|
961
|
+
**Before starting work on a new feature or non-trivial change, please open an issue first.**
|
|
962
|
+
This helps avoid duplicate effort and lets us align on scope and approach before any code is written.
|
|
963
|
+
|
|
964
|
+
### Getting Started
|
|
965
|
+
|
|
966
|
+
1. Open an issue describing what you'd like to add or change
|
|
967
|
+
2. You'll be informed if there's someone working on it and given the green light if it's the right call
|
|
968
|
+
2. Fork the repository and create a branch off `main`
|
|
969
|
+
```
|
|
970
|
+
git checkout -b feature/your-feature-name
|
|
971
|
+
```
|
|
972
|
+
3. Make your changes and add tests where appropriate
|
|
973
|
+
4. Open a pull request referencing the issue
|
|
974
|
+
|
|
975
|
+
For bug fixes and small improvements, feel free to skip the issue and go straight to a PR.
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
690
979
|
## License
|
|
691
980
|
|
|
692
981
|
MIT
|