Metadata-Version: 2.4
Name: canvasser
Version: 0.1.0
Summary: Round-trip Canvas assignment dates through a CSV, by driving a browser
Author: Jei Blanchard
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/doctorjei/canvasser
Project-URL: Source, https://github.com/doctorjei/canvasser
Project-URL: Issues, https://github.com/doctorjei/canvasser/issues
Keywords: canvas,lms,instructure,automation,playwright,teaching
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.40
Dynamic: license-file

# 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](https://elearning.ufl.edu/instructor-help/api-tokens/) 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.**

```bash
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

```bash
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

```bash
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`:

```bash
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:

```bash
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](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.
