Skip to main content

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:

  1. --secrets-file PATH (--username supplies the name)
  2. $GATORLINK_USERNAME / $GATORLINK_PASSWORD
  3. canvas.env in the state directory above (mode 600)
  4. 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_id is 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 identifier push resolves 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, push converts every value into course time before comparing or writing, preserving the instant, and prints what it did. A sheet edited in Tokyo saying 2026-08-29 12:59 and a New York course holding 2026-08-28 23:59 are the same deadline. If iana= 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 PM is 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)

Source distribution for canvasser 0.1.0
File Size Uploaded
canvasser-0.1.0.tar.gz 84.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canvasser 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page