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:
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
examcolumn is exposed asassessment. 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
examcolumn.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/assessmentas 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.