krz/hutch-stats
Server-side utility for calculating contributions for sourcehut users.
clone: git clone https://gitbay.org/krz/hutch-stats.git
main: src/srht_contrib/scripts/enqueue_actors.py · raw
1from __future__ import annotations
2
3import argparse
4from datetime import UTC, datetime, timedelta
5from pathlib import Path
6import re
7
8from sqlalchemy import select
9
10from srht_contrib.config import Settings
11from srht_contrib.db import make_session_factory
12from srht_contrib.models import TrackedActor
13
14
15USERNAME_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,61}[A-Za-z0-9])?$")
16
17
18def _normalize_actor(raw_username: str) -> str | None:
19 username = raw_username.strip()
20 if not username or username.startswith("#"):
21 return None
22 if username.startswith("~"):
23 username = username[1:]
24 if not USERNAME_RE.fullmatch(username):
25 return None
26 return f"~{username}"
27
28
29def _iter_usernames(path: Path) -> list[str]:
30 usernames: list[str] = []
31 seen: set[str] = set()
32 for raw_line in path.read_text(encoding="utf-8").splitlines():
33 actor = _normalize_actor(raw_line)
34 if actor is None:
35 continue
36 if actor in seen:
37 continue
38 seen.add(actor)
39 usernames.append(actor)
40 return usernames
41
42
43def enqueue_actors(username_file: Path, *, stagger_seconds: int = 60, start_at: datetime | None = None) -> int:
44 settings = Settings()
45 session_factory = make_session_factory(settings)
46 usernames = _iter_usernames(username_file)
47 queued_at = start_at or datetime.now(tz=UTC)
48 inserted = 0
49
50 batch_size = 250
51 queued_in_batch = 0
52
53 with session_factory() as db:
54 for index, actor in enumerate(usernames):
55 next_poll_after = queued_at + timedelta(seconds=index * stagger_seconds)
56 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor))
57 if tracked_actor is None:
58 tracked_actor = TrackedActor(
59 actor=actor,
60 is_active=True,
61 discovery_state="queued",
62 queued_for_discovery_at=queued_at,
63 next_poll_after=next_poll_after,
64 recent_backfill_status="pending",
65 )
66 db.add(tracked_actor)
67 inserted += 1
68 else:
69 tracked_actor.is_active = True
70 if tracked_actor.queued_for_discovery_at is None:
71 tracked_actor.queued_for_discovery_at = queued_at
72 if tracked_actor.last_polled_at is None and tracked_actor.discovery_state != "indexed":
73 tracked_actor.discovery_state = "queued"
74 tracked_actor.next_poll_after = next_poll_after
75
76 queued_in_batch += 1
77 if queued_in_batch >= batch_size:
78 db.commit()
79 queued_in_batch = 0
80
81 if queued_in_batch:
82 db.commit()
83
84 return inserted
85
86
87def main() -> None:
88 parser = argparse.ArgumentParser(description="Durably enqueue SourceHut actors without polling them immediately.")
89 parser.add_argument(
90 "username_file",
91 nargs="?",
92 default="srht_usernames.txt",
93 help="Path to a newline-delimited SourceHut username file.",
94 )
95 parser.add_argument(
96 "--stagger-seconds",
97 type=int,
98 default=60,
99 help="Seconds to space out each actor's first eligible poll time.",
100 )
101 args = parser.parse_args()
102
103 inserted = enqueue_actors(Path(args.username_file), stagger_seconds=args.stagger_seconds)
104 print(f"Enqueued {inserted} new actors from {args.username_file}.")
105
106
107if __name__ == "__main__":
108 main()