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.
Files changed (54) hide show
  1. scripts/export_dsl.py +60 -0
  2. scripts/generate_parquet_sql.py +195 -0
  3. scripts/generate_rule_manual.py +1091 -0
  4. scripts/import_rules.py +103 -0
  5. scripts/import_sql.py +167 -0
  6. scripts/import_sql_files.py +126 -0
  7. scripts/rewrite_sql_duckdb.py +271 -0
  8. scripts/run_center.py +286 -0
  9. src/__init__.py +0 -0
  10. src/app.py +22 -0
  11. src/config.py +119 -0
  12. src/database/__init__.py +0 -0
  13. src/database/connection.py +57 -0
  14. src/database/migrate.py +29 -0
  15. src/models/__init__.py +0 -0
  16. src/models/alert.py +161 -0
  17. src/models/analytics.py +218 -0
  18. src/models/data_source.py +78 -0
  19. src/models/execution.py +142 -0
  20. src/models/model_category.py +63 -0
  21. src/models/rule.py +380 -0
  22. src/models/rule_version.py +69 -0
  23. src/services/__init__.py +0 -0
  24. src/services/execution_service.py +218 -0
  25. src/services/export_service.py +75 -0
  26. src/services/import_service.py +151 -0
  27. src/services/sql_adapter.py +440 -0
  28. src/tui/app.py +216 -0
  29. src/tui/screens/__init__.py +0 -0
  30. src/tui/screens/alert_detail.py +194 -0
  31. src/tui/screens/execution_detail.py +131 -0
  32. src/tui/screens/help_screen.py +85 -0
  33. src/tui/screens/import_screen.py +67 -0
  34. src/tui/screens/rule_detail.py +178 -0
  35. src/tui/screens/rule_edit.py +179 -0
  36. src/tui/startup_check.py +128 -0
  37. src/tui/tabs/__init__.py +0 -0
  38. src/tui/tabs/alert.py +315 -0
  39. src/tui/tabs/analytics.py +256 -0
  40. src/tui/tabs/approval.py +170 -0
  41. src/tui/tabs/dashboard.py +174 -0
  42. src/tui/tabs/execution.py +234 -0
  43. src/tui/tabs/query.py +544 -0
  44. src/tui/tabs/settings.py +314 -0
  45. src/tui/widgets/__init__.py +0 -0
  46. src/utils/__init__.py +0 -0
  47. src/utils/csv_importer.py +228 -0
  48. src/utils/dsl_generator.py +223 -0
  49. src/utils/excel_parser.py +290 -0
  50. xforge-0.4.3.dist-info/METADATA +10 -0
  51. xforge-0.4.3.dist-info/RECORD +54 -0
  52. xforge-0.4.3.dist-info/WHEEL +5 -0
  53. xforge-0.4.3.dist-info/entry_points.txt +5 -0
  54. 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()