Usage¶
Install (editable)¶
pip install -e .
Install into per-user venv¶
Linux/macOS:
make install
~/.course_venv/bin/course
Windows:
make.bat install
%USERPROFILE%\.course_venv\Scripts\course.exe
Run¶
course
Full CLI flag reference is available in CLI Reference.
Classroom50 (foundation50)¶
Wraps Classroom50 instructor tooling (gh teacher) for roster list/sync/export.
Does not reimplement Classroom50. Requires GitHub CLI and the Classroom50 teacher
extension.
A few adapter constants mirror rules enforced by the extension itself – the assignment
slug shape, the flags refused alongside --empty-repo, and the roster CSV header – so
this surface is coupled to the installed version. It was verified against gh teacher
v1.45.0, which is pinned rather than tracking latest. --classroom50-preflight checks
only that the extension exists, not which version it is. The couplings that can drift on an
upgrade are listed under “Version coupling with gh teacher” in the README.
course --classroom50-preflight
course --list-classroom50-classrooms --classroom50-org my-org
course --list-classroom50-roster --classroom50-org my-org --classroom50-classroom short-name
course --list-classroom50-assignments --classroom50-org my-org --classroom50-classroom short-name
course --sync-classroom50 -sc50 --classroom50-org my-org --classroom50-classroom short-name
course --export-classroom50-roster classroom50_roster.csv
Download of student submissions is available only on the full human course CLI:
course --download-classroom50 --classroom50-org my-org --classroom50-classroom short-name \
--classroom50-assignment assignment-slug --classroom50-download-dest ./c50_submissions
See the Classroom50 section in CLI Reference for all flags.
Agent-safe entrypoints¶
Dedicated modules force agent mode (COURSE_AGENT_MODE=1) and expose a
restricted surface for AI agents. Destructive or high-blast-radius operations
are refused; use the interactive course CLI as a human when those are needed.
Module |
Allowed (typical) |
Refused (examples) |
|---|---|---|
|
preflight, list-*, sync, export |
download |
|
preflight, list-assignments/members, search-user, sync |
unenroll, grade, invite, announce, download, messages, pages |
|
preflight, list-courses/students, sync |
unenroll, grade, download |
|
search, details, list-, export-, count |
modify, restore-db, import-apply, delete |
Examples:
python -m course_hoanganhduc.c50_agent preflight
python -m course_hoanganhduc.c50_agent list-classrooms --org my-org
python -m course_hoanganhduc.c50_agent sync --org my-org --classroom short-name --db students.db
python -m course_hoanganhduc.canvas_agent list-members --course-id 12345
python -m course_hoanganhduc.gclass_agent list-courses
python -m course_hoanganhduc.db_agent search "Nguyen"
Allowlists (agent mode, fail-closed when a course/org id is required):
CLASSROOM50_ORG_ALLOWLIST— Classroom50 org short-namesCANVAS_COURSE_ALLOWLIST— Canvas course IDsGCLASS_COURSE_ALLOWLIST— Google Classroom course IDsGCLASS_ACCOUNT_ALLOWLIST— exact primary emails for the narrow Classroom administrator smoke-test path
Comma-separated values. Empty allowlist with a required id fails closed.
Skills in ai-agents-skills (profile course-management) route through these
modules: classroom50, course-canvas, course-google-classroom,
course-db.
Clear stored settings¶
course --clear-config
course --clear-credentials
course -ccfg
course -ccred
Tip: --google-credentials-path and --google-token-path copy the files into
the default config folder with standard filenames, even if you only set them in
a separate command before running --sync-google-classroom.
Configuration and file locations¶
The tool reads settings from config.json stored in a course-specific folder
determined by .course_code. On first run, you will be prompted for a course
code (for example, MAT3500) and it will be cached in .course_code.
When you load a config file with --config, it is copied into the default
config folder as config.json.
Sample config: sample/config/config.sample.json.
Default config locations:
Windows:
%APPDATA%\course\<course_code>\config.jsonmacOS:
~/Library/Application Support/course/<course_code>/config.jsonLinux:
~/.config/course/<course_code>/config.json
Credential and token files live in the same folder by default:
credentials.json(Google service account)token.pickle(Google OAuth tokens)
You can override paths via CREDENTIALS_PATH and TOKEN_PATH in the config
file.
When you provide Google Classroom credentials/token paths via CLI or the menu,
the files are copied into the default folder with the standard names.
You can also set GOOGLE_CLASSROOM_COURSE_ID in config.json to skip the
course selection prompt.
Final evaluation weights can be configured via WEIGHT_CC, WEIGHT_GK,
and WEIGHT_CK in config.json. The weights must sum to 1.0.
The organization field in VCF exports can be customized via UNIVERSITY_NAME
in config.json.
OCR dependencies and setup¶
Local OCR and PDF conversion require system tools. Install them before using
--ocr-service tesseract or features that convert PDF pages to images.
Windows (PowerShell):
winget install -e --id UB-Mannheim.TesseractOCR
winget install -e --id oschwartz10612.Poppler
macOS (Homebrew):
brew install tesseract poppler
Linux:
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y tesseract-ocr poppler-utils
# Fedora
sudo dnf install -y tesseract poppler-utils
# Arch
sudo pacman -S tesseract poppler
Verify:
tesseract --version
pdftoppm -h
Note: Post-OCR AI refinement is disabled; improve scan quality or switch OCR engines if text quality is poor.
Canvas and Google Classroom setup¶
Populate the following keys in config.json (or load from a JSON file with
--config):
CANVAS_LMS_API_URLCANVAS_LMS_API_KEYCANVAS_LMS_COURSE_IDGOOGLE_CLASSROOM_GRADE_CATEGORY_METHODCREDENTIALS_PATHTOKEN_PATHGOOGLE_SHEET_URLGOOGLE_SHEET_LECTURER_TOPICS_URLGOOGLE_SHEET_STUDENT_REGISTRATION_URLINTERNSHIP_REGISTRATION_SHEET_URL
Canvas operations will use these defaults unless overridden by flags like
--canvas-course-id.
Common workflows¶
Sync Canvas roster into the local database:
course --sync-canvas
course -sc
Notes:
Canvas sync now stores submission comments and rubric evaluations per assignment in the database.
List auto-generated CLI short aliases:
course --list-cli-aliases
course -lca
Google Classroom workflows¶
Administrator command names¶
Installing the package creates two console commands. course remains the main,
legacy course-management CLI; Google Classroom assignment creation is deliberately
isolated in course-gclass-admin. The administrator console command and its Python
module invocation are equivalent:
course-gclass-admin --help
python3 -m course_hoanganhduc.gclass_admin_cli --help
When the per-user virtual environment installed by make install is not activated,
use either executable from that environment explicitly:
~/.course_venv/bin/course-gclass-admin --help
~/.course_venv/bin/python -m course_hoanganhduc.gclass_admin_cli --help
Create an assignment (interactive administrator)¶
Assignment creation uses the dedicated course-gclass-admin command. It does not
reuse the legacy CLI’s broad scopes, token.pickle, or
GOOGLE_CLASSROOM_CREDENTIALS/GOOGLE_CLASSROOM_TOKEN variables, and
gclass_agent explicitly refuses this operation.
Start with an offline preview. This reads only the assignment spec; it does not resolve or inspect credential paths, require an account, open OAuth, write a file, or call Google:
course-gclass-admin create-assignment \
--course-id d:my-course \
--spec sample/google_classroom/assignment-minimal.sample.json \
--dry-run
The supported execution modes have intentionally different safety boundaries:
Mode |
Credentials and network |
Confirmation and permitted result |
|---|---|---|
|
None; parses only the local assignment spec. |
No prompt. Produces the canonical operation preview and makes no mutation. |
Interactive creation |
Verifies the OAuth account and active canonical course. |
One readable |
|
Requires an existing coursework token, never opens a browser, and refreshes the token automatically when needed. |
No prompt. Limited to |
Agent-safe draft |
Requires an existing token, explicit agent mode, and exact account/course allowlists; never opens a browser. |
A separately approved digest permits only the minimal draft smoke-test shape, with duplicate detection and strict read-back. |
Authorize the exact primary email address once. The initial Google browser consent is
still necessary because it grants the application the coursework-write,
course-lookup, and identity scopes; it is Google’s authorization boundary, not a
package confirmation. The CLI no longer asks the user to type AUTH <email>,
CREATE <digest>, or
SHARE <digest>. Normal creation has only the readable y/N prompt shown above,
while token replacement is selected explicitly with authorize --replace-token.
Subsequent creation commands reuse the stored token. If it is expired and has a refresh token, the command refreshes it before contacting Classroom and stores the updated JSON atomically; no browser is opened. A revoked, non-refreshable, or otherwise invalid token fails closed and must be replaced explicitly:
course-gclass-admin authorize --account teacher@example.edu
course-gclass-admin create-assignment \
--account teacher@example.edu \
--course-id d:my-course \
--spec sample/google_classroom/assignment-minimal.sample.json
A host with no browser needs nothing extra: authorize finds no browser to open,
starts no loopback listener, and prints the consent URL to open on a machine that has
one. That browser then fails to load http://127.0.0.1:8911/, which is expected;
its address bar holds the redirect. Paste that URL back at the hidden prompt in the
same terminal and the command exchanges it for the token. --no-open-browser takes
the same path on a host that does have a browser.
The paste is checked for the exact http://127.0.0.1:8911/ boundary and one
nonempty OAuth code and state before it is used, a redirect from a different run is
refused by the state it carries, and a malformed paste is asked for again rather than
ending the run. It stays out of shell history and process arguments because the prompt
hides it. Never reuse an earlier redirect URL; its code is single-use.
complete-loopback --port PORT remains for the other case: an authorization that is
already listening here, started with a browser that then could not reach this host.
Read the port from the redirect URL and deliver it from a second terminal:
course-gclass-admin complete-loopback --port PORT
Interactive creation displays one control-character-safe summary containing the
verified account, canonical course name and ID, assignment title, release mode,
material/rubric counts, and Drive-sharing modes, then asks Create assignment?
[y/N]. It resolves a course alias with courses.get and sends the canonical
course ID to the create endpoint. The exported assignment state machine performs the
same d:/p: alias resolution before its first write.
For trusted local automation with a pre-existing coursework token, --yes skips
that prompt only when the frozen plan is a DRAFT with no Drive-sharing effect:
course-gclass-admin create-assignment \
--account teacher@example.edu \
--course-id 123456789012 \
--spec sample/google_classroom/assignment-minimal.sample.json \
--yes
Published, scheduled, and Drive-sharing assignments remain interactive. Agent mode is refused in both normal paths.
A separately scoped smoke-test path exists for a cooperative, explicitly authorized
agent. The ordinary gclass_agent entrypoint continues to refuse creation. The
smoke-test path accepts only a minimal no-material/no-rubric draft and requires an
existing token, explicit agent mode, exact account/course allowlists, a canonical
course ID, --yes, and an approval digest. First prepare the envelope:
COURSE_AGENT_MODE=1 \
GCLASS_ACCOUNT_ALLOWLIST=teacher@example.edu \
GCLASS_COURSE_ALLOWLIST=123456789012 \
course-gclass-admin prepare-agent-safe-draft \
--account teacher@example.edu \
--course-id 123456789012 \
--spec sample/google_classroom/assignment-test-draft.sample.json
Preparation may refresh the isolated token and metadata, but it performs no Google
Classroom mutation. It is not authorization. After a user or trusted harness approves
the exact emitted envelope, run create-assignment with the same inputs plus
--agent-safe-draft --yes --expect-approval-digest DIGEST. The command serializes
the all-state duplicate scan through read-back with a per-token lock. It reuses one
identical developer-associated draft, blocks every collision, and never retries an
ambiguous create or failed read-back.
These checks prevent accidental use through the restricted entrypoint but are not an OS security boundary: code running under the same user can import Python functions, alter environment variables, and access that user’s token files. Put credentials behind a separate OS identity, keychain, or user-presence broker if hostile same-user agents are in scope.
Live safety and terminal outcomes¶
Before either administrator create path writes coursework, it requests only
id,name,courseState for the course and requires an explicit ACTIVE state. A
missing state fails closed. The interactive path resolves d:/p: aliases to
Google’s canonical ID; the agent-safe path forbids aliases and verifies that the
lookup returns the exact allowlisted canonical ID. The agent-safe path additionally
projects only the fields required for its all-state duplicate scan, create receipt,
and final fetch. It accepts a result only when the
stored item is a developer-associated DRAFT whose title, optional description,
assignment type, all-student targeting, submission-modification mode, empty materials,
and absent schedule, deadline, points, topic, and grading period exactly match the
approved shape. The success receipt is emitted only after that fetch-by-ID check.
Mutation calls use execute(num_retries=0) to disable
google-api-python-client retries. The dedicated one-shot HTTP transport also
disables response-triggered OAuth replay, connection/read/status retries, and redirect
following. Thus a Classroom mutation is sent at most once by the supported transport;
normal access-token refresh may still occur before that request.
An ambiguous transport failure, HTTP 408/429/5xx response, malformed successful create
response, or missing created ID is reported as error: outcome_unknown with exit
status 3. This is terminal: inspect Classroom before deciding whether to issue a new
command, because create has no caller idempotency key. A create whose ID is known but
whose response or strict read-back cannot be validated is reported as
error: partial_create with exit status 4 and the recovery coursework ID when
available. Neither case is automatically retried or deleted.
Assignment spec¶
The spec is a strict UTF-8 JSON object. Duplicate keys, unknown fields, symlinks, non-regular files, and files over 1 MiB are rejected. The stable REST v1 assignment fields map as follows:
JSON field |
Accepted value and behavior |
|---|---|
|
Required UTF-8 text, 1–3000 characters, not whitespace-only. |
|
Optional UTF-8 text up to 30,000 characters; omit or use |
|
|
|
Optional timezone-aware RFC 3339 timestamp at least 60 seconds in the future. |
|
Optional timezone-aware RFC 3339 timestamp in the future and, if scheduled,
later than |
|
Optional non-negative integer. Zero means ungraded. |
|
Optional array of at most 20 writable materials. Omitted, |
|
Optional object. |
|
|
|
Optional existing topic ID. |
|
Optional object with |
|
Optional inline scored or unscored rubric; omit or use |
Writable material forms are deliberately explicit:
{
"materials": [
{"type": "drive_file", "file_id": "DRIVE_ID", "share_mode": "VIEW"},
{"type": "link", "url": "https://example.edu/guide"},
{"type": "youtube", "video_id": "VIDEO_ID"}
]
}
Drive share_mode may be VIEW, EDIT, or STUDENT_COPY. This command
attaches an existing Drive file ID; it does not upload or search Drive. Link inputs
must be absolute HTTPS URLs without embedded credentials. Google Forms, Gemini Gems,
and NotebookLM notebooks are read-only material variants in stable REST v1 and are
therefore rejected on create.
An inline rubric has scoring_mode SCORED or UNSCORED and a criteria
array. A rubric allows 1–50 criteria and 1–10 levels per criterion. In a scored
rubric every level has finite, non-negative, distinct points within its criterion
and levels are ordered monotonically by points. In an unscored rubric every level
omits points and has a non-empty title. A spreadsheet-sourced rubric and preview
learning goals are intentionally outside this stable surface.
See sample/google_classroom/assignment-full.sample.json for every supported
field. Replace its IDs and timestamps before use. The implementation follows the
CourseWork resource,
Material resource,
and rubric limitations.
Rubric creation is staged: create the assignment as a draft, create its rubric, then publish or schedule only if the rubric succeeded. Stable rubric mutation can also depend on Google Workspace for Education licensing. If a later rubric or release stage fails, the command reports the surviving assignment ID and does not delete it automatically. Scheduled-time lead and due-time constraints are checked again immediately before the staged release; if time elapsed during draft and rubric creation makes them stale, the assignment remains a draft and its recovery IDs are reported.
Coursework credentials¶
Create a Desktop-app OAuth client in Google Cloud, enable the Classroom API, and add this fixed scope set to the consent configuration:
https://www.googleapis.com/auth/classroom.coursework.studentshttps://www.googleapis.com/auth/classroom.courses.readonlyhttps://www.googleapis.com/auth/userinfo.email
The identity scope exposes only the authenticated account’s primary email. The
broader Classroom profile.emails scope is deliberately not requested. Google can
additionally report the OpenID Connect openid and email identity scopes. The
command accepts only those documented additions while still requiring every
requested scope and rejecting every unrelated extra scope. The token records the
actual reported grant set; tokens without valid grant evidence must be reauthorized.
Credential resolution uses the first applicable source:
--credentials/--tokencommand flags. Paths must be absolute and outside Git worktrees. A custom client path requires a token path; token-only mode is allowed for an already-authorized token.COURSE_GCLASS_CREDENTIALS/COURSE_GCLASS_COURSEWORK_TOKENenvironment variables, with the same pairing rule.Platform defaults:
Linux:
$XDG_CONFIG_HOME/course/google-classroomor~/.config/course/google-classroom.macOS:
~/Library/Application Support/course/google-classroom.Windows path resolution uses
%APPDATA%\\course\\google-classroom, but native Windows authorization and mutation currently fail closed until ACL and reparse protections have native verification.
The default OAuth client is credentials.json. Tokens are JSON files under
tokens/ named with the first 20 hexadecimal characters of the expected account’s
SHA-256 digest. Metadata contains only hashes/fingerprints and timestamps. The code
does not inspect GOOGLE_CLASSROOM_CREDENTIALS, GOOGLE_CLASSROOM_TOKEN, or
token.pickle for this workflow.
On POSIX systems, every client/token/metadata file must be a regular, non-symlink file owned by the current user with no group or other permissions. Credential path components must be owned by the current user or root and must not be writable by other users (root-owned sticky temporary directories are the one exception). Set permissions before authorization:
chmod 700 ~/.config/course ~/.config/course/google-classroom
chmod 600 ~/.config/course/google-classroom/credentials.json
Tokens are written atomically with mode 0600 while a nonblocking per-token lock
is held. Input OAuth and token endpoints are ignored and replaced with Google’s
canonical HTTPS endpoints; service-account files are rejected. OAuth uses PKCE and a
random 127.0.0.1 loopback port with a bounded wait. Authorization explicitly
requests offline consent. A fresh or replacement authorization is stored only if
Google returns a durable refresh token; otherwise an existing token remains
unchanged. Dependency loggers that can emit the callback request or token response
are suppressed for the duration of the interactive exchange. The authenticated
Google OAuth UserInfo primary email must be verified and exactly match --account
before a token is stored or a course is inspected.
Inspect resolution and file safety without network access, refresh, OAuth, or writes:
course-gclass-admin auth-status --account teacher@example.edu
course-gclass-admin auth-status \
--account teacher@example.edu \
--credentials /absolute/path/credentials.json \
--token /absolute/path/coursework-token.json \
--show-paths
Without --show-paths, status output identifies files by basename and reports the
selected source (cli, environment, or default) without printing absolute
locations. Add --show-paths only when the full resolved client and token paths are
needed for troubleshooting. Status remains offline and does not prove that Google
currently accepts the token or that the expected account is signed in.
Status distinguishes safe token syntax from local usability. An expired token with no refresh token is reported unusable and returns a nonzero status; offline status does not claim that the account is currently authenticated. Readiness is conservative near expiry and rejects malformed access-only/no-expiry and refresh-only/future-expiry combinations that the online Google credential loader cannot use.
Use authorize --replace-token only when intentionally replacing an existing
token. Without that explicit flag, an existing token is left untouched; replacement
still requires an interactive Google OAuth flow. Existing legacy tokens cannot be
migrated safely and must be authorized afresh.
Command routing and separate credentials¶
course-gclass-admin currently owns assignment preview, coursework authorization
and status, and assignment creation only. The existing course CLI retains course
and student listing, roster sync, grading, unenrollment, and submission/Drive download.
The restricted python -m course_hoanganhduc.gclass_agent entrypoint exposes only
preflight, course/student listing, and roster sync; it continues to refuse creation,
grading, unenrollment, and downloads.
The two credential profiles are intentionally separate and are not interchangeable.
The coursework token uses strict JSON, account-bound storage, and the fixed narrow
scope set documented above; it does not grant the Classroom roster/profile,
Drive-download, topic, or Sheets permissions used by the legacy workflows. The legacy
token.pickle is not accepted by course-gclass-admin.
Offline verification and recorded smoke result¶
From the repository root after make install, the assignment builder, credential
resolver and one-shot transport, administrator CLI, restricted-agent regression,
Classroom50 regression, and CLI parser can be tested locally without contacting
Google:
~/.course_venv/bin/python scripts/test_gclass_coursework.py
~/.course_venv/bin/python scripts/test_gclass_coursework_auth.py
~/.course_venv/bin/python scripts/test_gclass_admin_cli.py
~/.course_venv/bin/python scripts/test_gclass_invite.py
~/.course_venv/bin/python scripts/test_c50_admin_cli.py
~/.course_venv/bin/python scripts/test_course_agents.py
~/.course_venv/bin/python scripts/test_classroom50.py
~/.course_venv/bin/python scripts/test_roster_csv.py
~/.course_venv/bin/python scripts/test_cli_flags.py
~/.course_venv/bin/python -m compileall -q course_hoanganhduc
The current offline run passes 259 tests across those eight unittest scripts, all 223
CLI flag parse cases, and the package compilation check. test_roster_csv.py and
test_gclass_invite.py need no optional dependency and also run under a bare
python3.
The recorded opt-in live smoke test used
sample/google_classroom/assignment-test-draft.sample.json. It created one minimal
DRAFT with no attachment, rubric, schedule, deadline, points, topic, or targeted
students, then fetched it by coursework ID and passed the strict field comparison.
The documentation intentionally omits the account, token location, course ID, and
coursework ID.
Legacy roster, grading, download, and unenroll workflows¶
Sync Google Classroom roster into the local database:
course --sync-google-classroom
course -sgc
Notes:
Canvas and Google Classroom score sync normalizes grades to a 10-point scale when max points are available.
Student ID inference for MAT Excel updates only works with VNU University of Science, Hanoi email format.
GOOGLE_CLASSROOM_GRADE_CATEGORY_METHODcontrols topic/category aggregation (average, sum, weighted; default: average).GOOGLE_CLASSROOM_CC_TOPICS,GOOGLE_CLASSROOM_GK_TOPICS,GOOGLE_CLASSROOM_CK_TOPICSmap CC/GK/CK to Google Classroom topic names (comma-separated).Required Google APIs: Classroom, Drive (submission downloads), Sheets (Google Sheet imports).
If you enable new APIs or scopes, delete
token.pickleor re-run to re-auth.If topic mappings are not set, the tool auto-matches topic names using phrases like “Chuyên cần”, “Giữa kỳ/giữa kì”, “Cuối kỳ/cuối kì”.
When Canvas and Google Classroom grades conflict, reports include both sources and interactive flows prompt which source to use.
Setup quick steps:
Create/choose a Google Cloud project.
Enable APIs: Classroom API, Drive API, Google Sheets API.
Configure OAuth consent screen (external/internal as needed).
Create OAuth client credentials (Desktop app) and download
credentials.json.Place
credentials.jsonand (after first run)token.picklein the config folder or pass paths via CLI.
Grade Google Classroom assignments:
course --grade-google-classroom
course --grade-google-classroom --gc-coursework-id 1234567890 --gc-grade-score 8
Notes:
--gc-include-gradedincludes already graded submissions.--gc-apply-allgrades all listed submissions without selection.
Download latest Google Classroom submissions and run checks:
course --download-google-classroom-submissions
course --download-google-classroom-submissions --gc-download-coursework-id 1234567890 --gc-download-dest-dir ./gclassroom_submissions
Invite local database students to a Google Classroom course. Anyone already enrolled, already a teacher of the course, or already holding a pending invitation is skipped, so re-running the command is a no-op:
course --invite-google-classroom --google-course-id 1234567890
course --invite-google-classroom --gc-invite-domain hus.edu.vn --gc-invite-all
course --invite-google-classroom --gc-invite-section K70A9 --gc-invite-class "Intro to AI"
course --invite-google-classroom --gc-invite-email student1@hus.edu.vn,student2@hus.edu.vn
course --invite-google-classroom --gc-invite-role TEACHER --gc-invite-email ta@hus.edu.vn
course --invite-google-classroom --dry-run
Notes:
Candidates come from the local database only.
--gc-invite-emailfilters that database; an address that is not in it is reported asskipped_not_in_dbrather than invited, so import it first with--add-google-sheetor--add-csv.Without
--gc-invite-allyou pick from a numbered list.Successful invitations record
Google_ID,Google_Invitation_ID, andGoogle_Classroom_Invited_Atin the local database.Google_IDis filled only when empty and is never overwritten; the recorded value is what makes the second run cost nouserProfiles.getcalls.Google_Invitation_IDandGoogle_Classroom_Invited_Atdescribe the latest invitation, so each successful invitation replaces them.A
403is a course-wide policy failure rather than a per-student one, so the run stops there and the remaining candidates are reported asskipped_aborted.A
400 FAILED_PRECONDITIONmeans the student already holds this role or greater, or the account is disabled; it is reported asskipped_preconditionand is not a failure.The Classroom50 half is a separate command; see CLI Reference under Classroom50.
Sheet to both platforms, end to end:
course --add-google-sheet "<SHEET_URL>" # 1. Sheet -> database
course --invite-google-classroom --google-course-id <COURSE_ID> # 2. database -> Classroom
course-c50-admin roster-import --org <ORG> --classroom <SHORT> --db students.db
Step 1 also accepts a plain CSV, which needs no Google account:
course --add-csv roster.csv # 1. CSV -> database
Step 3 is a separate command on purpose: it refuses in agent mode and without a terminal, and it
carries its own confirmation naming both of its effects. Each half is idempotent, so re-running
the one that failed is the whole recovery procedure. roster-import reads the organization
membership before deciding that a matching roster means there is nothing to do, so a run that
died after the roster commit but before the invitations still re-sends them; --force imports
a matching roster outright when that check cannot run.
Unenroll Google Classroom students by email domain:
course --unenroll-google-classroom --gc-unenroll-domain gmail.com
course --unenroll-google-classroom --gc-unenroll-domain gmail.com,outlook.com --gc-unenroll-all
course --unenroll-google-classroom --gc-unenroll-email student1@example.com,student2@outlook.com
course --unenroll-google-classroom --gc-unenroll-select
course --unenroll-google-classroom --gc-unenroll-missing-student-id
course --unenroll-google-classroom --gc-unenroll-domain gmail.com --dry-run
Notes:
Successful unenroll removes matching students from the local database (by Email or Google_ID).
Grade resubmissions (lists assignments that need regrading, excludes Roll Call Attendance, and prompts per student unless default is enabled). When keeping old grade, the newer submission is assigned the most recent graded score from the submission history:
course --grade-resubmission
course --grade-resubmission --keep-old-grade
course -grs
course -grs --keep-old-grade
Update a MAT*.xlsx file with grades from the local database:
course --update-mat-excel MAT3500-3-Toan-roi-rac-4TC.xlsx
course -ume MAT3500-3-Toan-roi-rac-4TC.xlsx
Export a roster to CSV:
course --export-roster
course -ero
Preview an import (no write):
course --preview-import students.xlsx
course -pi students.xlsx
Import students from a Google Sheet URL:
course --add-google-sheet "https://docs.google.com/spreadsheets/d/FILE_ID/edit#gid=0"
course --add-google-sheet "<SHEET_URL>" --dry-run
Notes:
Google Sheets API must be enabled for the project tied to your credentials.
--dry-runreads and merges the sheet exactly as a real import would, prints every field a newer submission would replace, and writes nothing.A row whose student number is not 8 digits is kept, not dropped: the value is stored as
Invalid Student ID,Student IDis left empty, and the student is named. A row that names nobody is dropped, also by name.
Import students from a roster CSV with fixed columns:
course --add-csv roster.csv
course --add-csv roster.csv --dry-run
Notes:
Student IDandNameare required, and so is at least one ofEmailandGitHub Username; without one of those the roster feeds neither platform.GitHub ID,Section, andClassare read when present and every other column is ignored.Header names are matched case- and accent-insensitively, so
Mã sinh viên,Họ và tên,Email,GitHub usernameandLớpall work. A header that matches nothing is refused, and the message lists the columns that were actually read.Rows with no student id or no name are skipped and counted; they do not fail the run.
The file is decoded as UTF-8 (BOM or not), then Windows-1252, then Latin-1, and the fallback encoding is named when one is used.
course-c50-admin roster-import --csvmakes the opposite choice and refuses anything but UTF-8, because a mis-decoded name would be pushed to GitHub.--dry-runpreviews the first rows and writes nothing.This does not replace
--add-file, which still handles Excel, PDF, and theMAT*.xlsxfiles with their own header quirks.
Notes:
MAT*.xlsx imports ignore score columns (CC, GK, CK, totals); only roster fields are imported.
Import internship registrations (Skills/Wishlist):
course --import-internships "https://docs.google.com/spreadsheets/d/FILE_ID/edit#gid=0"
course --import-internships sample/internship_data.csv
course --import-registrations "https://docs.google.com/spreadsheets/d/FILE_ID/edit#gid=0"
course --import-registrations sample/internship_registrations_data.csv
Manage company contact data in companies.db:
course --import-companies "Danh sách công ty liên hệ (không xóa).xlsx"
course --import-companies "https://docs.google.com/spreadsheets/d/FILE_ID/edit#gid=0"
course --export-companies companies_export.xlsx
Notes: - Internship and Company imports support both Vietnamese and English column headers.
List students by email domain:
course --list-email-domain gmail.com,outlook.com
List students with duplicate names (or display names):
course --list-duplicate-names
course --list-duplicate-names --duplicate-name-field google
course --list-duplicate-names --duplicate-name-field canvas --duplicate-name-format csv --duplicate-name-output duplicate_canvas
course --list-duplicate-names --duplicate-name-field "Some Custom Field" --duplicate-name-format json --duplicate-name-output dupes.json
Notes:
--duplicate-name-fieldacceptsname,google,canvas, or a custom field name.--duplicate-name-formatsupportstxt,csv, orjson(default:txt).
List students missing IDs:
course --list-missing-ids
course --list-missing-ids google,canvas --missing-ids-format csv --missing-ids-output missing_ids.csv
List Google Classroom students:
course --list-google-students
course --list-google-students --google-course-id 1234567890
List students by submission status:
course --list-submission-status google:turned_in
course --list-submission-status canvas:submitted
course --list-submission-status google:NEW@Quiz 1
course --list-submission-status google:NEW@Quiz 1,Quiz 2
Notes:
Google Classroom values:
NEW,CREATED,TURNED_IN,RETURNED,RECLAIMED_BY_STUDENT.Canvas values:
UNSUBMITTED,SUBMITTED,GRADED,PENDING_REVIEW,COMPLETE.
Combine listing filters (AND semantics):
course --list-email-domain gmail.com --list-missing-ids student
course --list-email-domain gmail.com --list-submission-status google:turned_in
course --list-missing-ids student --list-submission-status google:NEW
course --list-email-domain gmail.com --list-missing-ids student --list-submission-status google:CREATED
course --list-submission-status canvas:UNSUBMITTED --list-missing-ids student
course --list-email-domain gmail.com --list-submission-status google:TURNED_IN@Quiz 1
course --list-email-domain gmail.com --list-submission-status google:TURNED_IN@Quiz 1,Quiz 2
Notes:
--list-duplicate-namesand Canvas/Google roster listings cannot be combined with other listing flags.Export options (like
--missing-ids-output) are ignored in combined mode; the merged list prints to the console.
Unenroll Canvas students:
course --unenroll-canvas --canvas-unenroll-domain gmail.com
course --unenroll-canvas --canvas-unenroll-email student1@example.com,student2@outlook.com
course --unenroll-canvas --canvas-unenroll-select
course --unenroll-canvas --canvas-unenroll-missing-student-id
course --unenroll-canvas --canvas-unenroll-domain gmail.com --dry-run
Notes:
Successful unenroll removes matching students from the local database (by Email or Canvas ID).
Export an anonymized roster:
course --export-anonymized
course -ean
Generate a weekly workflow template:
course --generate-weekly-workflow
course -gww
Run weekly automation:
course --run-weekly-automation --weekly-assignment-id 123456 --weekly-teacher-canvas-id 987654
course -rwa --weekly-assignment-id 123456 --weekly-teacher-canvas-id 987654
If --weekly-assignment-id is omitted, the tool scans weekly_reports/ to list
assignments already processed and then runs on closed assignments not yet in the reports.
Run weekly automation locally (no GitHub repo needed):
course --run-weekly-local --weekly-assignment-id 123456 --weekly-local-root "C:\\path\\to\\course-folder"
course -rwl --weekly-assignment-id 123456 --weekly-local-root "C:\\path\\to\\course-folder"
Weekly automation guide¶
Local weekly run:
Ensure
students.dbexists in the target folder.Ensure
config.jsonis configured (Canvas API URL/key, course ID, OCR keys if needed).Run
course --run-weekly-localwith--weekly-assignment-idand an optional--weekly-local-rootto choose where reports are stored.Reports and evidence are stored in
weekly_reports/<timestamp>/with astudents.db.bakbackup.If the assignment ID is omitted, the tool processes closed assignments not yet listed in
weekly_reports/.
GitHub Actions weekly run:
Store
students.dbin the repo that hosts the workflow.Configure secrets
CANVAS_API_URL,CANVAS_API_KEY, and optional OCR keys.Generate the workflow via
course --generate-weekly-workflow.The workflow clones the toolkit repo, runs weekly checks, archives artifacts into
weekly_reports/<timestamp>/, removes the toolkit clone, and commits updates.
Course calendar builder¶
Build a course calendar from first-week sessions and export TXT/Markdown/ICS files. If the input file is omitted, the tool prompts for dates and times interactively.
Example input file:
course_code: MAT3508
course_name: Discrete Mathematics
weeks: 15
extra_week: yes
holiday: 2026-01-01
holiday: 2026-02-17
session: 2026-01-05 08:00-10:00 | Room 101 | Lecture
session: 2026-01-07 08:00-10:00 | Room 101 | Lecture
Notes:
- Course code and course name are required for calendar titles. Provide them in the input file, config (COURSE_CODE, COURSE_NAME), CLI (--calendar-course-code, --calendar-course-name), or cache the course code in .course_code.
- Fixed Vietnamese holidays (1/1, 4/30, 5/1, 9/2) are automatically excluded.
- Add lunar holidays such as Tet or Hung Vuong via holiday: lines or interactive input.
- Add unofficial holidays via unofficial_holiday: or extra_holiday: lines (comma-separated dates).
- Holidays are auto-fetched via AI using the default provider.
- A make-up week is added only when holidays skip sessions.
- Start time must be earlier than end time.
- Sample input: sample/calendar/course_calendar_sample_input.txt (triggers a make-up week).
- Unofficial-holiday sample: sample/calendar/course_calendar_with_unofficial_holidays.txt.
- Add unofficial holidays with --calendar-extra-holidays 2026-03-10,2026-04-05.
Usage:
course --build-course-calendar --calendar-input first_week.txt --calendar-course-code MAT3508 --calendar-course-name "Discrete Mathematics"
course --build-course-calendar
course -bcc --calendar-input first_week.txt --calendar-course-code MAT3508 --calendar-course-name "Discrete Mathematics"
course -bcc
Import an existing iCal file into Canvas (requires CANVAS_LMS_API_URL, CANVAS_LMS_API_KEY, CANVAS_LMS_COURSE_ID):
course --import-canvas-calendar-ics course_calendar.ics --skip-duplicates --dry-run
course -icci course_calendar.ics --skip-duplicates -dr
# Use --force to import without duplicate checks.
Outputs: course_calendar.txt, course_calendar.md, course_calendar.ics in the chosen output directory.
Use --calendar-output-dir and --calendar-output-base to customize the location and base filename.
Backup and restore
——————
course --backup-db
course --restore-db
course --backup-config
course --restore-config
course -bd
course -rd
course -bc
course -rc
Data validation report¶
course --validate-data
course -vd
Dry-run mode¶
Preview changes without writing files:
course --update-mat-excel MAT3500-3-Toan-roi-rac-4TC.xlsx --dry-run --export-grade-diff
course -ume MAT3500-3-Toan-roi-rac-4TC.xlsx -dr --export-grade-diff
Student detail sort order (for --all-details and --export-all-details):
course --export-all-details students.txt --student-sort-method first_last
course --export-all-details students.txt --student-sort-method last_first
course --export-all-details students.txt --student-sort-method id
course -E students.txt --student-sort-method first_last
course -E students.txt --student-sort-method last_first
course -E students.txt --student-sort-method id
You can also set STUDENT_SORT_METHOD in config.json (first_last, last_first, id).
Logging¶
Rotating logs are written to the config folder by default. Configure:
LOG_DIRLOG_LEVELLOG_MAX_BYTESLOG_BACKUP_COUNT
Backup retention is controlled by:
DB_BACKUP_KEEPCONFIG_BACKUP_KEEP
A brief per-run summary is appended to run_report.txt in the working directory.
Canvas announcements¶
Create an announcement from a short message (manual input or a TXT file), optionally refine with AI, preview, and post:
course --add-canvas-announcement --announcement-title "Week 3" --announcement-message "Please submit by Friday"
course --add-canvas-announcement --announcement-title "Reminder" --announcement-file announcement.txt --refine gemini
course -aa --announcement-title "Week 3" --announcement-message "Please submit by Friday"
course -aa --announcement-title "Reminder" --announcement-file announcement.txt --refine gemini
Use --dry-run to preview without posting. Omit --refine to post the original text without AI.
Sample inputs/outputs:
- sample/announcements/announcement_input.txt
- sample/announcements/announcement_refined_output.txt
- sample/announcements/announcement_input_vi.txt
- sample/announcements/announcement_refined_output_vi.txt
Override grades¶
Place override_grades.xlsx in the working directory (see sample/overrides/override_grades.xlsx for the format).
Required columns: Mã Sinh Viên or Họ và Tên, plus at least one of CC/GK/CK (order does not matter). STT and Lý do are optional.
Common header aliases are accepted, for example MSSV, Mã SV, Họ tên, Midterm (Giữa kỳ), Final (Cuối kỳ), CC (Chuyên cần), Reason (Lý do).
Non-empty CC/GK/CK cells override computed grades, and Lý do is appended to the final evaluation output when present.
When using Canvas gradebook CSVs, Unposted Final Score is used if Final Score is missing or all-zero for CC/GK/CK.
Assignment-group scores are omitted from the final evaluation report when all component scores are 0.
Final evaluation TXT output includes the weighted formula used for the total score.
AI report refinement¶
Set REPORT_REFINE_METHOD to gemini, huggingface, or local in config.json (requires the corresponding API key for remote providers).
When AI refinement runs, the report includes the default model and the model actually used.
Local LLM settings (defaults to Ollama):
LOCAL_LLM_COMMAND(default:ollama)LOCAL_LLM_MODEL(default:llama3.2:3b)LOCAL_LLM_ARGS(optional extra CLI args)LOCAL_LLM_GGUF_DIR(default:C:\llm, scanned recursively for.gguffiles)
Runtime overrides: --local-llm-command, --local-llm-model, --local-llm-args, --local-llm-gguf-dir.
Installing local AI models (examples):
- Ollama: https://ollama.com/ then run ollama pull llama3.2:3b and set LOCAL_LLM_COMMAND=ollama, LOCAL_LLM_MODEL=llama3.2:3b.
- llama.cpp: build llama-cli (https://github.com/ggerganov/llama.cpp), set LOCAL_LLM_COMMAND to the llama-cli path and LOCAL_LLM_ARGS to include -m <path-to-gguf>.
Use --refine local or set REPORT_REFINE_METHOD=local to use the local model.
AI model verification and listing¶
Verify credentials and connectivity:
course --test-ai
course -tai
Verify the local model:
course --test-ai local
course -tai local
Detect locally installed models (Ollama or llama.cpp compatible):
course --detect-local-ai
course -dla
Test a specific model name:
course --test-ai gemini --test-ai-model gemini-2.5-flash
course -tai gemini -tam gemini-2.5-flash
List available models:
course --list-ai-models gemini
course -lam gemini
When an AI call is rate-limited, the tool retries and may switch to a different available model with similar capabilities.
Submission quality checks¶
Meaningfulness checks can be tuned via config keys:
QUALITY_MIN_CHARSQUALITY_UNIQUE_CHAR_RATIO_MINQUALITY_REPEAT_CHAR_RATIO_MAXQUALITY_VN_CHAR_RATIO_MINQUALITY_ALNUM_RATIO_MINQUALITY_SYMBOL_RATIO_MAXQUALITY_EMPTY_LINE_RATIO_MAXQUALITY_MATH_DENSITY_THRESHOLDQUALITY_LENGTH_RATIO_LOWQUALITY_LENGTH_RATIO_MEDIUMQUALITY_LENGTH_RATIO_HIGH
Troubleshooting¶
If
coursecannot findstudents.db, confirm you are running the command in the intended working directory.If OCR commands are missing, recheck your PATH or reinstall Tesseract/Poppler.
If Canvas/Google Classroom calls fail, verify API keys and course IDs in
config.json.If Classroom50 preflight fails, confirm
ghauth and that the Classroom50 teacher extension is installed.If an agent entrypoint exits with
allowlist_required/not_allowlisted, set the matching*_ALLOWLISTenvironment variable.Agent entrypoints refusing an operation is intentional; use the full
courseCLI as a human for downloads, unenroll, grading, and DB mutations.