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,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,32 @@
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
+ Requires-Dist: Django==5.2
20
+ Requires-Dist: Flask==3.1.1
21
+ Requires-Dist: mysql-connector-repackaged==0.3.1
22
+ Requires-Dist: odoo==1.0
23
+ Requires-Dist: Pillow==11.2.1
24
+ Requires-Dist: psycopg2==2.9.10
25
+ Requires-Dist: python-dotenv==1.1.1
26
+ Requires-Dist: python-dateutil==2.9.0.post0
27
+ Requires-Dist: setuptools==63.2.0
28
+ Requires-Dist: SQLAlchemy==2.0.41
29
+ Requires-Dist: Werkzeug==3.1.3
30
+
31
+ ## License
32
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
@@ -0,0 +1,29 @@
1
+ validator/__init__.py,sha256=udxDzUicPfxBOAQvzsnl3pHur9VUppKbWMgg35hpiww,244
2
+ validator/exceptions.py,sha256=y2v7CaXmeGFHWcnigtLl4U-sFta_jMiXkGKXWIIVglY,366
3
+ validator/factory.py,sha256=9I_dIq22QxZxCqtCECp1T5K0GeyD_YuGc8xtw4eexMs,794
4
+ validator/core/__init__.py,sha256=ZcqlXJSk03i_CVzmIN-nVe1UOyvwwO5jhbEj7f62Y_o,59
5
+ validator/core/validator.py,sha256=00qVnbH-EJC5KALlaoUBLAfsszAFLcoSxfRbmy0amyk,12751
6
+ validator/database/__init__.py,sha256=vpGhoYt32oiYyJVsoWZkRKBgDZp8bTKeW2jXKVoNKA8,135
7
+ validator/database/detector.py,sha256=mPNZlOgwNBPNwL_XcKSmT6H5Bq370nMkqqMMOqkM9WY,9568
8
+ validator/database/manager.py,sha256=Ezz4NUh22Hz2puh-NJggSGCaw3lAGyp3V88plMeBBVU,6232
9
+ validator/rules/__init__.py,sha256=y1HsA25CIZoNmTDJp9CHspdybsNtrWEiX8w_NNANuGw,963
10
+ validator/rules/array.py,sha256=tx8FCDqn-27Vs7tgtjeoCE9ceDMVrdBEb2-pq-lNLuo,2809
11
+ validator/rules/base.py,sha256=UeUXJlNkBGxKhgNvcamJqoZKTh7HO5v-t0eAAks_MKM,2894
12
+ validator/rules/basic.py,sha256=fMk0s5_IEQu27X1wndP_ocNyOZ1upXJCn7fR9YYbI74,1527
13
+ validator/rules/comparison.py,sha256=9xb2mO5GiThR1iO8867ex2o7olQC2Bnew6MBdD2UtEo,9402
14
+ validator/rules/conditional.py,sha256=8_O1etzCyCGeD8lmfhegJ1uzhkBSjTEVdufbZmkqgP4,12993
15
+ validator/rules/date.py,sha256=18JIKTO5nzFtCnJEMXm4OUbneqTMB7HGPN6jUkxGU4Y,5278
16
+ validator/rules/files.py,sha256=vu_TZFffDPzDojyTsFXSQ6MmQm3WU6ppFfz7-wuDy98,6550
17
+ validator/rules/format.py,sha256=UeeIAkFn0CdzC5bS9tI78_lbPYRgcpaUujxdmyCSdzE,3744
18
+ validator/rules/string.py,sha256=vYzu4ICKY9FCuGahmsQCoJLmnlBF7uNvVazFj9DQ438,3178
19
+ validator/rules/type.py,sha256=Tu-EOBkTtxkcCe0ANXavurZC449n63iE_VXVzc3BIiM,1596
20
+ validator/rules/utilities.py,sha256=AIm9JRGYf6cTCSkivg3gTj3U5DnXlCAJ5ej1yUSa1dU,9724
21
+ validator/services/__init__.py,sha256=zzKTmqL7v4niFGWHJBfWLqgJ0iTaW_69OzYZN8uInzQ,210
22
+ validator/services/rule_conflict.py,sha256=s1RJNUY5d0WtSMHkrKulBCgJ2BZL2GE0Eu5pdAoiIbM,4943
23
+ validator/services/rule_error_handler.py,sha256=MGvvkP6hbZLpVXxC3xpzg15OmVdPlk7l0M2Srmy5VfM,1729
24
+ validator/services/rule_preparer.py,sha256=jRcMNjqq2xyZjO64Pim8jWmja5DmTzf0V_uuHG0lJTg,5621
25
+ safeshield-1.0.0.dist-info/LICENSE,sha256=qugtRyKckyaks6hd2xyxOFSOYM6au1N80pMXuMTPvC4,1090
26
+ safeshield-1.0.0.dist-info/METADATA,sha256=UNfuutZuSxKkc4MTaVOIbVynevAieQqkm88GDyEIT2I,1223
27
+ safeshield-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
28
+ safeshield-1.0.0.dist-info/top_level.txt,sha256=iUtV3dlHOIiMfLuY4pruY00lFni8JzOkQ3Nh1II19OE,10
29
+ safeshield-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ validator
validator/__init__.py ADDED
@@ -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']
@@ -0,0 +1,250 @@
1
+ import os
2
+ import inspect
3
+ import warnings
4
+ from typing import Dict, Any, Optional
5
+ from urllib.parse import urlparse
6
+
7
+ class DatabaseAutoDetector:
8
+ @classmethod
9
+ def detect(cls) -> Optional[Dict[str, Any]]:
10
+ """Main detection method that tries all available detectors"""
11
+ detectors = [
12
+ cls._detect_odoo,
13
+ cls._detect_django,
14
+ cls._detect_flask_env,
15
+ cls._detect_sqlalchemy,
16
+ cls._detect_env_vars
17
+ ]
18
+
19
+ for detector in detectors:
20
+ try:
21
+ config = detector()
22
+ if config:
23
+ if cls._validate_config(config):
24
+ return config
25
+ except Exception as e:
26
+ print(f"Warning: Detector {detector.__name__} failed: {str(e)}")
27
+ continue
28
+
29
+ print("No database configuration detected!")
30
+ return None
31
+
32
+ @classmethod
33
+ def _validate_config(cls, config: Dict[str, Any]) -> bool:
34
+ """Validate the detected configuration for all database types"""
35
+ required_keys = ['type', 'host', 'database']
36
+
37
+ # Daftar tipe database yang membutuhkan username
38
+ requires_username = [
39
+ 'postgresql', 'mysql', 'mariadb', 'mssql',
40
+ 'oracle', 'cockroachdb', 'redshift'
41
+ ]
42
+
43
+ # SQLite adalah kasus khusus
44
+ if config.get('type') == 'sqlite':
45
+ return 'database' in config
46
+
47
+ # Validasi untuk database lainnya
48
+ if not all(key in config for key in required_keys):
49
+ return False
50
+
51
+ # Periksa username untuk database yang membutuhkannya
52
+ if config['type'] in requires_username and 'username' not in config:
53
+ return False
54
+
55
+ return True
56
+
57
+ @classmethod
58
+ def _detect_odoo(cls) -> Optional[Dict[str, Any]]:
59
+ """Detect Odoo database configuration"""
60
+ try:
61
+ from odoo.tools import config
62
+ return {
63
+ 'type': 'postgresql', # Odoo selalu menggunakan PostgreSQL
64
+ 'host': config['db_host'] or 'localhost',
65
+ 'port': str(config['db_port'] or 5432),
66
+ 'username': config['db_user'] or 'odoo',
67
+ 'password': config['db_password'] or '',
68
+ 'database': config['db_name']
69
+ }
70
+ except ImportError:
71
+ try:
72
+ # Cara 2: Deteksi melalui environment variables Odoo
73
+ db_name = os.getenv('ODOO_DATABASE') or os.getenv('DB_NAME')
74
+ if db_name:
75
+ return {
76
+ 'type': 'postgresql',
77
+ 'host': os.getenv('DB_HOST', 'localhost'),
78
+ 'port': os.getenv('DB_PORT', '5432'),
79
+ 'username': os.getenv('DB_USER', 'odoo'),
80
+ 'password': os.getenv('DB_PASSWORD', ''),
81
+ 'database': db_name
82
+ }
83
+ except Exception:
84
+ pass
85
+ return None
86
+
87
+ @classmethod
88
+ def _detect_django(cls) -> Optional[Dict[str, Any]]:
89
+ try:
90
+ from django.conf import settings
91
+ db = settings.DATABASES['default']
92
+ engine = db['ENGINE'].split('.')[-1]
93
+
94
+ engine_map = {
95
+ 'postgresql': 'postgresql',
96
+ 'mysql': 'mysql',
97
+ 'sqlite3': 'sqlite'
98
+ }
99
+
100
+ return {
101
+ 'type': engine_map.get(engine, engine),
102
+ 'host': db.get('HOST', 'localhost'),
103
+ 'port': db.get('PORT', '5432' if engine == 'postgresql' else '3306'),
104
+ 'username': db['USER'],
105
+ 'password': db['PASSWORD'],
106
+ 'database': db['NAME']
107
+ }
108
+ except ImportError:
109
+ return None
110
+ except Exception: # Tangkap error spesifik Django
111
+ return None
112
+
113
+ @classmethod
114
+ def _detect_sqlalchemy(cls) -> Optional[Dict[str, Any]]:
115
+ try:
116
+ from sqlalchemy import create_engine
117
+ from sqlalchemy.engine.url import make_url
118
+
119
+ # Cari engine SQLAlchemy di stack frame
120
+ for frame in inspect.stack():
121
+ for val in frame.frame.f_locals.values():
122
+ if isinstance(val, type(create_engine('sqlite://'))): # type: ignore
123
+ url = make_url(str(val.url))
124
+ return {
125
+ 'type': url.drivername.split('+')[0],
126
+ 'host': url.host or 'localhost',
127
+ 'port': str(url.port) if url.port else '5432' if 'postgres' in url.drivername else '3306',
128
+ 'username': url.username,
129
+ 'password': url.password or '',
130
+ 'database': url.database
131
+ }
132
+ return None
133
+ except ImportError:
134
+ return None
135
+
136
+ @classmethod
137
+ def _detect_env_vars(cls) -> Optional[Dict[str, Any]]:
138
+ from dotenv import load_dotenv
139
+ load_dotenv()
140
+
141
+ standard_vars = {
142
+ 'DB_TYPE': os.getenv('DB_TYPE'),
143
+ 'DB_HOST': os.getenv('DB_HOST'),
144
+ 'DB_USER': os.getenv('DB_USER'),
145
+ 'DB_PASSWORD': os.getenv('DB_PASSWORD'),
146
+ 'DB_NAME': os.getenv('DB_NAME')
147
+ }
148
+
149
+ if any(standard_vars.values()):
150
+ return {
151
+ 'type': standard_vars['DB_TYPE'] or 'mysql',
152
+ 'host': standard_vars['DB_HOST'] or 'localhost',
153
+ 'username': standard_vars['DB_USER'] or 'root',
154
+ 'password': standard_vars['DB_PASSWORD'] or '',
155
+ 'database': standard_vars['DB_NAME'] or ''
156
+ }
157
+ return None
158
+
159
+ @classmethod
160
+ def _detect_flask_env(cls) -> Optional[Dict[str, Any]]:
161
+ """Deteksi konfigurasi database dari Flask app"""
162
+ try:
163
+ from flask import current_app
164
+ if not current_app or not hasattr(current_app, 'config'):
165
+ return None
166
+
167
+ # Handle Flask-SQLAlchemy
168
+ if 'SQLALCHEMY_DATABASE_URI' in current_app.config:
169
+ uri = current_app.config['SQLALCHEMY_DATABASE_URI']
170
+ return cls._parse_sqlalchemy_uri(uri)
171
+
172
+ # Handle Flask default database config
173
+ elif 'DATABASE_URL' in current_app.config:
174
+ uri = current_app.config['DATABASE_URL']
175
+ return cls._parse_sqlalchemy_uri(uri)
176
+
177
+ # Handle Flask-MySQLdb
178
+ elif 'MYSQL_DATABASE_HOST' in current_app.config:
179
+ return {
180
+ 'type': 'mysql',
181
+ 'host': current_app.config.get('MYSQL_DATABASE_HOST', 'localhost'),
182
+ 'port': current_app.config.get('MYSQL_DATABASE_PORT', 3306),
183
+ 'username': current_app.config.get('MYSQL_DATABASE_USER', 'root'),
184
+ 'password': current_app.config.get('MYSQL_DATABASE_PASSWORD', ''),
185
+ 'database': current_app.config.get('MYSQL_DATABASE_DB', '')
186
+ }
187
+ return None
188
+ except (ImportError, RuntimeError):
189
+ return None
190
+
191
+ @classmethod
192
+ def _parse_sqlalchemy_uri(cls, uri: str) -> Dict[str, Any]:
193
+ """Parse SQLAlchemy URI format"""
194
+ from urllib.parse import urlparse
195
+ parsed = urlparse(uri)
196
+
197
+ # Mapping database types
198
+ db_type_map = {
199
+ 'postgres': 'postgresql',
200
+ 'postgresql': 'postgresql',
201
+ 'mysql': 'mysql',
202
+ 'sqlite': 'sqlite',
203
+ 'mssql': 'mssql'
204
+ }
205
+
206
+ db_type = db_type_map.get(parsed.scheme.split('+')[0], parsed.scheme)
207
+
208
+ config = {
209
+ 'type': db_type,
210
+ 'host': parsed.hostname or 'localhost',
211
+ 'port': parsed.port or (5432 if db_type == 'postgresql' else 3306),
212
+ 'username': parsed.username or '',
213
+ 'password': parsed.password or '',
214
+ 'database': parsed.path[1:] if parsed.path else ''
215
+ }
216
+
217
+ # Handle SQLite special case
218
+ if db_type == 'sqlite':
219
+ config['database'] = parsed.path # Full path termasuk leading slash
220
+
221
+ return config
222
+
223
+ @classmethod
224
+ def _parse_rails_env_vars(cls) -> Optional[Dict[str, Any]]:
225
+ """Parse Rails-style ENV variables"""
226
+ def get_env(key, default=None):
227
+ return os.getenv(key) or os.getenv(f'DATABASE_{key}', default)
228
+
229
+ adapter = get_env('ADAPTER')
230
+ if not adapter:
231
+ return None
232
+
233
+ # Mapping adapter Rails
234
+ adapter_map = {
235
+ 'postgresql': 'postgresql',
236
+ 'mysql2': 'mysql',
237
+ 'sqlite3': 'sqlite'
238
+ }
239
+
240
+ db_type = adapter_map.get(adapter, adapter)
241
+
242
+ return {
243
+ 'type': db_type,
244
+ 'host': get_env('HOST', 'localhost'),
245
+ 'port': int(get_env('PORT', 5432 if db_type == 'postgresql' else 3306)),
246
+ 'username': get_env('USERNAME', 'root'),
247
+ 'password': get_env('PASSWORD', ''),
248
+ 'database': get_env('DATABASE', ''),
249
+ 'timeout': int(get_env('TIMEOUT', '5000'))
250
+ }