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.
- {validatedata-0.3.0/validatedata.egg-info → validatedata-0.4.0}/PKG-INFO +40 -6
- validatedata-0.3.0/PKG-INFO → validatedata-0.4.0/README.md +38 -23
- {validatedata-0.3.0 → validatedata-0.4.0}/pyproject.toml +3 -1
- validatedata-0.4.0/tests/test_check_rule.py +107 -0
- validatedata-0.4.0/tests/test_examples.py +391 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_functions.py +3 -1
- validatedata-0.4.0/tests/test_nested_shorthand.py +339 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/__init__.py +4 -2
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/validatedata.py +151 -17
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/validator.py +32 -16
- validatedata-0.3.0/README.md → validatedata-0.4.0/validatedata.egg-info/PKG-INFO +57 -5
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/SOURCES.txt +2 -0
- validatedata-0.3.0/tests/test_examples.py +0 -58
- {validatedata-0.3.0 → validatedata-0.4.0}/LICENSE +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/setup.cfg +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_async.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_new_features.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_new_types.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_shorthand.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/tests/test_types.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata/messages.py +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/dependency_links.txt +0 -0
- {validatedata-0.3.0 → validatedata-0.4.0}/validatedata.egg-info/requires.txt +0 -0
- {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
|
+
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={
|
|
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
|
-
>
|
|
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
|

|
|
21
3
|
[](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={
|
|
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
|
-
>
|
|
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.
|
|
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()
|