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: in use. A full semester of dates for a real course — 37 assignments, set and cleared — pulled to CSV, edited in a spreadsheet, and written back, ending in No changes. 37 sheet row(s) match Canvas exactly. Plain assignments and classic quizzes both. Writing is opt-in (--commit), and every field is checked three times: the typed values are read back out of the form before saving, Canvas's own error messages are read after, and the stored value is re-read from the page's state afterwards.

Not supported: assignments with per-student or per-section overrides. Saving Canvas's edit form submits every date card, so getting that wrong deletes an accommodation date. It is refused rather than attempted.

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
canvasser install-browser             # fetch Chromium up front (usually automatic)

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

That is the whole install. The first command that needs a browser will notice one is missing and offer to fetch it:

Chromium is not installed; canvasser cannot drive Canvas without it.
Download it now (~150 MB, one time)? [y/N]

Answer y and it continues into the command you asked for. To do it ahead of time, or in a script, run canvasser install-browser (add --with-deps on Linux to pull the system libraries Chromium needs; that part needs root).

Why there is a download at all. The playwright package on PyPI ships the automation library and its driver, not the browser — browser builds are large platform-specific native binaries, not Python, so no PyPI package carries them. Playwright pins an exact build per library version, which is the whole reason page behaviour is reproducible.

The browsers land in a shared per-user cache (~/.cache/ms-playwright and equivalents), so it is once per machine rather than once per virtualenv — verified under plain venvs, pipx, uv tool install, and throwaway uvx environments. Nothing is downloaded without asking: with no terminal attached (cron, CI, a pipe) canvasser declines and prints the command instead.

From a checkout instead

python3 -m venv .venv
./.venv/bin/pip install -e .
./.venv/bin/python -m canvasser install-browser --with-deps

Where it keeps things

The saved session, browser profile, optional secrets file, and any debug snapshots share one per-user directory — never the working directory, so nothing credential-bearing can be committed by accident. (Snapshots render whole Canvas pages, so they can contain student data; the directory is created mode 700.)

%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.
  • The declared zone says what 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.
  • iana= wins, but timezone= is a real fallback. Canvas is Rails, so Eastern Time (US & Canada) is an ActiveSupport::TimeZone name and maps to an IANA zone; if you delete iana= the friendly label still resolves. All 153 of Canvas's labels are recognised, including the ones IANA has since renamed (Kyiv, Greenland, Rangoon), and a label with Canvas's offset pair still attached is accepted. A label that cannot be read is reported, not silently ignored. Note EST and -05:00 deliberately do not resolve: one names half a year, the other says nothing about DST.
  • With no zone declared at all, 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 will not do

  • 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. Untested, so refused outright;
  • 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;
  • anything Canvas itself will reject. Its rules are enforced server-side and reported only on the page, so push both checks what it can up front and reads Canvas's answer back afterwards:
4 row(s) have dates Canvas will not accept:
    7289053: lock_at 2026-10-04 23:59 is before due_at 2026-10-09 23:59 -- Canvas
             refuses this ('Until date cannot be before due date')
    Fix these cells in the sheet; they will be skipped.

Ordering is caught before any page is loaded. Other rules — dates before the term start, for instance — surface as Canvas's own wording after the save attempt. Either way the row is skipped and the rest of the push proceeds.

Clearing a date works: leave the cells empty in a column the sheet carries, and push uses the form's own Clear control.

When something fails, it says why

Every failure prints its reason against the line it happened on, rather than a bare count. A post-write mismatch distinguishes Canvas still holds its previous value (the save was rejected — its message is on the edit form) from Canvas holds a third value (it accepted the write and then altered it).

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.9

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.9
File Size Uploaded
canvasser-0.1.9.tar.gz 106.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for canvasser 0.1.9
File Interpreter ABI Platform
canvasser-0.1.9-py3-none-any.whl Python 3 none any Details

Total release size: 218.7 kB

Release files / canvasser-0.1.9.tar.gz

Download URL canvasser-0.1.9.tar.gz
Size 106.7 kB
Tags Source
SHA-256 checksum
How to use checksums
319e456ad292d4cc0f469a77508957a575f521195916bb7345646d5b4636d7b7
BLAKE2b-256 checksum
How to use checksums
89062ade8e38a6384d86b7d439a09d7ea48bb1791af3440210cd41ca48b53a08
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.9-py3-none-any.whl

Download URL canvasser-0.1.9-py3-none-any.whl
Size 112.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
43f870f1f6ee63aeca350003cbd908b3b8ff972e8e43872632c98b99ebfc9361
BLAKE2b-256 checksum
How to use checksums
7f1444f4310863132fc3eeac22fede2c17685b7334cdc0ec554c49e5fbe262d2
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

This release

0.1.9 This release

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

0.1.0

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