safeshield 1.0.0__tar.gz

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.
Files changed (34) hide show
  1. safeshield-1.0.0/LICENSE +21 -0
  2. safeshield-1.0.0/PKG-INFO +21 -0
  3. safeshield-1.0.0/README.md +2 -0
  4. safeshield-1.0.0/safeshield.egg-info/PKG-INFO +21 -0
  5. safeshield-1.0.0/safeshield.egg-info/SOURCES.txt +32 -0
  6. safeshield-1.0.0/safeshield.egg-info/dependency_links.txt +1 -0
  7. safeshield-1.0.0/safeshield.egg-info/requires.txt +11 -0
  8. safeshield-1.0.0/safeshield.egg-info/top_level.txt +1 -0
  9. safeshield-1.0.0/setup.cfg +4 -0
  10. safeshield-1.0.0/setup.py +32 -0
  11. safeshield-1.0.0/validator/__init__.py +7 -0
  12. safeshield-1.0.0/validator/core/__init__.py +3 -0
  13. safeshield-1.0.0/validator/core/validator.py +299 -0
  14. safeshield-1.0.0/validator/database/__init__.py +5 -0
  15. safeshield-1.0.0/validator/database/detector.py +250 -0
  16. safeshield-1.0.0/validator/database/manager.py +162 -0
  17. safeshield-1.0.0/validator/exceptions.py +10 -0
  18. safeshield-1.0.0/validator/factory.py +26 -0
  19. safeshield-1.0.0/validator/rules/__init__.py +27 -0
  20. safeshield-1.0.0/validator/rules/array.py +77 -0
  21. safeshield-1.0.0/validator/rules/base.py +84 -0
  22. safeshield-1.0.0/validator/rules/basic.py +41 -0
  23. safeshield-1.0.0/validator/rules/comparison.py +240 -0
  24. safeshield-1.0.0/validator/rules/conditional.py +332 -0
  25. safeshield-1.0.0/validator/rules/date.py +154 -0
  26. safeshield-1.0.0/validator/rules/files.py +167 -0
  27. safeshield-1.0.0/validator/rules/format.py +105 -0
  28. safeshield-1.0.0/validator/rules/string.py +74 -0
  29. safeshield-1.0.0/validator/rules/type.py +42 -0
  30. safeshield-1.0.0/validator/rules/utilities.py +213 -0
  31. safeshield-1.0.0/validator/services/__init__.py +5 -0
  32. safeshield-1.0.0/validator/services/rule_conflict.py +133 -0
  33. safeshield-1.0.0/validator/services/rule_error_handler.py +42 -0
  34. safeshield-1.0.0/validator/services/rule_preparer.py +120 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Wunsun Tarniho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: safeshield
3
+ Version: 1.0.0
4
+ Summary: Library for Help Validation Control
5
+ Home-page: https://github.com/WunsunTarniho/py-guard
6
+ Author: Wunsun Tarniho
7
+ Author-email: wunsun58@gmail.com
8
+ Project-URL: Bug Tracker, https://github.com/WunsunTarniho/py-guard/issues
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+
20
+ ## License
21
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
@@ -0,0 +1,2 @@
1
+ ## License
2
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: safeshield
3
+ Version: 1.0.0
4
+ Summary: Library for Help Validation Control
5
+ Home-page: https://github.com/WunsunTarniho/py-guard
6
+ Author: Wunsun Tarniho
7
+ Author-email: wunsun58@gmail.com
8
+ Project-URL: Bug Tracker, https://github.com/WunsunTarniho/py-guard/issues
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+
20
+ ## License
21
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
@@ -0,0 +1,32 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ safeshield.egg-info/PKG-INFO
5
+ safeshield.egg-info/SOURCES.txt
6
+ safeshield.egg-info/dependency_links.txt
7
+ safeshield.egg-info/requires.txt
8
+ safeshield.egg-info/top_level.txt
9
+ validator/__init__.py
10
+ validator/exceptions.py
11
+ validator/factory.py
12
+ validator/core/__init__.py
13
+ validator/core/validator.py
14
+ validator/database/__init__.py
15
+ validator/database/detector.py
16
+ validator/database/manager.py
17
+ validator/rules/__init__.py
18
+ validator/rules/array.py
19
+ validator/rules/base.py
20
+ validator/rules/basic.py
21
+ validator/rules/comparison.py
22
+ validator/rules/conditional.py
23
+ validator/rules/date.py
24
+ validator/rules/files.py
25
+ validator/rules/format.py
26
+ validator/rules/string.py
27
+ validator/rules/type.py
28
+ validator/rules/utilities.py
29
+ validator/services/__init__.py
30
+ validator/services/rule_conflict.py
31
+ validator/services/rule_error_handler.py
32
+ validator/services/rule_preparer.py
@@ -0,0 +1,11 @@
1
+ Django==5.2
2
+ Flask==3.1.1
3
+ mysql_connector_repackaged==0.3.1
4
+ odoo==1.0
5
+ Pillow==11.2.1
6
+ psycopg2==2.9.10
7
+ python-dotenv==1.1.1
8
+ python_dateutil==2.9.0.post0
9
+ setuptools==63.2.0
10
+ SQLAlchemy==2.0.41
11
+ Werkzeug==3.1.3
@@ -0,0 +1 @@
1
+ validator
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ from setuptools import setup, find_packages
2
+ import os
3
+
4
+ def get_requirements():
5
+ with open('requirements.txt') as f:
6
+ return [line.strip() for line in f.readlines() if line.strip()]
7
+
8
+ setup(
9
+ name="safeshield", # Nama package di PyPI
10
+ version="1.0.0",
11
+ packages=find_packages(),
12
+ install_requires=get_requirements(),
13
+ author="Wunsun Tarniho",
14
+ author_email="wunsun58@gmail.com",
15
+ description="Library for Help Validation Control",
16
+ long_description=open('README.md').read(),
17
+ long_description_content_type="text/markdown",
18
+ url="https://github.com/WunsunTarniho/py-guard",
19
+ project_urls={
20
+ "Bug Tracker": "https://github.com/WunsunTarniho/py-guard/issues",
21
+ },
22
+ classifiers=[
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: OS Independent",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ "Topic :: Security",
30
+ ],
31
+ python_requires=">=3.10",
32
+ )
@@ -0,0 +1,7 @@
1
+
2
+ from .core import Validator
3
+ from .exceptions import ValidationException, RuleNotFoundException
4
+ from .factory import RuleFactory
5
+
6
+ __version__ = "1.0.0"
7
+ __all__ = ['Validator', 'ValidationException', 'RuleNotFoundException', 'RuleFactory']
@@ -0,0 +1,3 @@
1
+ from .validator import Validator
2
+
3
+ __all__ = ['Validator']
@@ -0,0 +1,299 @@
1
+ from typing import Optional, Dict, Any, List, Tuple, Union
2
+ from validator.exceptions import ValidationException, RuleNotFoundException
3
+ from validator.factory import RuleFactory
4
+ from validator.database import DatabaseManager, DatabaseAutoDetector
5
+ from validator.rules import ValidationRule
6
+ from validator.services.rule_conflict import RuleConflictChecker
7
+ from validator.services.rule_error_handler import RuleErrorHandler
8
+ from validator.services.rule_preparer import RulePreparer
9
+ import warnings
10
+ from collections.abc import Mapping
11
+
12
+ class Validator:
13
+ """Main validation class with proper abstractions"""
14
+ PRIORITY_RULES = {
15
+ 'bail', 'exclude_unless', 'exclude_if', 'exclude_with', 'exclude_without', 'required', 'required_if', 'required_unless',
16
+ 'required_with', 'required_without'
17
+ }
18
+
19
+ def __init__(
20
+ self,
21
+ data: Optional[Dict[str, Any]] = None,
22
+ rules: Optional[Dict[str, Union[
23
+ str,
24
+ List[Union[
25
+ str,
26
+ ValidationRule,
27
+ Tuple[str, Union[
28
+ str,
29
+ Union[
30
+ Tuple[str],
31
+ List[str]
32
+ ],
33
+ Union[
34
+ Tuple[str],
35
+ List[str]
36
+ ]
37
+ ]]
38
+ ]],
39
+ Tuple[Union[
40
+ str,
41
+ ValidationRule,
42
+ Tuple[str, Union[
43
+ str,
44
+ Union[
45
+ Tuple[str],
46
+ List[str]
47
+ ],
48
+ Union[
49
+ Tuple[str],
50
+ List[str]
51
+ ]
52
+ ]]
53
+ ]]
54
+ ]]] = None,
55
+ messages: Optional[Dict[str, str]] = None,
56
+ custom_attributes: Optional[Dict[str, str]] = None,
57
+ db_config: Optional[Dict[str, Any]] = None,
58
+ # rule_preparer: Optional[RulePreparer] = None,
59
+ # error_handler: Optional[RuleErrorHandler] = None
60
+ ):
61
+ self.data = data or {}
62
+ self._raw_rules = rules or {}
63
+ self._stop_on_first_failure = False
64
+ self._is_exclude = False
65
+
66
+ # Initialize dependencies
67
+ self.rule_preparer = RulePreparer(RuleFactory())
68
+ self.error_handler = RuleErrorHandler(messages, custom_attributes)
69
+
70
+ # Database configuration
71
+ self.db_config = db_config or DatabaseAutoDetector.detect()
72
+ if not self.db_config:
73
+ warnings.warn(
74
+ "No database config detected. exists/unique validations will be skipped!",
75
+ RuntimeWarning
76
+ )
77
+ self.db_manager = DatabaseManager(self.db_config) if self.db_config else None
78
+
79
+ def validate(self) -> bool:
80
+ """Validate the data against the rules"""
81
+ prepared_rules = self.rule_preparer.prepare(self._raw_rules)
82
+ self.error_handler.errors.clear()
83
+
84
+ validated = self._validate_rules(prepared_rules, priority_only=True)
85
+
86
+ # First pass: priority rules
87
+ if self.error_handler.has_errors and self._stop_on_first_failure:
88
+ return False
89
+
90
+ # Second pass: remaining rules
91
+ self._validate_rules(prepared_rules, priority_only=False)
92
+
93
+ if self.error_handler.has_errors:
94
+ return False
95
+ return True
96
+
97
+ def _validate_rules(self, prepared_rules: Dict[str, List[ValidationRule]], priority_only: bool):
98
+ validated = []
99
+ for field_pattern, rules in prepared_rules.items():
100
+ concrete_paths = self._resolve_wildcard_paths(field_pattern) if '*' in field_pattern else [field_pattern]
101
+
102
+ for actual_path in concrete_paths:
103
+ self._current_actual_path = actual_path
104
+ raw_values = self._get_nested_value(actual_path)
105
+ field_exists = self._field_exists_in_data(actual_path)
106
+
107
+ # Handle empty array case for wildcard
108
+ is_wildcard = '*' in field_pattern
109
+ is_empty_array = isinstance(raw_values, list) and len(raw_values) == 0
110
+
111
+ # Special case: wildcard path with empty array
112
+ if is_wildcard and is_empty_array:
113
+ values_to_validate = []
114
+ elif not is_wildcard and not is_empty_array:
115
+ values_to_validate = [raw_values]
116
+ else:
117
+ values_to_validate = [None] if not field_exists else (
118
+ [raw_values] if not isinstance(raw_values, list) else raw_values
119
+ )
120
+
121
+ for rule in rules:
122
+ if priority_only == (rule.rule_name in self.PRIORITY_RULES) and not self._is_exclude:
123
+ rule.set_field_exists(field_exists)
124
+
125
+ # Skip validation for empty array with wildcard unless it's a required rule
126
+ if is_wildcard and is_empty_array and rule.rule_name != 'required':
127
+ continue
128
+
129
+ for value in values_to_validate:
130
+ validated.append(self._apply_rule(field_pattern, value, rule))
131
+
132
+ delattr(self, '_current_actual_path')
133
+ self._is_exclude = False
134
+
135
+ return all(valid for valid in validated)
136
+
137
+ def _resolve_wildcard_paths(self, pattern: str) -> List[str]:
138
+ parts = pattern.split('.')
139
+
140
+ def _resolve(data: Any, current_path: str, remaining_parts: List[str]) -> List[str]:
141
+ if not remaining_parts:
142
+ return [current_path] if current_path else []
143
+
144
+ part = remaining_parts[0]
145
+ next_parts = remaining_parts[1:]
146
+
147
+ if part == '*':
148
+ results = []
149
+ # Kasus khusus: wildcard di akhir path
150
+ if not next_parts:
151
+ if isinstance(data, (list, tuple)):
152
+ for i in range(len(data)):
153
+ new_path = f"{current_path}.{i}" if current_path else str(i)
154
+ results.append(new_path)
155
+ elif isinstance(data, dict):
156
+ for key in data.keys():
157
+ new_path = f"{current_path}.{key}" if current_path else key
158
+ results.append(new_path)
159
+ else:
160
+ # Jika bukan collection, kembalikan path saat ini
161
+ if current_path:
162
+ results.append(current_path)
163
+ return results
164
+
165
+ # Wildcard di tengah path (seperti sebelumnya)
166
+ if isinstance(data, (list, tuple)):
167
+ for i, item in enumerate(data):
168
+ new_path = f"{current_path}.{i}" if current_path else str(i)
169
+ results.extend(_resolve(item, new_path, next_parts))
170
+ elif isinstance(data, dict):
171
+ for key, value in data.items():
172
+ new_path = f"{current_path}.{key}" if current_path else key
173
+ results.extend(_resolve(value, new_path, next_parts))
174
+ return results
175
+
176
+ # Handle regular path (sama seperti sebelumnya)
177
+ next_data = None
178
+ if isinstance(data, dict) and part in data:
179
+ next_data = data[part]
180
+ elif isinstance(data, (list, tuple)) and part.isdigit():
181
+ index = int(part)
182
+ if 0 <= index < len(data):
183
+ next_data = data[index]
184
+
185
+ if next_data is not None:
186
+ new_path = f"{current_path}.{part}" if current_path else part
187
+ return _resolve(next_data, new_path, next_parts)
188
+
189
+ return []
190
+
191
+ return _resolve(self.data, '', parts)
192
+
193
+ def _field_exists_in_data(self, field_path: str) -> bool:
194
+ parts = field_path.split('.')
195
+
196
+ def _check(data: Any, remaining_parts: List[str]) -> bool:
197
+ if not remaining_parts:
198
+ return True
199
+
200
+ part = remaining_parts[0]
201
+ next_parts = remaining_parts[1:]
202
+
203
+ if part == '*':
204
+ if isinstance(data, (list, tuple)):
205
+ return any(_check(item, next_parts) for item in data)
206
+ elif isinstance(data, dict):
207
+ return any(_check(value, next_parts) for value in data.values())
208
+ return False
209
+
210
+ if isinstance(data, dict) and part in data:
211
+ return _check(data[part], next_parts)
212
+ elif isinstance(data, (list, tuple)) and part.isdigit():
213
+ index = int(part)
214
+ return 0 <= index < len(data) and _check(data[index], next_parts)
215
+
216
+ return False
217
+
218
+ return _check(self.data, parts)
219
+
220
+ def _apply_rule(self, field: str, value: Any, rule: ValidationRule) -> bool:
221
+ try:
222
+ rule.set_validator(self)
223
+
224
+ # Format field name untuk pesan error (otomatis tangkap path aktual)
225
+ display_field = getattr(self, '_current_actual_path', field)
226
+
227
+ if not rule.validate(field, value, getattr(rule, 'params', [])):
228
+ msg = rule.message(display_field, getattr(rule, 'params', []))
229
+ self.error_handler.add_error(display_field, rule.rule_name, msg, value)
230
+ return False
231
+ else:
232
+ return True
233
+ except Exception as e:
234
+ display_field = getattr(self, '_current_actual_path', field)
235
+ self.error_handler.add_error(display_field, rule.rule_name, str(e), value)
236
+ return False
237
+
238
+ def _get_nested_value(self, path: str) -> Any:
239
+ parts = path.split('.')
240
+
241
+ def _get(data: Any, remaining_parts: List[str]) -> Any:
242
+ if not remaining_parts:
243
+ return data
244
+
245
+ part = remaining_parts[0]
246
+ next_parts = remaining_parts[1:]
247
+
248
+ if part == '*':
249
+ if isinstance(data, (list, tuple)):
250
+ return [_get(item, next_parts) for item in data]
251
+ elif isinstance(data, dict):
252
+ return [_get(value, next_parts) for value in data.values()]
253
+ return None
254
+
255
+ if isinstance(data, dict) and part in data:
256
+ return _get(data[part], next_parts)
257
+ elif isinstance(data, (list, tuple)) and part.isdigit():
258
+ index = int(part)
259
+ if 0 <= index < len(data):
260
+ return _get(data[index], next_parts)
261
+
262
+ return None
263
+
264
+ result = _get(self.data, parts)
265
+
266
+ # Flatten only one level for wildcard results
267
+ if isinstance(result, list):
268
+ flat_result = []
269
+ for item in result:
270
+ if isinstance(item, list):
271
+ flat_result.extend(item)
272
+ elif item is not None:
273
+ flat_result.append(item)
274
+ return flat_result if flat_result else None
275
+
276
+ return result
277
+
278
+ def add_rule(self, field: str, rules: Union[str, List[Union[str, ValidationRule]], ValidationRule]):
279
+ """Add new rules to a field"""
280
+ new_rules = self.rule_preparer._convert_to_rules(rules)
281
+ existing_rules = self.rule_preparer._convert_to_rules(self._raw_rules.get(field, []))
282
+ combined_rules = existing_rules + new_rules
283
+
284
+ RuleConflictChecker.check_conflicts(combined_rules)
285
+ self._raw_rules[field] = self.rule_preparer._deduplicate_rules(combined_rules)
286
+
287
+ def set_stop_on_first_failure(self, value: bool) -> None:
288
+ """Set whether to stop validation after first failure"""
289
+ self._stop_on_first_failure = value
290
+
291
+ @property
292
+ def errors(self) -> Dict[str, List[str]]:
293
+ """Get current validation errors"""
294
+ return self.error_handler.errors
295
+
296
+ @property
297
+ def has_errors(self) -> bool:
298
+ """Check if there are any validation errors"""
299
+ return self.error_handler.has_errors
@@ -0,0 +1,5 @@
1
+
2
+ from .detector import DatabaseAutoDetector
3
+ from .manager import DatabaseManager
4
+
5
+ __all__ = ['DatabaseDetector', 'DatabaseManager']