safeshield 1.0.0__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.

@@ -0,0 +1,332 @@
1
+ from .base import ValidationRule
2
+ from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
+ from enum import Enum
4
+ import re
5
+ import inspect
6
+ from collections.abc import Iterable
7
+
8
+ # =============================================
9
+ # CONDITIONAL VALIDATION RULES
10
+ # =============================================
11
+
12
+ class RequiredIfRule(ValidationRule):
13
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
14
+ if len(params) < 2 or len(params) % 2 != 0:
15
+ return True
16
+
17
+ conditions = list(zip(params[::2], params[1::2]))
18
+
19
+ condition_met = False
20
+ for other_field, expected_value in conditions:
21
+ if not other_field or expected_value is None:
22
+ continue
23
+
24
+ actual_value = self.get_field_value(other_field, '')
25
+ if actual_value == expected_value:
26
+ condition_met = True
27
+ break
28
+
29
+ if not condition_met:
30
+ return True
31
+
32
+ return not self.is_empty(value)
33
+
34
+ def message(self, field: str, params: List[str]) -> str:
35
+ valid_conditions = []
36
+ if len(params) >= 2 and len(params) % 2 == 0:
37
+ valid_conditions = [
38
+ f"{params[i]} = {params[i+1]}"
39
+ for i in range(0, len(params), 2)
40
+ if params[i] and params[i+1] is not None
41
+ ]
42
+
43
+ if not valid_conditions:
44
+ return f"Invalid required_if rule configuration for {field}"
45
+
46
+ return f"The :name field is required when any of these are true: {', '.join(valid_conditions)}"
47
+
48
+ class RequiredAllIfRule(ValidationRule):
49
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
50
+ if len(params) < 2:
51
+ return False
52
+
53
+ conditions = [(f.strip(), v.strip()) for f, v in zip(params[::2], params[1::2])]
54
+
55
+ all_conditions_met = all(
56
+ self.get_field_value(f) == v
57
+ for f, v in conditions
58
+ )
59
+
60
+ if not all_conditions_met:
61
+ return True
62
+
63
+ return not self.is_empty(value)
64
+
65
+ def message(self, field: str, params: List[str]) -> str:
66
+ conditions = " AND ".join(f"{f} = {v}" for f, v in zip(params[::2], params[1::2]))
67
+ return f"The :name field is required when ALL conditions are met: {conditions}"
68
+
69
+ class RequiredUnlessRule(ValidationRule):
70
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
71
+ if len(params) < 2:
72
+ return False
73
+
74
+ other_field, other_value = params[0], params[1]
75
+ actual_value = self.get_field_value(other_field, '')
76
+
77
+ if actual_value == other_value:
78
+ return True
79
+
80
+ return not self.is_empty(value)
81
+
82
+ def message(self, field: str, params: List[str]) -> str:
83
+ return f"The :name field is required unless {params[0]} is {params[1]}."
84
+
85
+ class RequiredWithRule(ValidationRule):
86
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
87
+ if not params:
88
+ return False
89
+
90
+ if any(f in self.validator.data for f in params):
91
+ return not self.is_empty(value)
92
+ return True
93
+
94
+ def message(self, field: str, params: List[str]) -> str:
95
+ return f"The :name field is required when {', '.join(params)} is present."
96
+
97
+ class RequiredWithAllRule(ValidationRule):
98
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
99
+ if not params:
100
+ return False
101
+
102
+ if all(f in self.validator.data for f in params):
103
+ return not self.is_empty(value)
104
+ return True
105
+
106
+ def message(self, field: str, params: List[str]) -> str:
107
+ return f"The :name field is required when all of {', '.join(params)} are present."
108
+
109
+ class RequiredWithoutRule(ValidationRule):
110
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
111
+ if not params:
112
+ return False
113
+
114
+ if any(f not in self.validator.data for f in params):
115
+ return not self.is_empty(value)
116
+ return True
117
+
118
+ def message(self, field: str, params: List[str]) -> str:
119
+ return f"The :name field is required when {', '.join(params)} is not present."
120
+
121
+ class RequiredWithoutAllRule(ValidationRule):
122
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
123
+ if not params:
124
+ return False
125
+
126
+ if all(f not in self.validator.data for f in params):
127
+ return not self.is_empty(value)
128
+ return True
129
+
130
+ def message(self, field: str, params: List[str]) -> str:
131
+ return f"The :name field is required when none of {', '.join(params)} are present."
132
+
133
+ class ProhibitedIfRule(ValidationRule):
134
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
135
+ if len(params) < 2:
136
+ return False
137
+
138
+ other_field, other_value = params[0], params[1]
139
+ actual_value = self.get_field_value(other_field, '')
140
+
141
+ if actual_value != other_value:
142
+ return True
143
+
144
+ return self.is_empty(value)
145
+
146
+ def message(self, field: str, params: List[str]) -> str:
147
+ return f"The :name field is prohibited when {params[0]} is {params[1]}."
148
+
149
+ class ProhibitedUnlessRule(ValidationRule):
150
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
151
+ if len(params) < 2:
152
+ return False
153
+
154
+ other_field, other_value = params[0], params[1]
155
+ actual_value = self.get_field_value(other_field, '')
156
+
157
+ if actual_value == other_value:
158
+ return True
159
+
160
+ return self.is_empty(value)
161
+
162
+ def message(self, field: str, params: List[str]) -> str:
163
+ return f"The :name field is prohibited unless {params[0]} is {params[1]}."
164
+
165
+ class FilledIfRule(ValidationRule):
166
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
167
+ if len(params) < 2:
168
+ return False
169
+
170
+ other_field, other_value = params[0], params[1]
171
+ actual_value = self.get_field_value(other_field, '')
172
+
173
+ if actual_value != other_value:
174
+ return True
175
+
176
+ return value not in ('', None)
177
+
178
+ def message(self, field: str, params: List[str]) -> str:
179
+ return f"The :name field must be filled when {params[0]} is {params[1]}."
180
+
181
+ class RegexRule(ValidationRule):
182
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
183
+ if not params or not isinstance(value, str):
184
+ return False
185
+ try:
186
+ return bool(re.fullmatch(params[0], value))
187
+ except re.error:
188
+ return False
189
+
190
+ def message(self, field: str, params: List[str]) -> str:
191
+ return f"The :name format is invalid."
192
+
193
+ class NotRegexRule(ValidationRule):
194
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
195
+ if not params or not isinstance(value, str):
196
+ return True
197
+ print(not bool(re.search(params[0], value)))
198
+ try:
199
+ return not bool(re.search(params[0], value))
200
+ except re.error:
201
+ return True
202
+
203
+ def message(self, field: str, params: List[str]) -> str:
204
+ return f"The :name format is invalid."
205
+
206
+ class InRule(ValidationRule):
207
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
208
+ allowed_values = self._parse_option_values(field, params)
209
+ return (str(value) in allowed_values or value in allowed_values)
210
+
211
+ def message(self, field: str, params: List[str]) -> str:
212
+ allowed_values = self._parse_option_values(field, params)
213
+ return f"The selected :name must be in : {', '.join(map(str, allowed_values))}"
214
+
215
+ class NotInRule(ValidationRule):
216
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
217
+ not_allowed_values = self._parse_option_values(field, params)
218
+ return str(value) not in not_allowed_values
219
+
220
+ def message(self, field: str, params: List[str]) -> str:
221
+ not_allowed_values = self._parse_option_values(field, params)
222
+ return f"The selected :name must be not in : {', '.join(map(str, not_allowed_values))}"
223
+
224
+ class EnumRule(ValidationRule):
225
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
226
+ allowed_values = self._parse_option_values(field, params)
227
+ return (str(value) in allowed_values or value in allowed_values)
228
+
229
+ def message(self, field: str, params: List[str]) -> str:
230
+ allowed_values = self._parse_option_values(field, params)
231
+ return f"The :name must be one of: {', '.join(map(str, allowed_values))}"
232
+
233
+ class UniqueRule(ValidationRule):
234
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
235
+ if not params or not hasattr(self.validator, 'db_manager') or not self.validator.db_manager:
236
+ return False
237
+
238
+ table = params[0]
239
+ column = field if len(params) == 1 else params[1]
240
+
241
+ try:
242
+ # Optional: handle ignore case (id)
243
+ ignore_id = None
244
+ if len(params) > 2 and params[2].startswith('ignore:'):
245
+ ignore_field = params[2].split(':')[1]
246
+ ignore_id = self.get_field_value(ignore_field)
247
+ return self.validator.db_manager.is_unique(table, column, value, ignore_id)
248
+ except Exception as e:
249
+ print(f"Database error in UniqueRule: {e}")
250
+ return False
251
+
252
+ def message(self, field: str, params: List[str]) -> str:
253
+ return f"The :name has already been taken."
254
+
255
+ class ExistsRule(ValidationRule):
256
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
257
+ if not params or not hasattr(self.validator, 'db_manager') or not self.validator.db_manager:
258
+ return False
259
+
260
+ table = params[0]
261
+ column = field if len(params) == 1 else params[1]
262
+
263
+ try:
264
+ return self.validator.db_manager.exists(table, column, value)
265
+ except Exception as e:
266
+ print(f"Database error in ExistsRule: {e}")
267
+ return False
268
+
269
+ def message(self, field: str, params: List[str]) -> str:
270
+ return f"The selected :name is invalid."
271
+
272
+ class ConfirmedRule(ValidationRule):
273
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
274
+ confirmation_field = f"{field}_confirmation"
275
+
276
+ return value == self.get_field_value(confirmation_field, '')
277
+
278
+ def message(self, field: str, params: List[str]) -> str:
279
+ return f"The :name confirmation does not match."
280
+
281
+ class SameRule(ValidationRule):
282
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
283
+ if not params:
284
+ return False
285
+ return value == self.get_field_value(params[0])
286
+
287
+ def message(self, field: str, params: List[str]) -> str:
288
+ return f"The :name and {params[0]} must match."
289
+
290
+ class DifferentRule(ValidationRule):
291
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
292
+ if not params:
293
+ return False
294
+ return value != self.get_field_value(params[0])
295
+
296
+ def message(self, field: str, params: List[str]) -> str:
297
+ return f"The :name and {params[0]} must be different."
298
+
299
+ class AcceptedRule(ValidationRule):
300
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
301
+ if isinstance(value, str):
302
+ return value.lower() in ['yes', 'on', '1', 'true']
303
+ if isinstance(value, int):
304
+ return value == 1
305
+ if isinstance(value, bool):
306
+ return value
307
+ return False
308
+
309
+ def message(self, field: str, params: List[str]) -> str:
310
+ return f"The :name must be accepted."
311
+
312
+ class DeclinedRule(ValidationRule):
313
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
314
+ if isinstance(value, str):
315
+ return value.lower() in ['no', 'off', '0', 'false']
316
+ if isinstance(value, int):
317
+ return value == 0
318
+ if isinstance(value, bool):
319
+ return not value
320
+ return False
321
+
322
+ def message(self, field: str, params: List[str]) -> str:
323
+ return f"The :name must be declined."
324
+
325
+
326
+ class BailRule(ValidationRule):
327
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
328
+ self.validator._stop_on_first_failure = True
329
+ return True
330
+
331
+ def message(self, field: str, params: List[str]) -> str:
332
+ return ""
@@ -0,0 +1,154 @@
1
+ from .base import ValidationRule
2
+ from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
+ from datetime import datetime
4
+ from dateutil.parser import parse
5
+
6
+ class DateRule(ValidationRule):
7
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
8
+ if not isinstance(value, str):
9
+ return False
10
+
11
+ try:
12
+ parse(value)
13
+ return True
14
+ except ValueError:
15
+ return False
16
+
17
+ def message(self, field: str, params: List[str]) -> str:
18
+ return f"The :name is not a valid date."
19
+
20
+ class DateEqualsRule(ValidationRule):
21
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
22
+ if not params or not isinstance(value, str):
23
+ return False
24
+
25
+ value = self.get_field_value(value, value)
26
+
27
+ try:
28
+ date1 = parse(value)
29
+
30
+ params[0] = self.get_field_value(params[0], params[0])
31
+
32
+ date2 = parse(params[0])
33
+ return date1 == date2
34
+ except ValueError as e:
35
+ return False
36
+
37
+ def message(self, field: str, params: List[str]) -> str:
38
+ return f"The :name must be equal to {params[0]}."
39
+
40
+ class AfterRule(ValidationRule):
41
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
42
+ if not params or len(params) < 1:
43
+ return False
44
+
45
+ try:
46
+ # Parse input date
47
+ if isinstance(value, str):
48
+ date_value = parse(value)
49
+ elif isinstance(value, datetime):
50
+ date_value = value
51
+ else:
52
+ return False
53
+
54
+ # Parse comparison date
55
+ params[0] = self.get_field_value(params[0], params[0])
56
+ compare_date = parse(params[0])
57
+
58
+ return date_value > compare_date
59
+
60
+ except (ValueError, TypeError):
61
+ return False
62
+
63
+ def message(self, field: str, params: List[str]) -> str:
64
+ return f"The :name must be after {params[0]}"
65
+
66
+ class AfterOrEqualRule(ValidationRule):
67
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
68
+ if not params or len(params) < 1:
69
+ return False
70
+
71
+ try:
72
+ if isinstance(value, str):
73
+ date_value = parse(value)
74
+ elif isinstance(value, datetime):
75
+ date_value = value
76
+ else:
77
+ return False
78
+
79
+ params[0] = self.get_field_value(params[0], params[0])
80
+ compare_date = parse(params[0])
81
+
82
+ return date_value >= compare_date
83
+
84
+ except (ValueError, TypeError):
85
+ return False
86
+
87
+ def message(self, field: str, params: List[str]) -> str:
88
+ return f"The :name must be after or equal to {params[0]}"
89
+
90
+ class BeforeRule(ValidationRule):
91
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
92
+ if not params or len(params) < 1:
93
+ return False
94
+
95
+ value = self.get_field_value(value, value)
96
+
97
+ try:
98
+ if isinstance(value, str):
99
+ date_value = parse(value)
100
+ elif isinstance(value, datetime):
101
+ date_value = value
102
+ else:
103
+ return False
104
+
105
+ params[0] = self.get_field_value(params[0], params[0])
106
+ compare_date = parse(params[0])
107
+
108
+ return date_value < compare_date
109
+
110
+ except (ValueError, TypeError):
111
+ return False
112
+
113
+ def message(self, field: str, params: List[str]) -> str:
114
+ return f"The :name must be before {params[0]}"
115
+
116
+ class BeforeOrEqualRule(ValidationRule):
117
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
118
+ if not params or len(params) < 1:
119
+ return False
120
+
121
+ value = self.get_field_value(value, value)
122
+
123
+ try:
124
+ if isinstance(value, str):
125
+ date_value = parse(value)
126
+ elif isinstance(value, datetime):
127
+ date_value = value
128
+ else:
129
+ return False
130
+
131
+ params[0] = self.get_field_value(params[0], params[0])
132
+ compare_date = parse(params[0])
133
+
134
+ return date_value <= compare_date
135
+
136
+ except (ValueError, TypeError):
137
+ return False
138
+
139
+ def message(self, field: str, params: List[str]) -> str:
140
+ return f"The :name must be before or equal to {params[0]}"
141
+
142
+ class DateFormatRule(ValidationRule):
143
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
144
+ if not params or len(params) < 1 or not isinstance(value, str):
145
+ return False
146
+
147
+ try:
148
+ datetime.strptime(value, params[0])
149
+ return True
150
+ except ValueError:
151
+ return False
152
+
153
+ def message(self, field: str, params: List[str]) -> str:
154
+ return f"The :name must match the format {params[0]}"
@@ -0,0 +1,167 @@
1
+ from .base import ValidationRule
2
+ from typing import Any, Dict, List, Optional, Set, Union, Tuple, Type
3
+
4
+ class FileRule(ValidationRule):
5
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
6
+ # 1. Cek file object framework (Flask/Werkzeug/FastAPI)
7
+ if (hasattr(value, 'filename') and
8
+ hasattr(value, 'stream') and
9
+ hasattr(value, 'content_type')):
10
+ return True
11
+
12
+ # 2. Cek file-like object umum
13
+ if hasattr(value, 'read') and callable(value.read):
14
+ return True
15
+
16
+ # 3. Cek path file yang valid (string)
17
+ if isinstance(value, str):
18
+ return (
19
+ '.' in value and # Harus punya extension
20
+ not value.startswith('data:') and # Bukan data URI
21
+ not value.strip().startswith('<') # Bukan XML/HTML
22
+ )
23
+
24
+ # 4. Cek binary data langsung
25
+ if isinstance(value, (bytes, bytearray)):
26
+ return len(value) > 0 # Pastikan tidak kosong
27
+
28
+ return False
29
+
30
+ def message(self, field: str, params: List[str]) -> str:
31
+ return f"The {field} must be a valid file"
32
+
33
+ class DimensionsRule(ValidationRule):
34
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
35
+ try:
36
+ from PIL import Image
37
+ import io
38
+
39
+ # 1. Parse dimension rules
40
+ min_width = min_height = 0
41
+ max_width = max_height = float('inf')
42
+
43
+ for param in params:
44
+ try:
45
+ if param.startswith('min:'):
46
+ min_width, min_height = map(int, param[4:].split('x'))
47
+ elif param.startswith('max:'):
48
+ max_width, max_height = map(int, param[4:].split('x'))
49
+ else:
50
+ exact_width, exact_height = map(int, param.split('x'))
51
+ min_width = max_width = exact_width
52
+ min_height = max_height = exact_height
53
+ except (ValueError, IndexError):
54
+ continue
55
+
56
+ # 2. Load image with proper handling
57
+ img = None
58
+ try:
59
+ # Case 1: File-like object (Flask/Werkzeug/FastAPI)
60
+ if hasattr(value, 'read'):
61
+ value.seek(0) # Important for rewinding
62
+ img = Image.open(value)
63
+ value.seek(0) # Reset after reading
64
+
65
+ # Case 2: Bytes data
66
+ elif isinstance(value, bytes):
67
+ img = Image.open(io.BytesIO(value))
68
+
69
+ # Case 3: File path (string)
70
+ elif isinstance(value, str) and value.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
71
+ img = Image.open(value)
72
+
73
+ if img is None:
74
+ return False
75
+
76
+ # 3. Validate dimensions
77
+ width, height = img.size
78
+ return (min_width <= width <= max_width and
79
+ min_height <= height <= max_height)
80
+
81
+ except (IOError, OSError, AttributeError) as e:
82
+ print(f"Image loading failed: {str(e)}")
83
+ return False
84
+
85
+ except Exception as e:
86
+ print(f"Unexpected error in dimension validation: {str(e)}")
87
+ return False
88
+
89
+ def message(self, field: str, params: List[str]) -> str:
90
+ min_rules = [p for p in params if p.startswith('min:')]
91
+ max_rules = [p for p in params if p.startswith('max:')]
92
+ exact_rules = [p for p in params if ':' not in p]
93
+
94
+ messages = []
95
+ if exact_rules:
96
+ messages.append(f"exactly {exact_rules[0]}")
97
+ if min_rules:
98
+ messages.append(f"minimum {min_rules[0][4:]}")
99
+ if max_rules:
100
+ messages.append(f"maximum {max_rules[0][4:]}")
101
+
102
+ return f"Image {field} dimensions must be {' and '.join(messages)}"
103
+
104
+ class ExtensionsRule(ValidationRule):
105
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
106
+ if not params:
107
+ return False
108
+
109
+ filename = (
110
+ value.filename if hasattr(value, 'filename')
111
+ else str(value)
112
+ ).lower()
113
+
114
+ if '.' not in filename:
115
+ return False
116
+
117
+ ext = filename.rsplit('.', 1)[1]
118
+ return ext in [e.lower().strip() for e in params]
119
+
120
+ def message(self, field: str, params: List[str]) -> str:
121
+ return f"File {field} must have one of these extensions: {', '.join(params)}"
122
+
123
+ class ImageRule(ValidationRule):
124
+ VALID_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'}
125
+ VALID_MIME_TYPES = {
126
+ 'image/jpeg', 'image/png',
127
+ 'image/gif', 'image/bmp', 'image/webp'
128
+ }
129
+
130
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
131
+ # Check basic file attributes
132
+ if not hasattr(value, 'filename') and not isinstance(value, (str, bytes)):
133
+ return False
134
+
135
+ # Check extension if available
136
+ if hasattr(value, 'filename'):
137
+ ext = value.filename.rsplit('.', 1)[-1].lower()
138
+ if ext not in self.VALID_EXTENSIONS:
139
+ return False
140
+
141
+ # Check MIME type if available
142
+ if hasattr(value, 'content_type'):
143
+ if value.content_type not in self.VALID_MIME_TYPES:
144
+ return False
145
+
146
+ return True
147
+
148
+ def message(self, field: str, params: List[str]) -> str:
149
+ return f"The {field} must be a valid image file (JPEG, PNG, GIF, BMP, or WebP)"
150
+
151
+ class MimeTypesRule(ValidationRule):
152
+ def validate(self, field: str, value: Any, params: List[str]) -> bool:
153
+ if not params:
154
+ return False
155
+
156
+ # Check from content_type attribute
157
+ if hasattr(value, 'content_type'):
158
+ return value.content_type in params
159
+
160
+ # Check from mimetype attribute
161
+ if hasattr(value, 'mimetype'):
162
+ return value.mimetype in params
163
+
164
+ return False
165
+
166
+ def message(self, field: str, params: List[str]) -> str:
167
+ return f"File {field} must be one of these types: {', '.join(params)}"