upate handle index script..
This commit is contained in:
1 parent
0a281c571c
commit
7e4ed4b303
3 files changed
+150
-6
No files matched your search
@@ -14,4 +14,7 @@ How to read a folder: [[notes/How threads are organized]].
|
||||
Example of the note shape (single spine, no branches): [[archive/threads/How to archive a thread]]
|
||||
|
||||
- [[archive/threads/NOTimothyLottes]]
|
||||
- [[archive/threads/VPCOMPRESSB]]
|
||||
- [[archive/threads/VictorTaelin]]
|
||||
- [[archive/threads/rianflo]]
|
||||
- [[archive/threads/winning_tactic]]
|
||||
@@ -1,11 +1,19 @@
|
||||
"""Audit every handle index in the vault against real subdirectories.
|
||||
"""Audit every handle index in the vault against real subdirectories,
|
||||
and audit the top-level `archive/threads/index.md` against real
|
||||
handle dirs.
|
||||
|
||||
Reports per handle:
|
||||
For each handle dir:
|
||||
real — set of subdirectory basenames on disk
|
||||
links — set of basenames extracted from index.md wikilinks
|
||||
missing — subdirectories with no wikilink in index.md
|
||||
stale — wikilinks in index.md whose subdirectory does not exist
|
||||
|
||||
For the threads index (top-level archive/threads/index.md):
|
||||
real — set of handle dir basenames on disk
|
||||
links — set of handle basenames extracted from wikilinks
|
||||
missing — handle dirs with no wikilink
|
||||
stale — wikilinks whose handle dir does not exist
|
||||
|
||||
Usage:
|
||||
python scripts/twitter/audit_handle_index.py # text report
|
||||
python scripts/twitter/audit_handle_index.py --json # JSON report
|
||||
@@ -70,6 +78,26 @@ def audit_one(handle_dir: Path) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def audit_threads_index(threads_root: Path) -> dict[str, object]:
|
||||
"""Audit the top-level archive/threads/index.md against real
|
||||
handle dirs."""
|
||||
idx_path = threads_root / "index.md"
|
||||
real = {
|
||||
d.name for d in threads_root.iterdir() if d.is_dir() and not d.name.startswith(".")
|
||||
}
|
||||
links = _index_links(idx_path)
|
||||
missing = sorted(real - links)
|
||||
stale = sorted(links - real)
|
||||
return {
|
||||
"handle": "<threads>",
|
||||
"has_index": idx_path.is_file(),
|
||||
"real": sorted(real),
|
||||
"links": sorted(links),
|
||||
"missing": missing,
|
||||
"stale": stale,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
@@ -92,6 +120,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if not entry.is_dir() or entry.name.startswith("."):
|
||||
continue
|
||||
reports.append(audit_one(entry))
|
||||
reports.append(audit_threads_index(threads_root))
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(reports, indent=2, ensure_ascii=False))
|
||||
@@ -120,20 +149,25 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
print()
|
||||
if flagged:
|
||||
print(f"flagged {flagged} handle(s):")
|
||||
print(f"flagged {flagged} entry/entries:")
|
||||
for r in reports:
|
||||
if not (r["missing"] or r["stale"]):
|
||||
continue
|
||||
label = (
|
||||
"threads index"
|
||||
if r["handle"] == "<threads>"
|
||||
else f"{r['handle']} handle index"
|
||||
)
|
||||
if r["missing"]:
|
||||
missing_sample = ", ".join(r["missing"][:5])
|
||||
if len(r["missing"]) > 5:
|
||||
missing_sample += f", ... (+{len(r['missing']) - 5} more)"
|
||||
print(f" {r['handle']}: missing_in_index = {missing_sample}")
|
||||
print(f" {label}: missing_in_index = {missing_sample}")
|
||||
if r["stale"]:
|
||||
stale_sample = ", ".join(r["stale"])
|
||||
print(f" {r['handle']}: stale_in_index = {stale_sample}")
|
||||
print(f" {label}: stale_in_index = {stale_sample}")
|
||||
else:
|
||||
print("clean: no handle has missing or stale wikilinks")
|
||||
print("clean: no handle or threads index has missing or stale wikilinks")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from twitter.audit_handle_index import (
|
||||
audit_one,
|
||||
audit_threads_index,
|
||||
main,
|
||||
)
|
||||
|
||||
|
||||
def _write_handle(
|
||||
handle_dir: Path,
|
||||
wikilinks: list[str] | None,
|
||||
subdirs: list[str] | None,
|
||||
) -> None:
|
||||
handle_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name in subdirs or []:
|
||||
(handle_dir / name).mkdir()
|
||||
if wikilinks is not None:
|
||||
body = "\n".join(f"- [[archive/threads/{handle_dir.name}/{w}]]" for w in wikilinks)
|
||||
(handle_dir / "index.md").write_text(
|
||||
"---\ntitle: x\n---\n\n" + body + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
|
||||
def _write_threads_index(threads_root: Path, handles: list[str]) -> None:
|
||||
body = "\n".join(f"- [[archive/threads/{h}]]" for h in handles)
|
||||
(threads_root / "index.md").write_text(
|
||||
"---\ntitle: Threads\n---\n\n" + body + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
|
||||
class ThreadsIndexAuditTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp = Path(tempfile.mkdtemp(prefix="threads_audit_"))
|
||||
self.threads_root = self.tmp / "archive" / "threads"
|
||||
self.threads_root.mkdir(parents=True)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_threads_index_clean(self) -> None:
|
||||
_write_handle(self.threads_root / "alice", None, ["2024-01-01"])
|
||||
_write_handle(self.threads_root / "bob", None, ["2024-02-02"])
|
||||
_write_threads_index(self.threads_root, ["alice", "bob"])
|
||||
result = audit_threads_index(self.threads_root)
|
||||
self.assertEqual(result["missing"], [])
|
||||
self.assertEqual(result["stale"], [])
|
||||
self.assertEqual(set(result["real"]), {"alice", "bob"})
|
||||
|
||||
def test_threads_index_missing_handle(self) -> None:
|
||||
# bob exists on disk but is not wikilinked in threads index
|
||||
_write_handle(self.threads_root / "alice", None, [])
|
||||
_write_handle(self.threads_root / "bob", None, [])
|
||||
_write_threads_index(self.threads_root, ["alice"])
|
||||
result = audit_threads_index(self.threads_root)
|
||||
self.assertEqual(result["missing"], ["bob"])
|
||||
self.assertEqual(result["stale"], [])
|
||||
|
||||
def test_threads_index_stale_wikilink(self) -> None:
|
||||
# ghost is wikilinked but no handle dir exists
|
||||
_write_handle(self.threads_root / "alice", None, [])
|
||||
_write_threads_index(self.threads_root, ["alice", "ghost"])
|
||||
result = audit_threads_index(self.threads_root)
|
||||
self.assertEqual(result["missing"], [])
|
||||
self.assertEqual(result["stale"], ["ghost"])
|
||||
|
||||
|
||||
class AuditorCliTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp = Path(tempfile.mkdtemp(prefix="threads_audit_cli_"))
|
||||
self.threads_root = self.tmp / "archive" / "threads"
|
||||
self.threads_root.mkdir(parents=True)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_main_text_includes_threads_index_entry(self) -> None:
|
||||
_write_handle(self.threads_root / "alice", None, [])
|
||||
_write_handle(self.threads_root / "bob", None, [])
|
||||
_write_threads_index(self.threads_root, ["alice"])
|
||||
# bob is missing from the threads index; main() should flag it
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
rc = main(["--vault", str(self.tmp)])
|
||||
self.assertEqual(rc, 0)
|
||||
out = buf.getvalue()
|
||||
self.assertIn("alice", out)
|
||||
self.assertIn("bob", out)
|
||||
self.assertIn("threads index", out)
|
||||
self.assertIn("missing_in_index", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user