safeshield 1.5.1__py3-none-any.whl → 1.5.2__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.
@@ -2,10 +2,7 @@ from typing import Dict, List, Any, Optional, Union
2
2
  import re
3
3
 
4
4
  class RuleErrorHandler:
5
- """Enhanced validation error handler with complete Laravel-style placeholder support,
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, rule_name: str, rule_params: List[str], default_message: str, value: Any) -> None:
17
- """Add a formatted error message with complete placeholder support"""
18
- self._current_rule = rule_name
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, rule_name, default_message, value)
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, rule_name: str, default_message: str, value: Any) -> str:
26
- """Format error message with all supported placeholders"""
27
- attribute = self._get_attribute_name(field)
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 _stringify_value(self, value: Any) -> str:
56
- """Convert any value to a string representation"""
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 _get_message(self, field: str, rule_name: str, attribute: str, default: str) -> str:
64
- """Get the most specific error message available"""
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 # Field-specific rule message
67
- self.messages.get(field) or # Field-specific default
68
- self.messages.get(rule_name) or # Rule-specific default
69
- default # Fallback
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
- # Add numbered placeholders for field-value pairs (e.g., :other1, :value1, :other2, :value2)
89
- if self._is_field_value_rule() and self._current_params:
90
- field_value_pairs = self._get_field_value_pairs()
91
- if field_value_pairs:
92
- first_field, first_value = field_value_pairs[0]
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
- rules = self._convert_to_rules(rule_input)
24
- RuleConflictChecker.check_conflicts(rules)
25
- prepared_rules[field] = self._deduplicate_rules(rules)
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 identifier not in seen:
105
- seen.add(identifier)
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,,