94 lines
3.8 KiB
Python
94 lines
3.8 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, 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)
|