safeshield 1.5.1__py3-none-any.whl → 1.5.3__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.
Potentially problematic release.
This version of safeshield might be problematic. Click here for more details.
- {safeshield-1.5.1.dist-info → safeshield-1.5.3.dist-info}/METADATA +1 -1
- safeshield-1.5.3.dist-info/RECORD +31 -0
- validator/core/validator.py +21 -19
- validator/rules/array.py +46 -12
- validator/rules/base.py +42 -6
- validator/rules/basic.py +46 -4
- validator/rules/boolean.py +56 -57
- validator/rules/comparison.py +126 -34
- validator/rules/date.py +79 -27
- validator/rules/files.py +52 -11
- validator/rules/format.py +9 -0
- validator/rules/numeric.py +105 -36
- validator/rules/string.py +54 -9
- validator/rules/utilities.py +294 -131
- validator/services/rule_error_handler.py +24 -208
- validator/services/rule_preparer.py +21 -7
- safeshield-1.5.1.dist-info/RECORD +0 -31
- {safeshield-1.5.1.dist-info → safeshield-1.5.3.dist-info}/LICENSE +0 -0
- {safeshield-1.5.1.dist-info → safeshield-1.5.3.dist-info}/WHEEL +0 -0
- {safeshield-1.5.1.dist-info → safeshield-1.5.3.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,7 @@ from typing import Dict, List, Any, Optional, Union
|
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
4
|
class RuleErrorHandler:
|
|
5
|
-
|
|
6
|
-
including field-value pair parameters (field1,value1,field2,value2)"""
|
|
7
|
-
|
|
8
|
-
def __init__(self, messages: Dict[str, str], custom_attributes: Dict[str, str]):
|
|
5
|
+
def __init__(self, messages: Dict[str, str] = None, custom_attributes: Dict[str, str] = None):
|
|
9
6
|
self.messages = messages or {}
|
|
10
7
|
self.custom_attributes = custom_attributes or {}
|
|
11
8
|
self.errors: Dict[str, List[str]] = {}
|
|
@@ -13,225 +10,44 @@ class RuleErrorHandler:
|
|
|
13
10
|
self._current_params: Optional[List[str]] = None
|
|
14
11
|
self._current_value: Optional[Any] = None
|
|
15
12
|
|
|
16
|
-
def add_error(self, field: str,
|
|
17
|
-
"""Add
|
|
18
|
-
self._current_rule =
|
|
13
|
+
def add_error(self, field: str, rule: Any, rule_params: List[str], default_message: str, value: Any) -> None:
|
|
14
|
+
"""Add error message with support for all parameter formats"""
|
|
15
|
+
self._current_rule = rule
|
|
19
16
|
self._current_params = rule_params
|
|
20
17
|
self._current_value = value
|
|
21
18
|
|
|
22
|
-
message = self._format_message(field,
|
|
19
|
+
message = self._format_message(field, rule, default_message, value)
|
|
23
20
|
self.errors.setdefault(field, []).append(message)
|
|
24
21
|
|
|
25
|
-
def _format_message(self, field: str,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
value_str = self._stringify_value(value)
|
|
29
|
-
|
|
30
|
-
# Get the most specific message available
|
|
31
|
-
message = self._get_message(field, rule_name, attribute, default_message)
|
|
32
|
-
|
|
33
|
-
# Prepare all possible replacements
|
|
34
|
-
replacements = self._prepare_replacements(attribute, value_str)
|
|
35
|
-
|
|
36
|
-
# Apply replacements safely
|
|
37
|
-
return self._apply_replacements(message, replacements)
|
|
38
|
-
|
|
39
|
-
def _get_attribute_name(self, field: str) -> str:
|
|
40
|
-
"""Get the display name for a field with nested field support"""
|
|
41
|
-
# Check for exact match first
|
|
42
|
-
if field in self.custom_attributes:
|
|
43
|
-
return self.custom_attributes[field]
|
|
44
|
-
|
|
45
|
-
# Handle nested fields (e.g., 'user.profile.name')
|
|
46
|
-
parts = field.split('.')
|
|
47
|
-
for i in range(len(parts), 0, -1):
|
|
48
|
-
wildcard_key = '.'.join(parts[:i]) + '.*'
|
|
49
|
-
if wildcard_key in self.custom_attributes:
|
|
50
|
-
return self.custom_attributes[wildcard_key]
|
|
51
|
-
|
|
52
|
-
# Fallback to last part or field itself
|
|
53
|
-
return self.custom_attributes.get(parts[-1], field.replace('_', ' ').title())
|
|
22
|
+
def _format_message(self, field: str, rule: Any, default_message: str, value: Any) -> str:
|
|
23
|
+
message = self._select_message(field, rule.rule_name, field, default_message)
|
|
24
|
+
return self._replace_placeholders(message, rule.replacements(field, value))
|
|
54
25
|
|
|
55
|
-
def
|
|
56
|
-
"""Convert
|
|
26
|
+
def _stringify(self, value: Any) -> str:
|
|
27
|
+
"""Convert value to display string"""
|
|
57
28
|
if value is None:
|
|
58
|
-
return
|
|
29
|
+
return None
|
|
59
30
|
if isinstance(value, (list, dict, set)):
|
|
60
|
-
return ', '.join(str(v) for v in value) if value else ''
|
|
31
|
+
return ', '.join(str(v) for v in value) if value else 'none'
|
|
61
32
|
return str(value)
|
|
62
33
|
|
|
63
|
-
def
|
|
64
|
-
"""
|
|
34
|
+
def _select_message(self, field: str, rule_name: str, attribute: str, default: str) -> str:
|
|
35
|
+
"""Select the most specific error message available"""
|
|
65
36
|
return (
|
|
66
|
-
self.messages.get(f"{field}.{rule_name}") or
|
|
67
|
-
self.messages.get(field) or
|
|
68
|
-
self.messages.get(rule_name) or
|
|
69
|
-
default
|
|
37
|
+
self.messages.get(f"{field}.{rule_name}") or
|
|
38
|
+
self.messages.get(field) or
|
|
39
|
+
self.messages.get(rule_name) or
|
|
40
|
+
default
|
|
70
41
|
)
|
|
71
|
-
|
|
72
|
-
def _prepare_replacements(self, attribute: str, value_str: str) -> Dict[str, str]:
|
|
73
|
-
"""Prepare all placeholder replacements including field-value pairs"""
|
|
74
|
-
replacements = {
|
|
75
|
-
':attribute': attribute,
|
|
76
|
-
':input': value_str,
|
|
77
|
-
':value': value_str,
|
|
78
|
-
':values': self._get_values_param(),
|
|
79
|
-
':min': self._get_min_param(),
|
|
80
|
-
':max': self._get_max_param(),
|
|
81
|
-
':size': self._get_size_param(),
|
|
82
|
-
':other': self._get_other_param_display(),
|
|
83
|
-
':date': self._get_date_param(),
|
|
84
|
-
':format': self._get_format_param(),
|
|
85
|
-
':param': self._get_first_param(),
|
|
86
|
-
}
|
|
87
42
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
replacements[':other'] = self._get_attribute_name(first_field)
|
|
94
|
-
replacements[':value'] = first_value
|
|
95
|
-
|
|
96
|
-
for i, (field, val) in enumerate(field_value_pairs[1:], start=2):
|
|
97
|
-
replacements[f':other{i}'] = self._get_attribute_name(field)
|
|
98
|
-
replacements[f':value{i}'] = val
|
|
99
|
-
|
|
100
|
-
return replacements
|
|
101
|
-
|
|
102
|
-
def _is_field_value_rule(self) -> bool:
|
|
103
|
-
"""Check if the current rule uses field-value pairs"""
|
|
104
|
-
return self._current_rule and self._current_rule.lower() in {
|
|
105
|
-
'required_if', 'required_unless',
|
|
106
|
-
'exclude_if', 'exclude_unless',
|
|
107
|
-
'missing_if', 'missing_unless',
|
|
108
|
-
'present_if', 'present_unless',
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
def _get_field_value_pairs(self) -> List[tuple]:
|
|
112
|
-
"""Extract field-value pairs from parameters"""
|
|
113
|
-
if not self._current_params:
|
|
114
|
-
return []
|
|
115
|
-
|
|
116
|
-
pairs = []
|
|
117
|
-
params = list(self._current_params).copy()
|
|
118
|
-
|
|
119
|
-
# Process parameters in pairs (field, value)
|
|
120
|
-
while len(params) >= 2:
|
|
121
|
-
field = params.pop(0)
|
|
122
|
-
value = params.pop(0)
|
|
123
|
-
pairs.append((field, value))
|
|
124
|
-
|
|
125
|
-
return pairs
|
|
126
|
-
|
|
127
|
-
def _apply_replacements(self, message: str, replacements: Dict[str, str]) -> str:
|
|
128
|
-
"""Safely apply all replacements to the message"""
|
|
129
|
-
for placeholder, replacement in replacements.items():
|
|
130
|
-
if replacement is not None:
|
|
131
|
-
# Use regex to avoid partial replacements
|
|
132
|
-
safe_replacement = re.escape(str(replacement))
|
|
133
|
-
safe_replacement = safe_replacement.replace('\\', r'')
|
|
134
|
-
message = re.sub(
|
|
135
|
-
re.escape(placeholder) + r'(?![a-zA-Z0-9_])',
|
|
136
|
-
safe_replacement,
|
|
137
|
-
message
|
|
138
|
-
)
|
|
43
|
+
def _replace_placeholders(self, message: str, replacements: Dict[str, str]) -> str:
|
|
44
|
+
"""Safely replace all placeholders in message"""
|
|
45
|
+
for ph, val in replacements.items():
|
|
46
|
+
if val:
|
|
47
|
+
message = message.replace(ph, self._stringify(val))
|
|
139
48
|
return message
|
|
140
49
|
|
|
141
|
-
def _get_other_param_display(self) -> Optional[str]:
|
|
142
|
-
"""Get display names for other fields with proper formatting"""
|
|
143
|
-
other_fields = self._get_raw_other_fields()
|
|
144
|
-
if not other_fields:
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
display_names = [self._get_attribute_name(f) for f in other_fields]
|
|
148
|
-
|
|
149
|
-
if len(display_names) == 1:
|
|
150
|
-
return display_names[0]
|
|
151
|
-
if len(display_names) == 2:
|
|
152
|
-
return f"{display_names[0]} and {display_names[1]}"
|
|
153
|
-
return f"{', '.join(display_names[:-1])}, and {display_names[-1]}"
|
|
154
|
-
|
|
155
|
-
def _get_raw_other_fields(self) -> List[str]:
|
|
156
|
-
"""Extract field references from rule parameters"""
|
|
157
|
-
if not self._current_rule or not self._current_params:
|
|
158
|
-
return []
|
|
159
|
-
|
|
160
|
-
rule = self._current_rule.lower()
|
|
161
|
-
|
|
162
|
-
# Rules with field-value pairs (field1,value1,field2,value2,...)
|
|
163
|
-
if rule in {
|
|
164
|
-
'required_if', 'required_unless', 'exclude_if', 'exclude_unless',
|
|
165
|
-
'accepted_if', 'accepted_all_if', 'declined_if', 'declined_all_if',
|
|
166
|
-
'accepted_unless', 'declined_unless',
|
|
167
|
-
'missing_if', 'missing_unless', 'present_if', 'present_unless'
|
|
168
|
-
}:
|
|
169
|
-
return self._current_params[::2] # Take every even index
|
|
170
|
-
|
|
171
|
-
# Rules with multi field references
|
|
172
|
-
if rule in {
|
|
173
|
-
'required_with', 'required_with_all', 'required_without', 'required_without_all',
|
|
174
|
-
'prohibits', 'exclude_with', 'exclude_without',
|
|
175
|
-
'missing_with', 'missing_with_all',
|
|
176
|
-
'present_with', 'present_with_all', 'same', 'different'
|
|
177
|
-
}:
|
|
178
|
-
return self._current_params
|
|
179
|
-
|
|
180
|
-
return []
|
|
181
|
-
|
|
182
|
-
def _get_min_param(self) -> Optional[str]:
|
|
183
|
-
"""Get min parameter from rule"""
|
|
184
|
-
if not self._current_params:
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
if self._current_rule and self._current_rule.startswith(('min', 'between', 'digits_between')):
|
|
188
|
-
return self._current_params[0]
|
|
189
|
-
return None
|
|
190
|
-
|
|
191
|
-
def _get_max_param(self) -> Optional[str]:
|
|
192
|
-
"""Get max parameter from rule"""
|
|
193
|
-
if not self._current_params or len(self._current_params) < 2:
|
|
194
|
-
return None
|
|
195
|
-
|
|
196
|
-
if self._current_rule and self._current_rule.startswith(('max', 'between', 'digits_between')):
|
|
197
|
-
return self._current_params[1] if self._current_rule.startswith('between') else self._current_params[0]
|
|
198
|
-
return None
|
|
199
|
-
|
|
200
|
-
def _get_size_param(self) -> Optional[str]:
|
|
201
|
-
"""Get size parameter from rule"""
|
|
202
|
-
if self._current_rule and self._current_rule.startswith('size') and self._current_params:
|
|
203
|
-
return self._current_params[0]
|
|
204
|
-
return None
|
|
205
|
-
|
|
206
|
-
def _get_values_param(self) -> Optional[str]:
|
|
207
|
-
"""Get values list for in/not_in rules"""
|
|
208
|
-
if (self._current_rule and
|
|
209
|
-
self._current_rule.startswith(('in', 'not_in')) and
|
|
210
|
-
self._current_params):
|
|
211
|
-
return ', '.join(self._current_params)
|
|
212
|
-
return None
|
|
213
|
-
|
|
214
|
-
def _get_date_param(self) -> Optional[str]:
|
|
215
|
-
"""Get date parameter for date rules"""
|
|
216
|
-
if (self._current_rule and
|
|
217
|
-
self._current_rule.startswith(('after', 'before', 'after_or_equal', 'before_or_equal')) and
|
|
218
|
-
self._current_params):
|
|
219
|
-
return self._current_params[0]
|
|
220
|
-
return None
|
|
221
|
-
|
|
222
|
-
def _get_format_param(self) -> Optional[str]:
|
|
223
|
-
"""Get format parameter"""
|
|
224
|
-
if (self._current_rule and
|
|
225
|
-
self._current_rule.startswith('date_format') and
|
|
226
|
-
self._current_params):
|
|
227
|
-
return self._current_params[0]
|
|
228
|
-
return None
|
|
229
|
-
|
|
230
|
-
def _get_first_param(self) -> Optional[str]:
|
|
231
|
-
"""Get first parameter from rule"""
|
|
232
|
-
return self._current_params[0] if self._current_params else None
|
|
233
|
-
|
|
234
50
|
@property
|
|
235
51
|
def has_errors(self) -> bool:
|
|
236
|
-
"""Check if any errors exist"""
|
|
52
|
+
"""Check if any validation errors exist"""
|
|
237
53
|
return bool(self.errors)
|
|
@@ -13,6 +13,7 @@ class RulePreparer:
|
|
|
13
13
|
"""Handles rule preparation and parsing"""
|
|
14
14
|
def __init__(self, rule_factory: RuleFactory):
|
|
15
15
|
self.rule_factory = rule_factory
|
|
16
|
+
self.added_rules = {}
|
|
16
17
|
|
|
17
18
|
def prepare(self, raw_rules: Dict[str, Union[str, List[Union[str, Rule]]]]) -> Dict[str, List[Rule]]:
|
|
18
19
|
"""Convert raw rules to prepared validation rules"""
|
|
@@ -20,10 +21,16 @@ class RulePreparer:
|
|
|
20
21
|
for field, rule_input in raw_rules.items():
|
|
21
22
|
if not rule_input:
|
|
22
23
|
continue
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
prepared_rules[field] = self._generate_rule(field, rule_input)
|
|
26
|
+
|
|
26
27
|
return prepared_rules
|
|
28
|
+
|
|
29
|
+
def _generate_rule(self, field, rule_input):
|
|
30
|
+
rules = self._convert_to_rules(rule_input)
|
|
31
|
+
RuleConflictChecker.check_conflicts(rules)
|
|
32
|
+
|
|
33
|
+
return self._deduplicate_rules(field, rules)
|
|
27
34
|
|
|
28
35
|
def _convert_to_rules(self, rule_input: Union[str, List[Union[str, Rule]], Rule, Tuple[Union[str, Rule], str], Tuple[Union[str, Rule], str]]) -> List[Rule]:
|
|
29
36
|
"""Convert mixed rule input to list of Rule objects"""
|
|
@@ -95,13 +102,20 @@ class RulePreparer:
|
|
|
95
102
|
def _parse_params(self, params: Union[Tuple, List, str, Enum]):
|
|
96
103
|
return set(tuple(x) if isinstance(x, list) else x for x in params)
|
|
97
104
|
|
|
98
|
-
def _deduplicate_rules(self, rules: List[Rule]) -> List[Rule]:
|
|
105
|
+
def _deduplicate_rules(self, field: str, rules: List[Rule]) -> List[Rule]:
|
|
99
106
|
"""Remove duplicate rules based on name and parameters"""
|
|
100
|
-
seen = set()
|
|
101
107
|
unique = []
|
|
102
108
|
for rule in rules:
|
|
103
109
|
identifier = (rule.rule_name, tuple(rule.params))
|
|
104
|
-
if
|
|
105
|
-
|
|
110
|
+
if rule.rule_name != 'when':
|
|
111
|
+
if identifier not in self.added_rules.get(field, []):
|
|
112
|
+
if isinstance(self.added_rules.get(field), set):
|
|
113
|
+
self.added_rules[field].add(identifier)
|
|
114
|
+
else:
|
|
115
|
+
self.added_rules[field] = set()
|
|
116
|
+
self.added_rules[field].add(identifier)
|
|
117
|
+
|
|
118
|
+
unique.append(rule)
|
|
119
|
+
else:
|
|
106
120
|
unique.append(rule)
|
|
107
121
|
return unique
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
validator/__init__.py,sha256=udxDzUicPfxBOAQvzsnl3pHur9VUppKbWMgg35hpiww,244
|
|
2
|
-
validator/exceptions.py,sha256=y2v7CaXmeGFHWcnigtLl4U-sFta_jMiXkGKXWIIVglY,366
|
|
3
|
-
validator/factory.py,sha256=SJ2iJmTnFS-4ccH-gzZl_7CAW7Z6YJkZbMImu-po80w,1163
|
|
4
|
-
validator/core/__init__.py,sha256=ZcqlXJSk03i_CVzmIN-nVe1UOyvwwO5jhbEj7f62Y_o,59
|
|
5
|
-
validator/core/validator.py,sha256=la-kzp82iCii2bq4hV76F-7datWPb27vmyHvpg2itR0,12726
|
|
6
|
-
validator/database/__init__.py,sha256=O-cB6-MhNapJ3iwe5jvifbMfr1dPjXLtEdfNTKIu0hc,171
|
|
7
|
-
validator/database/detector.py,sha256=Vac7oVL26GjU6expGo01-6mgUtXqldr-jirzpYokZBM,9597
|
|
8
|
-
validator/database/manager.py,sha256=XJM_I0WaWfZWV710duAc32p1gtiP2or-MAj75WPw1oM,6478
|
|
9
|
-
validator/rules/__init__.py,sha256=z3Vk3R5CRgjeqyDWZxdofD2tBMTgdyYVuFmo1mKOTj4,830
|
|
10
|
-
validator/rules/array.py,sha256=AqVoBR_chSqxPec14Av5KmR9NAByovXDNNu2oeId4-U,2528
|
|
11
|
-
validator/rules/base.py,sha256=PadT5Ko3Zs_yr5EPxOfTw-IdS7uJKhGpaekF1k4tvYk,4359
|
|
12
|
-
validator/rules/basic.py,sha256=LWYs4fPCA6_lTtOmtTgvjGqjVxxSdklj9fsCgSAMy2Y,4918
|
|
13
|
-
validator/rules/boolean.py,sha256=vy6huFJ5JidpsoJ0WSvcydiU7a8aYFj225UswggSGAE,5748
|
|
14
|
-
validator/rules/comparison.py,sha256=W22Gsafnu8zC0Ue-28PDB36fB8VDO3bMS5MDSg5VUHE,13368
|
|
15
|
-
validator/rules/date.py,sha256=yolYaTIvQTN1LBje5SM8i7EmNzOxV_eUwnOokmqZrMs,5812
|
|
16
|
-
validator/rules/files.py,sha256=c7deO8fEiFNCx4jq1B2sJXhxTqGzTVq4kK2EscSNhKI,10946
|
|
17
|
-
validator/rules/format.py,sha256=Medw57PJwfElxA-DgxiZ5GHqvqPhUCQPFMGCGTDne8w,7070
|
|
18
|
-
validator/rules/numeric.py,sha256=nkYVc8VtrWJ3Kt7JDLPsbg7ZaN3F0zJMnbf8Y5gxNsk,6824
|
|
19
|
-
validator/rules/string.py,sha256=p8ZQfd0XaWIjksg_8ta3f6PEnXlxjRzlSJx1GohZ7yk,5237
|
|
20
|
-
validator/rules/utilities.py,sha256=dLbmSvaTdC2kK7_6V1MrPmtXr5u6YvJKnHMJSByslKk,12236
|
|
21
|
-
validator/services/__init__.py,sha256=zzKTmqL7v4niFGWHJBfWLqgJ0iTaW_69OzYZN8uInzQ,210
|
|
22
|
-
validator/services/rule_conflict.py,sha256=T3IhWLmZsRUccJ4oFO-OKRjrc5Xt7r71kktxjjj2IA8,9505
|
|
23
|
-
validator/services/rule_error_handler.py,sha256=1Tka8PZA5skgf0VGUVj0evdL9Xqq2Zb9Vns_XcUMgZ8,10385
|
|
24
|
-
validator/services/rule_preparer.py,sha256=4khRjdely_0Z5mxFwf1bKIid076_xDuNh2XBO_fGerE,4706
|
|
25
|
-
validator/utils/__init__.py,sha256=Yzo-xv285Be-a233M4duDdYtscuHiuBbPSX_C8yViJI,20
|
|
26
|
-
validator/utils/string.py,sha256=0YACzeEaWNEOR9_7O9A8D1ItIbtWfOJ8IfrzcB8VMYA,515
|
|
27
|
-
safeshield-1.5.1.dist-info/LICENSE,sha256=qugtRyKckyaks6hd2xyxOFSOYM6au1N80pMXuMTPvC4,1090
|
|
28
|
-
safeshield-1.5.1.dist-info/METADATA,sha256=wg2r8qCTKIwdHcKUpQf6Xf_nDrubGkzo2spCdDBze_g,2313
|
|
29
|
-
safeshield-1.5.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
30
|
-
safeshield-1.5.1.dist-info/top_level.txt,sha256=iUtV3dlHOIiMfLuY4pruY00lFni8JzOkQ3Nh1II19OE,10
|
|
31
|
-
safeshield-1.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|