data-sitter 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
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/Contract.py +23 -31
- data_sitter/FieldResolver.py +31 -18
- data_sitter/cli.py +1 -1
- data_sitter/field_types/BaseField.py +24 -15
- data_sitter/field_types/FieldTypes.py +9 -0
- data_sitter/field_types/FloatField.py +13 -4
- data_sitter/field_types/IntegerField.py +2 -0
- data_sitter/field_types/NumericField.py +11 -9
- data_sitter/field_types/StringField.py +15 -13
- data_sitter/rules/Enums.py +7 -0
- data_sitter/rules/LogicalRule.py +68 -0
- data_sitter/rules/MatchedRule.py +17 -14
- data_sitter/rules/Parser/alias_parameters_parser.py +0 -20
- data_sitter/rules/ProcessedRule.py +24 -0
- data_sitter/rules/RuleRegistry.py +49 -28
- data_sitter/rules/__init__.py +7 -1
- data_sitter/utils/logger_config.py +1 -1
- data_sitter-0.1.6.dist-info/METADATA +220 -0
- data_sitter-0.1.6.dist-info/RECORD +30 -0
- {data_sitter-0.1.4.dist-info → data_sitter-0.1.6.dist-info}/WHEEL +1 -1
- data_sitter-0.1.4.dist-info/METADATA +0 -9
- data_sitter-0.1.4.dist-info/RECORD +0 -26
- {data_sitter-0.1.4.dist-info → data_sitter-0.1.6.dist-info}/entry_points.txt +0 -0
- {data_sitter-0.1.4.dist-info → data_sitter-0.1.6.dist-info}/top_level.txt +0 -0
data_sitter/Contract.py
CHANGED
@@ -8,7 +8,7 @@ from pydantic import BaseModel
|
|
8
8
|
from .Validation import Validation
|
9
9
|
from .field_types import BaseField
|
10
10
|
from .FieldResolver import FieldResolver
|
11
|
-
from .rules import
|
11
|
+
from .rules import ProcessedRule, RuleRegistry, RuleParser
|
12
12
|
|
13
13
|
|
14
14
|
class ContractWithoutFields(Exception):
|
@@ -20,9 +20,9 @@ class ContractWithoutName(Exception):
|
|
20
20
|
|
21
21
|
|
22
22
|
class Field(NamedTuple):
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
name: str
|
24
|
+
type: str
|
25
|
+
rules: List[str]
|
26
26
|
|
27
27
|
|
28
28
|
class Contract:
|
@@ -37,8 +37,8 @@ class Contract:
|
|
37
37
|
self.fields = fields
|
38
38
|
self.rule_parser = RuleParser(values)
|
39
39
|
self.field_resolvers = {
|
40
|
-
|
41
|
-
for
|
40
|
+
_type: FieldResolver(RuleRegistry.get_type(_type), self.rule_parser)
|
41
|
+
for _type in list({field.type for field in self.fields}) # Unique types
|
42
42
|
}
|
43
43
|
|
44
44
|
@classmethod
|
@@ -66,21 +66,18 @@ class Contract:
|
|
66
66
|
def field_validators(self) -> Dict[str, BaseField]:
|
67
67
|
field_validators = {}
|
68
68
|
for field in self.fields:
|
69
|
-
field_resolver = self.field_resolvers[field.
|
70
|
-
field_validators[field.
|
69
|
+
field_resolver = self.field_resolvers[field.type]
|
70
|
+
field_validators[field.name] = field_resolver.get_field_validator(field.name, field.rules)
|
71
71
|
return field_validators
|
72
72
|
|
73
73
|
@cached_property
|
74
|
-
def rules(self) -> Dict[str, List[
|
74
|
+
def rules(self) -> Dict[str, List[ProcessedRule]]:
|
75
75
|
rules = {}
|
76
76
|
for field in self.fields:
|
77
|
-
field_resolver = self.field_resolvers[field.
|
78
|
-
rules[field.
|
77
|
+
field_resolver = self.field_resolvers[field.type]
|
78
|
+
rules[field.name] = field_resolver.get_processed_rules(field.rules)
|
79
79
|
return rules
|
80
80
|
|
81
|
-
def model_validate(self, item: dict):
|
82
|
-
return self.pydantic_model.model_validate(item).model_dump()
|
83
|
-
|
84
81
|
def validate(self, item: dict) -> Validation:
|
85
82
|
return Validation.validate(self.pydantic_model, item)
|
86
83
|
|
@@ -88,8 +85,8 @@ class Contract:
|
|
88
85
|
def pydantic_model(self) -> BaseModel:
|
89
86
|
return type(self.name, (BaseModel,), {
|
90
87
|
"__annotations__": {
|
91
|
-
|
92
|
-
for
|
88
|
+
name: field_validator.get_annotation()
|
89
|
+
for name, field_validator in self.field_validators.items()
|
93
90
|
}
|
94
91
|
})
|
95
92
|
|
@@ -99,11 +96,11 @@ class Contract:
|
|
99
96
|
"name": self.name,
|
100
97
|
"fields": [
|
101
98
|
{
|
102
|
-
"
|
103
|
-
"
|
104
|
-
"
|
99
|
+
"name": name,
|
100
|
+
"type": field_validator.type_name.value,
|
101
|
+
"rules": [rule.parsed_rule for rule in self.rules.get(name, [])]
|
105
102
|
}
|
106
|
-
for
|
103
|
+
for name, field_validator in self.field_validators.items()
|
107
104
|
],
|
108
105
|
"values": self.rule_parser.values
|
109
106
|
}
|
@@ -119,19 +116,14 @@ class Contract:
|
|
119
116
|
"name": self.name,
|
120
117
|
"fields": [
|
121
118
|
{
|
122
|
-
"
|
123
|
-
"
|
124
|
-
"
|
125
|
-
|
126
|
-
|
127
|
-
"parsed_rule": rule.parsed_rule,
|
128
|
-
"rule_params": rule.rule_params,
|
129
|
-
"parsed_values": rule.parsed_values,
|
130
|
-
}
|
131
|
-
for rule in self.rules.get(field_name, [])
|
119
|
+
"name": name,
|
120
|
+
"type": field_validator.type_name.value,
|
121
|
+
"rules": [
|
122
|
+
rule.get_front_end_repr()
|
123
|
+
for rule in self.rules.get(name, [])
|
132
124
|
]
|
133
125
|
}
|
134
|
-
for
|
126
|
+
for name, field_validator in self.field_validators.items()
|
135
127
|
],
|
136
128
|
"values": self.rule_parser.values
|
137
129
|
}
|
data_sitter/FieldResolver.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
from typing import Dict, List, Type
|
1
|
+
from typing import Dict, List, Type, Union
|
2
2
|
|
3
3
|
from .field_types import BaseField
|
4
|
-
from .rules import MatchedRule,
|
4
|
+
from .rules import Rule, ProcessedRule, LogicalRule, MatchedRule, RuleRegistry, LogicalOperator
|
5
5
|
from .rules.Parser import RuleParser
|
6
6
|
|
7
7
|
|
@@ -9,6 +9,10 @@ class RuleNotFoundError(Exception):
|
|
9
9
|
"""No matching rule found for the given parsed rule."""
|
10
10
|
|
11
11
|
|
12
|
+
class MalformedLogicalRuleError(Exception):
|
13
|
+
"""Logical rule structure not recognised."""
|
14
|
+
|
15
|
+
|
12
16
|
class FieldResolver:
|
13
17
|
field_class: Type[BaseField]
|
14
18
|
rule_parser: RuleParser
|
@@ -21,23 +25,32 @@ class FieldResolver:
|
|
21
25
|
self.rules = RuleRegistry.get_rules_for(field_class)
|
22
26
|
self._match_rule_cache = {}
|
23
27
|
|
24
|
-
def
|
25
|
-
|
28
|
+
def get_field_validator(self, name: str, parsed_rules: List[Union[str, dict]]) -> BaseField:
|
29
|
+
field_validator = self.field_class(name)
|
30
|
+
processed_rules = self.get_processed_rules(parsed_rules)
|
31
|
+
validators = [pr.get_validator(field_validator) for pr in processed_rules]
|
32
|
+
field_validator.validators = validators
|
33
|
+
return field_validator
|
34
|
+
|
35
|
+
def get_processed_rules(self, parsed_rules: List[Union[str, dict]]) -> List[ProcessedRule]:
|
36
|
+
processed_rules = []
|
26
37
|
for parsed_rule in parsed_rules:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
if isinstance(parsed_rule, dict):
|
39
|
+
if len(keys := tuple(parsed_rule)) != 1 or (operator := keys[0]) not in LogicalOperator:
|
40
|
+
raise MalformedLogicalRuleError()
|
41
|
+
if operator == LogicalOperator.NOT and not isinstance(parsed_rule[operator], list):
|
42
|
+
parsed_rule = {operator: [parsed_rule[operator]]} # NOT operator can be a single rule
|
43
|
+
processed_rule = LogicalRule(operator, self.get_processed_rules(parsed_rule[operator]))
|
44
|
+
elif isinstance(parsed_rule, str):
|
45
|
+
processed_rule = self._match_rule(parsed_rule)
|
46
|
+
if not processed_rule:
|
47
|
+
raise RuleNotFoundError(f"Rule not found for parsed rule: '{parsed_rule}'")
|
48
|
+
else:
|
49
|
+
raise TypeError(f'Parsed Rule type not recognised: {type(parsed_rule)}')
|
50
|
+
processed_rules.append(processed_rule)
|
51
|
+
return processed_rules
|
52
|
+
|
53
|
+
def _match_rule(self, parsed_rule: str) -> MatchedRule:
|
41
54
|
if parsed_rule in self._match_rule_cache:
|
42
55
|
return self._match_rule_cache[parsed_rule]
|
43
56
|
|
data_sitter/cli.py
CHANGED
@@ -1,18 +1,24 @@
|
|
1
1
|
from abc import ABC
|
2
|
-
from typing import Annotated, List, Optional, Type
|
2
|
+
from typing import Annotated, Callable, List, Optional, Type
|
3
3
|
|
4
4
|
from pydantic import AfterValidator
|
5
|
+
|
6
|
+
from .FieldTypes import FieldTypes
|
5
7
|
from ..rules import register_rule, register_field
|
6
8
|
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
class NotInitialisedError(Exception):
|
11
|
+
"""The field instance is initialised without validators"""
|
12
|
+
|
13
|
+
|
14
|
+
def aggregated_validator(validators: List[Callable], is_optional: bool):
|
15
|
+
def validator(value):
|
10
16
|
if is_optional and value is None:
|
11
17
|
return value
|
12
18
|
for validator_func in validators:
|
13
19
|
validator_func(value)
|
14
20
|
return value
|
15
|
-
return
|
21
|
+
return validator
|
16
22
|
|
17
23
|
@register_field
|
18
24
|
class BaseField(ABC):
|
@@ -20,39 +26,42 @@ class BaseField(ABC):
|
|
20
26
|
is_optional: bool
|
21
27
|
validators = None
|
22
28
|
field_type = None
|
29
|
+
type_name = FieldTypes.BASE
|
23
30
|
|
24
31
|
def __init__(self, name: str) -> None:
|
25
32
|
self.name = name
|
26
33
|
self.is_optional = True
|
27
|
-
self.validators =
|
34
|
+
self.validators = None
|
28
35
|
|
29
36
|
@register_rule("Is not null")
|
30
37
|
def validator_not_null(self):
|
31
|
-
def
|
32
|
-
if self.is_optional:
|
33
|
-
return value
|
38
|
+
def validator(value):
|
34
39
|
if value is None:
|
35
40
|
raise ValueError("Value cannot be null.")
|
36
41
|
return value
|
37
42
|
|
38
43
|
self.is_optional = False
|
39
|
-
|
44
|
+
return validator
|
40
45
|
|
41
46
|
def validate(self, value):
|
47
|
+
if self.validators is None:
|
48
|
+
raise NotInitialisedError()
|
42
49
|
for validator in self.validators:
|
43
50
|
validator(value)
|
44
51
|
|
45
52
|
def get_annotation(self):
|
53
|
+
if self.validators is None:
|
54
|
+
raise NotInitialisedError()
|
46
55
|
field_type = Optional[self.field_type] if self.is_optional else self.field_type
|
47
56
|
return Annotated[field_type, AfterValidator(aggregated_validator(self.validators, self.is_optional))]
|
48
57
|
|
49
58
|
@classmethod
|
50
59
|
def get_parents(cls: Type["BaseField"]) -> List[Type["BaseField"]]:
|
51
|
-
if cls
|
60
|
+
if cls == BaseField:
|
52
61
|
return []
|
53
|
-
ancestors =
|
62
|
+
ancestors = set()
|
54
63
|
for base in cls.__bases__:
|
55
|
-
if base
|
56
|
-
ancestors.
|
57
|
-
ancestors.
|
58
|
-
return ancestors
|
64
|
+
if issubclass(base, BaseField):
|
65
|
+
ancestors.add(base)
|
66
|
+
ancestors.update(base.get_parents())
|
67
|
+
return list(ancestors)
|
@@ -1,17 +1,26 @@
|
|
1
|
+
from .FieldTypes import FieldTypes
|
1
2
|
from .NumericField import NumericField
|
2
3
|
from ..rules import register_field, register_rule
|
4
|
+
from decimal import Decimal
|
3
5
|
|
4
6
|
|
5
7
|
@register_field
|
6
8
|
class FloatField(NumericField):
|
7
9
|
field_type = float
|
10
|
+
type_name = FieldTypes.FLOAT
|
11
|
+
|
8
12
|
|
9
13
|
@register_rule("Has at most {decimal_places:Integer} decimal places")
|
10
14
|
def validate_max_decimal_places(self, decimal_places: int):
|
11
15
|
def validator(value):
|
12
|
-
|
13
|
-
|
14
|
-
if
|
16
|
+
decimal_str = str(Decimal(str(value)).normalize())
|
17
|
+
# If no decimal point or only zeros after decimal, it has 0 decimal places
|
18
|
+
if '.' not in decimal_str:
|
19
|
+
decimal_places_count = 0
|
20
|
+
else:
|
21
|
+
decimal_places_count = len(decimal_str.split('.')[1])
|
22
|
+
|
23
|
+
if decimal_places_count > decimal_places:
|
15
24
|
raise ValueError(f"Value must have at most {decimal_places} decimal places.")
|
16
25
|
return value
|
17
|
-
|
26
|
+
return validator
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from .FieldTypes import FieldTypes
|
1
2
|
from .NumericField import NumericField
|
2
3
|
from ..rules import register_field
|
3
4
|
|
@@ -5,3 +6,4 @@ from ..rules import register_field
|
|
5
6
|
@register_field
|
6
7
|
class IntegerField(NumericField):
|
7
8
|
field_type = int
|
9
|
+
type_name = FieldTypes.INT
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import Union
|
2
2
|
|
3
3
|
from .BaseField import BaseField
|
4
|
+
from .FieldTypes import FieldTypes
|
4
5
|
from ..rules import register_rule, register_field
|
5
6
|
|
6
7
|
Numeric = Union[int, float]
|
@@ -9,6 +10,7 @@ Numeric = Union[int, float]
|
|
9
10
|
@register_field
|
10
11
|
class NumericField(BaseField):
|
11
12
|
field_type = Numeric
|
13
|
+
type_name = FieldTypes.NUMERIC
|
12
14
|
|
13
15
|
@register_rule("Is not zero")
|
14
16
|
def validate_non_zero(self):
|
@@ -16,15 +18,15 @@ class NumericField(BaseField):
|
|
16
18
|
if value == 0:
|
17
19
|
raise ValueError("Value cannot be zero.")
|
18
20
|
return value
|
19
|
-
|
21
|
+
return validator
|
20
22
|
|
21
23
|
@register_rule("Is positive")
|
22
24
|
def validate_positive(self):
|
23
25
|
def validator(value: Numeric):
|
24
|
-
if value
|
26
|
+
if value <= 0:
|
25
27
|
raise ValueError("Value must be positive.")
|
26
28
|
return value
|
27
|
-
|
29
|
+
return validator
|
28
30
|
|
29
31
|
@register_rule("Is negative")
|
30
32
|
def validate_negative(self):
|
@@ -32,7 +34,7 @@ class NumericField(BaseField):
|
|
32
34
|
if value >= 0:
|
33
35
|
raise ValueError("Value must be less than zero.")
|
34
36
|
return value
|
35
|
-
|
37
|
+
return validator
|
36
38
|
|
37
39
|
@register_rule("Is at least {min_val:Number}")
|
38
40
|
def validate_min(self, min_val: Numeric):
|
@@ -40,7 +42,7 @@ class NumericField(BaseField):
|
|
40
42
|
if value < min_val:
|
41
43
|
raise ValueError(f"Value must be at least {min_val}.")
|
42
44
|
return value
|
43
|
-
|
45
|
+
return validator
|
44
46
|
|
45
47
|
@register_rule("Is at most {max_val:Number}")
|
46
48
|
def validate_max(self, max_val: Numeric):
|
@@ -48,7 +50,7 @@ class NumericField(BaseField):
|
|
48
50
|
if value > max_val:
|
49
51
|
raise ValueError(f"Value must not exceed {max_val}.")
|
50
52
|
return value
|
51
|
-
|
53
|
+
return validator
|
52
54
|
|
53
55
|
@register_rule("Is greater than {threshold:Number}")
|
54
56
|
def validate_greater_than(self, threshold: Numeric):
|
@@ -56,7 +58,7 @@ class NumericField(BaseField):
|
|
56
58
|
if value <= threshold:
|
57
59
|
raise ValueError(f"Value must be greater than {threshold}.")
|
58
60
|
return value
|
59
|
-
|
61
|
+
return validator
|
60
62
|
|
61
63
|
@register_rule("Is less than {threshold:Number}")
|
62
64
|
def validate_less_than(self, threshold: Numeric):
|
@@ -64,7 +66,7 @@ class NumericField(BaseField):
|
|
64
66
|
if value >= threshold:
|
65
67
|
raise ValueError(f"Value must be less than {threshold}.")
|
66
68
|
return value
|
67
|
-
|
69
|
+
return validator
|
68
70
|
|
69
71
|
@register_rule("Is between {min_val:Number} and {max_val:Number}", fixed_params={"negative": False})
|
70
72
|
@register_rule("Is not between {min_val:Number} and {max_val:Number}", fixed_params={"negative": True})
|
@@ -76,4 +78,4 @@ class NumericField(BaseField):
|
|
76
78
|
if not condition and not negative:
|
77
79
|
raise ValueError(f"Value must be between {min_val} and {max_val}.")
|
78
80
|
return value
|
79
|
-
|
81
|
+
return validator
|
@@ -2,12 +2,14 @@ import re
|
|
2
2
|
from typing import List
|
3
3
|
|
4
4
|
from .BaseField import BaseField
|
5
|
+
from .FieldTypes import FieldTypes
|
5
6
|
from ..rules import register_rule, register_field
|
6
7
|
|
7
8
|
|
8
9
|
@register_field
|
9
10
|
class StringField(BaseField):
|
10
11
|
field_type = str
|
12
|
+
type_name = FieldTypes.STRING
|
11
13
|
|
12
14
|
@register_rule("Is not empty")
|
13
15
|
def validate_not_empty(self):
|
@@ -15,7 +17,7 @@ class StringField(BaseField):
|
|
15
17
|
if value == "":
|
16
18
|
raise ValueError("String cannot be empty.")
|
17
19
|
return value
|
18
|
-
|
20
|
+
return validator
|
19
21
|
|
20
22
|
@register_rule("Starts with {prefix:String}")
|
21
23
|
def validate_starts_with(self, prefix: List[str]):
|
@@ -23,7 +25,7 @@ class StringField(BaseField):
|
|
23
25
|
if not value.startswith(prefix):
|
24
26
|
raise ValueError(f"Value must start with '{prefix}'.")
|
25
27
|
return value
|
26
|
-
|
28
|
+
return validator
|
27
29
|
|
28
30
|
@register_rule("Ends with {suffix:String}")
|
29
31
|
def validate_ends_with(self, suffix: List[str]):
|
@@ -31,7 +33,7 @@ class StringField(BaseField):
|
|
31
33
|
if not value.endswith(suffix):
|
32
34
|
raise ValueError(f"Value must end with '{suffix}'.")
|
33
35
|
return value
|
34
|
-
|
36
|
+
return validator
|
35
37
|
|
36
38
|
@register_rule("Is one of {possible_values:Strings}", fixed_params={"negative": False})
|
37
39
|
@register_rule("Is not one of {possible_values:Strings}", fixed_params={"negative": True})
|
@@ -43,7 +45,7 @@ class StringField(BaseField):
|
|
43
45
|
if not condition and not negative:
|
44
46
|
raise ValueError(f"Value '{value}' must be one of the possible values.")
|
45
47
|
return value
|
46
|
-
|
48
|
+
return validator
|
47
49
|
|
48
50
|
@register_rule("Has length between {min_val:Integer} and {max_val:Integer}")
|
49
51
|
def validate_length_between(self, min_val: int, max_val: int):
|
@@ -51,7 +53,7 @@ class StringField(BaseField):
|
|
51
53
|
if not (min_val < len(value) < max_val):
|
52
54
|
raise ValueError(f"Length must be between {min_val} and {max_val} characters.")
|
53
55
|
return value
|
54
|
-
|
56
|
+
return validator
|
55
57
|
|
56
58
|
@register_rule("Has maximum length {max_len:Integer}")
|
57
59
|
def validate_max_length(self, max_len: int):
|
@@ -59,7 +61,7 @@ class StringField(BaseField):
|
|
59
61
|
if len(value) > max_len:
|
60
62
|
raise ValueError(f"Length must not exceed {max_len} characters.")
|
61
63
|
return value
|
62
|
-
|
64
|
+
return validator
|
63
65
|
|
64
66
|
@register_rule("Has minimum length {min_len:Integer}")
|
65
67
|
def validate_min_length(self, min_len: int):
|
@@ -67,7 +69,7 @@ class StringField(BaseField):
|
|
67
69
|
if len(value) < min_len:
|
68
70
|
raise ValueError(f"Length must be at least {min_len} characters.")
|
69
71
|
return value
|
70
|
-
|
72
|
+
return validator
|
71
73
|
|
72
74
|
@register_rule("Is uppercase")
|
73
75
|
def validate_uppercase(self):
|
@@ -75,7 +77,7 @@ class StringField(BaseField):
|
|
75
77
|
if not value.isupper():
|
76
78
|
raise ValueError("Value must be in uppercase.")
|
77
79
|
return value
|
78
|
-
|
80
|
+
return validator
|
79
81
|
|
80
82
|
@register_rule("Is lowercase")
|
81
83
|
def validate_lowercase(self):
|
@@ -83,7 +85,7 @@ class StringField(BaseField):
|
|
83
85
|
if not value.islower():
|
84
86
|
raise ValueError("Value must be in lowercase.")
|
85
87
|
return value
|
86
|
-
|
88
|
+
return validator
|
87
89
|
|
88
90
|
@register_rule("Matches regex {pattern:String}")
|
89
91
|
def validate_matches_regex(self, pattern: str):
|
@@ -91,7 +93,7 @@ class StringField(BaseField):
|
|
91
93
|
if not re.match(pattern, value):
|
92
94
|
raise ValueError(f"Value does not match the required pattern {pattern}.")
|
93
95
|
return value
|
94
|
-
|
96
|
+
return validator
|
95
97
|
|
96
98
|
@register_rule("Is valid email")
|
97
99
|
def validate_email(self):
|
@@ -101,7 +103,7 @@ class StringField(BaseField):
|
|
101
103
|
if not re.match(EMAIL_REGEX, value):
|
102
104
|
raise ValueError("Invalid email format.")
|
103
105
|
return value
|
104
|
-
|
106
|
+
return validator
|
105
107
|
|
106
108
|
@register_rule("Is valid URL")
|
107
109
|
def validate_url(self):
|
@@ -111,7 +113,7 @@ class StringField(BaseField):
|
|
111
113
|
if not re.match(URL_REGEX, value):
|
112
114
|
raise ValueError("Invalid URL format.")
|
113
115
|
return value
|
114
|
-
|
116
|
+
return validator
|
115
117
|
|
116
118
|
@register_rule("Has no digits")
|
117
119
|
def validate_no_digits(self):
|
@@ -119,4 +121,4 @@ class StringField(BaseField):
|
|
119
121
|
if any(char.isdigit() for char in value):
|
120
122
|
raise ValueError("Value must not contain any digits.")
|
121
123
|
return value
|
122
|
-
|
124
|
+
return validator
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Callable, List
|
2
|
+
|
3
|
+
from .Enums import LogicalOperator
|
4
|
+
from .ProcessedRule import ProcessedRule, LogicalParsedRule
|
5
|
+
|
6
|
+
if TYPE_CHECKING: # pragma: no cover
|
7
|
+
from ..field_types import BaseField
|
8
|
+
|
9
|
+
|
10
|
+
def and_or_validator(validators: List[Callable], operator: LogicalOperator) -> Callable:
|
11
|
+
def validator(value):
|
12
|
+
exceptions = []
|
13
|
+
for validator_ in validators:
|
14
|
+
try:
|
15
|
+
validator_(value)
|
16
|
+
exceptions.append(None) # No error, validation passed
|
17
|
+
except Exception as e:
|
18
|
+
exceptions.append(str(e)) # Store error message
|
19
|
+
|
20
|
+
if operator == LogicalOperator.OR and all(exceptions):
|
21
|
+
raise ValueError(f"None of the conditions were met. Errors: {exceptions}")
|
22
|
+
if operator == LogicalOperator.AND and any(exceptions):
|
23
|
+
exceptions = list(filter(None, exceptions))
|
24
|
+
raise ValueError(f"Not all conditions were met. Errors: {exceptions}")
|
25
|
+
return value
|
26
|
+
return validator
|
27
|
+
|
28
|
+
|
29
|
+
def not_validator(validator_: Callable):
|
30
|
+
def validator(value):
|
31
|
+
try:
|
32
|
+
validator_(value)
|
33
|
+
except Exception:
|
34
|
+
return value # Validation passes if the condition fails
|
35
|
+
else:
|
36
|
+
raise ValueError("Condition was met, but expected NOT to be met.")
|
37
|
+
return validator
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
class LogicalRule(ProcessedRule):
|
42
|
+
operator: LogicalOperator
|
43
|
+
processed_rules: List[ProcessedRule]
|
44
|
+
|
45
|
+
def __init__(self, operator: LogicalOperator, processed_rules: List[ProcessedRule]):
|
46
|
+
if operator not in LogicalOperator:
|
47
|
+
raise TypeError(f'Logical Operator not recognised: {operator}')
|
48
|
+
if not processed_rules:
|
49
|
+
raise ValueError("Logical rules must have at least one rule.")
|
50
|
+
if operator == LogicalOperator.NOT and len(processed_rules) != 1:
|
51
|
+
raise TypeError(f'Not Operator can only contain one rule. Cotains: {len(processed_rules)}')
|
52
|
+
self.operator = operator
|
53
|
+
self.processed_rules = processed_rules
|
54
|
+
|
55
|
+
@property
|
56
|
+
def parsed_rule(self) -> LogicalParsedRule:
|
57
|
+
return {self.operator: [pr.parsed_rule for pr in self.processed_rules]}
|
58
|
+
|
59
|
+
def get_validator(self, field_instance: "BaseField") -> Callable:
|
60
|
+
if self.operator in (LogicalOperator.OR, LogicalOperator.AND):
|
61
|
+
return and_or_validator([pr.get_validator(field_instance) for pr in self.processed_rules], self.operator)
|
62
|
+
elif self.operator == LogicalOperator.NOT:
|
63
|
+
return not_validator(self.processed_rules[0].get_validator(field_instance))
|
64
|
+
else:
|
65
|
+
raise TypeError(f'Logical Operator not recognised: {self.operator}')
|
66
|
+
|
67
|
+
def get_front_end_repr(self) -> dict:
|
68
|
+
return {self.operator: [pr.get_front_end_repr() for pr in self.processed_rules]}
|
data_sitter/rules/MatchedRule.py
CHANGED
@@ -1,23 +1,20 @@
|
|
1
|
-
from typing import TYPE_CHECKING, Any, Dict
|
1
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict
|
2
2
|
|
3
3
|
from .Rule import Rule
|
4
4
|
from .RuleRegistry import RuleRegistry
|
5
|
+
from .ProcessedRule import ProcessedRule, MatchedParsedRule
|
5
6
|
from .Parser.parser_utils import get_value_from_reference
|
6
7
|
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from field_types import BaseField
|
8
|
+
if TYPE_CHECKING: # pragma: no cover
|
9
|
+
from ..field_types import BaseField
|
9
10
|
|
10
11
|
|
11
12
|
class RuleParsedValuesMismatch(Exception):
|
12
13
|
pass
|
13
14
|
|
14
15
|
|
15
|
-
class
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
class MatchedRule(Rule):
|
20
|
-
parsed_rule: str
|
16
|
+
class MatchedRule(ProcessedRule):
|
17
|
+
parsed_rule: MatchedParsedRule
|
21
18
|
parsed_values: Dict[str, Any]
|
22
19
|
values: Dict[str, Any]
|
23
20
|
|
@@ -48,10 +45,16 @@ class MatchedRule(Rule):
|
|
48
45
|
if set(self.rule_params) != parsed_values_values:
|
49
46
|
raise RuleParsedValuesMismatch(f"Rule Params: {self.rule_params}, Parsed Values: {parsed_values_values}")
|
50
47
|
|
51
|
-
def
|
48
|
+
def get_validator(self, field_instance: "BaseField") -> Callable:
|
52
49
|
field_class = RuleRegistry.get_type(self.field_type)
|
53
50
|
if not isinstance(field_instance, field_class):
|
54
|
-
raise
|
55
|
-
|
56
|
-
|
57
|
-
|
51
|
+
raise TypeError(f"Cannot add rule to {type(field_instance).__name__}, expected {self.field_type}.")
|
52
|
+
return self.rule_setter(self=field_instance, **self.resolved_values)
|
53
|
+
|
54
|
+
def get_front_end_repr(self) -> dict:
|
55
|
+
return {
|
56
|
+
"rule": self.field_rule,
|
57
|
+
"parsed_rule": self.parsed_rule,
|
58
|
+
"rule_params": self.rule_params,
|
59
|
+
"parsed_values": self.parsed_values,
|
60
|
+
}
|
@@ -56,23 +56,3 @@ alias_parameters_types = {
|
|
56
56
|
"String": parse_string,
|
57
57
|
"Strings": parse_array_of("String", parse_string),
|
58
58
|
}
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
# class Store()
|
67
|
-
# pass
|
68
|
-
|
69
|
-
|
70
|
-
# values = {"classes": ["UNCLASSIFIED"], "min_length": 5, "max_length": 50}
|
71
|
-
|
72
|
-
|
73
|
-
# alias_parser = Parser("Value in {possible_values:Strings}", extra_types=alias_parameters_types)
|
74
|
-
# # print(alias_parser.parse("Value In ['UNCLASSIFIED', 'CLASSIFIED']"))
|
75
|
-
|
76
|
-
# with Store(values=values) as store:
|
77
|
-
# print(alias_parser.parse("Value In $values.classes"))
|
78
|
-
# print(store.)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import TYPE_CHECKING, Dict, List, Union
|
3
|
+
|
4
|
+
from .Rule import Rule
|
5
|
+
from .Enums import LogicalOperator
|
6
|
+
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
8
|
+
from ..field_types import BaseField
|
9
|
+
|
10
|
+
MatchedParsedRule = str
|
11
|
+
LogicalParsedRule = Dict[LogicalOperator, List["ParsedRule"]]
|
12
|
+
ParsedRule = Union[MatchedParsedRule, LogicalParsedRule]
|
13
|
+
|
14
|
+
|
15
|
+
class ProcessedRule(Rule, ABC):
|
16
|
+
parsed_rule: ParsedRule
|
17
|
+
|
18
|
+
@abstractmethod
|
19
|
+
def get_validator(self, field_instance: "BaseField"):
|
20
|
+
pass # pragma: no cover
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def get_front_end_repr(self) -> dict:
|
24
|
+
pass # pragma: no cover
|
@@ -1,64 +1,85 @@
|
|
1
1
|
from itertools import chain
|
2
|
-
from
|
3
|
-
|
2
|
+
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Type
|
3
|
+
|
4
4
|
|
5
5
|
from .Rule import Rule
|
6
6
|
from ..utils.logger_config import get_logger
|
7
|
+
from ..field_types.FieldTypes import FieldTypes
|
7
8
|
|
8
9
|
|
9
|
-
if TYPE_CHECKING:
|
10
|
+
if TYPE_CHECKING: # pragma: no cover
|
10
11
|
from field_types.BaseField import BaseField
|
11
12
|
|
12
13
|
logger = get_logger(__name__)
|
13
14
|
|
14
15
|
|
15
|
-
class
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@classmethod
|
20
|
-
def register_rule(cls, field_rule: str, fixed_params: dict = None):
|
21
|
-
def _register(func: callable):
|
22
|
-
field_type, func_name = func.__qualname__.split(".")
|
23
|
-
logger.debug("Registering function '%s' for %s. Rule: %s", func_name, field_type, field_rule)
|
16
|
+
class RuleMetadata(NamedTuple):
|
17
|
+
rule: str
|
18
|
+
fixed_params: dict
|
24
19
|
|
25
|
-
rule = Rule(field_type, field_rule, func, fixed_params)
|
26
|
-
cls.rules[field_type].append(rule)
|
27
|
-
logger.debug("Function '%s' Registered", func_name)
|
28
|
-
return func
|
29
20
|
|
30
|
-
|
21
|
+
class RuleRegistry:
|
22
|
+
rules: Dict[str, List[Rule]] = {}
|
23
|
+
type_map: Dict[str, Type["BaseField"]] = {}
|
31
24
|
|
32
25
|
@classmethod
|
33
26
|
def register_field(cls, field_class: Type["BaseField"]) -> Type["BaseField"]:
|
34
|
-
|
27
|
+
field_type_name = field_class.type_name
|
28
|
+
cls.type_map[field_class.type_name] = field_class
|
29
|
+
cls.rules[field_class.type_name] = []
|
30
|
+
|
31
|
+
for method in field_class.__dict__.values():
|
32
|
+
metadata: RuleMetadata = getattr(method, "_rule_metadata", None)
|
33
|
+
if metadata is None:
|
34
|
+
continue
|
35
|
+
rule = Rule(
|
36
|
+
field_type=field_type_name,
|
37
|
+
field_rule=metadata.rule,
|
38
|
+
rule_setter=method,
|
39
|
+
fixed_params=metadata.fixed_params
|
40
|
+
)
|
41
|
+
cls.add_rule(field_class, rule)
|
35
42
|
return field_class
|
36
43
|
|
37
44
|
@classmethod
|
38
|
-
def
|
39
|
-
|
45
|
+
def add_rule(cls, field_class: Type["BaseField"], rule: Rule):
|
46
|
+
if field_class.type_name not in cls.rules:
|
47
|
+
raise ValueError(f"Field not registered: {field_class.type_name}")
|
48
|
+
cls.rules[field_class.type_name].append(rule)
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def get_type(cls, type_name: str) -> Type["BaseField"]:
|
52
|
+
return cls.type_map.get(type_name)
|
40
53
|
|
41
54
|
@classmethod
|
42
55
|
def get_rules_for(cls, field_class: Type["BaseField"]):
|
43
|
-
if field_class.
|
44
|
-
return cls.rules[
|
56
|
+
if field_class.type_name == FieldTypes.BASE:
|
57
|
+
return cls.rules[FieldTypes.BASE]
|
45
58
|
parent_rules = list(chain.from_iterable(cls.get_rules_for(p) for p in field_class.get_parents()))
|
46
|
-
return cls.rules[field_class.
|
59
|
+
return cls.rules[field_class.type_name] + parent_rules
|
47
60
|
|
48
61
|
@classmethod
|
49
62
|
def get_rules_definition(cls):
|
50
63
|
return [
|
51
64
|
{
|
52
|
-
"field":
|
53
|
-
"parent_field": [p.
|
54
|
-
"rules": cls.rules.get(
|
65
|
+
"field": name,
|
66
|
+
"parent_field": [p.type_name for p in field_class.get_parents()],
|
67
|
+
"rules": cls.rules.get(name, [])
|
55
68
|
}
|
56
|
-
for
|
69
|
+
for name, field_class in cls.type_map.items()
|
57
70
|
]
|
58
71
|
|
59
72
|
|
60
73
|
def register_rule(rule: str, fixed_params: dict = None):
|
61
|
-
|
74
|
+
def _register(func: Callable):
|
75
|
+
setattr(func, "_rule_metadata",
|
76
|
+
RuleMetadata(
|
77
|
+
rule=rule,
|
78
|
+
fixed_params=fixed_params or {}
|
79
|
+
)
|
80
|
+
)
|
81
|
+
return func
|
82
|
+
return _register
|
62
83
|
|
63
84
|
|
64
85
|
def register_field(field_class: type):
|
data_sitter/rules/__init__.py
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
from .Rule import Rule
|
2
2
|
from .Parser import RuleParser
|
3
|
+
from .Enums import LogicalOperator
|
3
4
|
from .MatchedRule import MatchedRule
|
5
|
+
from .LogicalRule import LogicalRule
|
6
|
+
from .ProcessedRule import ProcessedRule
|
4
7
|
from .RuleRegistry import RuleRegistry, register_rule, register_field
|
5
8
|
|
6
9
|
|
7
10
|
__all__ = [
|
8
11
|
"Rule",
|
9
|
-
"MatchedRule",
|
10
12
|
"RuleParser",
|
13
|
+
"MatchedRule",
|
14
|
+
"LogicalRule",
|
15
|
+
"ProcessedRule",
|
11
16
|
"RuleRegistry",
|
12
17
|
"register_rule",
|
13
18
|
"register_field",
|
19
|
+
"LogicalOperator",
|
14
20
|
]
|
@@ -11,7 +11,7 @@ DEFAULT_LEVEL = "INFO"
|
|
11
11
|
VALID_LOG_LEVEL = ["CRITICAL", "FATAL", "ERROR", "WARN", "WARNING", "INFO", "DEBUG", "NOTSET"]
|
12
12
|
LOG_LEVEL = environ.get("LOG_LEVEL", DEFAULT_LEVEL)
|
13
13
|
|
14
|
-
if LOG_LEVEL not in VALID_LOG_LEVEL:
|
14
|
+
if LOG_LEVEL not in VALID_LOG_LEVEL: # pragma: no cover
|
15
15
|
LOG_LEVEL = DEFAULT_LEVEL
|
16
16
|
|
17
17
|
|
@@ -0,0 +1,220 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: data-sitter
|
3
|
+
Version: 0.1.6
|
4
|
+
Summary: A Python library that reads data contracts and generates Pydantic models for seamless data validation.
|
5
|
+
Author-email: Lázaro Pereira Candea <lazaro@candea.es>
|
6
|
+
Requires-Python: >=3.8
|
7
|
+
Description-Content-Type: text/markdown
|
8
|
+
Requires-Dist: python-dotenv==1.0.1
|
9
|
+
Requires-Dist: PyYAML==6.0.2
|
10
|
+
Requires-Dist: parse_type==0.6.4
|
11
|
+
Requires-Dist: pydantic==2.10.5
|
12
|
+
Provides-Extra: dev
|
13
|
+
Requires-Dist: pytest==8.3.5; extra == "dev"
|
14
|
+
Requires-Dist: pytest-cov==6.0.0; extra == "dev"
|
15
|
+
Requires-Dist: pytest-mock==3.14.0; extra == "dev"
|
16
|
+
Requires-Dist: twine==6.1.0; extra == "dev"
|
17
|
+
Requires-Dist: build==1.2.2.post1; extra == "dev"
|
18
|
+
|
19
|
+
# Data-Sitter
|
20
|
+
|
21
|
+

|
22
|
+
|
23
|
+
## Overview
|
24
|
+
|
25
|
+
Data-Sitter is a Python library designed to simplify data validation by converting data contracts into Pydantic models. This allows for easy and efficient validation of structured data, ensuring compliance with predefined rules and constraints.
|
26
|
+
|
27
|
+
## Features
|
28
|
+
|
29
|
+
- Define structured data contracts in JSON format.
|
30
|
+
- Generate Pydantic models automatically from contracts.
|
31
|
+
- Enforce validation rules at the field level.
|
32
|
+
- Support for rule references within the contract.
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
```sh
|
37
|
+
pip install data-sitter
|
38
|
+
```
|
39
|
+
|
40
|
+
## Development and Deployment
|
41
|
+
|
42
|
+
### CI/CD Pipeline
|
43
|
+
|
44
|
+
The project uses GitHub Actions for continuous integration and deployment:
|
45
|
+
|
46
|
+
1. **Pull Request Checks**
|
47
|
+
- Automatically checks if the version has been bumped in `pyproject.toml`
|
48
|
+
- Fails if the version is the same as in the main branch
|
49
|
+
- Ensures every PR includes a version update
|
50
|
+
|
51
|
+
2. **Automatic Releases**
|
52
|
+
- When code is merged to the main branch:
|
53
|
+
- Builds the package
|
54
|
+
- Publishes to PyPI automatically
|
55
|
+
- Uses PyPI API token for secure authentication
|
56
|
+
|
57
|
+
To set up the CI/CD pipeline:
|
58
|
+
|
59
|
+
1. Create a PyPI API token:
|
60
|
+
- Go to [PyPI Account Settings](https://pypi.org/manage/account/)
|
61
|
+
- Create a new API token with "Upload" scope
|
62
|
+
- Copy the token
|
63
|
+
|
64
|
+
2. Add the token to GitHub:
|
65
|
+
- Go to your repository's Settings > Secrets and variables > Actions
|
66
|
+
- Create a new secret named `PYPI_API_TOKEN`
|
67
|
+
- Paste your PyPI API token
|
68
|
+
|
69
|
+
### Setting Up Development Environment
|
70
|
+
|
71
|
+
To set up a development environment with all the necessary tools, install the package with development dependencies:
|
72
|
+
|
73
|
+
```sh
|
74
|
+
pip install -e ".[dev]"
|
75
|
+
```
|
76
|
+
|
77
|
+
This will install:
|
78
|
+
- The package in editable mode
|
79
|
+
- Testing tools (pytest, pytest-cov, pytest-mock)
|
80
|
+
- Build tools (build, twine)
|
81
|
+
|
82
|
+
### Building the Package
|
83
|
+
|
84
|
+
To build the package, run:
|
85
|
+
|
86
|
+
```sh
|
87
|
+
python -m build
|
88
|
+
```
|
89
|
+
|
90
|
+
This will create a `dist` directory containing both a source distribution (`.tar.gz`) and a wheel (`.whl`).
|
91
|
+
|
92
|
+
### Deploying to PyPI
|
93
|
+
|
94
|
+
To upload to PyPI:
|
95
|
+
|
96
|
+
```sh
|
97
|
+
twine upload dist/*
|
98
|
+
```
|
99
|
+
|
100
|
+
You'll be prompted for your PyPI username and password. For security, it's recommended to use an API token instead of your password.
|
101
|
+
|
102
|
+
## Usage
|
103
|
+
|
104
|
+
### Creating a Pydantic Model from a Contract
|
105
|
+
|
106
|
+
To convert a data contract into a Pydantic model, follow these steps:
|
107
|
+
|
108
|
+
```python
|
109
|
+
from data_sitter import Contract
|
110
|
+
|
111
|
+
contract_dict = {
|
112
|
+
"name": "test",
|
113
|
+
"fields": [
|
114
|
+
{
|
115
|
+
"name": "FID",
|
116
|
+
"type": "Integer",
|
117
|
+
"rules": ["Positive"]
|
118
|
+
},
|
119
|
+
{
|
120
|
+
"name": "SECCLASS",
|
121
|
+
"type": "String",
|
122
|
+
"rules": [
|
123
|
+
"Validate Not Null",
|
124
|
+
"Value In ['UNCLASSIFIED', 'CLASSIFIED']",
|
125
|
+
]
|
126
|
+
}
|
127
|
+
],
|
128
|
+
}
|
129
|
+
|
130
|
+
contract = Contract.from_dict(contract_dict)
|
131
|
+
pydantic_contract = contract.pydantic_model
|
132
|
+
```
|
133
|
+
|
134
|
+
### Using Rule References
|
135
|
+
|
136
|
+
Data-Sitter allows you to define reusable values in the `values` key and reference them in field rules using `$values.[key]`. For example:
|
137
|
+
|
138
|
+
```json
|
139
|
+
{
|
140
|
+
"name": "example_contract",
|
141
|
+
"fields": [
|
142
|
+
{
|
143
|
+
"name": "CATEGORY",
|
144
|
+
"type": "String",
|
145
|
+
"rules": ["Value In $values.categories"]
|
146
|
+
},
|
147
|
+
{
|
148
|
+
"name": "NAME",
|
149
|
+
"type": "String",
|
150
|
+
"rules": [
|
151
|
+
"Length Between $values.min_length and $values.max_length"
|
152
|
+
]
|
153
|
+
}
|
154
|
+
|
155
|
+
],
|
156
|
+
"values": {"categories": ["A", "B", "C"], "min_length": 5,"max_length": 50}
|
157
|
+
}
|
158
|
+
```
|
159
|
+
|
160
|
+
## Available Rules
|
161
|
+
|
162
|
+
The available validation rules can be retrieved programmatically:
|
163
|
+
|
164
|
+
```python
|
165
|
+
from data_sitter import RuleRegistry
|
166
|
+
|
167
|
+
rules = RuleRegistry.get_rules_definition()
|
168
|
+
print(rules)
|
169
|
+
```
|
170
|
+
|
171
|
+
### Rule Definitions
|
172
|
+
|
173
|
+
Below are the available rules grouped by field type:
|
174
|
+
|
175
|
+
#### Base
|
176
|
+
|
177
|
+
- Is not null
|
178
|
+
|
179
|
+
#### String - (Inherits from `Base`)
|
180
|
+
|
181
|
+
- Is not empty
|
182
|
+
- Starts with {prefix:String}
|
183
|
+
- Ends with {suffix:String}
|
184
|
+
- Is not one of {possible_values:Strings}
|
185
|
+
- Is one of {possible_values:Strings}
|
186
|
+
- Has length between {min_val:Integer} and {max_val:Integer}
|
187
|
+
- Has maximum length {max_len:Integer}
|
188
|
+
- Has minimum length {min_len:Integer}
|
189
|
+
- Is uppercase
|
190
|
+
- Is lowercase
|
191
|
+
- Matches regex {pattern:String}
|
192
|
+
- Is valid email
|
193
|
+
- Is valid URL
|
194
|
+
- Has no digits
|
195
|
+
|
196
|
+
#### Numeric - (Inherits from `Base`)
|
197
|
+
|
198
|
+
- Is not zero
|
199
|
+
- Is positive
|
200
|
+
- Is negative
|
201
|
+
- Is at least {min_val:Number}
|
202
|
+
- Is at most {max_val:Number}
|
203
|
+
- Is greater than {threshold:Number}
|
204
|
+
- Is less than {threshold:Number}
|
205
|
+
- Is not between {min_val:Number} and {max_val:Number}
|
206
|
+
- Is between {min_val:Number} and {max_val:Number}
|
207
|
+
|
208
|
+
#### Integer - (Inherits from `Numeric`)
|
209
|
+
|
210
|
+
#### Float - (Inherits from `Numeric`)
|
211
|
+
|
212
|
+
- Has at most {decimal_places:Integer} decimal places
|
213
|
+
|
214
|
+
## Contributing
|
215
|
+
|
216
|
+
Contributions are welcome! Feel free to submit issues or pull requests in the [GitHub repository](https://github.com/lcandea/data-sitter).
|
217
|
+
|
218
|
+
## License
|
219
|
+
|
220
|
+
Data-Sitter is licensed under the MIT License.
|
@@ -0,0 +1,30 @@
|
|
1
|
+
data_sitter/Contract.py,sha256=ykeBA_gr7r7MO4FYvdDrstUzGiq7dyIIipkOZRk8qkA,4042
|
2
|
+
data_sitter/FieldResolver.py,sha256=Bh7_MTTO7E87S31dQq3tvkL9E_K-4EDlh3NJSn0eLU0,2732
|
3
|
+
data_sitter/Validation.py,sha256=5jdIQZyTrEmXZ_SJP0lq-EEFKrGbYH6z4EQ56oFR7Ck,1474
|
4
|
+
data_sitter/__init__.py,sha256=qbE-wU8ELMFwOMG4UTK0lmzn5XF2MK3rc22E8ROgypo,113
|
5
|
+
data_sitter/cli.py,sha256=SBmxNC508qt8-C4x2IS6XNZLihtfeALw34QLLYr_p_Q,1686
|
6
|
+
data_sitter/field_types/BaseField.py,sha256=2s5wJjz1NoNWvSa-69mMxVf7f5wptuxr6UGVubM6MAQ,1997
|
7
|
+
data_sitter/field_types/FieldTypes.py,sha256=ntuguQtLnVon1cB2YvG0p2c1r0zk67qw6o4qfJHxLlY,158
|
8
|
+
data_sitter/field_types/FloatField.py,sha256=gHmiSg8Eft57T_J8covrEctMEGa0zuT6R3xJP2UlyIY,945
|
9
|
+
data_sitter/field_types/IntegerField.py,sha256=Ll6eool8Rwo2pzyXQz95TjJpzdgW9ShtCK1uHmOi_pQ,213
|
10
|
+
data_sitter/field_types/NumericField.py,sha256=uJ6ZB8vJg0xsY1grrVL6k8Heygm1O84fQjEtVg1uJjw,2916
|
11
|
+
data_sitter/field_types/StringField.py,sha256=ayxo5d_9xzNR9rAWS6dHNKiRapkFdsCxMtT4K5Qb7ek,4493
|
12
|
+
data_sitter/field_types/__init__.py,sha256=GdssttQCJksGcZn7oPM53vOsqOL6R5xRiRJDEtr38Ww,293
|
13
|
+
data_sitter/rules/Enums.py,sha256=W-3vXP7NWgkgZexrn8J9EytqZoaWK3EwIX0q5O3QJKY,105
|
14
|
+
data_sitter/rules/LogicalRule.py,sha256=4HRPw62HSpmMz_Yr2en4s1eVijlPurPDBHfwSliBS3o,2794
|
15
|
+
data_sitter/rules/MatchedRule.py,sha256=DudYNI50EpeIvsFQ7cMpKI1fziBG_yfMPNUZ7OWdygo,2162
|
16
|
+
data_sitter/rules/ProcessedRule.py,sha256=5EWjTJ6wgDih1cm8uxJOghLDQs3XK_ywGTGF5hhNly8,640
|
17
|
+
data_sitter/rules/Rule.py,sha256=xE31dUwLHD3IzcmPfdqLuXS_rmw-kFdIaCKHvpIArig,2115
|
18
|
+
data_sitter/rules/RuleRegistry.py,sha256=1ZMl-5u-6DZ1AGYZA77s6DpAxeN64EFEF-XKVKeQy-U,2698
|
19
|
+
data_sitter/rules/__init__.py,sha256=SHQZYp4VNzuygP-ZPegEXwmgj-_oUqEchi9TdbasA6U,465
|
20
|
+
data_sitter/rules/Parser/RuleParser.py,sha256=7biF5N3Cf3Rf5bgB4pXUpBaZ4r5EL1I9YHvSTjdydBA,2127
|
21
|
+
data_sitter/rules/Parser/__init__.py,sha256=F8qJ7luwq0C65e7pNOzBHB2sF1lMcvIFYfDNJj6XQTc,205
|
22
|
+
data_sitter/rules/Parser/alias_parameters_parser.py,sha256=xUgOFJCm42w1eUmZOQ2OsOhCsKGHev5g4gsm_kizciA,1529
|
23
|
+
data_sitter/rules/Parser/parser_utils.py,sha256=ypI021uYJTsHAoKGShAfnhd5xQGtqqTGTHozleefsLQ,642
|
24
|
+
data_sitter/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
+
data_sitter/utils/logger_config.py,sha256=fBbDNZOsmDnO6TtLHI1tty4EXi2AnbrA-OgkhXcm1Aw,1235
|
26
|
+
data_sitter-0.1.6.dist-info/METADATA,sha256=H3QVZTUxe4F2FtWVEhe0JEmlgZG4n8krQCyBqp5XgdQ,5597
|
27
|
+
data_sitter-0.1.6.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
28
|
+
data_sitter-0.1.6.dist-info/entry_points.txt,sha256=1I7xxqFZvA78wmDx7NGavttAb8JFWM3Wxgehftx_5C4,53
|
29
|
+
data_sitter-0.1.6.dist-info/top_level.txt,sha256=Q7N21PYeqIdRbDvZQCJXhbbv0PFIf876gu1_DpInH_E,12
|
30
|
+
data_sitter-0.1.6.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: data-sitter
|
3
|
-
Version: 0.1.4
|
4
|
-
Summary: A Python library that reads data contracts and generates Pydantic models for seamless data validation.
|
5
|
-
Author-email: Lázaro Pereira Candea <lazaro@candea.es>
|
6
|
-
Requires-Dist: python-dotenv==1.0.1
|
7
|
-
Requires-Dist: PyYAML==6.0.2
|
8
|
-
Requires-Dist: parse_type==0.6.4
|
9
|
-
Requires-Dist: pydantic==2.10.5
|
@@ -1,26 +0,0 @@
|
|
1
|
-
data_sitter/Contract.py,sha256=ow6CdT7XRbxBy4tY5av3_L9d8qv59nw70ih-jVMKlBU,4561
|
2
|
-
data_sitter/FieldResolver.py,sha256=0XSA-doOY4cM_Ikd09hUxykYZOLR5pHwkbNiTmdtweQ,1819
|
3
|
-
data_sitter/Validation.py,sha256=5jdIQZyTrEmXZ_SJP0lq-EEFKrGbYH6z4EQ56oFR7Ck,1474
|
4
|
-
data_sitter/__init__.py,sha256=qbE-wU8ELMFwOMG4UTK0lmzn5XF2MK3rc22E8ROgypo,113
|
5
|
-
data_sitter/cli.py,sha256=1ICrtokqV5RvvWhzWKAeS5ZUSUpiviQyy2JSK71ER10,1666
|
6
|
-
data_sitter/field_types/BaseField.py,sha256=mCEy9hFhwus1gW1P6ctbUCUrZ9rJMtreOvfiy9MkO5Q,1799
|
7
|
-
data_sitter/field_types/FloatField.py,sha256=75zYGwI65GYIiZWgb5cwb3QoWukp3X-YcrmqhIxOe1w,675
|
8
|
-
data_sitter/field_types/IntegerField.py,sha256=o__5z3bg6wsx7FIfJbBYZW5b760-WSZw_05J-OSKXR0,147
|
9
|
-
data_sitter/field_types/NumericField.py,sha256=s8aEkk42HYY30citCaBjIynLGfKE7gPayvVq3fdVd2Y,2981
|
10
|
-
data_sitter/field_types/StringField.py,sha256=wXlfCTPGPDr2j_lHW6LqtAFKNBoff6nk0tE5Xt02xsQ,4645
|
11
|
-
data_sitter/field_types/__init__.py,sha256=GdssttQCJksGcZn7oPM53vOsqOL6R5xRiRJDEtr38Ww,293
|
12
|
-
data_sitter/rules/MatchedRule.py,sha256=uHXuo7Np-Bq7IOHaHMYFmYFPRT8aYDEKCdFKcvWf4DM,1946
|
13
|
-
data_sitter/rules/Rule.py,sha256=xE31dUwLHD3IzcmPfdqLuXS_rmw-kFdIaCKHvpIArig,2115
|
14
|
-
data_sitter/rules/RuleRegistry.py,sha256=iqxcbzypk-_S4ukASNjEe9TD7J-f2psXWR0GVOPlK0E,2134
|
15
|
-
data_sitter/rules/__init__.py,sha256=_cTO0SUkW_WW2VBx2NGd8n5TUio7gptkBr9MorW2ZZk,289
|
16
|
-
data_sitter/rules/Parser/RuleParser.py,sha256=7biF5N3Cf3Rf5bgB4pXUpBaZ4r5EL1I9YHvSTjdydBA,2127
|
17
|
-
data_sitter/rules/Parser/__init__.py,sha256=F8qJ7luwq0C65e7pNOzBHB2sF1lMcvIFYfDNJj6XQTc,205
|
18
|
-
data_sitter/rules/Parser/alias_parameters_parser.py,sha256=jsx_JWzkA4lY2nq4hzc4fG7_nnh7yLxmVj6WIP1Mm68,1933
|
19
|
-
data_sitter/rules/Parser/parser_utils.py,sha256=ypI021uYJTsHAoKGShAfnhd5xQGtqqTGTHozleefsLQ,642
|
20
|
-
data_sitter/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
-
data_sitter/utils/logger_config.py,sha256=w9E4jWfGJnkC9tZz4qrolSqglKm4jEB8l6vjC-qfj8A,1215
|
22
|
-
data_sitter-0.1.4.dist-info/METADATA,sha256=dKf83KLklS2JXoiWJCcEBGi6EqKYUcVn5XPXoL-bQBc,353
|
23
|
-
data_sitter-0.1.4.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
24
|
-
data_sitter-0.1.4.dist-info/entry_points.txt,sha256=1I7xxqFZvA78wmDx7NGavttAb8JFWM3Wxgehftx_5C4,53
|
25
|
-
data_sitter-0.1.4.dist-info/top_level.txt,sha256=Q7N21PYeqIdRbDvZQCJXhbbv0PFIf876gu1_DpInH_E,12
|
26
|
-
data_sitter-0.1.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|