#!/usr/bin/env python3 # Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Import the command history that predates the table holding it. python tool/backfill_analytics.py --db signal_bot.sqlite3 --dry-run python tool/backfill_analytics.py --db signal_bot.sqlite3 Two sources, joined so that neither double-counts the other -- see ``signal_bot.analytics.backfill`` for why, which is the part worth reading before trusting the numbers this prints. The logic lives in ``signal_bot/``, where the 100% coverage gate can see it. This file is the acquisition and the argparse, and lives in ``tool/``, which is outside ``--cov`` and covered instead by ``tests/test_tool_imports.py``. ``--db`` is required and has no default, for the reason ManageDeps gives: this writes to a database that holds a real group's history, and a default is how the wrong one gets written to. Run it against a copy first. It is insert-only and re-runnable, but "I ran it on the live file to see what it would do" is not a thing to want back. """ from __future__ import annotations import argparse import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from signal_bot.analytics import backfill, journal from signal_bot.external import run from signal_bot.store import open_store UNIT = "signal-bot.service" def read_journal(path: str | None) -> list[str]: """The bot's log, from a file or from journald itself. Shells out through ``signal_bot.external``, the one module in this repository allowed to start a process. ``tests/test_external.py`` keeps that list closed by grepping every file for the import itself, so this one routes through the sanctioned seam rather than joining the list -- and note that naming the import in prose is enough to trip that grep. """ if path is not None: return Path(path).read_text(encoding="utf-8").splitlines() result = run( ["journalctl", "--user", "-u", UNIT, "--no-pager", "-o", "short-iso"], timeout=120.0, ) if not result.ok: sys.stderr.write(f"could not read the journal: {result.stderr.strip()}\n") return [] return result.stdout.splitlines() def main(argv: list[str] | None = None) -> int: """Parse arguments, run one pass, and print what it did.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--db", required=True, help="the database to write to") parser.add_argument("--log", help="a captured journal, instead of reading journald") parser.add_argument( "--dry-run", action="store_true", help="report what would be imported, and write nothing", ) args = parser.parse_args(argv) lines = read_journal(args.log) sys.stdout.write(f"journal lines read: {len(lines)}\n") records = list(journal.parse(lines)) sys.stdout.write(f"command lines found: {len(records)}\n") with open_store(args.db) as store: report, events = backfill.plan(store, records) if args.dry_run: sys.stdout.write("-- dry run, nothing written --\n") else: backfill.apply(store, events) for line in report.lines(): sys.stdout.write(f"{line}\n") return 0 if __name__ == "__main__": raise SystemExit(main())