import csv
import os
import re
import time
from collections import defaultdict
import requests
# The term. Dates are in UTC.
TERM_START = "2026-09-01T00:00:00Z"
TERM_END = "2026-12-20T00:00:00Z"
BASE = "https://api-demo.scoutworks.app/api/v1"
API_KEY = os.environ.get(
"SCOUTWORKS_API_KEY",
"sw_live_473adcc6c7236916021f0f24559db36576a53bab071d0f0c08a025adb510b6ab",
)
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"
def get(url, params):
"""GET one page. Waits and tries again if the rate limit is reached."""
while True:
res = session.get(url, params=params, timeout=30)
if res.status_code == 429:
time.sleep(res.json().get("retryAfter", 30))
continue
if not res.ok:
error = res.json()["error"]
raise SystemExit(f"{res.status_code} {error['code']}: {error['message']}")
return res.json()
def read_all(path, **filters):
"""Every item in a list, following nextCursor to the last page."""
items, params = [], {"limit": 200, **filters}
while True:
page = get(BASE + path, params)
items += page["data"]
if page["nextCursor"] is None:
return items
params["cursor"] = page["nextCursor"]
def safe(value):
"""Stops a spreadsheet running a cell that starts with = + - or @."""
text = str(value)
return "'" + text if text.lstrip().startswith(("=", "+", "-", "@")) else text
meetings = [
e for e in read_all("/events", **{"from": TERM_START, "to": TERM_END})
if e["type"] == "programme_meeting"
]
print(f"{len(meetings)} meetings. Reading the registers…")
# section → member id → { "name": …, meeting id → "P" or "A" }
registers = defaultdict(dict)
# section → the meetings that section has a register for
section_meetings = defaultdict(list)
for meeting in meetings:
seen = set()
for a in read_all(f"/events/{meeting['id']}/attendees"):
if a["attendance"] is None or a["section"] is None:
continue # Not taken yet.
section = a["section"]["name"]
row = registers[section].setdefault(
a["member"]["id"],
{"name": (a["member"]["lastName"], a["member"]["firstName"])},
)
row[meeting["id"]] = "P" if a["attendance"] == "present" else "A"
seen.add(section)
for section in seen:
section_meetings[section].append(meeting)
used = set()
for section, rows in sorted(registers.items()):
dates = section_meetings[section]
stem = "attendance-" + re.sub(r"[^a-z0-9]+", "-", section.lower()).strip("-")
filename, n = stem + ".csv", 2
while filename in used: # Two names like "Cubs 1" and "Cubs-1" must not share a file.
filename, n = f"{stem}-{n}.csv", n + 1
used.add(filename)
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["Last name", "First name"] + [m["startsAt"][:10] for m in dates] + ["Present", "Attendance"])
for row in sorted(rows.values(), key=lambda r: r["name"]):
marks = [row.get(m["id"], "") for m in dates]
present, taken = marks.count("P"), marks.count("P") + marks.count("A")
writer.writerow(
[safe(row["name"][0]), safe(row["name"][1])]
+ marks
+ [f"{present} of {taken}", f"{round(100 * present / taken)}%"]
)
print(f"Saved {filename} with {len(dates)} meetings")