xforge 0.4.3__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.
- scripts/export_dsl.py +60 -0
- scripts/generate_parquet_sql.py +195 -0
- scripts/generate_rule_manual.py +1091 -0
- scripts/import_rules.py +103 -0
- scripts/import_sql.py +167 -0
- scripts/import_sql_files.py +126 -0
- scripts/rewrite_sql_duckdb.py +271 -0
- scripts/run_center.py +286 -0
- src/__init__.py +0 -0
- src/app.py +22 -0
- src/config.py +119 -0
- src/database/__init__.py +0 -0
- src/database/connection.py +57 -0
- src/database/migrate.py +29 -0
- src/models/__init__.py +0 -0
- src/models/alert.py +161 -0
- src/models/analytics.py +218 -0
- src/models/data_source.py +78 -0
- src/models/execution.py +142 -0
- src/models/model_category.py +63 -0
- src/models/rule.py +380 -0
- src/models/rule_version.py +69 -0
- src/services/__init__.py +0 -0
- src/services/execution_service.py +218 -0
- src/services/export_service.py +75 -0
- src/services/import_service.py +151 -0
- src/services/sql_adapter.py +440 -0
- src/tui/app.py +216 -0
- src/tui/screens/__init__.py +0 -0
- src/tui/screens/alert_detail.py +194 -0
- src/tui/screens/execution_detail.py +131 -0
- src/tui/screens/help_screen.py +85 -0
- src/tui/screens/import_screen.py +67 -0
- src/tui/screens/rule_detail.py +178 -0
- src/tui/screens/rule_edit.py +179 -0
- src/tui/startup_check.py +128 -0
- src/tui/tabs/__init__.py +0 -0
- src/tui/tabs/alert.py +315 -0
- src/tui/tabs/analytics.py +256 -0
- src/tui/tabs/approval.py +170 -0
- src/tui/tabs/dashboard.py +174 -0
- src/tui/tabs/execution.py +234 -0
- src/tui/tabs/query.py +544 -0
- src/tui/tabs/settings.py +314 -0
- src/tui/widgets/__init__.py +0 -0
- src/utils/__init__.py +0 -0
- src/utils/csv_importer.py +228 -0
- src/utils/dsl_generator.py +223 -0
- src/utils/excel_parser.py +290 -0
- xforge-0.4.3.dist-info/METADATA +10 -0
- xforge-0.4.3.dist-info/RECORD +54 -0
- xforge-0.4.3.dist-info/WHEEL +5 -0
- xforge-0.4.3.dist-info/entry_points.txt +5 -0
- xforge-0.4.3.dist-info/top_level.txt +2 -0
scripts/export_dsl.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""一键导出:SQLite 规则 → regula DSL JSON 文件。
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python scripts/export_dsl.py
|
|
6
|
+
python scripts/export_dsl.py --model 靠企吃企
|
|
7
|
+
python scripts/export_dsl.py --status approved
|
|
8
|
+
python scripts/export_dsl.py --rule-id 1 --rule-id 2
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
19
|
+
|
|
20
|
+
from src.database.connection import init_db
|
|
21
|
+
from src.services.export_service import export_to_dsl
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
import argparse
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
description="导出规则 → regula DSL JSON"
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--model", "-m", type=str, help="按模型筛选")
|
|
31
|
+
parser.add_argument("--status", "-s", type=str, help="按状态筛选")
|
|
32
|
+
parser.add_argument("--rule-id", type=int, action="append", dest="rule_ids",
|
|
33
|
+
help="指定规则 ID(可重复)")
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
init_db()
|
|
37
|
+
|
|
38
|
+
result = export_to_dsl(
|
|
39
|
+
rule_ids=args.rule_ids,
|
|
40
|
+
model=args.model,
|
|
41
|
+
status=args.status,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
print(f"[export] 完成: 生成 {result.generated}, 失败 {result.failed}")
|
|
45
|
+
if result.files:
|
|
46
|
+
print(f"[export] 输出目录: {result.files[0].parent}")
|
|
47
|
+
for f in result.files[:10]:
|
|
48
|
+
print(f" - {f.name}")
|
|
49
|
+
if len(result.files) > 10:
|
|
50
|
+
print(f" ... 共 {len(result.files)} 个文件")
|
|
51
|
+
|
|
52
|
+
if result.errors:
|
|
53
|
+
print(f"\n[export] 错误 ({len(result.errors)}):")
|
|
54
|
+
for err in result.errors[:10]:
|
|
55
|
+
print(f" - {err}")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""从 rules_dsl/sql/*.sql 生成 parquet 直查版 SQL → rules_dsl/sql/parquet/
|
|
3
|
+
|
|
4
|
+
转换规则:
|
|
5
|
+
- ml_kqcq_zzsfp: 拼音别名 → 中文 parquet 列名 (read_parquet)
|
|
6
|
+
- ml_cd_company / ml_base_fdjjr / ml_base_fdjjr_bx: 列名不变
|
|
7
|
+
- 保留 DuckDB 语法 (string_agg, regexp_matches, any_value 等)
|
|
8
|
+
- 跳过注释行,避免旧代码块被误转换
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
18
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
20
|
+
|
|
21
|
+
import polars as pl
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from src.services.sql_adapter import build_column_map
|
|
25
|
+
|
|
26
|
+
SQL_DIR = _PROJECT_ROOT / "rules_dsl" / "sql"
|
|
27
|
+
PARQUET_OUT_DIR = SQL_DIR / "parquet"
|
|
28
|
+
CONFIG_PATH = _PROJECT_ROOT / "config.yaml"
|
|
29
|
+
|
|
30
|
+
# 补充修正:build_column_map 模糊匹配会误伤的列
|
|
31
|
+
# parquet 列名 → 正确拼音别名(覆盖 build_column_map 的错误结果)
|
|
32
|
+
_ALIAS_FIXES: dict[str, dict[str, str]] = {
|
|
33
|
+
"ml_kqcq_zzsfp": {
|
|
34
|
+
"发票代码": "fpdm", # 发票代码,模糊匹配错映射到 fphm(发票号码)
|
|
35
|
+
"单位": "dw", # 计量单位,模糊匹配错映射到 xfdwmc(销方名称)
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_config() -> dict:
|
|
41
|
+
with open(CONFIG_PATH, encoding="utf-8") as f:
|
|
42
|
+
return yaml.safe_load(f) or {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_subquery(table_name: str, parquet_path: str) -> str:
|
|
46
|
+
"""为一张表生成 read_parquet 子查询,保证别名唯一。
|
|
47
|
+
|
|
48
|
+
返回: 子查询 SQL 文本
|
|
49
|
+
格式:
|
|
50
|
+
(SELECT "开票日期" AS kpsj, ... FROM read_parquet('/path'))
|
|
51
|
+
"""
|
|
52
|
+
pq_path = str(Path(parquet_path).resolve())
|
|
53
|
+
df = pl.read_parquet(pq_path)
|
|
54
|
+
parquet_cols = list(df.columns)
|
|
55
|
+
fixes = _ALIAS_FIXES.get(table_name, {})
|
|
56
|
+
|
|
57
|
+
select_parts = []
|
|
58
|
+
used_aliases: set[str] = set()
|
|
59
|
+
dup_count: dict[str, int] = {}
|
|
60
|
+
|
|
61
|
+
if table_name == "ml_kqcq_zzsfp":
|
|
62
|
+
# 直接用 sql_adapter.build_column_map(读取 _MAPPING_FILE + 手动修正 + 模糊匹配)
|
|
63
|
+
col_map = build_column_map(table_name, parquet_cols)
|
|
64
|
+
|
|
65
|
+
for pc in parquet_cols:
|
|
66
|
+
# 覆盖修正优先
|
|
67
|
+
if pc in fixes:
|
|
68
|
+
alias = fixes[pc]
|
|
69
|
+
else:
|
|
70
|
+
alias = col_map.get(pc, pc)
|
|
71
|
+
|
|
72
|
+
# 去重:同一别名出现多次时,第一个保留,后续加 _2, _3...
|
|
73
|
+
if alias in used_aliases:
|
|
74
|
+
dup_count[alias] = dup_count.get(alias, 1) + 1
|
|
75
|
+
alias = f"{alias}_{dup_count[alias]}"
|
|
76
|
+
used_aliases.add(alias)
|
|
77
|
+
|
|
78
|
+
select_parts.append(f' "{pc}" AS {alias}')
|
|
79
|
+
else:
|
|
80
|
+
for pc in parquet_cols:
|
|
81
|
+
select_parts.append(f' "{pc}" AS "{pc}"')
|
|
82
|
+
|
|
83
|
+
col_list = ",\n".join(select_parts)
|
|
84
|
+
return (
|
|
85
|
+
f"(\n"
|
|
86
|
+
f" SELECT\n"
|
|
87
|
+
f"{col_list}\n"
|
|
88
|
+
f" FROM read_parquet('{pq_path}')\n"
|
|
89
|
+
f")"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_comment_line(line: str) -> bool:
|
|
94
|
+
"""判断是否为纯注释行。"""
|
|
95
|
+
stripped = line.strip()
|
|
96
|
+
return stripped.startswith('--') or stripped == ''
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def replace_table_refs(sql: str, data_bindings: dict[str, str]) -> str:
|
|
100
|
+
"""替换 SQL 中所有表引用为 read_parquet 子查询(跳过注释行)。"""
|
|
101
|
+
lines = sql.split('\n')
|
|
102
|
+
result_lines: list[str] = []
|
|
103
|
+
|
|
104
|
+
for line in lines:
|
|
105
|
+
if _is_comment_line(line):
|
|
106
|
+
result_lines.append(line)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
modified = line
|
|
110
|
+
for table_name, pq_path in sorted(
|
|
111
|
+
data_bindings.items(), key=lambda x: -len(x[0])
|
|
112
|
+
):
|
|
113
|
+
subquery = build_subquery(table_name, pq_path)
|
|
114
|
+
|
|
115
|
+
# 模式: (FROM|JOIN) table_name alias → 替换
|
|
116
|
+
pattern = re.compile(
|
|
117
|
+
rf'\b((?:FROM|JOIN)\s+){re.escape(table_name)}\s+(\w+)\b',
|
|
118
|
+
re.IGNORECASE,
|
|
119
|
+
)
|
|
120
|
+
modified = pattern.sub(
|
|
121
|
+
lambda m, sq=subquery: (
|
|
122
|
+
f"{m.group(1)}{sq} AS {m.group(2)}"
|
|
123
|
+
),
|
|
124
|
+
modified,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 模式: FROM table_name 无别名 (CTE 内等) → 自动生成别名
|
|
128
|
+
pattern_no_alias = re.compile(
|
|
129
|
+
rf'\b(FROM\s+){re.escape(table_name)}\b(?!\s+\w)',
|
|
130
|
+
re.IGNORECASE,
|
|
131
|
+
)
|
|
132
|
+
modified = pattern_no_alias.sub(
|
|
133
|
+
lambda m, sq=subquery, tn=table_name: (
|
|
134
|
+
f"{m.group(1)}{sq} AS _{tn}"
|
|
135
|
+
),
|
|
136
|
+
modified,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
result_lines.append(modified)
|
|
140
|
+
|
|
141
|
+
return '\n'.join(result_lines)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _strip_comments(sql: str) -> str:
|
|
145
|
+
"""去掉 SQL 注释行,返回纯净的 SQL 语句。"""
|
|
146
|
+
lines = []
|
|
147
|
+
for line in sql.split('\n'):
|
|
148
|
+
stripped = line.strip()
|
|
149
|
+
if stripped.startswith('--'):
|
|
150
|
+
continue
|
|
151
|
+
lines.append(line)
|
|
152
|
+
return '\n'.join(lines).strip().rstrip(';')
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main() -> None:
|
|
156
|
+
config = load_config()
|
|
157
|
+
data_bindings = config.get("data_bindings", {})
|
|
158
|
+
if not data_bindings:
|
|
159
|
+
print("[generate] config.yaml 中无 data_bindings 配置,退出")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
PARQUET_OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
sql_files = sorted(SQL_DIR.glob("*.sql"))
|
|
165
|
+
generated = 0
|
|
166
|
+
skipped = 0
|
|
167
|
+
|
|
168
|
+
for fp in sql_files:
|
|
169
|
+
sql_text = fp.read_text(encoding="utf-8").strip()
|
|
170
|
+
|
|
171
|
+
# 跳过 TODO 占位文件
|
|
172
|
+
sql_no_comments = _strip_comments(sql_text)
|
|
173
|
+
if sql_no_comments in ("SELECT 1", "SELECT 1;", "SELECT 1;"):
|
|
174
|
+
skipped += 1
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# 生成 parquet 版
|
|
178
|
+
header = (
|
|
179
|
+
f"-- Parquet 直查版: {fp.name}\n"
|
|
180
|
+
f"-- 来源: rules_dsl/sql/{fp.name}\n"
|
|
181
|
+
f"-- 可直接在 DuckDB 中执行,无需依赖项目 VIEW\n"
|
|
182
|
+
f"-- 生成脚本: scripts/generate_parquet_sql.py\n\n"
|
|
183
|
+
)
|
|
184
|
+
transformed = replace_table_refs(sql_text, data_bindings)
|
|
185
|
+
out_path = PARQUET_OUT_DIR / fp.name
|
|
186
|
+
out_path.write_text(header + transformed + "\n", encoding="utf-8")
|
|
187
|
+
print(f" [OK] {fp.name} → parquet/{fp.name}")
|
|
188
|
+
generated += 1
|
|
189
|
+
|
|
190
|
+
print(f"\n[generate] 生成 {generated} 个, 跳过 {skipped} 个 (TODO/SELECT 1)")
|
|
191
|
+
print(f"[generate] 输出目录: {PARQUET_OUT_DIR}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|