Skip to content

cass.actions

Orchestration layer: config discovery, pull flows, setup checks. The CLI and viewer call into this package and never below it directly.

cass.actions

Actions — business logic for cass.

Pull orchestration and configuration.

get_config

get_config() -> Config

Return the lazily-loaded project config.

config

Config discovery, loading, and writing for cass.

Searches for cass.toml starting from the current directory and walking upward, unless set_config_path points at an explicit file (--config). Config is loaded lazily on first access via get_config().

CanvasModuleSpec dataclass

CanvasModuleSpec(name: str, published: bool = False)

Desired state for a Canvas module declared in cass.toml.

CanvasAssignmentSpec dataclass

CanvasAssignmentSpec(
    name: str,
    points: float = 0.0,
    submission_types: list[str] = (
        lambda: ["online_url"]
    )(),
    due_at: str = "",
    published: bool = False,
    group: str = "",
)

Desired state for a Canvas assignment declared in cass.toml.

CanvasQuizRef dataclass

CanvasQuizRef(file: Path)

A quiz file declared in cass.toml, resolved relative to the config file.

parse_canvas_course_url

parse_canvas_course_url(raw: str) -> tuple[str, int] | None

Extract (base_url, course_id) from a Canvas course URL.

set_config_path

set_config_path(path: Path | None) -> None

Point config discovery at an explicit cass.toml (--config).

Pass None to return to walking up from the working directory.

Raises:

Type Description
SystemExit

If path does not exist.

find_project_root

find_project_root(
    start: Path | None = None,
    config_path: Path | None = None,
) -> Path

Find the project root.

With config_path (or a path set via set_config_path) the root is that file's directory. Otherwise walk up from start (default: cwd) looking for cass.toml.

config_file

config_file() -> Path

Return the config file in use (explicit path or <root>/cass.toml).

config_file_path

config_file_path() -> Path | None

Return the path to the config file, or None if it doesn't exist.

get_config

get_config() -> Config

Return the lazily-loaded project config.

reset_config

reset_config() -> None

Clear the cached config (for re-reading after writes).

read_config_data

read_config_data(path: Path) -> dict[str, Any]

Load a TOML config file into a mutable dict.

format_toml_value

format_toml_value(value: object) -> str

Render a scalar or flat list as a TOML value.

update_config

update_config(
    path: Path,
    *,
    canvas_base_url: str | None = None,
    canvas_course_id: int | None = None,
    canvas_time_zone: str | None = None,
) -> None

Update Canvas settings in cass.toml while preserving other data.

A leftover [classroom] table from older versions is removed on every rewrite.

write_config

write_config(
    path: Path,
    canvas_base_url: str = "",
    canvas_course_id: int = 0,
) -> None

Write a cass.toml file.

doctor

Prerequisite checks for cass setup (cass init doctor).

check_prerequisites

check_prerequisites() -> list[Check]

Run all setup checks and return results.

pull

Pull orchestration — fetch Canvas data into the local database.

pull_students

pull_students(cfg: Config, console: Console) -> None

CLI step: pull the Canvas roster.

pull_assignments

pull_assignments(cfg: Config, console: Console) -> None

CLI step: pull Canvas assignments.

pull_submissions

pull_submissions(cfg: Config, console: Console) -> None

CLI step: pull Canvas submissions for every stored assignment.

pull_all

pull_all(
    cfg: Config, on_progress: ProgressCallback | None = None
) -> None

Run a full pull: course name, students, assignments, submissions.

Shared by cass pull and the viewer's progress page. Progress is reported through on_progress(step, detail) with steps students, assignments, submissions, and done.

Raises:

Type Description
RuntimeError

when local Canvas edits would be overwritten.

quiz_responses

Quiz responses — every student's answers to a classic quiz or survey.

Canvas's student analysis report is wide: one "<id>: <text>" column per question, each followed by its score column. parse_student_analysis reshapes it to one QuizResponse per student x attempt x question, and add_timing fills in what the report lacks: each student's due date (after overrides and extensions) and whether Canvas closed the attempt itself.

QuizResponse dataclass

QuizResponse(
    student: str,
    user_id: int | None,
    sis_id: str,
    section: str,
    attempt: int | None,
    submitted_at: str,
    question_id: int,
    position: int,
    question: str,
    answer: str,
    score: float | None,
    due_at: str | None = None,
    late: bool | None = None,
    started_at: str | None = None,
    auto_submitted: bool | None = None,
)

One student's answer to one question on one attempt.

Timing fields are None when unknown: started_at and auto_submitted exist only for a student's latest attempt.

row
row(
    columns: tuple[str, ...] = RESPONSE_COLUMNS,
) -> list[str]

Values for columns (default RESPONSE_COLUMNS) as CSV strings.

parse_student_analysis

parse_student_analysis(text: str) -> list[QuizResponse]

Reshape a student analysis report CSV to one row per answer.

The latest-only report has no attempt column; its responses get attempt=None until add_timing fills it in. Submission times are normalized to ISO 8601 UTC (...Z).

Parameters:

Name Type Description Default
text str

Report CSV as downloaded from Canvas.

required

Returns:

Type Description
list[QuizResponse]

Responses in report order, questions in report column order.

add_timing

add_timing(
    responses: list[QuizResponse],
    *,
    due_by_user: dict[int, str | None],
    quiz_submissions: list[CanvasQuizSubmission],
    default_due: str | None = None,
) -> list[QuizResponse]

Fill due dates, lateness, and auto-submission flags in place.

  • due_at: the student's own due date (cached_due_date, which respects overrides and extensions, and may be None), else default_due for students missing from due_by_user.
  • late: submitted_at > due_at when both are known.
  • attempt: the latest finished attempt number, for latest-only reports.
  • started_at / auto_submitted: from the student's latest quiz submission, on that attempt only. auto_submitted means Canvas closed the attempt (finished_at >= end_at): a time limit ran out, or an attempt left open was force-submitted when the course concluded.

Returns:

Type Description
list[QuizResponse]

The same list, for chaining.

fetch_quiz_responses

fetch_quiz_responses(
    client: CanvasClient,
    quiz: CanvasQuiz,
    *,
    all_attempts: bool = False,
) -> list[QuizResponse]

Download, reshape, and time-stamp every student's answers to a quiz.

Parameters:

Name Type Description Default
client CanvasClient

Canvas client for the quiz's course.

required
quiz CanvasQuiz

Classic quiz or survey.

required
all_attempts bool

Include every attempt, not only each student's latest.

False

quizzes

Quiz authoring — quiz files, specs, and Canvas settings diffs.

A quiz file is TOML: settings at the top level, questions as [[questions]]. load_quiz_file validates it into a QuizSpec; dump_quiz_file writes one back so export and create --from round-trip.

AnswerSpec dataclass

AnswerSpec(text: str, correct: bool = False)

One answer on a question; correct becomes Canvas weight 100.

QuestionSpec dataclass

QuestionSpec(
    text: str,
    type: str = "essay_question",
    name: str = "",
    points: float | None = None,
    answers: list[AnswerSpec] = list(),
)

One question in a quiz file.

QuizSpec dataclass

QuizSpec(
    title: str,
    quiz_type: str,
    group: str = "",
    points: float | None = None,
    description: str = "",
    unlock_at: str = "",
    due_at: str = "",
    lock_at: str = "",
    attempts: int = 1,
    time_limit: int = 0,
    hide_results: str = "",
    scoring_policy: str = "keep_highest",
    shuffle_answers: bool = False,
    one_question_at_a_time: bool = False,
    published: bool = False,
    questions: list[QuestionSpec] = list(),
)

Desired settings and questions for a Canvas Classic quiz.

default_question_points

default_question_points(quiz_type: str) -> float

Points a question gets when the file leaves them out.

load_quiz_file

load_quiz_file(path: Path) -> QuizSpec

Read and validate a quiz TOML file.

Raises:

Type Description
SystemExit

Naming the file and offending key for any problem.

dump_quiz_file

dump_quiz_file(spec: QuizSpec, path: Path | None) -> str

Render spec as quiz-file TOML, writing it to path when given.

quiz_spec_from_canvas

quiz_spec_from_canvas(
    quiz: CanvasQuiz,
    questions: list[CanvasQuizQuestion],
    group_name: str,
    tz: str,
) -> QuizSpec

Build a spec from a live quiz, with times in the course zone.

quiz_form_fields

quiz_form_fields(
    spec: QuizSpec,
    tz: str,
    *,
    assignment_group_id: int | None = None,
) -> dict[str, object]

Canvas quiz fields for spec (Canvas names, without the quiz[] wrapper).

Empty strings clear a field on Canvas; time_limit 0 is sent as "".

quiz_settings_diff

quiz_settings_diff(
    spec: QuizSpec,
    live: CanvasQuiz,
    tz: str,
    *,
    group_id: int | None = None,
) -> dict[str, object]

Canvas fields whose live value differs from spec.

Times compare by instant, so an offset-only difference is not a change. Questions are never compared.

strip_html

strip_html(text: str) -> str

Drop tags, unescape entities, and collapse whitespace.

Block-level tags become spaces so adjacent paragraphs do not run together; inline tags such as <b> vanish without adding a space.

sync_quizzes

sync_quizzes(
    client: CanvasClient,
    specs: list[QuizSpec],
    group_ids: dict[str, int],
    tz: str,
    *,
    apply: bool,
) -> list[tuple[str, str, str]]

Reconcile quiz specs against live quizzes, matched by title.

Questions on an existing quiz are never compared or modified.

Returns:

Type Description
list[tuple[str, str, str]]

(action, "quiz", name) rows: create, update (with the changed

list[tuple[str, str, str]]

fields), or skip.