Development and typed data

The toolkit uses ordinary dictionaries for banks and questions, with shared types in coursebanks.models describing validated data and workflow results. YAML remains the authoring format described in the YAML schema.

Setup and checks

Create and activate the repository’s .venv as described in the getting-started guide, then install the package and development tools:

python -m pip install -e . --group dev
./scripts/run_checks.sh

The Bash script runs from the repository root and stops at the first failed check. It uses the active python interpreter; set QBT_PYTHON to another interpreter path if necessary. For example, from the repository root without activating the environment:

QBT_PYTHON=.venv/bin/python ./scripts/run_checks.sh

On platforms without Bash, run the same commands individually from the root:

python -m pytest
python -m ruff check .
python -m ruff format . --check
python -m mypy
python -m basedpyright
python -c 'from pydoclint.main import main; main()' src

Ruff uses the repository’s explicit rule set and an 80-character line length. Mypy and BasedPyright check src/ and tests/ in strict mode against the minimum supported Python version, 3.14. BasedPyright resolves dependencies from .venv; use that environment for the script to keep dependency resolution consistent across both checkers. The development requirements include types-PyYAML because PyYAML does not supply complete inline annotations. Pydoclint checks NumPy-style parameter documentation for source modules. Targeted DOC502 and DOC503 suppressions document places where exceptions are propagated through called functions or implicit mapping access rather than direct raise statements. The current examples are YAML, so they are validated by pytest rather than a Python type checker. The check script also builds the Sphinx documentation with warnings treated as errors.

Documentation site

Build the user guide and generated Python API reference locally with:

QBT_PYTHON=.venv/bin/python ./scripts/build_docs.sh

The generated site is written to site/ by default. Open site/index.html in a browser to review it. Publishing the reviewed output to Codeberg’s pages branch is described in the release guide.

Distribution checks

Build the wheel and source distribution, inspect their required contents, install each into a separate temporary environment, and exercise the installed command with:

QBT_PYTHON=.venv/bin/python ./scripts/check_distribution.sh

The script runs the installed entry point from outside the checkout and removes its temporary environments and artifacts when it finishes.

Release tooling

The development dependency group includes Towncrier, Bump My Version, Twine, Sphinx, MyST Parser, Furo, and ghp-import. Run scripts/check_release.sh to verify the changelog configuration and confirm that the three version declarations agree without changing files. For fragment naming, version increments, release preparation, review, commits, and tags, follow the release guide.

Continuous integration

The Woodpecker workflow in .woodpecker/checks.yaml uses Python 3.14 on Linux for pushes and pull requests handled by the configured Codeberg CI service. It creates the same .venv layout used by local development, installs the development dependency group, invokes scripts/run_checks.sh as the single definition of code-quality checks, validates the release configuration, and then runs the distribution check. The repository must be activated at Codeberg’s Woodpecker service before the workflow will run remotely. When adding Python examples, add their directory to both checker configurations.

Run python -m ruff format . to apply formatting and python -m ruff check . --fix for safe lint fixes. Review changes before accepting any unsafe fixes. Type-checker suppressions must be narrow and explain an external typing limitation; do not disable strict checks to accept an invalid application value.

Untrusted and validated banks

load_yaml reads safe YAML and returns a recursive YamlMapping without requiring a complete bank. parse_bank checks schema fields, accumulates diagnostics, and constructs a Bank and its Question dictionaries from checked values. load_bank combines these operations and returns a validated Bank directly.

from pathlib import Path

from coursebanks.core import build, load_bank
from coursebanks.models import Bank, BuildReport

bank: Bank = load_bank("examples/sample_course/questions.yaml")
archive: Path
report: BuildReport
archive, report = build(bank, "build")
print(archive, report["count"], report["sha256"])

For incomplete drafts, use load_yaml and save_bank; neither requires schema compliance. validate remains a diagnostic entry point for arbitrary Python input and returns accumulated errors without modifying it. require_valid_bank remains a compatibility wrapper for arbitrary safe data, but now constructs a copy rather than returning the original dictionary. Existing code that used load_bank for drafts must switch to load_yaml.

Bank construction retains optional authoring metadata at the bank and question levels and leaves omitted defaults omitted. The result is independent of the source draft, including nested choices and metadata. Known fields are constructed explicitly; the parser does not cast dictionaries to Bank or Question. Unknown string field names are copied separately without replacing checked fields. Revalidate after editing a bank because Python types cannot enforce positive numbers, enum membership, unique IDs, or answer correctness. build performs validation before creating outputs, and import_qti constructs a validated bank through the same parser. Lower-level XML and QTI generation functions expect validated inputs.

YamlValue describes safe scalar values, lists, mappings, sets, and tuples used by YAML pairs. Dates, timestamps, and binary metadata are supported alongside ordinary strings, numbers, booleans, and null. Mapping keys and set members must be scalar values. Bank and question field names must be strings; nested metadata mappings can retain other scalar keys. Nonrecursive aliases retain their values as independent copies; recursive aliases are rejected with a diagnostic. External Python container narrowing is confined to yaml_data.py, where members are checked before being admitted into this recursive type.

Shared types

Type

Contract

Question

Required single-answer question fields, ordered string choices, optional format, and an optional choice-shuffling override; validation enforces allowed values.

Bank

Bank identity, an ordered question list, and an optional choice-shuffling default; validation enforces schema version, nonemptiness, and uniqueness.

QTIReport

Structural question count, ordered question IDs, and the verified assessment ZIP member.

BuildReport

QTI report plus course/bank identity, difficulty counts, and archive SHA-256.

UsageRecord

Course, bank, term, assessment, and question ID for an administered question.

PlannedUse

Proposed usage with area and difficulty; generating it does not record administration.

The package includes a py.typed marker so downstream type checkers can use these annotations. Neither typed reports nor passing local checks certify Canvas rendering or import acceptance; those remain separate integration checks in the cleanup plan.

External archive and XML parsing

coursebanks.archive_io.validate_archive inspects the ZIP central directory before any XML member is decompressed. It rejects more than 100 entries, duplicate names, members larger than 16 MiB uncompressed, totals larger than 32 MiB uncompressed, and per-member compression ratios above 100:1. read_member also bounds the observed decompressed bytes, and import repeats validation when it reopens the archive after structural verification.

All external XML parsing goes through coursebanks.xml_io.parse_xml, which uses defusedxml with DTDs, entities, and external references forbidden. Both manifest and assessment members are protected, including the assessment reread during import. Malformed XML and forbidden constructs become ValueError diagnostics handled by the CLI with exit code 2. This policy rejects even benign DTD declarations; supported QTI packages must not depend on them.

Standard-library ElementTree remains in use for XML construction, serialization, element types, and parser exception types. Those imports have narrowly documented S405 exemptions. The two test modules that parse only locally generated XML have scoped S405/S314 exemptions; application parsing rules remain enabled. The development dependencies include types-defusedxml for strict checking. Archive limits reduce expansion risk but do not establish complete ZIP safety, QTI conformance, or Canvas compatibility.