Redrob-hackathon / validate_submission.py
Mohit0708's picture
Initial commit
7b833a7
Raw
History Blame Contribute Delete
5.04 kB
#!/usr/bin/env python3
"""
Validate submission CSV per challenge rules (sections 2–3).
Row 1 = header. Rows 2–101 = exactly 100 data rows. CSV only.
"""
import csv
import re
import sys
from pathlib import Path
REQUIRED_HEADER = ["candidate_id", "rank", "score", "reasoning"]
CANDIDATE_ID_PATTERN = re.compile(r"^CAND_[0-9]{7}$")
DATA_ROW_START = 2
EXPECTED_DATA_ROWS = 100
def validate_submission(csv_path):
errors = []
path = Path(csv_path)
if path.suffix.lower() != ".csv":
errors.append("Filename must use a .csv extension.")
elif not path.stem:
errors.append("Filename must be your registered participant ID (e.g. team_xxx.csv).")
try:
with open(path, "r", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
try:
header = next(reader)
except StopIteration:
errors.append("Row 1 must be the header row; file is empty.")
return errors
# Row 1: column names and their order come from this line only
if header != REQUIRED_HEADER:
errors.append(
"Row 1 (header) must be exactly:\n"
f" {','.join(REQUIRED_HEADER)}\n"
f"Found:\n"
f" {','.join(header)}"
)
data_rows = []
for row in reader:
if any(cell.strip() for cell in row):
data_rows.append(row)
except UnicodeDecodeError:
errors.append("File must be UTF-8 encoded.")
return errors
except OSError as e:
errors.append(f"Cannot read file: {e}")
return errors
n = len(data_rows)
if n != EXPECTED_DATA_ROWS:
errors.append(
f"After the header (row 1), there must be exactly {EXPECTED_DATA_ROWS} "
f"data rows (rows {DATA_ROW_START}{DATA_ROW_START + EXPECTED_DATA_ROWS - 1}); "
f"found {n}."
)
seen_ids = set()
seen_ranks = set()
by_rank = []
for i, cells in enumerate(data_rows):
row_num = DATA_ROW_START + i
if len(cells) != len(REQUIRED_HEADER):
errors.append(
f"Row {row_num}: expected {len(REQUIRED_HEADER)} columns "
f"({','.join(REQUIRED_HEADER)}), got {len(cells)}."
)
continue
row = dict(zip(REQUIRED_HEADER, cells))
cid = row["candidate_id"].strip()
rank_s = row["rank"].strip()
score_s = row["score"].strip()
if not cid:
errors.append(f"Row {row_num}: candidate_id is required.")
elif not CANDIDATE_ID_PATTERN.match(cid):
errors.append(
f"Row {row_num}: candidate_id must be CAND_XXXXXXX (7 digits)."
)
elif cid in seen_ids:
errors.append(f"Row {row_num}: duplicate candidate_id '{cid}'.")
else:
seen_ids.add(cid)
try:
rank = int(rank_s)
if str(rank) != rank_s:
raise ValueError
if not 1 <= rank <= 100:
errors.append(f"Row {row_num}: rank must be between 1 and 100.")
elif rank in seen_ranks:
errors.append(f"Row {row_num}: duplicate rank {rank}.")
else:
seen_ranks.add(rank)
except ValueError:
errors.append(f"Row {row_num}: rank must be an integer (1–100).")
rank = None
try:
score = float(score_s)
except ValueError:
errors.append(f"Row {row_num}: score must be a float.")
score = None
if rank is not None and score is not None and cid:
by_rank.append((rank, score, cid))
missing = set(range(1, 101)) - seen_ranks
if missing:
errors.append(
f"Each rank 1–100 must appear exactly once; missing: {sorted(missing)}"
)
by_rank.sort(key=lambda x: x[0])
for i in range(len(by_rank) - 1):
r1, s1, _ = by_rank[i]
r2, s2, _ = by_rank[i + 1]
if s1 < s2:
errors.append(
f"score must be non-increasing by rank: "
f"rank {r1} ({s1}) < rank {r2} ({s2})."
)
for i in range(len(by_rank) - 1):
r1, s1, c1 = by_rank[i]
r2, s2, c2 = by_rank[i + 1]
if s1 == s2 and c1 > c2:
errors.append(
f"Equal scores at ranks {r1} and {r2}: "
f"tie-break requires candidate_id ascending "
f"({c1!r} > {c2!r})."
)
return errors
def main():
if len(sys.argv) != 2:
print("Usage: python validate_submission.py <participant_id>.csv")
sys.exit(1)
errors = validate_submission(sys.argv[1])
if errors:
print(f"Validation failed ({len(errors)} issue(s)):\n")
for e in errors:
print(f"- {e}")
sys.exit(1)
print("Submission is valid.")
if __name__ == "__main__":
main()