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.
Files changed (26) hide show
  1. {validatedata-0.2.6/validatedata.egg-info → validatedata-0.4.0}/PKG-INFO +307 -18
  2. {validatedata-0.2.6 → validatedata-0.4.0}/README.md +305 -17
  3. {validatedata-0.2.6 → validatedata-0.4.0}/pyproject.toml +3 -1
  4. validatedata-0.4.0/tests/test_async.py +194 -0
  5. validatedata-0.4.0/tests/test_check_rule.py +107 -0
  6. validatedata-0.4.0/tests/test_examples.py +391 -0
  7. {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_functions.py +3 -1
  8. validatedata-0.4.0/tests/test_nested_shorthand.py +339 -0
  9. validatedata-0.4.0/tests/test_shorthand.py +525 -0
  10. validatedata-0.4.0/validatedata/__init__.py +14 -0
  11. {validatedata-0.2.6 → validatedata-0.4.0}/validatedata/messages.py +3 -1
  12. validatedata-0.4.0/validatedata/validatedata.py +672 -0
  13. {validatedata-0.2.6 → validatedata-0.4.0}/validatedata/validator.py +68 -45
  14. {validatedata-0.2.6 → validatedata-0.4.0/validatedata.egg-info}/PKG-INFO +307 -18
  15. {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/SOURCES.txt +4 -0
  16. validatedata-0.2.6/tests/test_examples.py +0 -58
  17. validatedata-0.2.6/validatedata/__init__.py +0 -3
  18. validatedata-0.2.6/validatedata/validatedata.py +0 -285
  19. {validatedata-0.2.6 → validatedata-0.4.0}/LICENSE +0 -0
  20. {validatedata-0.2.6 → validatedata-0.4.0}/setup.cfg +0 -0
  21. {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_new_features.py +0 -0
  22. {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_new_types.py +0 -0
  23. {validatedata-0.2.6 → validatedata-0.4.0}/tests/test_types.py +0 -0
  24. {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/dependency_links.txt +0 -0
  25. {validatedata-0.2.6 → validatedata-0.4.0}/validatedata.egg-info/requires.txt +0 -0
  26. {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.2.6
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={'keys': {
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
- For common cases rules can be expressed as compact strings:
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' # string
279
- 'str:20' # string of exactly 20 characters
280
- 'int:10' # int of length 10
281
- 'email' # email address
282
- 'email:msg:invalid email address' # email with custom message
283
- 'int:1:to:100' # int in range 1 to 100
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
- Mixed shorthand and dict rules in the same list:
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'^[^\d\W_]+[\w\d_-]{2,31}$', 'expression-message': 'invalid username'},
292
- 'email:msg:invalid email',
293
- {'type': 'str', 'length': 8, 'message': 'password must be 8 characters'}
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