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.
assignments ¶
Assignment sync: reconcile declared assignments and groups with the live course.
MissingAssignmentGroupsError ¶
Bases: RuntimeError
Assignment groups are missing on Canvas and creating them wasn't asked for.
sync_assignment_groups ¶
sync_assignment_groups(
client: CanvasClient, wanted: list[str], *, create: bool
) -> tuple[list[tuple[str, str, str]], dict[str, int]]
Resolve assignment groups by name and create the missing ones when asked.
Groups match by name, case-insensitively. Writes always go through
client; dry runs pass a dry-run client.
Raises:
| Type | Description |
|---|---|
MissingAssignmentGroupsError
|
Some groups are missing and |
Returns:
| Type | Description |
|---|---|
list[tuple[str, str, str]]
|
|
dict[str, int]
|
and the Canvas id of every wanted group by lower-cased name (a dry-run |
tuple[list[tuple[str, str, str]], dict[str, int]]
|
placeholder for a group this call creates). |
sync_assignments ¶
sync_assignments(
client: CanvasClient,
specs: list[CanvasAssignmentSpec],
group_ids: dict[str, int],
tz: str,
*,
created: list[CanvasAssignmentResponse] | None = None,
) -> list[tuple[str, str, str]]
Reconcile assignment specs against live assignments, matched by name.
Names match case-insensitively. An existing assignment gets its points,
due date and published state updated where the spec sets them and they
differ; its group and submission types are left alone. A spec without
published leaves the live assignment as it is, and a new assignment
stays unpublished. Writes always go through client; dry runs pass a
dry-run client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_ids
|
dict[str, int]
|
Canvas group ids by lower-cased name, from
|
required |
tz
|
str
|
Course time zone, for due dates written without an offset. |
required |
created
|
list[CanvasAssignmentResponse] | None
|
When given, each assignment this call creates is appended to it. |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, str, str]]
|
|
list[tuple[str, str, str]]
|
changed fields), or skip. |
canvas_records ¶
JSON records that combine several Canvas objects, for --json output.
Field names are Canvas's own (msgspec.to_builtins honours renames such as
content-type).
file_tree_records ¶
Every folder, then every file, each tagged with kind.
folder_records ¶
Every folder with its path relative to the course files root added.
quiz_detail_record ¶
quiz_detail_record(
quiz: CanvasQuiz,
questions: list[CanvasQuizQuestion],
*,
from_statistics: bool,
) -> dict[str, Any]
A quiz with its questions and where the questions were read from.
module_detail_record ¶
A module with its items.
config ¶
Config discovery, loading, and writing for cass.
Uses an explicit file when one is given (set_config_path / --config,
else $CASS_CONFIG); otherwise searches for cass.toml starting from the
current directory and walking upward. Config is loaded lazily on first access
via get_config().
ModuleItemSpec
dataclass
¶
ModuleItemSpec(
type: str,
title: str = "",
file: str = "",
source: Path | None = None,
url: str = "",
indent: int = 0,
published: bool | None = None,
)
A module item declared under [[canvas.modules.items]].
CanvasModuleSpec
dataclass
¶
CanvasModuleSpec(
name: str,
published: bool | None = None,
items: list[ModuleItemSpec] | None = None,
)
Desired state for a Canvas module declared in cass.toml.
published is None when the entry leaves it out: sync leaves the
live module as it is. items is None when the module has no
items key (never reconciled) and [] when it declares
items = [] (an empty module).
CanvasAssignmentSpec
dataclass
¶
CanvasAssignmentSpec(
name: str,
points: float = 0.0,
submission_types: list[str] = (
lambda: ["online_url"]
)(),
due_at: str = "",
published: bool | None = None,
group: str = "",
)
Desired state for a Canvas assignment declared in cass.toml.
published is None when the entry leaves it out: sync leaves the
live assignment as it is, and creates a new one unpublished.
CanvasQuizRef
dataclass
¶
A quiz file declared in cass.toml, resolved relative to the config file.
canonical_item_type ¶
Return the canonical module item type for name (case-insensitive).
parse_canvas_course_url ¶
Extract (base_url, course_id) from a Canvas course URL.
set_config_path ¶
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 the project root.
With config_path (or a path from set_config_path or
$CASS_CONFIG) the root is that file's directory. Otherwise walk up
from start (default: cwd) looking for cass.toml.
config_file ¶
Return the config file in use (explicit path or <root>/cass.toml).
config_file_path ¶
Return the path to the config file, or None if it doesn't exist.
load_config ¶
Read a config file; the one in use (config_file()) by default.
Loading an explicit path leaves the cached project config untouched.
Raises:
| Type | Description |
|---|---|
SystemExit
|
If the file is missing or has no |
read_config_data ¶
Load a TOML config file into a mutable dict.
format_toml_value ¶
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 a cass.toml file.
doctor ¶
Prerequisite checks for cass setup (cass init doctor).
enrollments ¶
Enrollment dates and late-enrollee selection.
"Enrolled at" is the earliest created_at across all of a user's
enrollments (any type, any state, deleted included), so a student who
dropped and re-enrolled keeps their original date.
EnrollmentState ¶
Bases: StrEnum
Values accepted by people list --state.
LateEnrollee
dataclass
¶
A current student who first enrolled after a cutoff.
fetch_all_enrollments ¶
Fetch every enrollment of every type and state (no scores).
enrollment_dates ¶
Map user id to the earliest enrollment created_at.
Enrollments without created_at are ignored.
is_after ¶
Whether the instant iso is strictly later than cutoff.
Compares moments, not spellings (Z versus a local offset). A missing
iso is never after anything.
select_people ¶
select_people(
users: list[CanvasUser],
dates: dict[int, str],
cutoff: str = "",
) -> list[CanvasUser]
Keep users first enrolled after cutoff; an empty cutoff keeps all.
roster ¶
roster(
client: CanvasClient,
*,
states: Sequence[str] = (),
enrolled_after: str = "",
) -> tuple[list[CanvasUser], dict[int, str]]
The people to list and every user's enrolled-at date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
CanvasClient
|
Canvas client. |
required |
states
|
Sequence[str]
|
Enrollment states to list; empty means Canvas's default. |
()
|
enrolled_after
|
str
|
A user-entered date or time, read in the course time zone; keeps people of any role first enrolled after it. |
''
|
Returns:
| Type | Description |
|---|---|
tuple[list[CanvasUser], dict[int, str]]
|
The selected users, sorted by sortable name, and the enrolled-at map. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If enrolled_after is not a recognized time (before any request). |
person_record ¶
A user as a plain dict plus the cass enrolled_at key.
late_enrollees ¶
Current students (active or invited) first enrolled after cutoff.
files ¶
Course file operations — uploads with duplicate policies, folder subtrees.
OnDuplicate ¶
Bases: StrEnum
What to do when an upload's name already exists in the folder.
OnConflict ¶
Bases: StrEnum
What to do when a moved or renamed file collides with an existing name.
FileUpload
dataclass
¶
Result of an upload: the Canvas file, and whether the upload was skipped.
upload_course_file ¶
upload_course_file(
client: CanvasClient,
local_path: str,
*,
folder: str = "",
name: str | None = None,
on_duplicate: OnDuplicate = OnDuplicate.OVERWRITE,
unpublished: bool = False,
) -> FileUpload
Upload a local file, applying the duplicate policy.
SKIP leaves an existing file untouched only when it has the same name
and size; the same name with a different size is overwritten, and a
missing folder means the file is uploaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
CanvasClient
|
Canvas client. |
required |
local_path
|
str
|
Path to the local file. |
required |
folder
|
str
|
Destination folder path; |
''
|
name
|
str | None
|
Name in Canvas; defaults to the local file name. |
None
|
on_duplicate
|
OnDuplicate
|
Duplicate policy. |
OVERWRITE
|
unpublished
|
bool
|
Lock the uploaded file so students cannot see it. |
False
|
Returns:
| Type | Description |
|---|---|
FileUpload
|
The Canvas file and whether the upload was skipped. |
folder_subtree ¶
folder_subtree(
folders: list[CanvasFolder],
files: list[CanvasFile],
folder: CanvasFolder,
) -> tuple[list[CanvasFolder], list[CanvasFile]]
Select a folder, its descendants, and the files inside them.
module_items ¶
Module item rules: command-line arguments, content lookup, planning and applying.
ModuleItemLookupError ¶
Bases: RuntimeError
Declared module items could not be planned; no item request was sent.
ContentIndex
dataclass
¶
ContentIndex(
assignments: list[CanvasAssignmentResponse],
quizzes: list[CanvasQuiz],
pages: list[CanvasPage],
folders: list[CanvasFolder],
files: list[CanvasFile],
local_sizes: dict[Path, int] = dict(),
uploaded: dict[str, CanvasFile] = dict(),
)
ContentRef
dataclass
¶
ContentRef(
item_type: str,
content_id: int | None,
page_url: str = "",
external_url: str = "",
title: str = "",
file: CanvasFile | None = None,
file_path: str = "",
)
The Canvas content a declared module item points at.
FileUploadPlan
dataclass
¶
FileUploadPlan(
source: Path,
path: str,
existing: CanvasFile | None,
published: bool | None,
local_size: int = 0,
)
A local file to upload before its module item is created or kept.
ItemChange
dataclass
¶
ItemChange(
action: str,
spec: ModuleItemSpec | None,
live: CanvasModuleItem | None,
ref: ContentRef | None,
changed: list[str] = list(),
moved: bool = False,
upload: FileUploadPlan | None = None,
)
What sync does to one module item.
ModuleItemPlan
dataclass
¶
Planned item changes for one module.
items lists declared items in file order, then extras in their
current order, then removes.
ModuleItemApplyError ¶
Bases: RuntimeError
A request failed while applying a module item plan.
Attributes:
| Name | Type | Description |
|---|---|---|
applied |
Descriptions of the steps completed before the failure. |
ModuleSyncPlan
dataclass
¶
One module's item plan within a sync.
SyncItemsPlan
dataclass
¶
SyncItemsPlan(
index: ContentIndex,
modules: list[ModuleSyncPlan],
rows: list[tuple[str, str, str]],
counts: dict[str, int],
)
Item plans for every module that declares items, in config order.
check_item_add_args ¶
check_item_add_args(
item_type: str,
*,
content_id: int | None,
page_url: str | None,
url: str | None,
title: str | None,
) -> str
Check the reference flags given for item_type.
Returns:
| Type | Description |
|---|---|
str
|
The canonical item type. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the type is unknown, a required flag is missing, or a flag does not belong to the type. |
check_item_update_args ¶
Check that items update was given something to change.
Raises:
| Type | Description |
|---|---|
ValueError
|
If no option is given. |
normalize_url ¶
An ExternalUrl in the form Canvas stores it, for comparing URLs.
Canvas percent-encodes a URL that holds non-ASCII characters (and drops
a trailing / from it). An ASCII URL is stored as given, apart from
surrounding whitespace.
fetch_content_index ¶
Read the sizes of local sources, then fetch the course content once.
Raises:
| Type | Description |
|---|---|
ModuleItemLookupError
|
If a source is missing or is not a file; nothing has been fetched by then. |
resolve_content ¶
Find the Canvas content a declared item points at.
Titles match case-insensitively; Files match by path.
Raises:
| Type | Description |
|---|---|
ValueError
|
With the problem, if the content is missing or ambiguous. |
plan_module_items ¶
plan_module_items(
declared: list[ModuleItemSpec],
live_items: list[CanvasModuleItem],
index: ContentIndex,
*,
prune: bool,
module_name: str = "",
) -> ModuleItemPlan
Compare declared module items with the live ones. No I/O.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
declared
|
list[ModuleItemSpec]
|
Items in the order the config file lists them. |
required |
live_items
|
list[CanvasModuleItem]
|
Items in the order Canvas lists them (their |
required |
index
|
ContentIndex
|
Course content and local source sizes. |
required |
prune
|
bool
|
Remove undeclared items instead of keeping them as extras. |
required |
module_name
|
str
|
Names the module in error messages. |
''
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Listing every declared item whose content is missing or ambiguous. |
plan_module_spec_items ¶
plan_module_spec_items(
module: CanvasModuleSpec,
live_items: list[CanvasModuleItem],
index: ContentIndex,
*,
prune: bool,
) -> ModuleItemPlan | None
Plan module's items, or return None when it has no items key.
A module without items is never item-reconciled.
item_label ¶
Name an item for a plan row.
Declared items use the desired title (a File its path); undeclared live items use their current type and title.
item_plan_rows ¶
Table rows (action, kind, description) for a module's item plan.
One row per upload and per non-keep item, then a count of kept items.
apply_module_item_plan ¶
apply_module_item_plan(
client: CanvasClient,
module_id: int,
plan: ModuleItemPlan,
index: ContentIndex,
) -> list[str]
Apply plan to module module_id: uploads, creates, updates, removes, then positions, in that order.
Sends nothing at all when the plan has no changes. Uploaded files are
added to index.uploaded, so a later plan reuses them.
Returns:
| Type | Description |
|---|---|
list[str]
|
Descriptions of the applied steps, in order. |
Raises:
| Type | Description |
|---|---|
ModuleItemApplyError
|
When a request fails; it lists the steps completed before the failure. Nothing is rolled back. |
plan_sync_items ¶
plan_sync_items(
client: CanvasClient,
modules: list[CanvasModuleSpec],
module_ids: dict[str, int],
*,
created_assignments: list[CanvasAssignmentResponse],
created_quizzes: list[CanvasQuiz],
prune: bool,
) -> SyncItemsPlan | None
Plan the items of every module that declares them, before any write.
Fetches the content index once, makes the assignments and quizzes this sync created resolvable by title, then plans each module in config order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modules
|
list[CanvasModuleSpec]
|
Module specs from cass.toml. |
required |
module_ids
|
dict[str, int]
|
Canvas id for each module name (placeholders in a dry run). |
required |
created_assignments
|
list[CanvasAssignmentResponse]
|
Assignments this sync created. |
required |
created_quizzes
|
list[CanvasQuiz]
|
Quizzes this sync created. |
required |
prune
|
bool
|
Remove undeclared items instead of keeping them as extras. |
required |
Returns:
| Type | Description |
|---|---|
SyncItemsPlan | None
|
The plan, or None (with no requests sent) when no module declares |
SyncItemsPlan | None
|
|
Raises:
| Type | Description |
|---|---|
ModuleItemLookupError
|
Listing the lookup problems of every module at once. |
apply_sync_items ¶
Apply each module's item plan in config order; stop at the first failure.
Raises:
| Type | Description |
|---|---|
ModuleItemApplyError
|
When a request fails. |
modules ¶
Module sync: reconcile declared modules with the live course.
sync_modules ¶
sync_modules(
client: CanvasClient, specs: list[CanvasModuleSpec]
) -> tuple[list[tuple[str, str, str]], dict[str, int]]
Create missing modules and set each module's published state.
Modules match by name, case-insensitively. A spec without published
leaves the live module as it is; a module it creates stays unpublished.
Writes always go through client; dry runs pass a dry-run client.
Returns:
| Type | Description |
|---|---|
list[tuple[str, str, str]]
|
|
dict[str, int]
|
Canvas id of each declared module by its declared name (a dry-run |
tuple[list[tuple[str, str, str]], dict[str, int]]
|
placeholder for a module this call creates). |
pull ¶
Pull orchestration — fetch Canvas data into the local database.
pull_students ¶
CLI step: pull the Canvas roster.
pull_assignments ¶
CLI step: pull Canvas assignments.
pull_submissions ¶
CLI step: pull Canvas submissions for every stored assignment.
pull_all ¶
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_plan ¶
Question planning — compare a quiz file's questions with a live quiz.
plan_quiz_questions matches each file question to a live question (by id,
then unique name, then relative order) and records what must change. It does
no I/O. apply_question_plan then sends the plan to Canvas, the only
function here that calls the client.
QuestionChange
dataclass
¶
QuestionChange(
action: str,
spec: QuestionSpec | None,
live: CanvasQuizQuestion | None,
group: str = "",
changed: list[str] = list(),
relinks_answers: bool = False,
)
What happens to one question.
action is "add", "update", "delete", "keep" or "extra" (on Canvas, not
in the file, left alone).
GroupChange
dataclass
¶
GroupChange(
action: str,
spec: GroupSpec | None,
live: CanvasQuizGroup | None,
changed: list[str] = list(),
)
What happens to one question group (same actions as questions).
QuestionPlan
dataclass
¶
QuestionPlan(
quiz_id: int,
quiz_type: str,
questions: list[QuestionChange],
groups: list[GroupChange],
order: list[tuple[str, int | str]],
group_members: dict[str, list[int]],
order_changed: bool,
reordered_groups: list[str],
student_attempts: int = 0,
)
Every question and group change needed to make a quiz match its file.
questions lists file questions first (questions[i] is
spec.questions[i]), then deletes and extras in quiz order. order is
the desired top-level layout: ("question", index into questions) or
("group", name), extras last.
QuestionApplyError ¶
Bases: RuntimeError
A plan stopped part-way; applied lists the steps that succeeded.
match_live_answers ¶
The live answer each of texts keeps, by equal text; None for a new one.
Each live answer is used at most once, so duplicate texts pair in order.
plan_quiz_questions ¶
Plan the question and group changes that make state match spec.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
QuizSpec
|
Desired quiz, from a quiz file. |
required |
state
|
QuizState
|
The quiz as it is on Canvas. |
required |
prune
|
bool
|
Delete live questions and emptied groups the file leaves out; otherwise they are kept as extras. |
required |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
A file id is not on the quiz, two live groups share a
name, or a question to update has a type outside
|
format_question_plan ¶
One line per question and group, each starting with its action.
A final reorder questions line appears whenever the plan changes
anything, since every apply reorders the quiz.
question_plan_warning ¶
A warning when students have finished attempts, else None.
plan_summary ¶
Question counts by action, plus group changes when there are any.
For example "1 added, 1 updated, 0 deleted, 3 kept, 0 extra" or
"0 added, 0 updated, 0 deleted, 3 kept, 0 extra; groups: 1 updated".
question_update_fields ¶
question_update_fields(
change: QuestionChange,
quiz_type: str,
group_id: int | None,
) -> dict[str, object]
Canvas fields to send for an "update" change: the whole question.
Canvas fills every field an update leaves out from a copy of the question
saved when it was created, so a partial update would undo earlier edits.
Each update therefore sends text, name, type, points, answers and the
live question-level feedback (QUESTION_COMMENT_FIELDS).
Answers keep their Canvas ids: unchanged answers go back as the live answers; changed ones are the file's answers. A file answer whose text matches a live answer keeps that answer's id, HTML and feedback, since the file sets only text and correctness.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
change
|
QuestionChange
|
An update change; its spec holds the desired values. |
required |
quiz_type
|
str
|
Canvas quiz type, which decides default points. |
required |
group_id
|
int | None
|
Canvas id of the desired group, or None for no group. Sent
only when the question moves into a group; Canvas ignores
|
required |
apply_question_plan ¶
Send plan to Canvas in a fixed order and re-save the quiz.
Order:
- Create groups.
- Add, update and delete questions.
- Update groups.
- Reorder the quiz, whenever the plan changes anything. This keeps
top-level positions contiguous, which
canvas_layoutrelies on, and takes questions out of groups, since Canvas ignoresquiz_group_id: nullon update. - Reorder groups.
- Delete groups. Canvas deletes a group's questions with the group, so the questions the file keeps must already be out of it.
- Re-save the quiz (only if anything was sent).
Returns:
| Type | Description |
|---|---|
list[str]
|
A description of each step applied. |
Raises:
| Type | Description |
|---|---|
QuestionApplyError
|
A request failed. Nothing after it was sent; the error lists the steps that had already succeeded. |
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 ¶
Values for columns (default RESPONSE_COLUMNS) as CSV strings.
parse_student_analysis ¶
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,
tz: str = "UTC",
) -> 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 beNone), elsedefault_duefor students missing from due_by_user.late:submitted_at > due_atwhen both are known.attempt: for latest-only reports, the student's latest finished attempt. When the student has opened a newer attempt and not submitted it, the report holds the one before it.started_at/auto_submitted: from the student's latest quiz submission, on that attempt only, so they stay empty while a newer attempt is open.auto_submittedmeans 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.submitted_at,due_atandstarted_atare rewritten as ISO 8601 with tz's offset (Zfor UTC).
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.
Times are written in the course time zone.
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
¶
One answer on a question; correct becomes Canvas weight 100.
GroupSpec
dataclass
¶
A question group: Canvas draws pick_count of its questions per attempt.
QuestionSpec
dataclass
¶
QuestionSpec(
text: str,
type: str = "essay_question",
name: str = "",
points: float | None = None,
answers: list[AnswerSpec] = list(),
group: str = "",
id: int | None = None,
)
One question in a quiz file.
QuizSpec
dataclass
¶
QuizSpec(
title: str,
quiz_type: str,
group: str = "",
points: float | None = None,
description: str | None = None,
unlock_at: str | None = None,
due_at: str | None = None,
lock_at: str | None = None,
attempts: int | None = None,
time_limit: int | None = None,
hide_results: str | None = None,
scoring_policy: str | None = None,
shuffle_answers: bool | None = None,
one_question_at_a_time: bool | None = None,
published: bool | None = None,
groups: list[GroupSpec] = list(),
questions: list[QuestionSpec] = list(),
)
Desired settings and questions for a Canvas Classic quiz.
A setting left as None means "leave as is": updates never send it, and
create_quiz fills it from QUIZ_CREATE_DEFAULTS. An empty group
and a None points are likewise never sent.
QuizState
dataclass
¶
QuizState(
quiz: CanvasQuiz,
questions: list[CanvasQuizQuestion],
groups: list[CanvasQuizGroup],
from_statistics: bool = False,
student_attempts: int = 0,
)
A quiz as it is on Canvas: settings, questions in quiz order, groups.
question_type_alias ¶
Short quiz-file name for a Canvas question type, or the type unchanged.
with_create_defaults ¶
A copy of spec with each unset setting filled from QUIZ_CREATE_DEFAULTS.
default_question_points ¶
Points a question gets when the file leaves them out.
is_default_question_name ¶
True for Canvas's default question names ("Question", "Question 3") and "".
file_layout ¶
Top-level quiz entries in file order.
("question", i) is an ungrouped question at spec.questions[i];
("group", name) appears once, at its first question's place.
group_members ¶
Group name → indices into spec.questions, in file order.
question_form_fields ¶
Canvas question fields for question (answers only when it has any).
answer_form_fields ¶
Canvas answer fields for answers; a correct answer gets weight 100.
group_form_fields ¶
Canvas body for creating group (points per item default by quiz type).
load_quiz_file ¶
Read and validate a quiz TOML file.
Raises:
| Type | Description |
|---|---|
SystemExit
|
Naming the file and offending key for any problem. |
dump_quiz_file ¶
Render spec as quiz-file TOML, writing it to path when given.
canvas_layout ¶
canvas_layout(
questions: list[CanvasQuizQuestion],
groups: list[CanvasQuizGroup],
) -> list[tuple[str, int]]
Top-level quiz entries in Canvas order.
The questions API reports every question's position as null, so the
order is rebuilt from what Canvas does report: questions in API list
order, and each group's position from the group endpoint.
With n top-level entries (ungrouped questions plus groups), each group takes its position clamped to 1..n, in (position, id) order; a group whose slot is taken moves to the next free one. Groups with a null position go last. Ungrouped questions fill the remaining slots in list order. The result is exact when top-level positions run 1..n, which every cass apply leaves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
questions
|
list[CanvasQuizQuestion]
|
Questions in the order the questions API lists them. |
required |
groups
|
list[CanvasQuizGroup]
|
The quiz's question groups. |
required |
ordered_questions ¶
ordered_questions(
questions: list[CanvasQuizQuestion],
groups: list[CanvasQuizGroup],
) -> list[CanvasQuizQuestion]
All questions in quiz order, each group expanded in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
questions
|
list[CanvasQuizQuestion]
|
Questions in the order the questions API lists them; a group's members keep that order. |
required |
groups
|
list[CanvasQuizGroup]
|
The quiz's question groups. |
required |
group_name_of ¶
Name of the group holding question, or "" when it has none.
groups_by_name ¶
Live question groups keyed by name.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Two groups share a name. |
fetch_quiz_state ¶
fetch_quiz_state(
client: CanvasClient,
quiz_id: int,
*,
allow_statistics: bool = False,
include_submissions: bool = False,
) -> QuizState
Read a quiz, its questions in quiz order, and its question groups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
CanvasClient
|
Canvas client. |
required |
quiz_id
|
int
|
Canvas quiz ID. |
required |
allow_statistics
|
bool
|
Accept questions rebuilt from quiz statistics (concluded courses); otherwise that raises. |
False
|
include_submissions
|
bool
|
Also count finished student attempts. |
False
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Questions came from statistics and that is not allowed. |
quiz_spec_from_canvas ¶
quiz_spec_from_canvas(
quiz: CanvasQuiz,
questions: list[CanvasQuizQuestion],
group_name: str,
tz: str,
*,
groups: Sequence[CanvasQuizGroup] = (),
) -> QuizSpec
Build a spec from a live quiz, with times in the course zone.
Questions come out in quiz order, carrying their Canvas ids and group names.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Two groups share a name. |
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).
Settings left as None are omitted. 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.
Only the settings spec sets are compared. Times compare by instant, so an offset-only difference is not a change. Questions are never compared.
strip_html ¶
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,
*,
created: list[CanvasQuiz] | None = None,
) -> list[tuple[str, str, str]]
Reconcile quiz specs against live quizzes, matched by title.
Questions on an existing quiz are never compared or modified. Writes
always go through client; dry runs pass a dry-run client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
created
|
list[CanvasQuiz] | None
|
When given, each quiz this call creates is appended to it. |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, str, str]]
|
|
list[tuple[str, str, str]]
|
fields), or skip. |
skill ¶
Install the agent skill that ships inside the cass package.
default_skills_dir ¶
Return Claude Code's user skills directory, ~/.claude/skills.
skill_files ¶
Return every file in the packaged skill as (relative path, contents).
install_skill ¶
Copy the packaged skill to target_dir/cass and return that path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_dir
|
Path
|
Skills directory to install into; created if missing. |
required |
replace
|
bool
|
Delete an existing |
required |
Raises:
| Type | Description |
|---|---|
FileExistsError
|
The target exists and |
status ¶
Project status: config, auth, and local database overview.
StatusCounts
dataclass
¶
Row counts for the main Canvas tables.
ProjectStatus
dataclass
¶
ProjectStatus(
config_path: str,
course_url: str | None,
course_id: int | None,
auth_source: str | None,
db_path: str | None,
course_name: str | None,
counts: StatusCounts | None,
pending_changes: int | None,
)
Snapshot of a project's configuration and local database.
count_pending ¶
Count pending column changes across all tables and rows.
project_status ¶
Collect the status of the project whose config is at config_path.
submissions ¶
Plan and apply excuse and clear-late on submissions.
The planners are pure. Only the targets_* fetchers, submission_rows,
plan_submission_command and apply_submission_changes talk to Canvas.
SubmissionTarget
dataclass
¶
SubmissionTarget(
assignment: CanvasAssignmentResponse,
submission: CanvasSubmissionResponse,
student_name: str,
enrolled_at: str | None = None,
)
One student's submission on one assignment, considered for a change.
SubmissionChange
dataclass
¶
A planned action for a target: excuse, clear-late or skip.
SubmissionPlan
dataclass
¶
SubmissionPlan(
changes: list[SubmissionChange],
rows: list[tuple[str, str, str, str]],
counts: dict[str, int],
warnings: list[str],
)
Everything an excuse or clear-late command shows and applies.
SubmissionApplyError ¶
Bases: RuntimeError
A request failed while applying submission changes.
Attributes:
| Name | Type | Description |
|---|---|---|
applied |
Descriptions of the changes sent before the failure. |
is_protected_grade ¶
Whether the submission holds a score a teacher could have entered.
Canvas's missing-submission policy writes its score with missing set
and no grader_id; those are what late enrollees need excused. A
teacher's grade, including partial credit on a missing submission, has a
grader and stays protected. So does a policy score on a submission a
teacher marked Missing by hand in the gradebook: Canvas records that
teacher as the grader.
due_before_enrollment ¶
Whether the student's own due date fell strictly before they enrolled.
pick_students ¶
pick_students(
submissions: list[CanvasSubmissionResponse],
student_ids: Sequence[int],
assignment_name: str,
) -> list[CanvasSubmissionResponse]
Select submissions for student_ids, in that order, once each.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If any ID has no submission on the assignment. |
plan_excuse ¶
plan_excuse(
targets: list[SubmissionTarget],
*,
include_graded: bool = False,
) -> list[SubmissionChange]
Plan excusing each target, skipping excused and (by default) graded ones.
is_graded_as_group ¶
Whether Canvas grades assignment once for each whole group.
plan_clear_late ¶
Plan clearing the late status of late targets.
Skipped: assignments graded as a group, where Canvas would write the status to every group member, and excused submissions, which setting a late status would un-excuse.
change_rows ¶
Table rows of (action, assignment, student, detail) for display.
change_counts ¶
Count changes by action, in first-seen order.
submission_rows ¶
submission_rows(
client: CanvasClient,
assignment: CanvasAssignmentResponse,
*,
student_ids: Sequence[int] = (),
) -> list[CanvasSubmissionResponse]
Submissions on assignment for listing.
With student_ids, those students in that order; otherwise everyone, sorted by sortable name.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If any ID has no submission on the assignment. |
submissions_may_be_pending ¶
True when assignment may be too new for Canvas to have its submissions.
Canvas creates a new assignment's submissions a few seconds after the assignment, so listing them straight away comes back empty.
targets_for_students ¶
targets_for_students(
client: CanvasClient,
assignment: CanvasAssignmentResponse,
student_ids: Sequence[int],
) -> list[SubmissionTarget]
Targets for explicit students on one assignment.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If any ID has no submission on the assignment. |
targets_enrolled_after ¶
targets_enrolled_after(
client: CanvasClient,
cutoff: str,
*,
assignment: CanvasAssignmentResponse | None = None,
) -> list[SubmissionTarget]
Targets for students first enrolled after cutoff.
Keeps each late enrollee's submissions on published assignments whose due date (the student's own) fell before they enrolled. Ordered by student name, user ID, due date, then assignment name.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If assignment is given and unpublished. |
check_target_options ¶
check_target_options(
*,
has_assignment: bool,
student_ids: Sequence[int],
enrolled_after: str,
) -> None
Require exactly one of student_ids and enrolled_after.
Raises:
| Type | Description |
|---|---|
ValueError
|
On a missing or conflicting choice, or students without an assignment. |
RuntimeError
|
If enrolled_after is not a recognized time. |
plan_submission_command ¶
plan_submission_command(
client: CanvasClient,
kind: SubmissionCommand,
*,
assignment: CanvasAssignmentResponse | None,
student_ids: Sequence[int],
enrolled_after: str,
include_graded: bool,
) -> SubmissionPlan
Select targets and plan an excuse or clear-late command.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
CanvasClient
|
Canvas client. |
required |
kind
|
SubmissionCommand
|
The command. |
required |
assignment
|
CanvasAssignmentResponse | None
|
The assignment to limit to; required with student_ids. |
required |
student_ids
|
Sequence[int]
|
Explicit students ( |
required |
enrolled_after
|
str
|
A user-entered date or time ( |
required |
include_graded
|
bool
|
Excuse graded submissions too ( |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
On a bad combination of target options (before any request). |
RuntimeError
|
On an unknown student, an unparseable date (before any
request), or an unpublished assignment with |
apply_submission_changes ¶
Send each non-skip change, in order, one request each.
Returns:
| Type | Description |
|---|---|
list[str]
|
A description per change sent, such as |
Clear-late sends late_policy_status=none, which clears late and
missing and restores deducted points on the current submission.
Canvas resets the status when the student submits again, so a later late
resubmission is marked late again.
Raises:
| Type | Description |
|---|---|
SubmissionApplyError
|
When a request fails; it lists the changes sent before the failure. Nothing is rolled back. Other errors, such as an unreadable response to a request that was sent, propagate as is. |