data-sitter 0.1.3__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.
@@ -1,89 +1,124 @@
1
+ import re
1
2
  from typing import List
2
3
 
3
4
  from .BaseField import BaseField
5
+ from .FieldTypes import FieldTypes
4
6
  from ..rules import register_rule, register_field
5
7
 
6
8
 
7
9
  @register_field
8
10
  class StringField(BaseField):
9
11
  field_type = str
12
+ type_name = FieldTypes.STRING
10
13
 
11
14
  @register_rule("Is not empty")
12
15
  def validate_not_empty(self):
13
16
  def validator(value: str):
14
17
  if value == "":
15
- raise ValueError("The value is empty")
18
+ raise ValueError("String cannot be empty.")
16
19
  return value
17
- self.validators.append(validator)
20
+ return validator
18
21
 
19
22
  @register_rule("Starts with {prefix:String}")
20
23
  def validate_starts_with(self, prefix: List[str]):
21
24
  def validator(value: str):
22
25
  if not value.startswith(prefix):
23
- raise ValueError(f"The value '{value}' does not start with '{prefix}'.")
26
+ raise ValueError(f"Value must start with '{prefix}'.")
24
27
  return value
25
- self.validators.append(validator)
28
+ return validator
26
29
 
27
- @register_rule("Ends with {sufix:String}")
28
- def validate_ends_with(self, sufix: List[str]):
30
+ @register_rule("Ends with {suffix:String}")
31
+ def validate_ends_with(self, suffix: List[str]):
29
32
  def validator(value: str):
30
- if not value.endswith(sufix):
31
- raise ValueError(f"The value '{value}' does not ends with '{sufix}'.")
33
+ if not value.endswith(suffix):
34
+ raise ValueError(f"Value must end with '{suffix}'.")
32
35
  return value
33
- self.validators.append(validator)
36
+ return validator
34
37
 
35
- @register_rule("Value in {possible_values:Strings}")
36
- def validate_in(self, possible_values: List[str]):
38
+ @register_rule("Is one of {possible_values:Strings}", fixed_params={"negative": False})
39
+ @register_rule("Is not one of {possible_values:Strings}", fixed_params={"negative": True})
40
+ def validate_in(self, possible_values: List[str], negative: bool):
37
41
  def validator(value: str):
38
- if value not in possible_values:
39
- raise ValueError(f"The value '{value}' is not in the list.")
42
+ condition = value in possible_values
43
+ if condition and negative:
44
+ raise ValueError(f"Value '{value}' is not allowed.")
45
+ if not condition and not negative:
46
+ raise ValueError(f"Value '{value}' must be one of the possible values.")
40
47
  return value
41
- self.validators.append(validator)
48
+ return validator
42
49
 
43
- @register_rule("Length between {min_val:Integer} and {max_val:Integer}")
50
+ @register_rule("Has length between {min_val:Integer} and {max_val:Integer}")
44
51
  def validate_length_between(self, min_val: int, max_val: int):
45
52
  def validator(value: str):
46
53
  if not (min_val < len(value) < max_val):
47
- raise ValueError(f"Length {len(value)} is not in between {min_val} and {max_val}.")
54
+ raise ValueError(f"Length must be between {min_val} and {max_val} characters.")
48
55
  return value
49
- self.validators.append(validator)
56
+ return validator
50
57
 
51
- @register_rule("Maximum length of {max_len:Integer}")
58
+ @register_rule("Has maximum length {max_len:Integer}")
52
59
  def validate_max_length(self, max_len: int):
53
60
  def validator(value: str):
54
61
  if len(value) > max_len:
55
- raise ValueError(f"Length {len(value)} is longer than {max_len}.")
62
+ raise ValueError(f"Length must not exceed {max_len} characters.")
56
63
  return value
57
- self.validators.append(validator)
64
+ return validator
58
65
 
59
- @register_rule("Length shorter than {max_len:Integer}")
60
- def validate_shorter_than(self, max_len: int):
66
+ @register_rule("Has minimum length {min_len:Integer}")
67
+ def validate_min_length(self, min_len: int):
61
68
  def validator(value: str):
62
- if len(value) >= max_len:
63
- raise ValueError(f"Length {len(value)} is not in shorter than {max_len}.")
69
+ if len(value) < min_len:
70
+ raise ValueError(f"Length must be at least {min_len} characters.")
64
71
  return value
65
- self.validators.append(validator)
72
+ return validator
66
73
 
67
- @register_rule("Minimum length of {min_len:Integer}")
68
- def validate_min_length(self, min_len: int):
74
+ @register_rule("Is uppercase")
75
+ def validate_uppercase(self):
69
76
  def validator(value: str):
70
- if len(value) < min_len:
71
- raise ValueError(f"Length {len(value)} is shorter than {min_len}.")
77
+ if not value.isupper():
78
+ raise ValueError("Value must be in uppercase.")
72
79
  return value
73
- self.validators.append(validator)
80
+ return validator
74
81
 
75
- @register_rule("Length longer than {min_len:Integer}")
76
- def validate_longer_than(self, min_len: int):
82
+ @register_rule("Is lowercase")
83
+ def validate_lowercase(self):
77
84
  def validator(value: str):
78
- if len(value) <= min_len:
79
- raise ValueError(f"Length {len(value)} is not in longer than {min_len}.")
85
+ if not value.islower():
86
+ raise ValueError("Value must be in lowercase.")
80
87
  return value
81
- self.validators.append(validator)
88
+ return validator
82
89
 
83
- @register_rule("Is uppercase")
84
- def validate_uppercase(self):
90
+ @register_rule("Matches regex {pattern:String}")
91
+ def validate_matches_regex(self, pattern: str):
85
92
  def validator(value: str):
86
- if not value.isupper():
87
- raise ValueError("Not Uppercase")
93
+ if not re.match(pattern, value):
94
+ raise ValueError(f"Value does not match the required pattern {pattern}.")
95
+ return value
96
+ return validator
97
+
98
+ @register_rule("Is valid email")
99
+ def validate_email(self):
100
+ EMAIL_REGEX = r"^[\w\.-]+@[\w\.-]+\.\w+$"
101
+
102
+ def validator(value: str):
103
+ if not re.match(EMAIL_REGEX, value):
104
+ raise ValueError("Invalid email format.")
105
+ return value
106
+ return validator
107
+
108
+ @register_rule("Is valid URL")
109
+ def validate_url(self):
110
+ URL_REGEX = r"^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$"
111
+
112
+ def validator(value: str):
113
+ if not re.match(URL_REGEX, value):
114
+ raise ValueError("Invalid URL format.")
115
+ return value
116
+ return validator
117
+
118
+ @register_rule("Has no digits")
119
+ def validate_no_digits(self):
120
+ def validator(value: str):
121
+ if any(char.isdigit() for char in value):
122
+ raise ValueError("Value must not contain any digits.")
88
123
  return value
89
- self.validators.append(validator)
124
+ return validator
@@ -0,0 +1,7 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class LogicalOperator(StrEnum):
5
+ AND = "AND"
6
+ OR = "OR"
7
+ NOT = "NOT"
@@ -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]}
@@ -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 InvalidFieldTypeError(TypeError):
16
- """Raised when attempting to add a rule to an incompatible field type."""
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 add_to_instance(self, field_instance: "BaseField"):
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 InvalidFieldTypeError(
55
- f"Cannot add rule to {type(field_instance).__name__}, expected {self.field_type}."
56
- )
57
- self.rule_setter(self=field_instance, **self.resolved_values)
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
data_sitter/rules/Rule.py CHANGED
@@ -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
- raise RuleFunctionParamsMismatch(f"Rule Params: {self.rule_params}, Setter Params: {rule_setter_params}")
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)
@@ -1,64 +1,85 @@
1
1
  from itertools import chain
2
- from collections import defaultdict
3
- from typing import TYPE_CHECKING, Dict, List, Type
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 RuleRegistry:
16
- rules: Dict[str, List[Rule]] = defaultdict(list)
17
- type_map: Dict[str, Type["BaseField"]] = {}
18
-
19
- @classmethod
20
- def register_rule(cls, field_rule: str):
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)
26
- cls.rules[field_type].append(rule)
27
- logger.debug("Function '%s' Registered", func_name)
28
- return func
29
20
 
30
- return _register
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
- cls.type_map[field_class.__name__] = field_class
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 get_type(cls, field_type: str) -> Type["BaseField"]:
39
- return cls.type_map.get(field_type)
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.__name__ == "BaseField":
44
- return cls.rules["BaseField"]
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.__name__] + parent_rules
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": field_name,
53
- "parent_field": [p.__name__ for p in field_class.get_parents()],
54
- "rules": cls.rules.get(field_name, [])
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 field_name, field_class in cls.type_map.items()
69
+ for name, field_class in cls.type_map.items()
57
70
  ]
58
71
 
59
72
 
60
- def register_rule(rule: str):
61
- return RuleRegistry.register_rule(rule)
73
+ def register_rule(rule: str, fixed_params: dict = None):
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):
@@ -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