from __future__ import annotations
|
|
import argparse
|
import csv
|
import getpass
|
import hashlib
|
import json
|
import os
|
from datetime import date, datetime, time
|
from pathlib import Path
|
from typing import Any
|
|
import pymysql
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
DEFAULT_OUT = ROOT / "data" / "mysql_seed_20260624"
|
TABLES = [
|
"dl_source_document",
|
"dl_case_import_batch",
|
"dl_case_record",
|
"dl_darkline_hypothesis",
|
"dl_case_reasoning_step",
|
"dl_expected_line",
|
"dl_event_node",
|
"dl_evidence",
|
"dl_evidence_node_link",
|
"dl_impact_target",
|
"dl_manifestation_bridge",
|
"dl_alternative_explanation",
|
"dl_chain_state",
|
]
|
|
|
def sha256_file(path: Path) -> str:
|
h = hashlib.sha256()
|
with path.open("rb") as f:
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
h.update(chunk)
|
return h.hexdigest()
|
|
|
def json_default(value: Any) -> str:
|
if isinstance(value, datetime):
|
return value.isoformat(sep=" ")
|
if isinstance(value, (date, time)):
|
return value.isoformat()
|
raise TypeError(f"Unsupported type: {type(value)!r}")
|
|
|
def get_password(args: argparse.Namespace) -> str:
|
if args.password:
|
return args.password
|
if os.environ.get("DARKLINE_MYSQL_PASSWORD"):
|
return os.environ["DARKLINE_MYSQL_PASSWORD"]
|
if os.environ.get("TIANXIA_MYSQL_PASSWORD"):
|
return os.environ["TIANXIA_MYSQL_PASSWORD"]
|
return getpass.getpass("MySQL password: ")
|
|
|
def connect(args: argparse.Namespace):
|
return pymysql.connect(
|
host=args.host,
|
port=args.port,
|
user=args.user,
|
password=get_password(args),
|
database=args.database,
|
charset="utf8mb4",
|
autocommit=True,
|
cursorclass=pymysql.cursors.DictCursor,
|
)
|
|
|
def write_manifest(out_dir: Path, files: list[Path]) -> None:
|
rows = []
|
for path in sorted(files):
|
rows.append(
|
{
|
"relative_path": str(path.relative_to(out_dir)).replace("\\", "/"),
|
"size_bytes": path.stat().st_size,
|
"sha256": sha256_file(path),
|
"artifact_status": "READY",
|
}
|
)
|
with (out_dir / "manifest.csv").open("w", newline="", encoding="utf-8-sig") as f:
|
writer = csv.DictWriter(
|
f,
|
fieldnames=["relative_path", "size_bytes", "sha256", "artifact_status"],
|
)
|
writer.writeheader()
|
writer.writerows(rows)
|
|
|
def main() -> None:
|
parser = argparse.ArgumentParser(description="Export darkline dl_ tables as portable seed data.")
|
parser.add_argument("--host", default=os.environ.get("DARKLINE_MYSQL_HOST", "127.0.0.1"))
|
parser.add_argument("--port", type=int, default=int(os.environ.get("DARKLINE_MYSQL_PORT", "3306")))
|
parser.add_argument("--user", default=os.environ.get("DARKLINE_MYSQL_USER", "root"))
|
parser.add_argument("--password", default=None)
|
parser.add_argument("--database", default=os.environ.get("DARKLINE_MYSQL_DATABASE", "tianxia"))
|
parser.add_argument("--out-dir", default=str(DEFAULT_OUT))
|
args = parser.parse_args()
|
|
out_dir = Path(args.out_dir)
|
tables_dir = out_dir / "tables"
|
tables_dir.mkdir(parents=True, exist_ok=True)
|
|
conn = connect(args)
|
schema_parts: list[str] = []
|
summary: dict[str, Any] = {
|
"seed_id": "darkline_mysql_seed_20260624",
|
"database": args.database,
|
"tables": {},
|
"exported_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
}
|
generated_files: list[Path] = []
|
|
with conn.cursor() as cur:
|
for table in TABLES:
|
cur.execute(f"SHOW CREATE TABLE `{table}`")
|
create_sql = cur.fetchone()["Create Table"]
|
schema_parts.append(f"DROP TABLE IF EXISTS `{table}`;\n{create_sql};")
|
|
cur.execute(f"SELECT * FROM `{table}`")
|
rows = cur.fetchall()
|
out_path = tables_dir / f"{table}.jsonl"
|
with out_path.open("w", encoding="utf-8") as f:
|
for row in rows:
|
f.write(json.dumps(row, ensure_ascii=False, default=json_default) + "\n")
|
generated_files.append(out_path)
|
summary["tables"][table] = {
|
"row_count": len(rows),
|
"jsonl": str(out_path.relative_to(out_dir)).replace("\\", "/"),
|
"sha256": sha256_file(out_path),
|
}
|
|
conn.close()
|
|
schema_path = out_dir / "schema.sql"
|
schema_path.write_text("\n\n".join(schema_parts) + "\n", encoding="utf-8")
|
generated_files.append(schema_path)
|
|
order_path = out_dir / "table_order.json"
|
order_path.write_text(json.dumps(TABLES, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
generated_files.append(order_path)
|
|
summary_path = out_dir / "seed_summary.json"
|
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
generated_files.append(summary_path)
|
|
readme_path = out_dir / "README.md"
|
readme_path.write_text(
|
"# darkline MySQL seed data\n\n"
|
"本目录用于在另一台机器复原 darkline 案例数据。\n\n"
|
"导入方式见项目根目录 `darkline安装文档.md`。\n\n"
|
"包含表:\n\n"
|
+ "\n".join(f"- `{table}`" for table in TABLES)
|
+ "\n",
|
encoding="utf-8",
|
)
|
generated_files.append(readme_path)
|
|
write_manifest(out_dir, generated_files)
|
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
|
if __name__ == "__main__":
|
main()
|