canvasser
Automation for UF Canvas (ufl.instructure.com) that navigates the site as a person
would, rather than through the REST API.
That choice is deliberate. UF discontinued API token support after Instructure began capping non-admin tokens at 30 days, and advises retiring token-based scripts. Browser automation is the better-supported path for this institution.
Status: the round trip works, live. Dates pulled to CSV, edited in a spreadsheet, and written back to Canvas — verified against a real course, for both plain assignments and classic quizzes. Writing is opt-in (
--commit) and every write is read back and checked.
What it does
A round trip: pull dates to a CSV, edit them in a spreadsheet, push them back.
canvasser status # is the stored session still authenticated?
canvasser login # authenticate (GatorLink + Duo)
canvasser courses # list courses, with ids
canvasser settings 580777 # course details, sections, navigation
canvasser pull 580777 # assignment dates -> CSV
canvasser push dates-580777.csv # show what would change; writes nothing
canvasser push dates-580777.csv --commit # actually write it
push previews by default. Running it repeatedly while editing a sheet cannot touch the
course; only --commit writes.
Requirements
- Python 3.10+ (developed and tested on 3.13)
- A UF GatorLink account with Duo MFA
- No display required — runs headless
Install
pip install canvasser
playwright install chromium # on Linux: playwright install --with-deps chromium
The second line is not optional and is not automatic. pip installs the Playwright
library; the browser it drives is a separate download. Running canvasser without it
fails immediately and says exactly this.
From a checkout instead
python3 -m venv .venv
./.venv/bin/pip install -e .
./.venv/bin/python -m playwright install --with-deps chromium
Where it keeps things
The saved session, browser profile, and optional secrets file share one per-user directory — never the working directory, so nothing credential-bearing can be committed by accident:
%LOCALAPPDATA%\canvasser |
Windows |
~/Library/Application Support/canvasser |
macOS |
$XDG_STATE_HOME/canvasser |
otherwise — usually ~/.local/state/canvasser |
Set $CANVASSER_HOME to put it somewhere else; that is the only thing that overrides the
platform default. Use it to keep state on a durable or encrypted volume, or to run two
identities side by side.
Credentials
Resolved per field, first source that has them wins:
--secrets-file PATH(--usernamesupplies the name)$GATORLINK_USERNAME/$GATORLINK_PASSWORDcanvas.envin the state directory above (mode600)- an interactive prompt
There is deliberately no --password flag. OpenSSH is the model: argv is not private —
it lands in shell history and /proc/<pid>/cmdline is world-readable. For scripted runs,
sshpass answers the prompt, exactly as it does for ssh:
sshpass -f ~/.canvas-pw canvasser --username jjb pull 580777
Prompts read /dev/tty, not stdin, so this works even when stdin is a pipe. --no-prompt
disables prompting entirely for unattended use; -v reports which source each value came
from, never the value itself.
Duo
login sends a Duo push to your phone — have it in hand. Duo's Verified Push shows a
number that must be tapped; canvasser prints it and re-reads it every 5 seconds in case the
push is re-sent. --factor passcode reads a 6-digit code instead.
If you receive a Duo push you were not expecting, deny it.
Your MFA stays intact: the stored password is something you know, the phone is something you have. No TOTP seed is stored anywhere. Once Duo remembers the device (~10 hours), runs need no interaction at all.
Selecting a course
courses and pull share three independent scope axes:
| Axis | Default | Flags |
|---|---|---|
| enrollment | both active and archived | --active / --archived |
| publish state | any | --published / --unpublished |
| favorite | favorites only | --favorite / --unmarked; --all opens every axis |
Naming both sides of an axis unions them — --active --archived is every enrollment,
which is what the words say. Favorites is the one deliberately narrowed default — it is the list you curate in Canvas
itself, via the star on the Courses page. Canvas's own "Current Enrollments" is not a useful
definition of current: on a long-lived account it is mostly sandboxes and dev shells.
pull takes a course id or a name fragment, and an ambiguous fragment is an error rather
than a guess. Omit it entirely for an interactive picker:
canvasser pull 580777
canvasser pull "Comp Engr Design"
canvasser pull --course-file ~/current-class.txt
CANVASSER_COURSE=580777 canvasser pull
canvasser pull # numbered picker
The datesheet CSV
# canvasser datesheet v3.1,,course=580777,timezone=Eastern Time (US & Canada),,,,iana=America/New_York,,
Assignment Details,,,,unlock_at,,due_at,,lock_at,
assignment_id,override_id,title,assign_to,open_date,open_time,due_date,due_time,close_date,close_time
7289050,,01 - Equipment Demonstration,Everyone,2026-08-21,00:00,2026-08-28,23:59,2026-09-02,23:59
Three date fields, each split into a date column and a time column so a spreadsheet can
bulk-shift them: open_* is Canvas's unlock_at ("Available from"), due_* is due_at,
close_* is lock_at ("Until").
- Only
assignment_idis required. Delete any other column, any row, and both preamble rows — the file still reads. Columns are matched by name, so order does not matter either. - An absent column means "leave this alone"; an empty cell means "clear it". Deleting the
close_*columns will not wipe your lock dates. - Rows are matched on
(assignment_id, override_id), never on title — renaming an assignment in the spreadsheet cannot retarget a write. - The timezone is declared once, in row 1, twice over:
timezone=is Canvas's familiar name for people,iana=is the identifierpushresolves wall clocks through. Values themselves carry no offset. iana=says what zone the sheet's own times are written in — nothing more. If it is not the course's zone,pushconverts every value into course time before comparing or writing, preserving the instant, and prints what it did. A sheet edited in Tokyo saying2026-08-29 12:59and a New York course holding2026-08-28 23:59are the same deadline. Ifiana=is missing, the times are taken to be course-local already.- Times are minute-only. Canvas's time box has no seconds field, so seconds cannot be
written;
11:59 PMis what a person types and Canvas applies its own:59. Seconds you type are accepted and dropped. - Dates and times are read forgivingly, because spreadsheets reformat them:
2026-02-01,2/1/2026,1 Feb 2026,2026年2月1日;23:59,11:59 PM,2359. A numeric date that could be US or European is resolved US-first and reported; one that is real in only one order is read that way silently; one that is real in neither is refused.
What push refuses to do
Each of these is refused because it is untested, not because it is hard:
- assignments with per-section or per-student overrides — saving Canvas's edit form submits every date card, so a wrong move there would silently delete a student's accommodation date;
- clearing a date;
- a time that does not exist, in the hour skipped by a daylight-saving change;
- a date with no time, when the sheet's zone differs from the course's — converting would have to assume a time, and a different assumed time lands on a different date.
After writing, push re-reads the assignment's own page state and compares date and time,
so a save that silently did not take is reported rather than assumed.
Data handling
This repository is public.
- Credentials never live in the repo. They belong in the state directory described under "Where it keeps things", which is deliberately outside any working tree.
storage_state.json(the saved session) is credential-equivalent — it grants Canvas access with no password.- The browser profile holds live session cookies and is as sensitive as the password.
- Debug snapshots render real Canvas pages, which can include student data. They are written outside the repository on purpose.
- Pulled CSVs are gitignored: they will contain per-student rows once individual overrides are in scope.
License
GPL-3.0-or-later. See LICENSE.
This program is free software: you may redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It comes with absolutely no warranty.
Scope: this is a UF tool
CANVAS_BASE_URL is overridable, but the login path is not: the SSO deep link
(/login/saml/355) and the identity provider (login.ufl.edu) are UF's, and the second
factor assumes Duo. Another institution's Canvas will read fine only if you can already
authenticate to it some other way — sign-in is not portable as written.
Release files for canvasser 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| canvasser-0.1.0.tar.gz | 84.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| canvasser-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 174.8 kB
Release files / canvasser-0.1.0.tar.gz
| Download URL | canvasser-0.1.0.tar.gz |
|---|---|
| Size | 84.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ccdb26742742e60a2a28c038124f080a93d6ea8e5b3508ea29ef790cfd57735
|
|
BLAKE2b-256 checksum How to use checksums |
8b1948189fdfcd7f7185659003440368d37e182b1b6e7c3ae57d9bd1f389cff5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / canvasser-0.1.0-py3-none-any.whl
| Download URL | canvasser-0.1.0-py3-none-any.whl |
|---|---|
| Size | 90.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
580268627d964f0ca6623584647aa155a745a2fa846b4bee9406aab36a42ee0b
|
|
BLAKE2b-256 checksum How to use checksums |
1842114a5a95018cd396666e7ebafe0000468380d0a30b985d9a38b0f07fe3c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|