validatedata 0.3.0__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 (24) hide show
  1. {validatedata-0.3.0/validatedata.egg-info → validatedata-0.4.0}/PKG-INFO +40 -6
  2. validatedata-0.3.0/PKG-INFO → validatedata-0.4.0/README.md +38 -23
  3. {validatedata-0.3.0 → validatedata-0.4.0}/pyproject.toml +3 -1
  4. validatedata-0.4.0/tests/test_check_rule.py +107 -0
  5. validatedata-0.4.0/tests/test_examples.py +391 -0
  6. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_functions.py +3 -1
  7. validatedata-0.4.0/tests/test_nested_shorthand.py +339 -0
  8. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/__init__.py +4 -2
  9. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/validatedata.py +151 -17
  10. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/validator.py +32 -16
  11. validatedata-0.3.0/README.md → validatedata-0.4.0/validatedata.egg-info/PKG-INFO +57 -5
  12. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/SOURCES.txt +2 -0
  13. validatedata-0.3.0/tests/test_examples.py +0 -58
  14. {validatedata-0.3.0 → validatedata-0.4.0}/LICENSE +0 -0
  15. {validatedata-0.3.0 → validatedata-0.4.0}/setup.cfg +0 -0
  16. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_async.py +0 -0
  17. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_new_features.py +0 -0
  18. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_new_types.py +0 -0
  19. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_shorthand.py +0 -0
  20. {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_types.py +0 -0
  21. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/messages.py +0 -0
  22. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/dependency_links.txt +0 -0
  23. {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/requires.txt +0 -0
  24. {validatedata-0.3.0 → 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.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,6 +38,7 @@ 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
 
@@ -44,11 +46,11 @@ pip install phonenumbers
44
46
  from validatedata import validate_data
45
47
 
46
48
  # with shorthand
47
- rule={'keys': {
49
+ rule={
48
50
  'username': 'str|min:3|max:32',
49
51
  'email': 'email',
50
52
  'age': 'int|min:18',
51
- }}
53
+ }
52
54
 
53
55
 
54
56
  result = validate_data(
@@ -62,14 +64,14 @@ else:
62
64
  print(result.errors)
63
65
  ```
64
66
 
65
- > **Python 3.7+:** For simple cases you can omit the `keys` wrapper and pass a bare field map directly:
67
+ > With the `keys` wrapper
66
68
  >
67
69
  > ```python
68
- > rule = {
70
+ > rule = {'keys': {
69
71
  > 'username': 'str|min:3|max:32',
70
72
  > 'email': 'email',
71
73
  > 'age': 'int|min:18',
72
- > }
74
+ > }}
73
75
  > ```
74
76
  >
75
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).
@@ -789,6 +791,38 @@ result = validate_data(
789
791
  result.errors # ['company.address.postcode: value is not of required length']
790
792
  ```
791
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.
792
826
  **List of typed items:**
793
827
 
794
828
  ```python
@@ -1,21 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: validatedata
3
- Version: 0.3.0
4
- Summary: An easier way to validate data in python
5
- Author-email: Edward Kigozi <edwardinbytes@gmail.com>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/Edward-K1/validatedata
8
- Project-URL: Repository, https://github.com/Edward-K1/validatedata.git
9
- Project-URL: Issues, https://github.com/Edward-K1/validatedata/issues
10
- Keywords: validate,data,validation
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.7
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: python-dateutil
17
- Dynamic: license-file
18
-
19
1
  # Validatedata
20
2
  ![build workflow](https://github.com/Edward-K1/validatedata/actions/workflows/test.yml/badge.svg)
21
3
  [![PyPI version](https://badge.fury.io/py/validatedata.svg)](https://badge.fury.io/py/validatedata)
@@ -37,6 +19,7 @@ pip install phonenumbers
37
19
  ```
38
20
 
39
21
  ---
22
+ 📖 **[Read the full documentation](https://validatedata.readthedocs.io)**
40
23
 
41
24
  ## Quick Start
42
25
 
@@ -44,11 +27,11 @@ pip install phonenumbers
44
27
  from validatedata import validate_data
45
28
 
46
29
  # with shorthand
47
- rule={'keys': {
30
+ rule={
48
31
  'username': 'str|min:3|max:32',
49
32
  'email': 'email',
50
33
  'age': 'int|min:18',
51
- }}
34
+ }
52
35
 
53
36
 
54
37
  result = validate_data(
@@ -62,14 +45,14 @@ else:
62
45
  print(result.errors)
63
46
  ```
64
47
 
65
- > **Python 3.7+:** For simple cases you can omit the `keys` wrapper and pass a bare field map directly:
48
+ > With the `keys` wrapper
66
49
  >
67
50
  > ```python
68
- > rule = {
51
+ > rule = {'keys': {
69
52
  > 'username': 'str|min:3|max:32',
70
53
  > 'email': 'email',
71
54
  > 'age': 'int|min:18',
72
- > }
55
+ > }}
73
56
  > ```
74
57
  >
75
58
  > The `keys` form is recommended when you need to pair field rules with top-level options (such as `strict_keys` in a future release).
@@ -789,6 +772,38 @@ result = validate_data(
789
772
  result.errors # ['company.address.postcode: value is not of required length']
790
773
  ```
791
774
 
775
+ **Mirror-structure shorthand (0.4.0+):**
776
+
777
+ Instead of wrapping every nested dict in `{'type': 'dict', 'fields': {...}}`, you can write a rule that mirrors the shape of your data:
778
+ ```python
779
+ data = {
780
+ 'app': {
781
+ 'name': 'QuickScript',
782
+ 'version': '1.0.0',
783
+ }
784
+ }
785
+
786
+ # before — explicit form
787
+ rule = {'keys': {
788
+ 'app': {
789
+ 'type': 'dict',
790
+ 'fields': {
791
+ 'name': {'type': 'str', 'range': (3, 'any')},
792
+ 'version': {'type': 'semver'},
793
+ }
794
+ }
795
+ }}
796
+
797
+ # after — rule mirrors the data
798
+ rule = {
799
+ 'app': {
800
+ 'name': 'str|min:3',
801
+ 'version': 'semver',
802
+ }
803
+ }
804
+ ```
805
+
806
+ 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.
792
807
  **List of typed items:**
793
808
 
794
809
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "validatedata"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  authors = [
9
9
  { name="Edward Kigozi", email="edwardinbytes@gmail.com" },
10
10
  ]
@@ -26,6 +26,8 @@ classifiers = [
26
26
  Homepage = "https://github.com/Edward-K1/validatedata"
27
27
  Repository = "https://github.com/Edward-K1/validatedata.git"
28
28
  Issues = "https://github.com/Edward-K1/validatedata/issues"
29
+ Documentation = "https://validatedata.readthedocs.io"
30
+
29
31
 
30
32
  [tool.setuptools]
31
33
  packages = ["validatedata"]
@@ -0,0 +1,107 @@
1
+ """
2
+ Tests for the 0.4.0 rule-validation features:
3
+ - VALID_RULE_KEYS exported frozenset
4
+ - Unknown rule key detection with did-you-mean suggestions
5
+ - check_rule() public function
6
+ """
7
+
8
+ import unittest
9
+
10
+ from validatedata import check_rule, VALID_RULE_KEYS, validate_data
11
+ from .base import BaseTest
12
+
13
+
14
+ class TestValidRuleKeys(BaseTest):
15
+
16
+ def test_is_frozenset(self):
17
+ self.assertIsInstance(VALID_RULE_KEYS, frozenset)
18
+
19
+ def test_contains_expected_keys(self):
20
+ for key in ('type', 'keys', 'fields', 'items', 'range', 'length',
21
+ 'nullable', 'strict', 'message', 'transform', 'depends_on'):
22
+ self.assertIn(key, VALID_RULE_KEYS, f"Expected '{key}' in VALID_RULE_KEYS")
23
+
24
+ def test_is_immutable(self):
25
+ with self.assertRaises(AttributeError):
26
+ VALID_RULE_KEYS.add('fake_key')
27
+
28
+
29
+ class TestUnknownKeyDetection(BaseTest):
30
+
31
+ def test_unknown_key_raises_value_error(self):
32
+ with self.assertRaises(ValueError):
33
+ validate_data(['hello'], [{'type': 'str', 'nulable': True}])
34
+
35
+ def test_error_message_includes_bad_key(self):
36
+ with self.assertRaises(ValueError) as ctx:
37
+ validate_data(['hello'], [{'type': 'str', 'nulable': True}])
38
+ self.assertIn('nulable', str(ctx.exception))
39
+
40
+ def test_did_you_mean_suggestion(self):
41
+ with self.assertRaises(ValueError) as ctx:
42
+ validate_data(['hello'], [{'type': 'str', 'nulable': True}])
43
+ self.assertIn('nullable', str(ctx.exception))
44
+
45
+ def test_no_suggestion_for_gibberish(self):
46
+ # A key with no close match should still raise but without a suggestion
47
+ with self.assertRaises(ValueError) as ctx:
48
+ validate_data(['hello'], [{'type': 'str', 'zzzzfake': True}])
49
+ self.assertNotIn('Did you mean', str(ctx.exception))
50
+
51
+ def test_multiple_unknown_keys_reported(self):
52
+ with self.assertRaises(ValueError) as ctx:
53
+ validate_data(['hello'], [{'type': 'str', 'nulable': True, 'strikt': True}])
54
+ msg = str(ctx.exception)
55
+ self.assertIn('nulable', msg)
56
+ self.assertIn('strikt', msg)
57
+
58
+ def test_message_suffix_keys_are_allowed(self):
59
+ # Any '<key>-message' key should not raise
60
+ rule = [{'type': 'int', 'range': (1, 10), 'range-message': 'out of range'}]
61
+ self.assertTrue(validate_data([5], rule).ok)
62
+
63
+ def test_valid_rule_dict_does_not_raise(self):
64
+ rule = [{'type': 'str', 'nullable': True, 'length': 5, 'strict': True}]
65
+ try:
66
+ validate_data(['hello'], rule)
67
+ except ValueError:
68
+ self.fail("validate_data raised ValueError on a valid rule dict")
69
+
70
+
71
+ class TestCheckRule(BaseTest):
72
+
73
+ def test_valid_rule_passes_silently(self):
74
+ try:
75
+ check_rule({'type': 'str', 'nullable': True})
76
+ except ValueError:
77
+ self.fail("check_rule raised ValueError on a valid rule")
78
+
79
+ def test_unknown_key_raises_value_error(self):
80
+ with self.assertRaises(ValueError):
81
+ check_rule({'type': 'str', 'nulable': True})
82
+
83
+ def test_did_you_mean_in_message(self):
84
+ with self.assertRaises(ValueError) as ctx:
85
+ check_rule({'type': 'str', 'nulable': True})
86
+ self.assertIn('nullable', str(ctx.exception))
87
+
88
+ def test_keys_wrapper_is_valid(self):
89
+ # The canonical {'keys': {...}} form must not raise
90
+ try:
91
+ check_rule({'keys': {'username': {'type': 'str'}}})
92
+ except ValueError:
93
+ self.fail("check_rule raised ValueError on canonical {'keys': {...}} rule")
94
+
95
+ def test_returns_none_on_success(self):
96
+ result = check_rule({'type': 'int', 'range': (1, 100)})
97
+ self.assertIsNone(result)
98
+
99
+ def test_does_not_accept_path_as_argument(self):
100
+ # check_rule is a clean public API — path param must not be exposed
101
+ import inspect
102
+ sig = inspect.signature(check_rule)
103
+ self.assertNotIn('path', sig.parameters)
104
+
105
+
106
+ if __name__ == '__main__':
107
+ unittest.main()