====== Classroom 50: CLI-Referenz ====== Zwei Kommandos aus [[https://github.com/BZZ-Commons/pygrader50|pygrader50]]: eines bewertet, eines überträgt. Dazu ein Migrationsskript. ===== python -m pygrader50 — bewerten ===== Wird vom Classroom-50-Runner im Studi-Checkout aufgerufen. Keine Argumente — alles kommt aus der Umgebung. ==== Umgebung ==== ^ Variable ^ Bedeutung ^ Pflicht ^ | ''CLASSROOM'' | Kurzname des Klassenzimmers | ja | | ''ASSIGNMENT'' | Slug der Aufgabe | ja | | ''SUBMISSION_TAG'' | ''submit/-'' | ja | | ''OWNER'' / ''USERNAME'' | GitHub-Login der besitzenden Person | ersatzweise ''GITHUB_ACTOR'' | | ''ASSIGNMENT_TYPE'' | ''individual'' oder ''group'' | Vorgabe ''individual'' | | ''COMMIT_URL'', ''RELEASE_URL'', ''REVIEW_URL'' | Links fürs Payload | ersatzweise aus ''GITHUB_SERVER_URL'' / ''GITHUB_REPOSITORY'' / ''GITHUB_SHA'' | | ''RUNNER_TEMP'' | Wurzel für das entpackte Bundle | ohne die Variable entfällt die Bundle-Suche | | ''PYGRADER50_CONFIG_DIR'' | zusätzlicher Config-Ort, nur lokal | nein | ==== Ausgabe ==== ^ Datei ^ Inhalt ^ | ''./result.json'' | ''classroom50/result/v1'', vom Runner ans Release gehängt | | ''./release-body.md'' | Feedback-Tabellen, Release-Text und Job-Summary | ==== Exit-Codes ==== ^ Code ^ Bedeutung ^ | ''0'' | Bewertung abgeschlossen — **auch** wenn alle Tests scheitern | | ''1'' | Infrastrukturfehler: Umgebung fehlt, Konfiguration kaputt, Absturz. Der Runner zeichnet die Abgabe als ''error'' auf | Eine fehlende Bewertungs-Konfiguration ist **kein** Fehler: 0/0 mit Warnung im Log, Exit 0. ==== Lokal ausprobieren ==== cd /pfad/zum/studi-repo CLASSROOM=m320-ix25 \ ASSIGNMENT=m320-lu04-a4-objektkommunikation \ SUBMISSION_TAG=submit/2026-08-13T12-00-00Z-abc1234 \ OWNER=anna \ python -m pygrader50 cat release-body.md ===== python -m pygrader50.moodle — übertragen ===== Liest ''scores.json'' aus dem Config-Repo und schickt die Punkte an Moodle. Je Kombination aus Aufgabe und Person geht die **neueste** Abgabe raus. python -m pygrader50.moodle SCORES [Optionen] ==== Argumente und Optionen ==== ^ Option ^ Wirkung ^ | ''SCORES'' | Pfad zu ''/scores.json'' (Pflicht) | | ''--assignment SLUG'' | nur diese Aufgabe | | ''--user LOGIN'' | nur diesen GitHub-Login | | ''--state PFAD'' | Zustandsfile; ohne Angabe wird jedes Mal alles übertragen | | ''--force'' | auch Unverändertes erneut senden | | ''--dry-run'' | nur anzeigen, nichts senden; funktioniert ohne Zugangsdaten | | ''--no-feedback'' | ohne Feedback-Text (spart einen API-Aufruf pro Abgabe) | Der bei ''--user'' angegebene Login muss im Moodle-Kurs eingeschrieben sein. Der eigene Lehrer-Account ist es meist nicht — Moodle antwortet dann ''No matching assignment found'', obwohl die Aktivität existiert. ==== Umgebung ==== ^ Variable ^ Bedeutung ^ | ''MOODLE_URL'' | Basis-URL der Moodle-Instanz, z.B. ''https://moodle.bzz.ch'' | | ''MOODLE_TOKEN'' | Webservice-Token | | ''MOODLE_FUNCTION'' | Vorgabe ''mod_externalassignment_update_grade'' | | ''GH_TOKEN'' / ''GITHUB_TOKEN'' | optional, liest die Release-Bodies für den Feedback-Text | Ohne ''--dry-run'' sind ''MOODLE_URL'' und ''MOODLE_TOKEN'' Pflicht. ==== Zustandsfile ==== { "schema": "pygrader50/moodle-state/v1", "entries": { "m323-lu01-a02-imperativer-ggt/graphics80": { "submission": "submit/2026-08-13T08-41-09Z-35bdcb2", "score": 5, "max-score": 7 } } } Übersprungen wird nur, wenn Abgabe **und** Punktzahl identisch sind — eine Nachbewertung derselben Abgabe geht also erneut raus. Nur erfolgreiche Übertragungen werden vermerkt; gescheiterte versucht der nächste Lauf wieder. ==== Exit-Codes ==== ^ Code ^ Bedeutung ^ | ''0'' | alles übertragen oder übersprungen | | ''1'' | mindestens eine Übertragung scheiterte, oder Zugangsdaten/Datei fehlen | Ein Fehler bricht den Lauf **nicht** ab: die übrigen Abgaben gehen trotzdem raus. ==== Beispiele ==== # Trockenlauf über alles, ohne Zugangsdaten python -m pygrader50.moodle /scores.json --dry-run --no-feedback # Eine einzelne Person nachtragen python -m pygrader50.moodle /scores.json --user anna # Eine Aufgabe nach einer Nachbewertung komplett neu schicken python -m pygrader50.moodle /scores.json --assignment --force # Wie der Nachtlauf python -m pygrader50.moodle /scores.json \ --state /moodle-state.json Ohne lokalen Klon des Config-Repos reicht die Datei allein: gh api repos//classroom50/contents//scores.json \ -H 'Accept: application/vnd.github.raw' > scores.json GH_TOKEN=$(gh auth token) python -m pygrader50.moodle scores.json --dry-run ==== Von Hand auslösen ==== gh workflow run moodle-sync.yaml --repo /classroom50 \ -f classroom= -f dry_run=true Oder im Config-Repo unter **Actions → Moodle Sync → Run workflow**. Bleibt das Klassenzimmer leer, laufen alle Ordner mit einer ''scores.json''. ===== scripts/remove-legacy-classroom-yml.sh — migrieren ===== Entfernt den alten GitHub-Classroom-Workflow aus den Template-Repos einer migrierten Klasse und aus den bereits angenommenen Studi-Repos. scripts/remove-legacy-classroom-yml.sh [--apply] ^ Aufruf ^ Wirkung ^ | ohne ''--apply'' | Trockenlauf, listet jedes Ziel als ''would rm'' oder ''absent'' | | mit ''--apply'' | löscht ''.github/workflows/classroom.yml'', ein Commit pro Repo | Die Zielliste kommt aus dem ''template''-Block von ''/assignments.json'', **nicht** aus dem Repo-Listing der Template-Organisation. Wiederholbar; fehlende Dateien sind ''absent'', kein Fehler. Exit 1, sobald ein Repo scheitert. Hintergrund und die nötige Token-Rotation: [[.:migration|Migration]]. ===== scripts/sync-template-pins.py — Pins aktualisieren ===== Hebt die Werkzeug-Versionen in den Template-Repos einer Klasse und legt ''.python-version'' an. scripts/sync-template-pins.py [--apply] ^ Was ^ Verhalten ^ | ''pylint'', ''pytest'' | werden überall gesetzt, fehlende Zeilen angehängt | | ''httpx'', ''pytest-asyncio'' | nur dort gehoben, wo sie schon stehen | | alles andere | bleibt Zeile für Zeile erhalten, inkl. Kommentaren | | ''.python-version'' | wird auf die konfigurierte Version gesetzt | Die Versionen stehen als Block am Kopf des Skripts und werden einmal pro Semester angefasst. Wiederholbar: ein aktuelles Template meldet ''ok''. ''pytest-asyncio'' muss mit ''pytest'' mitziehen — 0.23.8 und pytest 9 lassen sich nicht gemeinsam auflösen. Pakete, welche die Lernenden im Rahmen der Aufgabe selbst eintragen sollen (Flask in den lu06-Aufgaben), gehören **nicht** in die Pin-Liste. ===== scripts/publish-wiki.py — diese Seiten aktualisieren ===== Spiegelt den Ordner ''wiki/'' aus dem Repo hierher. Pfad = Seiten-ID: ''wiki/howto/git/classroom50/start.txt'' wird zu ''howto:git:classroom50:start''. scripts/publish-wiki.py [--apply] [--summary "Text"] Geschrieben wird nur, was abweicht — ein erneuter Lauf meldet alles als ''gleich'' und hinterlässt keine leeren Versionen. Zugangsdaten kommen aus der Umgebung: ''DOKUWIKI_TOKEN'', sonst ''DOKUWIKI_USER'' und ''DOKUWIKI_PASSWORD''. Ein untauglicher API-Token wird **nicht abgelehnt, sondern ignoriert** — die Aufrufe laufen dann als Gast weiter und scheitern erst beim Schreiben mit einer 401, die wie ein falsches Passwort aussieht. Das Skript prüft deshalb vor dem Schreiben per ''core.whoAmI'', als wer es angemeldet ist, gibt das aus und fällt bei totem Token auf die Anmeldung per ''core.login'' zurück. ===== gh teacher — Klassenzimmer verwalten ===== gh extension install foundation50/gh-teacher gh auth refresh -h github.com -s admin:org,read:org,repo,workflow gh teacher classroom list gh teacher autograder show gh teacher autograder list gh teacher autograder set-default --from bootstrap/autograder.py gh teacher rotate-service-token Vollständige Referenz: [[https://github.com/foundation50/classroom50/wiki|Wiki von foundation50/classroom50]].