125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""原子工具:Job JSON Schema 校验
|
|||
|
|
|
|||
|
|
校验 Job JSON 文件的合法性,composer 模式额外校验布局名称和 source 引用。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python validate_job.py job.json
|
|||
|
|
python validate_job.py job.json --dry-run # 校验 + 尝试连接数据源
|
|||
|
|
|
|||
|
|
输出 (stdout):
|
|||
|
|
{"status": "ok", "mode": "...", "summary": {...}}
|
|||
|
|
{"status": "error", "errors": [...]}
|
|||
|
|
|
|||
|
|
退出码: 0=通过, 1=失败
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
import json
|
|||
|
|
import argparse
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|||
|
|
|
|||
|
|
from ppt_station.models.job import Job
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(description="Job JSON 校验工具")
|
|||
|
|
parser.add_argument("job_json", help="Job JSON 文件路径")
|
|||
|
|
parser.add_argument("--dry-run", action="store_true",
|
|||
|
|
help="额外尝试连接数据源(不渲染)")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
job_path = Path(args.job_json)
|
|||
|
|
if not job_path.exists():
|
|||
|
|
print(json.dumps({"status": "error", "errors": [f"文件不存在: {job_path}"]},
|
|||
|
|
ensure_ascii=False))
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 1. 基础 Schema 校验
|
|||
|
|
errors = []
|
|||
|
|
try:
|
|||
|
|
with open(job_path, "r", encoding="utf-8") as f:
|
|||
|
|
raw = json.load(f)
|
|||
|
|
job = Job(**raw)
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(json.dumps({"status": "error", "errors": [f"JSON 语法错误: {e}"]},
|
|||
|
|
ensure_ascii=False))
|
|||
|
|
sys.exit(1)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(json.dumps({"status": "error", "errors": [f"Schema 校验失败: {e}"]},
|
|||
|
|
ensure_ascii=False))
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 2. Composer 模式额外校验
|
|||
|
|
if job.mode == "composer":
|
|||
|
|
from ppt_station.composer.layouts import LAYOUT_REGISTRY
|
|||
|
|
|
|||
|
|
all_sources = set(job.datasources.keys())
|
|||
|
|
if job.transforms:
|
|||
|
|
all_sources.update(job.transforms.keys())
|
|||
|
|
|
|||
|
|
for i, page in enumerate(job.pages):
|
|||
|
|
# 校验布局名称
|
|||
|
|
if page.layout not in LAYOUT_REGISTRY:
|
|||
|
|
available = ", ".join(LAYOUT_REGISTRY.keys())
|
|||
|
|
errors.append(f"pages[{i}]: 未知布局 '{page.layout}'。可用: {available}")
|
|||
|
|
|
|||
|
|
# 校验 datasource/source 引用
|
|||
|
|
if "datasource" in page.data:
|
|||
|
|
if page.data["datasource"] not in all_sources:
|
|||
|
|
errors.append(
|
|||
|
|
f"pages[{i}]: datasource '{page.data['datasource']}' "
|
|||
|
|
f"不存在于 datasources/transforms 中"
|
|||
|
|
)
|
|||
|
|
# source 值不在已知数据源中 → 视为脚注文本,不报错
|
|||
|
|
|
|||
|
|
# 校验嵌套 datasource/source 引用 (two_charts)
|
|||
|
|
for key in ("left", "right"):
|
|||
|
|
if key in page.data and isinstance(page.data[key], dict):
|
|||
|
|
sub = page.data[key]
|
|||
|
|
ds_ref = sub.get("datasource") or sub.get("source")
|
|||
|
|
if ds_ref and ds_ref not in all_sources:
|
|||
|
|
errors.append(
|
|||
|
|
f"pages[{i}].data.{key}: datasource/source '{ds_ref}' "
|
|||
|
|
f"不存在于 datasources/transforms 中"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if errors:
|
|||
|
|
print(json.dumps({"status": "error", "errors": errors}, ensure_ascii=False))
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 3. 构造摘要
|
|||
|
|
summary = {"mode": job.mode, "datasources": list(job.datasources.keys())}
|
|||
|
|
if job.mode == "template":
|
|||
|
|
summary["slides"] = len(job.slides)
|
|||
|
|
summary["template"] = job.template.path
|
|||
|
|
elif job.mode == "composer":
|
|||
|
|
summary["pages"] = len(job.pages)
|
|||
|
|
summary["theme"] = job.theme or "jp_finance"
|
|||
|
|
summary["layouts"] = [p.layout for p in job.pages]
|
|||
|
|
|
|||
|
|
if job.transforms:
|
|||
|
|
summary["transforms"] = list(job.transforms.keys())
|
|||
|
|
|
|||
|
|
# 4. 可选 dry-run:尝试连接数据源
|
|||
|
|
if args.dry_run and job.datasources:
|
|||
|
|
from ppt_station.connectors import ConnectorFactory
|
|||
|
|
|
|||
|
|
ds_status = {}
|
|||
|
|
for name, ds in job.datasources.items():
|
|||
|
|
try:
|
|||
|
|
print(f"尝试连接: {name}", file=sys.stderr)
|
|||
|
|
df = ConnectorFactory.load_data(name, ds)
|
|||
|
|
ds_status[name] = {"ok": True, "rows": len(df), "columns": list(df.columns)}
|
|||
|
|
except Exception as e:
|
|||
|
|
ds_status[name] = {"ok": False, "error": str(e)}
|
|||
|
|
summary["datasource_check"] = ds_status
|
|||
|
|
|
|||
|
|
print(json.dumps({"status": "ok", "summary": summary}, ensure_ascii=False))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|