scripts/xkb_usage.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""哪些腳本真的有人在跑——刪東西之前唯一該相信的證據。
scripts/ 底下有五十幾支可以直接執行的腳本。其中有些是遷移時期的一次性工具,
有些是每天跑的管線,有些是 Pan 偶爾手動叫的。從程式碼裡分不出來:
- 「有幾個檔案引用它」分不出來。action_recall 只被 recall_router 引用一次,
但它在每一次召回裡都會跑;migrate_schema 同樣只被引用一次,而它上一次
執行可能是半年前。
- 檔名分不出來。full_sync_v2 聽起來像現役的,init_rebuild_v2 聽起來像一次
性的,實際上要看誰在叫它。
- 我分不出來。哪支是手動工作流程的一環,只有用的人知道。
所以這裡不做分類,只做量測:每次有人把某支腳本當程式跑(不是 import),就記
一行。跑一段時間之後,「從來沒被叫過」就成了證據,而不是猜測——那正是「先
標記、記錄呼叫者,不要一開始就刪」需要的那份資料。
刻意的設計:
- 只在當成主程式執行時記,import 不記。被 import 的次數不代表被使用。
- 記的是腳本名、時間、參數的「形狀」(旗標名,不含值)。參數值可能含有
路徑或查詢內容,那不是這份紀錄該保存的東西。
- 寫不進去絕不影響腳本本身。這是觀測,不是功能。
用法(腳本裡):
if __name__ == "__main__":
xkb_usage.record(__file__)
raise SystemExit(main())
用法(看結果):
python3 scripts/xkb_usage.py report
python3 scripts/xkb_usage.py report --unused
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import xkb_paths
USAGE_PATH = xkb_paths.XKB_DATA_DIR / "script-usage.jsonl"
SCRIPTS_DIR = xkb_paths.SCRIPTS_DIR
def record(script: str | Path, argv: list[str] | None = None,
path: Path | None = None) -> None:
"""記一次執行。任何失敗都吞掉——觀測不能影響被觀測的東西。"""
try:
argv = sys.argv[1:] if argv is None else argv
# 只留旗標名。參數值可能是路徑或查詢字串,那是使用者的內容,
# 不是「這支腳本有沒有被用到」需要的資訊。
flags = sorted({a.split("=", 1)[0] for a in argv if a.startswith("-")})
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"script": Path(script).stem,
"flags": flags,
}
target = path or USAGE_PATH
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
except Exception: # noqa: BLE001 — 觀測失敗不能弄掛腳本
pass
def read(path: Path | None = None) -> list[dict]:
target = path or USAGE_PATH
if not target.exists():
return []
rows = []
try:
lines = target.read_text(encoding="utf-8", errors="ignore").splitlines()
except OSError:
return []
for line in lines:
line = line.strip()
if not line:
continue
try:
row = json.loads(line)
except ValueError:
continue
if isinstance(row, dict) and row.get("script"):
rows.append(row)
return rows
def runnable_scripts(scripts_dir: Path | None = None) -> list[str]:
"""可以當程式跑的腳本——有 __main__ 區塊的那些。"""
scripts_dir = scripts_dir or SCRIPTS_DIR
found = []
for path in sorted(scripts_dir.glob("*.py")):
try:
if '__name__ == "__main__"' in path.read_text(encoding="utf-8", errors="ignore"):
found.append(path.stem)
except OSError:
continue
return found
def summary(path: Path | None = None,
scripts_dir: Path | None = None) -> dict[str, dict]:
"""每支腳本被叫過幾次、最後一次什麼時候。從沒被叫過的也要在裡面。"""
counts = Counter(row["script"] for row in read(path))
last: dict[str, str] = {}
for row in read(path):
last[row["script"]] = row.get("ts", "")
result = {}
for name in runnable_scripts(scripts_dir):
result[name] = {"runs": counts.get(name, 0), "last": last.get(name, "")}
# 已經被刪掉、但紀錄裡還有的腳本也列出來,否則「這支還在被叫」會消失。
for name in counts:
if name not in result:
result[name] = {"runs": counts[name], "last": last.get(name, ""),
"missing": True}
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
rep = sub.add_parser("report", help="每支腳本被叫過幾次")
rep.add_argument("--unused", action="store_true", help="只列從沒被叫過的")
rep.add_argument("--json", action="store_true")
args = parser.parse_args()
data = summary()
if args.unused:
data = {k: v for k, v in data.items() if v["runs"] == 0}
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2))
return 0
if not data:
print("(沒有符合的腳本)")
return 0
total_rows = len(read())
if total_rows == 0:
print("紀錄還是空的——量測剛開始,現在還不能拿它當「沒人用」的證據。")
print()
width = max(len(k) for k in data)
for name, info in sorted(data.items(), key=lambda kv: (kv[1]["runs"], kv[0])):
mark = " ← 已不存在" if info.get("missing") else ""
when = info["last"][:19] or "從沒被叫過"
print(f" {name.ljust(width)} {info['runs']:>4} 次 {when}{mark}")
return 0
if __name__ == "__main__":
record(__file__)
raise SystemExit(main())