"""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, field 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 from .selection import Selection @dataclass class Overview: challenges: list # included repos with a README somebody filled in blank: list # included repos with no README, or an untouched template excluded: list = field(default_factory=list) # abandoned or postponed in challenges.yml undecided: list = field(default_factory=list) # repos not in challenges.yml unknown: list = field(default_factory=list) # repo paths in challenges.yml that do not exist @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, sel: Selection) -> Overview: chs, blank, excluded, undecided, seen = [], [], [], [], set() for repo in gl.repos(): seen.add(repo.path) c = parse_challenge(repo) c.selection = sel.states.get(repo.path) c.skip_checks = sel.skip_checks.get(repo.path, []) if not c.selection: undecided.append(c) elif not c.included: excluded.append(c) else: (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()), sorted(excluded, key=lambda c: c.repo.path.lower()), sorted(undecided, key=lambda c: c.repo.path.lower()), sorted((set(sel.states) | set(sel.skip_checks)) - seen)) def icons(chs: list) -> str: return "".join(c.icon for c in chs)