data-sitter 0.1.2__tar.gz → 0.1.4__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.
- {data_sitter-0.1.2 → data_sitter-0.1.4}/PKG-INFO +2 -1
- {data_sitter-0.1.2 → data_sitter-0.1.4}/README.md +27 -22
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/Contract.py +39 -4
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/FieldResolver.py +1 -3
- data_sitter-0.1.4/data_sitter/Validation.py +39 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/cli.py +1 -1
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/field_types/BaseField.py +5 -4
- data_sitter-0.1.4/data_sitter/field_types/FloatField.py +17 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/field_types/NumericField.py +22 -18
- data_sitter-0.1.4/data_sitter/field_types/StringField.py +122 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/Rule.py +19 -2
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/RuleRegistry.py +4 -4
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/PKG-INFO +2 -1
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/SOURCES.txt +1 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/requires.txt +1 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/pyproject.toml +2 -1
- {data_sitter-0.1.2 → data_sitter-0.1.4}/setup.py +2 -1
- data_sitter-0.1.2/data_sitter/field_types/FloatField.py +0 -7
- data_sitter-0.1.2/data_sitter/field_types/StringField.py +0 -89
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/__init__.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/field_types/IntegerField.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/field_types/__init__.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/MatchedRule.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/Parser/RuleParser.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/Parser/__init__.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/Parser/alias_parameters_parser.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/Parser/parser_utils.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/rules/__init__.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/utils/__init__.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter/utils/logger_config.py +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/dependency_links.txt +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/entry_points.txt +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/data_sitter.egg-info/top_level.txt +0 -0
- {data_sitter-0.1.2 → data_sitter-0.1.4}/setup.cfg +0 -0
@@ -1,8 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: data-sitter
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: A Python library that reads data contracts and generates Pydantic models for seamless data validation.
|
5
5
|
Author-email: Lázaro Pereira Candea <lazaro@candea.es>
|
6
6
|
Requires-Dist: python-dotenv==1.0.1
|
7
|
+
Requires-Dist: PyYAML==6.0.2
|
7
8
|
Requires-Dist: parse_type==0.6.4
|
8
9
|
Requires-Dist: pydantic==2.10.5
|
@@ -13,10 +13,8 @@ Data-Sitter is a Python library designed to simplify data validation by converti
|
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
16
|
-
You can install Data-Sitter directly from GitHub:
|
17
|
-
|
18
16
|
```sh
|
19
|
-
pip install
|
17
|
+
pip install data-sitter
|
20
18
|
```
|
21
19
|
|
22
20
|
## Usage
|
@@ -48,7 +46,7 @@ contract_dict = {
|
|
48
46
|
}
|
49
47
|
|
50
48
|
contract = Contract.from_dict(contract_dict)
|
51
|
-
pydantic_contract = contract.
|
49
|
+
pydantic_contract = contract.pydantic_model
|
52
50
|
```
|
53
51
|
|
54
52
|
### Using Rule References
|
@@ -94,39 +92,46 @@ Below are the available rules grouped by field type:
|
|
94
92
|
|
95
93
|
#### BaseField
|
96
94
|
|
97
|
-
-
|
95
|
+
- Is not null
|
98
96
|
|
99
97
|
#### StringField - (Inherits from `BaseField`)
|
100
98
|
|
101
99
|
- Is not empty
|
102
|
-
- Starts with
|
103
|
-
- Ends with
|
104
|
-
-
|
105
|
-
-
|
106
|
-
-
|
107
|
-
-
|
108
|
-
-
|
109
|
-
- Length longer than `{min_len:Integer}`
|
100
|
+
- Starts with {prefix:String}
|
101
|
+
- Ends with {suffix:String}
|
102
|
+
- Is not one of {possible_values:Strings}
|
103
|
+
- Is one of {possible_values:Strings}
|
104
|
+
- Has length between {min_val:Integer} and {max_val:Integer}
|
105
|
+
- Has maximum length {max_len:Integer}
|
106
|
+
- Has minimum length {min_len:Integer}
|
110
107
|
- Is uppercase
|
108
|
+
- Is lowercase
|
109
|
+
- Matches regex {pattern:String}
|
110
|
+
- Is valid email
|
111
|
+
- Is valid URL
|
112
|
+
- Has no digits
|
111
113
|
|
112
114
|
#### NumericField - (Inherits from `BaseField`)
|
113
115
|
|
114
|
-
-
|
115
|
-
-
|
116
|
-
-
|
117
|
-
-
|
118
|
-
-
|
119
|
-
-
|
120
|
-
-
|
121
|
-
-
|
116
|
+
- Is not zero
|
117
|
+
- Is positive
|
118
|
+
- Is negative
|
119
|
+
- Is at least {min_val:Number}
|
120
|
+
- Is at most {max_val:Number}
|
121
|
+
- Is greater than {threshold:Number}
|
122
|
+
- Is less than {threshold:Number}
|
123
|
+
- Is not between {min_val:Number} and {max_val:Number}
|
124
|
+
- Is between {min_val:Number} and {max_val:Number}
|
122
125
|
|
123
126
|
#### IntegerField - (Inherits from `NumericField`)
|
124
127
|
|
125
128
|
#### FloatField - (Inherits from `NumericField`)
|
126
129
|
|
130
|
+
- Has at most {decimal_places:Integer} decimal places
|
131
|
+
|
127
132
|
## Contributing
|
128
133
|
|
129
|
-
Contributions are welcome! Feel free to submit issues or pull requests in the [GitHub repository](https://github.com/
|
134
|
+
Contributions are welcome! Feel free to submit issues or pull requests in the [GitHub repository](https://github.com/lcandea/data-sitter).
|
130
135
|
|
131
136
|
## License
|
132
137
|
|
@@ -1,8 +1,11 @@
|
|
1
|
+
import json
|
2
|
+
import yaml
|
1
3
|
from typing import Any, Dict, List, NamedTuple
|
2
4
|
from functools import cached_property
|
3
5
|
|
4
6
|
from pydantic import BaseModel
|
5
7
|
|
8
|
+
from .Validation import Validation
|
6
9
|
from .field_types import BaseField
|
7
10
|
from .FieldResolver import FieldResolver
|
8
11
|
from .rules import MatchedRule, RuleRegistry, RuleParser
|
@@ -51,6 +54,14 @@ class Contract:
|
|
51
54
|
values=contract_dict.get("values", {}),
|
52
55
|
)
|
53
56
|
|
57
|
+
@classmethod
|
58
|
+
def from_json(cls, contract_json: str):
|
59
|
+
return cls.from_dict(json.loads(contract_json))
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def from_yaml(cls, contract_yaml: str):
|
63
|
+
return cls.from_dict(yaml.load(contract_yaml, yaml.Loader))
|
64
|
+
|
54
65
|
@cached_property
|
55
66
|
def field_validators(self) -> Dict[str, BaseField]:
|
56
67
|
field_validators = {}
|
@@ -68,10 +79,13 @@ class Contract:
|
|
68
79
|
return rules
|
69
80
|
|
70
81
|
def model_validate(self, item: dict):
|
71
|
-
|
72
|
-
|
82
|
+
return self.pydantic_model.model_validate(item).model_dump()
|
83
|
+
|
84
|
+
def validate(self, item: dict) -> Validation:
|
85
|
+
return Validation.validate(self.pydantic_model, item)
|
73
86
|
|
74
|
-
|
87
|
+
@cached_property
|
88
|
+
def pydantic_model(self) -> BaseModel:
|
75
89
|
return type(self.name, (BaseModel,), {
|
76
90
|
"__annotations__": {
|
77
91
|
field_name: field_validator.get_annotation()
|
@@ -79,7 +93,28 @@ class Contract:
|
|
79
93
|
}
|
80
94
|
})
|
81
95
|
|
82
|
-
|
96
|
+
@cached_property
|
97
|
+
def contract(self) -> dict:
|
98
|
+
return {
|
99
|
+
"name": self.name,
|
100
|
+
"fields": [
|
101
|
+
{
|
102
|
+
"field_name": field_name,
|
103
|
+
"field_type": field_validator.__class__.__name__,
|
104
|
+
"field_rules": [rule.parsed_rule for rule in self.rules.get(field_name, [])]
|
105
|
+
}
|
106
|
+
for field_name, field_validator in self.field_validators.items()
|
107
|
+
],
|
108
|
+
"values": self.rule_parser.values
|
109
|
+
}
|
110
|
+
|
111
|
+
def get_json_contract(self, indent: int=2) -> str:
|
112
|
+
return json.dumps(self.contract, indent=indent)
|
113
|
+
|
114
|
+
def get_yaml_contract(self, indent: int=2) -> str:
|
115
|
+
return yaml.dump(self.contract, Dumper=yaml.Dumper, indent=indent, sort_keys=False)
|
116
|
+
|
117
|
+
def get_front_end_contract(self) -> dict:
|
83
118
|
return {
|
84
119
|
"name": self.name,
|
85
120
|
"fields": [
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
from typing import Dict, List, Type
|
3
2
|
|
4
3
|
from .field_types import BaseField
|
@@ -32,8 +31,7 @@ class FieldResolver:
|
|
32
31
|
return matched_rules
|
33
32
|
|
34
33
|
def get_field_validator(self, field_name: str, parsed_rules: List[str]) -> BaseField:
|
35
|
-
|
36
|
-
validator = self.field_class(field_name, is_optional)
|
34
|
+
validator = self.field_class(field_name)
|
37
35
|
matched_rules = self.get_matched_rules(parsed_rules)
|
38
36
|
for matched_rule in matched_rules:
|
39
37
|
matched_rule.add_to_instance(validator)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from typing import Any, Dict, List, Type
|
3
|
+
|
4
|
+
from pydantic import BaseModel, ValidationError
|
5
|
+
|
6
|
+
|
7
|
+
class Validation():
|
8
|
+
item: Dict[str, Any]
|
9
|
+
errors: Dict[str, List[str]]
|
10
|
+
unknowns: Dict[str, Any]
|
11
|
+
|
12
|
+
def __init__(self, item: dict, errors: dict = None, unknowns: dict = None):
|
13
|
+
self.item = item
|
14
|
+
self.errors = errors if errors else None
|
15
|
+
self.unknowns = unknowns if unknowns else None
|
16
|
+
|
17
|
+
def to_dict(self) -> dict:
|
18
|
+
return {key: value for key in ["item", "errors", "unknowns"] if (value := getattr(self, key))}
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def validate(cls, PydanticModel: Type[BaseModel], input_item: dict) -> "Validation":
|
22
|
+
model_keys = PydanticModel.model_json_schema()['properties'].keys()
|
23
|
+
item = {key: None for key in model_keys} # Filling not present values with Nones
|
24
|
+
errors = defaultdict(list)
|
25
|
+
unknowns = {}
|
26
|
+
for key, value in input_item.items():
|
27
|
+
if key in item:
|
28
|
+
item[key] = value
|
29
|
+
else:
|
30
|
+
unknowns[key] = value
|
31
|
+
try:
|
32
|
+
validated = PydanticModel(**item).model_dump()
|
33
|
+
except ValidationError as e:
|
34
|
+
validated = item
|
35
|
+
for error in e.errors():
|
36
|
+
field = error['loc'][0] # Extract the field name
|
37
|
+
msg = error['msg']
|
38
|
+
errors[field].append(msg)
|
39
|
+
return Validation(item=validated, errors=dict(errors), unknowns=unknowns)
|
@@ -23,7 +23,7 @@ def main():
|
|
23
23
|
contract_path = Path(args.contract)
|
24
24
|
contract_dict = json.loads(contract_path.read_text(encoding))
|
25
25
|
contract = Contract.from_dict(contract_dict)
|
26
|
-
pydantic_contract = contract.
|
26
|
+
pydantic_contract = contract.pydantic_model
|
27
27
|
|
28
28
|
if file_path.suffix == '.csv':
|
29
29
|
with open(file_path, encoding=encoding) as f:
|
@@ -21,20 +21,21 @@ class BaseField(ABC):
|
|
21
21
|
validators = None
|
22
22
|
field_type = None
|
23
23
|
|
24
|
-
def __init__(self, name: str
|
24
|
+
def __init__(self, name: str) -> None:
|
25
25
|
self.name = name
|
26
|
-
self.is_optional =
|
26
|
+
self.is_optional = True
|
27
27
|
self.validators = []
|
28
28
|
|
29
|
-
@register_rule("
|
29
|
+
@register_rule("Is not null")
|
30
30
|
def validator_not_null(self):
|
31
31
|
def _validator(value):
|
32
32
|
if self.is_optional:
|
33
33
|
return value
|
34
34
|
if value is None:
|
35
|
-
raise ValueError("Value cannot be null")
|
35
|
+
raise ValueError("Value cannot be null.")
|
36
36
|
return value
|
37
37
|
|
38
|
+
self.is_optional = False
|
38
39
|
self.validators.append(_validator)
|
39
40
|
|
40
41
|
def validate(self, value):
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from .NumericField import NumericField
|
2
|
+
from ..rules import register_field, register_rule
|
3
|
+
|
4
|
+
|
5
|
+
@register_field
|
6
|
+
class FloatField(NumericField):
|
7
|
+
field_type = float
|
8
|
+
|
9
|
+
@register_rule("Has at most {decimal_places:Integer} decimal places")
|
10
|
+
def validate_max_decimal_places(self, decimal_places: int):
|
11
|
+
def validator(value):
|
12
|
+
if not isinstance(value, float):
|
13
|
+
raise ValueError("Value must be a floating-point number.")
|
14
|
+
if len(str(value).split(".")[1]) > decimal_places:
|
15
|
+
raise ValueError(f"Value must have at most {decimal_places} decimal places.")
|
16
|
+
return value
|
17
|
+
self.validators.append(validator)
|
@@ -10,66 +10,70 @@ Numeric = Union[int, float]
|
|
10
10
|
class NumericField(BaseField):
|
11
11
|
field_type = Numeric
|
12
12
|
|
13
|
-
@register_rule("
|
13
|
+
@register_rule("Is not zero")
|
14
14
|
def validate_non_zero(self):
|
15
15
|
def validator(value: Numeric):
|
16
16
|
if value == 0:
|
17
|
-
raise ValueError("Value
|
17
|
+
raise ValueError("Value cannot be zero.")
|
18
18
|
return value
|
19
19
|
self.validators.append(validator)
|
20
20
|
|
21
|
-
@register_rule("
|
21
|
+
@register_rule("Is positive")
|
22
22
|
def validate_positive(self):
|
23
23
|
def validator(value: Numeric):
|
24
24
|
if value < 0:
|
25
|
-
raise ValueError(
|
25
|
+
raise ValueError("Value must be positive.")
|
26
26
|
return value
|
27
27
|
self.validators.append(validator)
|
28
28
|
|
29
|
-
@register_rule("
|
29
|
+
@register_rule("Is negative")
|
30
30
|
def validate_negative(self):
|
31
31
|
def validator(value: Numeric):
|
32
32
|
if value >= 0:
|
33
|
-
raise ValueError(
|
33
|
+
raise ValueError("Value must be less than zero.")
|
34
34
|
return value
|
35
35
|
self.validators.append(validator)
|
36
36
|
|
37
|
-
@register_rule("
|
37
|
+
@register_rule("Is at least {min_val:Number}")
|
38
38
|
def validate_min(self, min_val: Numeric):
|
39
39
|
def validator(value: Numeric):
|
40
40
|
if value < min_val:
|
41
|
-
raise ValueError(f"Value
|
41
|
+
raise ValueError(f"Value must be at least {min_val}.")
|
42
42
|
return value
|
43
43
|
self.validators.append(validator)
|
44
44
|
|
45
|
-
@register_rule("
|
45
|
+
@register_rule("Is at most {max_val:Number}")
|
46
46
|
def validate_max(self, max_val: Numeric):
|
47
47
|
def validator(value: Numeric):
|
48
48
|
if value > max_val:
|
49
|
-
raise ValueError(f"Value
|
49
|
+
raise ValueError(f"Value must not exceed {max_val}.")
|
50
50
|
return value
|
51
51
|
self.validators.append(validator)
|
52
52
|
|
53
|
-
@register_rule("
|
53
|
+
@register_rule("Is greater than {threshold:Number}")
|
54
54
|
def validate_greater_than(self, threshold: Numeric):
|
55
55
|
def validator(value: Numeric):
|
56
56
|
if value <= threshold:
|
57
|
-
raise ValueError(f"Value
|
57
|
+
raise ValueError(f"Value must be greater than {threshold}.")
|
58
58
|
return value
|
59
59
|
self.validators.append(validator)
|
60
60
|
|
61
|
-
@register_rule("
|
61
|
+
@register_rule("Is less than {threshold:Number}")
|
62
62
|
def validate_less_than(self, threshold: Numeric):
|
63
63
|
def validator(value: Numeric):
|
64
64
|
if value >= threshold:
|
65
|
-
raise ValueError(f"Value
|
65
|
+
raise ValueError(f"Value must be less than {threshold}.")
|
66
66
|
return value
|
67
67
|
self.validators.append(validator)
|
68
68
|
|
69
|
-
@register_rule("
|
70
|
-
|
69
|
+
@register_rule("Is between {min_val:Number} and {max_val:Number}", fixed_params={"negative": False})
|
70
|
+
@register_rule("Is not between {min_val:Number} and {max_val:Number}", fixed_params={"negative": True})
|
71
|
+
def validate_between(self, min_val: Numeric, max_val: Numeric, negative: bool):
|
71
72
|
def validator(value: Numeric):
|
72
|
-
|
73
|
-
|
73
|
+
condition = (min_val < value < max_val)
|
74
|
+
if condition and negative:
|
75
|
+
raise ValueError(f"Value must not be between {min_val} and {max_val}.")
|
76
|
+
if not condition and not negative:
|
77
|
+
raise ValueError(f"Value must be between {min_val} and {max_val}.")
|
74
78
|
return value
|
75
79
|
self.validators.append(validator)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import re
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from .BaseField import BaseField
|
5
|
+
from ..rules import register_rule, register_field
|
6
|
+
|
7
|
+
|
8
|
+
@register_field
|
9
|
+
class StringField(BaseField):
|
10
|
+
field_type = str
|
11
|
+
|
12
|
+
@register_rule("Is not empty")
|
13
|
+
def validate_not_empty(self):
|
14
|
+
def validator(value: str):
|
15
|
+
if value == "":
|
16
|
+
raise ValueError("String cannot be empty.")
|
17
|
+
return value
|
18
|
+
self.validators.append(validator)
|
19
|
+
|
20
|
+
@register_rule("Starts with {prefix:String}")
|
21
|
+
def validate_starts_with(self, prefix: List[str]):
|
22
|
+
def validator(value: str):
|
23
|
+
if not value.startswith(prefix):
|
24
|
+
raise ValueError(f"Value must start with '{prefix}'.")
|
25
|
+
return value
|
26
|
+
self.validators.append(validator)
|
27
|
+
|
28
|
+
@register_rule("Ends with {suffix:String}")
|
29
|
+
def validate_ends_with(self, suffix: List[str]):
|
30
|
+
def validator(value: str):
|
31
|
+
if not value.endswith(suffix):
|
32
|
+
raise ValueError(f"Value must end with '{suffix}'.")
|
33
|
+
return value
|
34
|
+
self.validators.append(validator)
|
35
|
+
|
36
|
+
@register_rule("Is one of {possible_values:Strings}", fixed_params={"negative": False})
|
37
|
+
@register_rule("Is not one of {possible_values:Strings}", fixed_params={"negative": True})
|
38
|
+
def validate_in(self, possible_values: List[str], negative: bool):
|
39
|
+
def validator(value: str):
|
40
|
+
condition = value in possible_values
|
41
|
+
if condition and negative:
|
42
|
+
raise ValueError(f"Value '{value}' is not allowed.")
|
43
|
+
if not condition and not negative:
|
44
|
+
raise ValueError(f"Value '{value}' must be one of the possible values.")
|
45
|
+
return value
|
46
|
+
self.validators.append(validator)
|
47
|
+
|
48
|
+
@register_rule("Has length between {min_val:Integer} and {max_val:Integer}")
|
49
|
+
def validate_length_between(self, min_val: int, max_val: int):
|
50
|
+
def validator(value: str):
|
51
|
+
if not (min_val < len(value) < max_val):
|
52
|
+
raise ValueError(f"Length must be between {min_val} and {max_val} characters.")
|
53
|
+
return value
|
54
|
+
self.validators.append(validator)
|
55
|
+
|
56
|
+
@register_rule("Has maximum length {max_len:Integer}")
|
57
|
+
def validate_max_length(self, max_len: int):
|
58
|
+
def validator(value: str):
|
59
|
+
if len(value) > max_len:
|
60
|
+
raise ValueError(f"Length must not exceed {max_len} characters.")
|
61
|
+
return value
|
62
|
+
self.validators.append(validator)
|
63
|
+
|
64
|
+
@register_rule("Has minimum length {min_len:Integer}")
|
65
|
+
def validate_min_length(self, min_len: int):
|
66
|
+
def validator(value: str):
|
67
|
+
if len(value) < min_len:
|
68
|
+
raise ValueError(f"Length must be at least {min_len} characters.")
|
69
|
+
return value
|
70
|
+
self.validators.append(validator)
|
71
|
+
|
72
|
+
@register_rule("Is uppercase")
|
73
|
+
def validate_uppercase(self):
|
74
|
+
def validator(value: str):
|
75
|
+
if not value.isupper():
|
76
|
+
raise ValueError("Value must be in uppercase.")
|
77
|
+
return value
|
78
|
+
self.validators.append(validator)
|
79
|
+
|
80
|
+
@register_rule("Is lowercase")
|
81
|
+
def validate_lowercase(self):
|
82
|
+
def validator(value: str):
|
83
|
+
if not value.islower():
|
84
|
+
raise ValueError("Value must be in lowercase.")
|
85
|
+
return value
|
86
|
+
self.validators.append(validator)
|
87
|
+
|
88
|
+
@register_rule("Matches regex {pattern:String}")
|
89
|
+
def validate_matches_regex(self, pattern: str):
|
90
|
+
def validator(value: str):
|
91
|
+
if not re.match(pattern, value):
|
92
|
+
raise ValueError(f"Value does not match the required pattern {pattern}.")
|
93
|
+
return value
|
94
|
+
self.validators.append(validator)
|
95
|
+
|
96
|
+
@register_rule("Is valid email")
|
97
|
+
def validate_email(self):
|
98
|
+
EMAIL_REGEX = r"^[\w\.-]+@[\w\.-]+\.\w+$"
|
99
|
+
|
100
|
+
def validator(value: str):
|
101
|
+
if not re.match(EMAIL_REGEX, value):
|
102
|
+
raise ValueError("Invalid email format.")
|
103
|
+
return value
|
104
|
+
self.validators.append(validator)
|
105
|
+
|
106
|
+
@register_rule("Is valid URL")
|
107
|
+
def validate_url(self):
|
108
|
+
URL_REGEX = r"^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$"
|
109
|
+
|
110
|
+
def validator(value: str):
|
111
|
+
if not re.match(URL_REGEX, value):
|
112
|
+
raise ValueError("Invalid URL format.")
|
113
|
+
return value
|
114
|
+
self.validators.append(validator)
|
115
|
+
|
116
|
+
@register_rule("Has no digits")
|
117
|
+
def validate_no_digits(self):
|
118
|
+
def validator(value: str):
|
119
|
+
if any(char.isdigit() for char in value):
|
120
|
+
raise ValueError("Value must not contain any digits.")
|
121
|
+
return value
|
122
|
+
self.validators.append(validator)
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import functools
|
1
2
|
import string
|
2
3
|
from inspect import signature
|
3
4
|
from typing import Callable
|
@@ -15,12 +16,15 @@ class Rule:
|
|
15
16
|
field_type: str
|
16
17
|
field_rule: str
|
17
18
|
rule_setter: Callable
|
19
|
+
fixed_params: dict
|
18
20
|
|
19
|
-
def __init__(self, field_type: str, field_rule: str, rule_setter: Callable) -> None:
|
21
|
+
def __init__(self, field_type: str, field_rule: str, rule_setter: Callable, fixed_params: dict = None) -> None:
|
20
22
|
self.field_type = field_type
|
21
23
|
self.field_rule = field_rule
|
22
24
|
self.rule_setter = rule_setter
|
25
|
+
self.fixed_params = fixed_params or {}
|
23
26
|
self.__validate_rule_function_params()
|
27
|
+
self.__apply_fixed_params()
|
24
28
|
|
25
29
|
def __repr__(self):
|
26
30
|
return self.field_rule
|
@@ -39,6 +43,19 @@ class Rule:
|
|
39
43
|
if "self" not in rule_setter_params:
|
40
44
|
raise NotAClassMethod()
|
41
45
|
|
46
|
+
rule_setter_params = rule_setter_params - set(self.fixed_params)
|
42
47
|
rule_setter_params.remove("self")
|
43
48
|
if set(self.rule_params) != rule_setter_params:
|
44
|
-
|
49
|
+
rule_total_params = set(self.rule_params).union(set(self.fixed_params))
|
50
|
+
raise RuleFunctionParamsMismatch(f"Rule Params: {rule_total_params}, Setter Params: {rule_setter_params}")
|
51
|
+
|
52
|
+
def __apply_fixed_params(self):
|
53
|
+
if not self.fixed_params:
|
54
|
+
return
|
55
|
+
rule_setter_sign = signature(self.rule_setter)
|
56
|
+
all_params = rule_setter_sign.parameters
|
57
|
+
|
58
|
+
for param in self.fixed_params:
|
59
|
+
if param not in all_params:
|
60
|
+
raise ValueError(f"The fixed parameter '{param}' is not in the function '{self.rule_setter.__name__}'.")
|
61
|
+
self.rule_setter = functools.partial(self.rule_setter, **self.fixed_params)
|
@@ -17,12 +17,12 @@ class RuleRegistry:
|
|
17
17
|
type_map: Dict[str, Type["BaseField"]] = {}
|
18
18
|
|
19
19
|
@classmethod
|
20
|
-
def register_rule(cls, field_rule: str):
|
20
|
+
def register_rule(cls, field_rule: str, fixed_params: dict = None):
|
21
21
|
def _register(func: callable):
|
22
22
|
field_type, func_name = func.__qualname__.split(".")
|
23
23
|
logger.debug("Registering function '%s' for %s. Rule: %s", func_name, field_type, field_rule)
|
24
24
|
|
25
|
-
rule = Rule(field_type, field_rule, func)
|
25
|
+
rule = Rule(field_type, field_rule, func, fixed_params)
|
26
26
|
cls.rules[field_type].append(rule)
|
27
27
|
logger.debug("Function '%s' Registered", func_name)
|
28
28
|
return func
|
@@ -57,8 +57,8 @@ class RuleRegistry:
|
|
57
57
|
]
|
58
58
|
|
59
59
|
|
60
|
-
def register_rule(rule: str):
|
61
|
-
return RuleRegistry.register_rule(rule)
|
60
|
+
def register_rule(rule: str, fixed_params: dict = None):
|
61
|
+
return RuleRegistry.register_rule(rule, fixed_params)
|
62
62
|
|
63
63
|
|
64
64
|
def register_field(field_class: type):
|
@@ -1,8 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: data-sitter
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: A Python library that reads data contracts and generates Pydantic models for seamless data validation.
|
5
5
|
Author-email: Lázaro Pereira Candea <lazaro@candea.es>
|
6
6
|
Requires-Dist: python-dotenv==1.0.1
|
7
|
+
Requires-Dist: PyYAML==6.0.2
|
7
8
|
Requires-Dist: parse_type==0.6.4
|
8
9
|
Requires-Dist: pydantic==2.10.5
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = 'data-sitter'
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.4"
|
8
8
|
description = "A Python library that reads data contracts and generates Pydantic models for seamless data validation."
|
9
9
|
authors = [
|
10
10
|
{name = 'Lázaro Pereira Candea', email = 'lazaro@candea.es'},
|
@@ -12,6 +12,7 @@ authors = [
|
|
12
12
|
dependencies = [
|
13
13
|
# Keep this in sync with setup.py
|
14
14
|
"python-dotenv==1.0.1",
|
15
|
+
"PyYAML==6.0.2",
|
15
16
|
"parse_type==0.6.4",
|
16
17
|
"pydantic==2.10.5",
|
17
18
|
]
|
@@ -3,11 +3,12 @@ from setuptools import setup, find_packages
|
|
3
3
|
|
4
4
|
setup(
|
5
5
|
name='data-sitter',
|
6
|
-
version='0.1.
|
6
|
+
version='0.1.4',
|
7
7
|
packages=find_packages(),
|
8
8
|
install_requires=[
|
9
9
|
# Keep this in sync with pyproject.toml
|
10
10
|
"python-dotenv==1.0.1",
|
11
|
+
"PyYAML==6.0.2",
|
11
12
|
"parse_type==0.6.4",
|
12
13
|
"pydantic==2.10.5",
|
13
14
|
],
|
@@ -1,89 +0,0 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
|
-
from .BaseField import BaseField
|
4
|
-
from ..rules import register_rule, register_field
|
5
|
-
|
6
|
-
|
7
|
-
@register_field
|
8
|
-
class StringField(BaseField):
|
9
|
-
field_type = str
|
10
|
-
|
11
|
-
@register_rule("Is not empty")
|
12
|
-
def validate_not_empty(self):
|
13
|
-
def validator(value: str):
|
14
|
-
if value == "":
|
15
|
-
raise ValueError("The value is empty")
|
16
|
-
return value
|
17
|
-
self.validators.append(validator)
|
18
|
-
|
19
|
-
@register_rule("Starts with {prefix:String}")
|
20
|
-
def validate_starts_with(self, prefix: List[str]):
|
21
|
-
def validator(value: str):
|
22
|
-
if not value.startswith(prefix):
|
23
|
-
raise ValueError(f"The value '{value}' does not start with '{prefix}'.")
|
24
|
-
return value
|
25
|
-
self.validators.append(validator)
|
26
|
-
|
27
|
-
@register_rule("Ends with {sufix:String}")
|
28
|
-
def validate_ends_with(self, sufix: List[str]):
|
29
|
-
def validator(value: str):
|
30
|
-
if not value.endswith(sufix):
|
31
|
-
raise ValueError(f"The value '{value}' does not ends with '{sufix}'.")
|
32
|
-
return value
|
33
|
-
self.validators.append(validator)
|
34
|
-
|
35
|
-
@register_rule("Value in {possible_values:Strings}")
|
36
|
-
def validate_in(self, possible_values: List[str]):
|
37
|
-
def validator(value: str):
|
38
|
-
if value not in possible_values:
|
39
|
-
raise ValueError(f"The value '{value}' is not in the list.")
|
40
|
-
return value
|
41
|
-
self.validators.append(validator)
|
42
|
-
|
43
|
-
@register_rule("Length between {min_val:Integer} and {max_val:Integer}")
|
44
|
-
def validate_length_between(self, min_val: int, max_val: int):
|
45
|
-
def validator(value: str):
|
46
|
-
if not (min_val < len(value) < max_val):
|
47
|
-
raise ValueError(f"Length {len(value)} is not in between {min_val} and {max_val}.")
|
48
|
-
return value
|
49
|
-
self.validators.append(validator)
|
50
|
-
|
51
|
-
@register_rule("Maximum length of {max_len:Integer}")
|
52
|
-
def validate_max_length(self, max_len: int):
|
53
|
-
def validator(value: str):
|
54
|
-
if len(value) > max_len:
|
55
|
-
raise ValueError(f"Length {len(value)} is longer than {max_len}.")
|
56
|
-
return value
|
57
|
-
self.validators.append(validator)
|
58
|
-
|
59
|
-
@register_rule("Length shorter than {max_len:Integer}")
|
60
|
-
def validate_shorter_than(self, max_len: int):
|
61
|
-
def validator(value: str):
|
62
|
-
if len(value) >= max_len:
|
63
|
-
raise ValueError(f"Length {len(value)} is not in shorter than {max_len}.")
|
64
|
-
return value
|
65
|
-
self.validators.append(validator)
|
66
|
-
|
67
|
-
@register_rule("Minimum length of {min_len:Integer}")
|
68
|
-
def validate_min_length(self, min_len: int):
|
69
|
-
def validator(value: str):
|
70
|
-
if len(value) < min_len:
|
71
|
-
raise ValueError(f"Length {len(value)} is shorter than {min_len}.")
|
72
|
-
return value
|
73
|
-
self.validators.append(validator)
|
74
|
-
|
75
|
-
@register_rule("Length longer than {min_len:Integer}")
|
76
|
-
def validate_longer_than(self, min_len: int):
|
77
|
-
def validator(value: str):
|
78
|
-
if len(value) <= min_len:
|
79
|
-
raise ValueError(f"Length {len(value)} is not in longer than {min_len}.")
|
80
|
-
return value
|
81
|
-
self.validators.append(validator)
|
82
|
-
|
83
|
-
@register_rule("Is uppercase")
|
84
|
-
def validate_uppercase(self):
|
85
|
-
def validator(value: str):
|
86
|
-
if not value.isupper():
|
87
|
-
raise ValueError("Not Uppercase")
|
88
|
-
return value
|
89
|
-
self.validators.append(validator)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|