Python API

The command-line interface is the primary user interface. These functions and typed dictionaries support Python integrations that need the same validation, QTI conversion, import, and rotation behavior.

Question banks and reports

Shared dictionary contracts for validated banks and workflow results.

These types describe existing dictionaries; they do not construct or validate values. Use core.load_bank for files or core.parse_bank for typed YAML data. Runtime dictionaries retain additional authoring metadata.

class coursebanks.models.Question[source]

Describe a validated single-answer question with ordered choices.

format

Optional rendering mode, plain or html. Omission means plain text.

Type:

str

shuffle_choices

Optional override for the bank’s choice-shuffling default.

Type:

bool

id

Unique question identifier following the schema’s identifier rules.

Type:

str

title

Nonempty short title used in instructor materials and QTI items.

Type:

str

area

Nonempty coverage area used to balance question selection.

Type:

str

topic

Nonempty topic describing the question’s subject.

Type:

str

difficulty

Difficulty label: Basic, Moderate, or Challenging.

Type:

str

assessments

Unique, nonblank assessment names restricting eligibility. An empty list permits any assessment. Names are matched exactly, including case and whitespace.

Type:

list[str]

minutes

Finite positive estimate of the time needed to answer the question.

Type:

float

points

Finite positive grading weight.

Type:

float

stem

Nonempty question text, interpreted according to format.

Type:

str

choices

Ordered mapping of at least two valid choice identifiers to nonempty answer text. Choice text is unique after trimming outer whitespace.

Type:

dict[str, str]

answer

Identifier of the single correct choice, present in choices.

Type:

str

explanation

Nonempty instructor explanation included in QTI feedback.

Type:

str

Notes

Instances are ordinary dictionaries, accessed by key. TypedDict supplies static contracts, not runtime validation. Use parse_bank or load_bank to enforce constraints, and revalidate after editing. Loading normalizes omitted eligibility to an empty list and retains authoring extensions.

class coursebanks.models.Bank[source]

Describe a validated schema-version-1 question bank.

schema_version

Schema version, currently the integer 1.

Type:

int

course

Nonempty course identifier used to scope question usage history.

Type:

str

bank_id

Stable bank identifier following the schema’s identifier rules. Together with course, it identifies a bank’s usage records.

Type:

str

title

Nonempty display title used for the QTI assessment.

Type:

str

default_shuffle_choices

Optional default controlling choice shuffling during QTI export. Omission means False.

Type:

bool

questions

Nonempty ordered list of validated questions. IDs are unique, and stems are unique after lowercasing and collapsing whitespace.

Type:

list[coursebanks.models.Question]

Notes

Instances are ordinary dictionaries. Type annotations do not enforce schema constraints; use parse_bank or load_bank and revalidate edits. Additional authoring metadata is retained in runtime dictionaries.

class coursebanks.models.QTIReport[source]

Describe successful structural verification of a QTI archive.

count

Number of question items found in the assessment document.

Type:

int

ids

Question identifiers in document order.

Type:

list[str]

assessment_file

Verified ZIP member containing the items. For a Canvas Common Cartridge export, this is the object bank resolved through the QTI wrapper’s source-bank reference.

Type:

str

Notes

Returned by verify_qti after its structural and scoring checks pass. This report does not certify full QTI conformance, Canvas acceptance, or rendered appearance.

class coursebanks.models.BuildReport[source]

Describe archive verification and provenance for a completed build.

course

Course identifier from the validated source bank.

Type:

str

bank_id

Identifier of the validated source bank.

Type:

str

difficulty

Counts of questions for each difficulty present in the bank.

Type:

dict[str, int]

sha256

Hexadecimal SHA-256 digest of the exact generated archive bytes.

Type:

str

Notes

The build function returns this dictionary and saves its contents to validation.json alongside the archive and instructor artifacts.

class coursebanks.models.UsageRecord[source]

Describe one administered question, scoped by course and bank.

course

Course identifier associated with the administration.

Type:

str

bank_id

Bank identifier associated with the administration.

Type:

str

term

Label identifying when the question was administered.

Type:

str

assessment

Name of the assessment on which the question was administered.

Type:

str

id

Identifier of the administered question.

Type:

str

Notes

Values are dictionary entries, not object attributes. History readers normalize the legacy exam column to assessment. Proposed selections do not become usage records in the history file until explicitly recorded.

class coursebanks.models.PlannedUse[source]

Describe proposed question use with coverage metadata.

area

Coverage area from the selected question.

Type:

str

difficulty

Difficulty label from the selected question.

Type:

str

Notes

Inheritance shares the usage-record dictionary structure; it does not indicate that the question has been administered. Creating a plan does not append records to usage history.

YAML validation and QTI export

YAML I/O, validation, and deterministic Canvas QTI 1.2 generation.

Supports single-answer multiple-choice questions with two or more choices. Canvas import acceptance must still be checked in the institution’s course.

coursebanks.core.load_yaml(path: str | Path) → YamlMapping[source]

Load one UTF-8 YAML mapping without validating its bank schema.

Parameters:

path – Source file containing exactly one YAML document.

Returns:

Normalized mapping, including supported optional authoring metadata. Use parse_bank to validate it or edit it as an incomplete draft.

Return type:

data

Raises:
  • ValueError – If YAML is malformed, uses unsupported constructs, has duplicate or merge keys, contains cycles, or does not produce a top-level mapping. Parser errors include the source path and available source marks.

  • OSError – If the source file cannot be read.

  • UnicodeError – If the source file cannot be decoded as UTF-8.

Notes

Uses a SafeLoader subclass and copies supported containers through yaml_value. Shared containers become independent copies. The source file is not changed, and legacy eligibility is not migrated here.

coursebanks.core.load_bank(path: str | Path) → Bank[source]

Load a UTF-8 YAML file and construct a validated bank.

Parameters:

path – Source question-bank YAML file.

Returns:

Validated bank with authoring metadata retained and legacy assessment eligibility normalized. Omitted format remains omitted.

Return type:

bank

Raises:
  • ValueError – If loading or schema validation fails. Schema errors are accumulated in the validation diagnostic.

  • OSError – If the source file cannot be read.

  • UnicodeError – If the source file cannot be decoded as UTF-8.

Notes

The file is not modified. Use load_yaml when loading incomplete drafts that do not yet satisfy the bank schema.

coursebanks.core.validate(bank: object) → list[str][source]

Collect schema diagnostics for external Python data.

Parameters:

bank – Candidate bank data, including values not yet normalized as YAML.

Returns:

Empty list for a valid bank; otherwise ordered diagnostic strings. Normalization failures and non-mapping inputs return a single error.

Return type:

errors

Notes

Normalizes supported YAML values before checking the schema. Expected input-validation failures are returned rather than raised. Input data is unchanged; successful validation does not replace it with a normalized Bank or certify Canvas rendering.

coursebanks.core.parse_bank(data: YamlMapping) → Bank[source]

Construct a validated bank from normalized YAML data.

Parameters:

data – Normalized YAML mapping to validate and copy.

Returns:

Validated schema-version-1 bank independent of the input, retaining additional authoring metadata and original question/choice order.

Return type:

bank

Raises:

ValueError – If schema checks fail. The message begins with Validation failed and includes the accumulated diagnostics.

Notes

Deep-copies the input before validation and construction. Legacy exam values become assessments; omitted assessments become an unrestricted empty list. Omitted format remains omitted. Revalidate after editing the result; type annotations alone cannot enforce schema constraints.

coursebanks.core.require_valid_bank(bank: object) → Bank[source]

Normalize external Python data and construct a validated bank.

Parameters:

bank – Candidate bank data to normalize through yaml_value.

Returns:

Validated bank copied from the input, with supported metadata kept.

Return type:

validated

Raises:

ValueError – If data contains unsupported YAML values or cycles, is not a mapping, or fails bank schema validation.

Notes

Compatibility entry point for arbitrary Python data. Prefer load_bank for files or parse_bank for normalized YAML mappings. Input is unchanged.

coursebanks.core.question_to_xml(q: Question, *, default_shuffle_choices: bool = False) → Element[source]

Encode a validated question with single-answer scoring and feedback.

Parameters:
  • q – Validated question with ordered choices and a correct-answer label.

  • default_shuffle_choices – Bank-level shuffling behavior used when the question has no override. Defaults to False.

Returns:

New QTI item element containing metadata, presentation, scoring, and explanation feedback for correct and incorrect responses.

Return type:

item

Notes

Choice IDs combine the question ID and choice label. The render_choice element receives shuffle="Yes" when the effective setting is true; the attribute is omitted for the QTI default of no shuffling. Correct answers score 100, while Canvas’s general-feedback condition precedes the correct-answer condition. Grading weight is stored separately in points_possible metadata. Assessment eligibility is a JSON array. Omitted format means plain text. Input is unchanged and is not revalidated; minutes and additional authoring metadata are not exported.

coursebanks.core.qti_xml(bank: Bank, questions: Sequence[Question] | None = None) → bytes[source]

Serialize an assessment as UTF-8 XML in the QTI 1.2 namespace.

Parameters:
  • bank – Validated bank supplying assessment identity and title.

  • questions – Optional question sequence replacing the bank’s full question list. None uses all bank questions in their existing order.

Returns:

XML bytes including the declaration, assessment, and root section.

Return type:

payload

Notes

Inputs are not revalidated or modified. An empty supplied sequence produces an empty section; create_qti rejects empty selections. Registers QTI as ElementTree’s default namespace before serialization.

coursebanks.core.manifest_xml(bank: Bank) → bytes[source]

Serialize a Canvas Common Cartridge assessment manifest.

Parameters:

bank – Validated bank supplying the bank ID for manifest/resource IDs.

Returns:

UTF-8 XML bytes with a declaration, one Canvas QTI assessment resource, and its associated Canvas metadata resource.

Return type:

payload

Notes

Does not write files or modify the bank. Registers the Common Cartridge content-packaging namespace as ElementTree’s default namespace.

coursebanks.core.assessment_meta_xml(bank: Bank, questions: Sequence[Question] | None = None) → bytes[source]

Serialize Canvas quiz and assignment metadata for an assessment.

Parameters:
  • bank – Validated bank supplying assessment identity and title.

  • questions – Optional selected questions used to calculate total points. None uses all questions in the bank.

Returns:

UTF-8 Canvas Common Cartridge metadata XML with an unpublished quiz assignment and the selected questions’ total points.

Return type:

payload

Notes

The metadata describes a one-attempt, unpublished quiz that Canvas does not make available automatically. Choice shuffling is represented at item level when requested by the YAML bank.

coursebanks.core.create_qti(bank: Bank, dest: str | Path, questions: Sequence[Question] | None = None) → Path[source]

Write a QTI ZIP with reproducible member metadata.

Parameters:
  • bank – Validated bank supplying assessment identity and default questions.

  • dest – Destination ZIP path. Parent directories are created as needed, and existing output is overwritten.

  • questions – Optional ordered sequence replacing the bank’s questions. None uses all bank questions; an empty sequence is rejected.

Returns:

Destination as a Path, without resolving it to an absolute path.

Return type:

path

Raises:
  • ValueError – If the selected question sequence is empty.

  • OSError – If directory creation or archive writing fails.

Notes

Writes a Canvas Common Cartridge manifest, assessment, and associated metadata document with fixed timestamps and permissions. Inputs must already be validated; no schema validation or post-write verification occurs here. The input bank is unchanged.

coursebanks.core.verify_qti(path: str | Path) → QTIReport[source]

Check supported archive structure, identifiers, and scoring shape.

Parameters:

path – QTI ZIP archive to read without extracting files.

Returns:

Question count, identifiers in document order, and the verified assessment member path from the manifest.

Return type:

report

Raises:
  • ValueError – If archive resource limits or member-name uniqueness checks fail, XML is unsafe or malformed, the manifest/resource reference is invalid, the QTI root is incorrect, or item checks fail.

  • OSError – If the archive cannot be read.

  • zipfile.BadZipFile – If the input is not a readable ZIP archive.

Notes

Applies the shared QTI archive limits before reading XML. Requires unique nonempty item IDs, at least two response labels, one varequal referencing a choice ID, one or two setvar elements per item, and any supplied render_choice shuffle value to be Yes or No. Resolves either a direct QTI resource or one Canvas Common Cartridge source-bank reference and rejects absent, absolute, or parent-relative member paths. XML parsing forbids DTDs and entities. This is not complete QTI validation: score values, supported presentation content, bank schema constraints, and Canvas rendering require other checks.

coursebanks.core.instructor_markdown(bank: Bank) → str[source]

Render instructor review text including answers and explanations.

Parameters:

bank – Validated bank supplying identity, question content, and metadata.

Returns:

Markdown string with an instructor-only notice, ordered questions and choices, metadata summaries, correct answers, and explanations. Unrestricted assessment eligibility is displayed as Any.

Return type:

markdown

Notes

Authored text is inserted without escaping or HTML conversion. The result contains answer keys. No files are written and input is unchanged.

coursebanks.core.question_catalog_csv(bank: Bank) → str[source]

Render an ordered CSV catalog including instructor answers.

Parameters:

bank – Validated bank containing the questions to list.

Returns:

CSV text with a header and one row per question in bank order. Columns are id, title, area, topic, difficulty, assessments, minutes, points, answer, stem, and explanation.

Return type:

catalog

Notes

Assessment lists are encoded as JSON arrays. Choices, rendering format, and additional authoring metadata are omitted. CSV quoting is handled by csv.DictWriter. No files are written and the input is unchanged.

coursebanks.core.build(bank: object, output_dir: str | Path) → tuple[Path, BuildReport][source]

Validate a bank and write its QTI archive and instructor artifacts.

Parameters:
  • bank – Candidate Python bank data, normalized and validated before writing.

  • output_dir – Destination directory, created as needed. Matching output filenames are overwritten.

Returns:

  • archive – Path to the generated {course}_{bank_id}_QTI12.zip archive.

  • report – Structural verification results, bank identity, difficulty counts, and the SHA-256 digest of the generated archive bytes.

Raises:
  • ValueError – If bank validation or generated-archive verification fails.

  • OSError – If creating directories, writing artifacts, or reading output fails.

  • UnicodeError – If text artifacts cannot be encoded as UTF-8.

  • csv.Error – If rendering the question catalog fails.

  • zipfile.BadZipFile – If the generated archive cannot be read during verification.

Notes

Bank validation completes before output is created. Writes the QTI ZIP, instructor_review.md, question_catalog.csv, and validation.json. The bank is unchanged. Later I/O or verification failures may leave partial output; generated files are not rolled back.

QTI import

Strict migration of simple QTI 1.2 single-answer MC questions to YAML.

Unsupported QTI item structures are rejected rather than silently discarded.

coursebanks.migrate.import_qti(path: str | Path, course: str, bank_id: str, title: str, metadata_csv: str | Path | None = None) → Bank[source]

Import supported single-answer QTI 1.2 items as a validated bank.

Parameters:
  • path – Input ZIP containing imsmanifest.xml and its referenced QTI document.

  • course – Course identifier for the resulting bank.

  • bank_id – Bank identifier for the resulting bank; must satisfy the YAML schema.

  • title – Display title for the resulting bank.

  • metadata_csv – Optional UTF-8 CSV keyed by the case-sensitive ID column. Matching rows provide authoring overrides. None means no CSV overrides.

Returns:

Validated schema-version-1 bank with questions in document order. Choices are relabeled A through Z in source order, the QTI shuffle setting is retained per question, and legacy exam eligibility is normalized to assessments.

Return type:

bank

Raises:
  • ValueError – If archive resource limits or member-name uniqueness checks fail, XML is unsafe or malformed, supported QTI structural checks fail, metadata cannot be interpreted, or the resulting bank is invalid. Final schema errors are prefixed with “Imported data needs attention”.

  • OSError – If the archive or metadata CSV cannot be read.

  • UnicodeError – If the metadata CSV cannot be decoded as UTF-8.

  • csv.Error – If metadata CSV parsing fails.

  • zipfile.BadZipFile – If the input is not a readable ZIP archive.

Notes

No files are written. Missing metadata uses field-specific defaults: Unclassified for area/topic, Moderate for difficulty, unrestricted eligibility, 2.5 minutes, 2 points, and unshuffled choices when QTI omits the shuffle attribute. CSV overrides and QTI metadata are resolved before schema validation; absent feedback receives an explicit placeholder.

coursebanks.migrate.save_bank(bank: object, out: str | Path) → Path[source]

Serialize YAML without schema validation and return the output path.

Parameters:
  • bank – Bank or draft data representable by yaml.safe_dump. Schema validation is not performed; call parse_bank first when compliance is required.

  • out – Destination YAML path. Parent directories are created as needed, and any existing file is overwritten.

Returns:

Destination as a Path, without resolving it to an absolute path.

Return type:

path

Raises:
  • OSError – If directory creation or writing fails.

  • UnicodeError – If output cannot be encoded as UTF-8.

  • yaml.YAMLError – If data cannot be represented as safe YAML.

Notes

Output preserves mapping insertion order, Unicode text, and supported optional metadata. YAML comments and original source formatting are not retained. Input data is not modified.

Assessment eligibility and rotation

Shared assessment eligibility rules and legacy YAML migration.

coursebanks.assessments.assessment_name(value: object) → str[source]

Validate an assessment name without changing its text.

Parameters:

value – User-defined assessment name. It must be a string containing at least one non-whitespace character.

Returns:

Original string with case and leading or trailing whitespace intact.

Return type:

name

Raises:

ValueError – If the value is not a string or is empty or whitespace-only.

Notes

Names are not limited to a predefined set. Eligibility comparisons use exact string equality; validation does not trim or normalize names.

coursebanks.assessments.question_assessments(data: YamlMapping) → list[str][source]

Validate assessment eligibility or convert a legacy exam field.

Parameters:

data – Question mapping containing an optional assessments list or legacy exam string. Other fields are ignored by this helper.

Returns:

New list of unique, nonblank assessment names. An omitted field or empty list gives unrestricted eligibility. Legacy exam Either also becomes an empty list; any other valid legacy name becomes a single-element list.

Return type:

names

Raises:

ValueError – If both eligibility fields are present, assessments is not a list, a name is invalid, or the list contains duplicate names.

Notes

Name order, case, and whitespace are preserved. Duplicate checks use exact equality. Either has special meaning only in the legacy exam field; an assessments entry named Either is an ordinary restriction. The input mapping and any contained list are not modified.

coursebanks.assessments.is_eligible(question: Question, assessment: str) → bool[source]

Check whether a validated question allows a named assessment.

Parameters:
  • question – Validated question whose assessments list restricts eligibility. An empty list, or an absent key, permits any valid assessment name.

  • assessment – Nonblank assessment name to compare using exact string equality.

Returns:

True if eligibility is unrestricted or the name occurs in the assessments list; otherwise False.

Return type:

eligible

Raises:

ValueError – If the requested assessment name is not a nonblank string.

Notes

This helper validates the requested name, not the question’s contents. It does not consult usage history or check whether the question has already been selected. Neither input is modified.

Assessment selection and recording without unintentional question reuse.

coursebanks.rotation.read_history(path: str | Path, bank: Bank) → list[UsageRecord][source]

Read recorded question use for the supplied course and bank.

Parameters:
  • path – Usage-history CSV encoded as UTF-8, with an optional byte-order mark. A missing file or a zero-byte file is treated as empty history.

  • bank – Validated bank whose course and bank ID identify the rows to return. Other banks’ rows are skipped.

Returns:

New usage records in file order. A legacy exam column is exposed as assessment. Additional CSV columns are omitted from records.

Return type:

records

Raises:
  • ValueError – If the header is invalid, a matching row lacks a value for a declared column, or a matching row has a blank assessment name.

  • OSError – If the file cannot be inspected or read.

  • UnicodeError – If the file cannot be decoded as UTF-8.

  • csv.Error – If CSV parsing fails.

Notes

Reading does not modify the file or bank. It does not check whether recorded question IDs still exist in the bank or reject repeated IDs. Column presence is checked for matching rows; empty cell text is allowed except for the assessment name.

coursebanks.rotation.choose(bank: Bank, assessment: str, count: int, used: Set[str], seed: str | int = 0) → list[Question][source]

Propose unused questions, balancing areas before difficulties.

Parameters:
  • bank – Validated bank containing the candidate questions.

  • assessment – Nonblank assessment name, matched exactly, including case and whitespace. Questions with unrestricted eligibility may also be used.

  • count – Nonnegative integer number of questions to select. Zero returns an empty list; booleans and non-integer values are rejected.

  • used – Question IDs to exclude. The caller supplies these IDs, typically from this bank’s usage history; no history file is read here.

  • seed – Seed for local random tie breaking, converted to text. Defaults to 0. Results are reproducible for the same inputs and question order.

Returns:

Selected questions in selection order, without repetition. Entries reference the original bank dictionaries rather than copies.

Return type:

questions

Raises:

ValueError – If the assessment name or count is invalid, or fewer eligible, unused questions remain than requested.

Notes

Selection is greedy. At each step, prefer the least represented area in the current selection, then the least represented difficulty, then break ties randomly. This is a balancing heuristic, not a coverage guarantee. The bank and used set are unchanged, and no usage is recorded.

coursebanks.rotation.append_record(path: str | Path, bank: Bank, term: str, assessment: str, ids: Sequence[str]) → None[source]

Append actually administered question IDs to usage history.

Parameters:
  • path – Destination CSV. Parent directories are created as needed. New or zero-byte files receive the standard header; existing files retain their column order and any legacy exam column.

  • bank – Validated bank supplying the course, bank ID, and eligible questions.

  • term – Nonblank label for when the questions were administered. Stored exactly as supplied, without trimming.

  • assessment – Nonblank assessment name, matched exactly against question eligibility and stored without normalization.

  • ids – Unique question IDs to append in the supplied order. Every ID must exist in the bank, be eligible, and have no recorded use in this course and bank, regardless of term or assessment. An empty sequence is allowed and may still create directories and a header-only file.

Returns:

Return after appending all requested usage records.

Return type:

None

Raises:
  • ValueError – If the term or assessment is invalid, IDs are duplicate, unknown, ineligible, or previously used, or existing history fails validation.

  • OSError – If reading history, creating directories, or writing records fails.

  • UnicodeError – If existing history cannot be decoded or output cannot be encoded.

  • csv.Error – If reading or writing CSV fails.

Notes

Input and existing-history validation occur before appending records. The bank is unchanged. Additional columns in existing history receive empty values for new rows.

coursebanks.rotation.multi_term_plan(bank: Bank, terms: Sequence[str], assessments: Sequence[tuple[str, int]], used: Set[str] | None = None, seed: str | int = 0) → list[PlannedUse][source]

Propose nonrepeating selections for named assessments in each term.

Parameters:
  • bank – Validated bank containing the candidate questions.

  • terms – Nonempty sequence of nonblank term labels, processed in order. Labels are preserved exactly; repeated labels are permitted.

  • assessments – Nonempty sequence of (name, count) pairs. Names must be nonblank and unique; counts must be nonnegative integers, excluding booleans. Every pair applies to every term, in the supplied order.

  • used – Previously used question IDs to exclude throughout the plan. Defaults to None, meaning no prior use. The supplied set is copied.

  • seed – Base seed for reproducible selection, defaulting to 0. Each call to choose receives the text seed/term/assessment as its seed.

Returns:

New records ordered by term, assessment, then question selection. Each record contains course, bank ID, term, assessment, question ID, area, and difficulty. A question ID appears at most once in the plan. All-zero counts produce an empty list.

Return type:

plan

Raises:

ValueError – If terms or assessments are empty, any label or count is invalid, assessment names repeat, or a selection has insufficient candidates.

Notes

Selections use choose’s greedy balancing and exclude all earlier selections in this proposal. Earlier choices are not reconsidered, so failure does not prove that no alternative allocation is possible. No history file is read or written, and caller-owned data is unchanged. A failed plan raises an exception rather than returning a partial plan.