Files
challenge_status_tracker/tracker/overview.py
T
Casually9790andClaude Opus 5.5 e00f40eaf4 Split ctf_status.py into a tracker package, config from .env only
ctf_status.py is now just the command line; the work lives in tracker/.
The rules that decide whether a challenge is believed are in
tracker/checks.py, one function per rule, listed in README_CHECKS and
REPO_CHECKS. Output is unchanged.

Read the config from .env only (ctf_status.env.example -> .env.example),
let single_run_lock() hold its own lock files, and rewrite the README as
a short overview of what the repo does and how to run it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:40:24 +02:00

79 lines
2.9 KiB
Python

"""Every challenge in the group, checked, grouped and sorted for the reports."""
from __future__ import annotations
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from .challenge import CATEGORIES, DIFFICULTIES, STATUS_ORDER, Challenge, difficulty_rank, parse_challenge
from .checks import is_testable, readme_problems, repo_blockers
from .gitlab import GitLab
@dataclass
class Overview:
challenges: list # repos with a README somebody filled in
blank: list # repos with no README, or an untouched template
@property
def categories(self) -> list[str]:
extra = sorted({c.category or "?" for c in self.challenges} - set(CATEGORIES))
return CATEGORIES + extra
@property
def difficulties(self) -> list[str]:
seen = {c.difficulty for c in self.challenges if c.difficulty}
unknown = ["?"] if any(not c.difficulty for c in self.challenges) else []
return sorted(set(DIFFICULTIES) | seen, key=difficulty_rank) + unknown
@property
def stages(self) -> list:
"""The five stages, plus None ("?") only if some status is unreadable."""
return STATUS_ORDER + ([None] if any(not c.stage for c in self.challenges) else [])
@property
def testable(self) -> list:
"""Everything infra can already deploy and test: Testing Ready or beyond."""
return sorted((c for c in self.challenges if is_testable(c)), key=self._ready_order)
@property
def blocked(self) -> list:
"""Testing Ready or beyond according to the README, but not the repo."""
return sorted((c for c in self.challenges if c.claims_testable and c.blockers),
key=self._ready_order)
def _ready_order(self, c: Challenge) -> tuple:
return (self.categories.index(c.category or "?"),
difficulty_rank(c.difficulty), c.title.lower())
def by_category(self) -> dict[str, list]:
out = defaultdict(list)
for c in self.challenges:
out[c.category or "?"].append(c)
for chs in out.values():
chs.sort(key=lambda c: (-STATUS_ORDER.index(c.stage) if c.stage else 1,
difficulty_rank(c.difficulty), c.title.lower()))
return out
def collect(gl: GitLab) -> Overview:
chs, blank = [], []
for repo in gl.repos():
c = parse_challenge(repo)
(chs if c.touched else blank).append(c)
for c in chs:
c.problems = readme_problems(c)
# only a Testing Ready+ claim is worth the extra downloads to check the repo
def check(c: Challenge) -> None:
gl.load_files(c.repo)
c.blockers = repo_blockers(c)
with ThreadPoolExecutor(max_workers=8) as pool:
list(pool.map(check, [c for c in chs if c.claims_testable]))
return Overview(chs, sorted(blank, key=lambda c: c.repo.path.lower()))
def icons(chs: list) -> str:
return "".join(c.icon for c in chs)