dash-devtools 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.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,338 @@
1
+ """
2
+ 資料庫遷移管理模組
3
+
4
+ 支援 Alembic + SQLModel 的自動遷移管理
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+
14
+ def _run_alembic(project: str, args: list[str], capture: bool = True) -> dict:
15
+ """執行 Alembic 指令
16
+
17
+ Args:
18
+ project: 專案路徑
19
+ args: Alembic 參數
20
+ capture: 是否擷取輸出
21
+
22
+ Returns:
23
+ dict 包含 success, stdout, stderr
24
+ """
25
+ project_path = Path(project).resolve()
26
+
27
+ # 檢查是否有 alembic.ini
28
+ alembic_ini = project_path / 'alembic.ini'
29
+ if not alembic_ini.exists():
30
+ return {
31
+ 'success': False,
32
+ 'error': '找不到 alembic.ini,請先執行 dash db init'
33
+ }
34
+
35
+ try:
36
+ result = subprocess.run(
37
+ ['alembic'] + args,
38
+ cwd=str(project_path),
39
+ capture_output=capture,
40
+ text=True,
41
+ timeout=60
42
+ )
43
+
44
+ return {
45
+ 'success': result.returncode == 0,
46
+ 'stdout': result.stdout if capture else '',
47
+ 'stderr': result.stderr if capture else '',
48
+ 'returncode': result.returncode
49
+ }
50
+ except FileNotFoundError:
51
+ return {
52
+ 'success': False,
53
+ 'error': '找不到 alembic 指令,請安裝: pip install alembic'
54
+ }
55
+ except subprocess.TimeoutExpired:
56
+ return {
57
+ 'success': False,
58
+ 'error': '指令執行逾時'
59
+ }
60
+ except Exception as e:
61
+ return {
62
+ 'success': False,
63
+ 'error': str(e)
64
+ }
65
+
66
+
67
+ def init_alembic(project: str) -> dict:
68
+ """初始化 Alembic 環境
69
+
70
+ Args:
71
+ project: 專案路徑
72
+
73
+ Returns:
74
+ dict 包含 success, alembic_dir 或 error
75
+ """
76
+ project_path = Path(project).resolve()
77
+ alembic_dir = project_path / 'alembic'
78
+ alembic_ini = project_path / 'alembic.ini'
79
+
80
+ # 檢查是否已初始化
81
+ if alembic_ini.exists():
82
+ return {
83
+ 'success': False,
84
+ 'error': 'Alembic 已初始化 (alembic.ini 已存在)'
85
+ }
86
+
87
+ # 執行 alembic init
88
+ try:
89
+ result = subprocess.run(
90
+ ['alembic', 'init', 'alembic'],
91
+ cwd=str(project_path),
92
+ capture_output=True,
93
+ text=True,
94
+ timeout=30
95
+ )
96
+
97
+ if result.returncode != 0:
98
+ return {
99
+ 'success': False,
100
+ 'error': result.stderr or '初始化失敗'
101
+ }
102
+
103
+ return {
104
+ 'success': True,
105
+ 'alembic_dir': str(alembic_dir)
106
+ }
107
+ except FileNotFoundError:
108
+ return {
109
+ 'success': False,
110
+ 'error': '找不到 alembic 指令,請安裝: pip install alembic'
111
+ }
112
+ except Exception as e:
113
+ return {
114
+ 'success': False,
115
+ 'error': str(e)
116
+ }
117
+
118
+
119
+ def get_migration_status(project: str) -> dict:
120
+ """取得遷移狀態
121
+
122
+ Args:
123
+ project: 專案路徑
124
+
125
+ Returns:
126
+ dict 包含 success, current, head, pending 或 error
127
+ """
128
+ project_path = Path(project).resolve()
129
+
130
+ # 取得目前版本
131
+ current_result = _run_alembic(project, ['current'])
132
+ if not current_result['success']:
133
+ # 如果沒有 DATABASE_URL,嘗試讀取本地狀態
134
+ if 'DATABASE_URL' in current_result.get('stderr', ''):
135
+ return {
136
+ 'success': False,
137
+ 'error': 'DATABASE_URL 環境變數未設定'
138
+ }
139
+ return current_result
140
+
141
+ current = current_result['stdout'].strip()
142
+ if not current:
143
+ current = '(尚未套用任何遷移)'
144
+
145
+ # 取得最新版本
146
+ head_result = _run_alembic(project, ['heads'])
147
+ head = head_result['stdout'].strip() if head_result['success'] else 'N/A'
148
+
149
+ # 取得待套用的遷移
150
+ history_result = _run_alembic(project, ['history', '--indicate-current'])
151
+ pending = []
152
+ if history_result['success']:
153
+ lines = history_result['stdout'].strip().split('\n')
154
+ in_pending = True
155
+ for line in lines:
156
+ if '(current)' in line or '(head)' in line:
157
+ in_pending = False
158
+ elif in_pending and line.strip() and '->' in line:
159
+ pending.append(line.strip())
160
+
161
+ return {
162
+ 'success': True,
163
+ 'current': current,
164
+ 'head': head,
165
+ 'pending': pending
166
+ }
167
+
168
+
169
+ def generate_migration(project: str, message: str, autogenerate: bool = True) -> dict:
170
+ """產生新的遷移檔
171
+
172
+ Args:
173
+ project: 專案路徑
174
+ message: 遷移訊息
175
+ autogenerate: 是否自動偵測變更
176
+
177
+ Returns:
178
+ dict 包含 success, migration_file, warnings 或 error
179
+ """
180
+ args = ['revision', '-m', message]
181
+ if autogenerate:
182
+ args.append('--autogenerate')
183
+
184
+ result = _run_alembic(project, args)
185
+
186
+ if not result['success']:
187
+ return {
188
+ 'success': False,
189
+ 'error': result.get('stderr') or result.get('error', '產生失敗')
190
+ }
191
+
192
+ # 解析輸出取得檔案路徑
193
+ output = result['stdout']
194
+ migration_file = None
195
+
196
+ # 尋找產生的檔案路徑
197
+ match = re.search(r'Generating (.+\.py)', output)
198
+ if match:
199
+ migration_file = match.group(1)
200
+
201
+ # 檢查危險操作
202
+ warnings = []
203
+ if migration_file and Path(migration_file).exists():
204
+ content = Path(migration_file).read_text()
205
+ dangerous_patterns = [
206
+ (r'\bop\.drop_table\b', 'DROP TABLE 操作'),
207
+ (r'\bop\.drop_column\b', 'DROP COLUMN 操作'),
208
+ (r'\bop\.drop_index\b', 'DROP INDEX 操作'),
209
+ ]
210
+ for pattern, desc in dangerous_patterns:
211
+ if re.search(pattern, content, re.IGNORECASE):
212
+ warnings.append(desc)
213
+
214
+ return {
215
+ 'success': True,
216
+ 'migration_file': migration_file,
217
+ 'warnings': warnings
218
+ }
219
+
220
+
221
+ def run_upgrade(project: str, revision: str = 'head', dry_run: bool = False) -> dict:
222
+ """升級資料庫
223
+
224
+ Args:
225
+ project: 專案路徑
226
+ revision: 目標版本
227
+ dry_run: 預覽模式
228
+
229
+ Returns:
230
+ dict 包含 success, current, sql 或 error
231
+ """
232
+ if dry_run:
233
+ # SQL 預覽模式
234
+ result = _run_alembic(project, ['upgrade', revision, '--sql'])
235
+ if result['success']:
236
+ return {
237
+ 'success': True,
238
+ 'sql': result['stdout']
239
+ }
240
+ return {
241
+ 'success': False,
242
+ 'error': result.get('stderr') or result.get('error', '預覽失敗')
243
+ }
244
+
245
+ # 實際執行升級
246
+ result = _run_alembic(project, ['upgrade', revision])
247
+
248
+ if not result['success']:
249
+ return {
250
+ 'success': False,
251
+ 'error': result.get('stderr') or result.get('error', '升級失敗')
252
+ }
253
+
254
+ # 取得目前版本
255
+ status = get_migration_status(project)
256
+
257
+ return {
258
+ 'success': True,
259
+ 'current': status.get('current', 'N/A')
260
+ }
261
+
262
+
263
+ def run_downgrade(project: str, revision: str) -> dict:
264
+ """降級資料庫
265
+
266
+ Args:
267
+ project: 專案路徑
268
+ revision: 目標版本 (可用 -1 表示降一個版本)
269
+
270
+ Returns:
271
+ dict 包含 success, current 或 error
272
+ """
273
+ result = _run_alembic(project, ['downgrade', revision])
274
+
275
+ if not result['success']:
276
+ return {
277
+ 'success': False,
278
+ 'error': result.get('stderr') or result.get('error', '降級失敗')
279
+ }
280
+
281
+ # 取得目前版本
282
+ status = get_migration_status(project)
283
+
284
+ return {
285
+ 'success': True,
286
+ 'current': status.get('current', 'N/A')
287
+ }
288
+
289
+
290
+ def check_migration_sync(project: str) -> dict:
291
+ """檢查 Model 與遷移是否同步
292
+
293
+ 用於驗證器,檢查是否有未產生遷移的 Model 變更
294
+
295
+ Args:
296
+ project: 專案路徑
297
+
298
+ Returns:
299
+ dict 包含 success, synced, details
300
+ """
301
+ project_path = Path(project).resolve()
302
+
303
+ # 檢查是否有 alembic
304
+ if not (project_path / 'alembic.ini').exists():
305
+ return {
306
+ 'success': True,
307
+ 'synced': None,
308
+ 'details': 'Alembic 未初始化'
309
+ }
310
+
311
+ # 嘗試產生遷移預覽
312
+ result = _run_alembic(project, [
313
+ 'check' # Alembic 1.9+ 的新功能
314
+ ])
315
+
316
+ # alembic check 不存在時,改用其他方式
317
+ if 'No such command' in result.get('stderr', ''):
318
+ # 舊版 Alembic,使用 revision --autogenerate 加 --dry-run
319
+ # 這會比較複雜,先簡單回傳
320
+ return {
321
+ 'success': True,
322
+ 'synced': None,
323
+ 'details': 'Alembic 版本過舊,無法自動檢查'
324
+ }
325
+
326
+ if result['success']:
327
+ return {
328
+ 'success': True,
329
+ 'synced': True,
330
+ 'details': 'Model 與遷移已同步'
331
+ }
332
+ else:
333
+ # 有差異
334
+ return {
335
+ 'success': True,
336
+ 'synced': False,
337
+ 'details': result.get('stderr', 'Model 與遷移不同步')
338
+ }
@@ -0,0 +1,183 @@
1
+ """
2
+ dbdiagram.io 整合工具
3
+
4
+ 自動產生資料庫圖表連結,供外包廠商查看。
5
+
6
+ 支援:
7
+ - Prisma schema → DBML → dbdiagram.io 連結
8
+ - DBML 檔案直接轉換
9
+ """
10
+
11
+ import base64
12
+ import urllib.parse
13
+ from pathlib import Path
14
+ import subprocess
15
+
16
+
17
+ def encode_dbml_to_link(dbml_content: str) -> str:
18
+ """將 DBML 編碼為 dbdiagram.io 分享連結
19
+
20
+ 編碼步驟:
21
+ 1. UTF-8 編碼
22
+ 2. Base64 轉換
23
+ 3. URL 編碼
24
+
25
+ Args:
26
+ dbml_content: DBML 文字內容
27
+
28
+ Returns:
29
+ dbdiagram.io 分享連結
30
+ """
31
+ # 移除自動產生的註解(節省 URL 長度)
32
+ clean_dbml = '\n'.join(
33
+ line for line in dbml_content.split('\n')
34
+ if not line.startswith('////')
35
+ ).strip()
36
+
37
+ # UTF-8 → Base64 → URL encode
38
+ base64_bytes = base64.b64encode(clean_dbml.encode('utf-8'))
39
+ encoded = urllib.parse.quote(base64_bytes.decode('ascii'))
40
+
41
+ return f'https://dbdiagram.io/d?c={encoded}'
42
+
43
+
44
+ def find_prisma_schema(project_path: str) -> Path | None:
45
+ """尋找專案中的 Prisma schema 檔案"""
46
+ project = Path(project_path)
47
+
48
+ # 常見位置
49
+ possible_paths = [
50
+ project / 'prisma' / 'schema.prisma',
51
+ project / 'schema.prisma',
52
+ ]
53
+
54
+ for path in possible_paths:
55
+ if path.exists():
56
+ return path
57
+
58
+ return None
59
+
60
+
61
+ def find_dbml_file(project_path: str) -> Path | None:
62
+ """尋找專案中的 DBML 檔案"""
63
+ project = Path(project_path)
64
+
65
+ # 常見位置
66
+ possible_paths = [
67
+ project / 'docs' / 'schema.dbml',
68
+ project / 'prisma' / 'dbml' / 'schema.dbml',
69
+ project / 'schema.dbml',
70
+ ]
71
+
72
+ for path in possible_paths:
73
+ if path.exists():
74
+ return path
75
+
76
+ return None
77
+
78
+
79
+ def generate_dbml_from_prisma(project_path: str) -> dict:
80
+ """從 Prisma schema 產生 DBML
81
+
82
+ 需要先安裝 prisma-dbml-generator:
83
+ npm install -D prisma-dbml-generator
84
+
85
+ 並在 schema.prisma 加入:
86
+ generator dbml {
87
+ provider = "prisma-dbml-generator"
88
+ output = "../docs"
89
+ outputName = "schema.dbml"
90
+ }
91
+
92
+ Returns:
93
+ {'success': bool, 'dbml_path': str, 'error': str}
94
+ """
95
+ project = Path(project_path)
96
+ schema_path = find_prisma_schema(project_path)
97
+
98
+ if not schema_path:
99
+ return {'success': False, 'error': '找不到 schema.prisma'}
100
+
101
+ # 檢查是否有 generator dbml
102
+ schema_content = schema_path.read_text()
103
+ if 'prisma-dbml-generator' not in schema_content:
104
+ return {
105
+ 'success': False,
106
+ 'error': '請在 schema.prisma 加入 generator dbml { provider = "prisma-dbml-generator" }'
107
+ }
108
+
109
+ # 執行 prisma generate
110
+ try:
111
+ result = subprocess.run(
112
+ ['npx', 'prisma', 'generate'],
113
+ cwd=project,
114
+ capture_output=True,
115
+ text=True
116
+ )
117
+
118
+ if result.returncode != 0:
119
+ return {'success': False, 'error': result.stderr}
120
+
121
+ # 尋找產生的 DBML 檔案
122
+ dbml_path = find_dbml_file(project_path)
123
+ if dbml_path:
124
+ return {'success': True, 'dbml_path': str(dbml_path)}
125
+ else:
126
+ return {'success': False, 'error': '產生失敗,找不到 DBML 檔案'}
127
+
128
+ except FileNotFoundError:
129
+ return {'success': False, 'error': '找不到 npx,請確認已安裝 Node.js'}
130
+
131
+
132
+ def generate_dbdiagram_link(project_path: str) -> dict:
133
+ """產生 dbdiagram.io 連結
134
+
135
+ 流程:
136
+ 1. 尋找現有的 DBML 檔案
137
+ 2. 若無,嘗試從 Prisma schema 產生
138
+ 3. 編碼為 dbdiagram.io 連結
139
+
140
+ Returns:
141
+ {
142
+ 'success': bool,
143
+ 'link': str,
144
+ 'embed_link': str,
145
+ 'source': str, # 'dbml' or 'prisma'
146
+ 'error': str
147
+ }
148
+ """
149
+ # 尋找現有的 DBML
150
+ dbml_path = find_dbml_file(project_path)
151
+
152
+ if not dbml_path:
153
+ # 嘗試從 Prisma 產生
154
+ gen_result = generate_dbml_from_prisma(project_path)
155
+ if not gen_result['success']:
156
+ return {'success': False, 'error': gen_result['error']}
157
+ dbml_path = Path(gen_result['dbml_path'])
158
+
159
+ # 讀取 DBML
160
+ try:
161
+ dbml_content = dbml_path.read_text()
162
+ except Exception as e:
163
+ return {'success': False, 'error': f'讀取 DBML 失敗: {e}'}
164
+
165
+ # 編碼為連結
166
+ link = encode_dbml_to_link(dbml_content)
167
+ embed_link = link.replace('/d?', '/embed?')
168
+
169
+ return {
170
+ 'success': True,
171
+ 'link': link,
172
+ 'embed_link': embed_link,
173
+ 'source': 'dbml',
174
+ 'dbml_path': str(dbml_path)
175
+ }
176
+
177
+
178
+ def save_link_to_file(project_path: str, link: str) -> str:
179
+ """儲存連結到檔案"""
180
+ output_path = Path(project_path) / 'docs' / 'dbdiagram-link.txt'
181
+ output_path.parent.mkdir(parents=True, exist_ok=True)
182
+ output_path.write_text(link)
183
+ return str(output_path)