otterden-lms-interface 0.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. otterden_lms_interface-0.7.3/.githooks/pre-commit +11 -0
  2. otterden_lms_interface-0.7.3/.github/pull_request_template.md +34 -0
  3. otterden_lms_interface-0.7.3/.github/workflows/release.yml +81 -0
  4. otterden_lms_interface-0.7.3/.gitignore +22 -0
  5. otterden_lms_interface-0.7.3/AGENTS.md +37 -0
  6. otterden_lms_interface-0.7.3/CODEOWNERS +1 -0
  7. otterden_lms_interface-0.7.3/PKG-INFO +17 -0
  8. otterden_lms_interface-0.7.3/README.md +286 -0
  9. otterden_lms_interface-0.7.3/course_plans/cst334_compact.yaml +458 -0
  10. otterden_lms_interface-0.7.3/course_plans/cst334_from_sheet.yaml +646 -0
  11. otterden_lms_interface-0.7.3/course_plans/cst463.yaml +322 -0
  12. otterden_lms_interface-0.7.3/example_files/peer-review-questions.yaml +95 -0
  13. otterden_lms_interface-0.7.3/example_files/report.pdf +0 -0
  14. otterden_lms_interface-0.7.3/lms_interface/__init__.py +13 -0
  15. otterden_lms_interface-0.7.3/lms_interface/backends.py +95 -0
  16. otterden_lms_interface-0.7.3/lms_interface/canvas_interface.py +1307 -0
  17. otterden_lms_interface-0.7.3/lms_interface/classes.py +369 -0
  18. otterden_lms_interface-0.7.3/lms_interface/cleanup_missing_ui.py +326 -0
  19. otterden_lms_interface-0.7.3/lms_interface/course_plan.py +3227 -0
  20. otterden_lms_interface-0.7.3/lms_interface/helpers.py +849 -0
  21. otterden_lms_interface-0.7.3/lms_interface/interfaces.py +43 -0
  22. otterden_lms_interface-0.7.3/lms_interface/peer_review.py +489 -0
  23. otterden_lms_interface-0.7.3/lms_interface/privacy.py +282 -0
  24. otterden_lms_interface-0.7.3/lms_interface/tests/__init__.py +1 -0
  25. otterden_lms_interface-0.7.3/lms_interface/tests/test_canvas_assignment_submissions.py +72 -0
  26. otterden_lms_interface-0.7.3/lms_interface/tests/test_canvas_interface.py +766 -0
  27. otterden_lms_interface-0.7.3/lms_interface/tests/test_course_plan.py +672 -0
  28. otterden_lms_interface-0.7.3/lms_interface/tests/test_helpers.py +421 -0
  29. otterden_lms_interface-0.7.3/lms_interface/tests/test_manual_grade.py +78 -0
  30. otterden_lms_interface-0.7.3/lms_interface/tests/test_peer_review.py +274 -0
  31. otterden_lms_interface-0.7.3/lms_interface/tests/test_privacy.py +102 -0
  32. otterden_lms_interface-0.7.3/pyproject.toml +54 -0
  33. otterden_lms_interface-0.7.3/schemas/course_plan.schema.yaml +404 -0
  34. otterden_lms_interface-0.7.3/scripts/git_bump.sh +153 -0
  35. otterden_lms_interface-0.7.3/scripts/install_git_hooks.sh +8 -0
  36. otterden_lms_interface-0.7.3/scripts/validate_course_plan.py +104 -0
  37. otterden_lms_interface-0.7.3/scripts/vendor_into_project.py +843 -0
  38. otterden_lms_interface-0.7.3/test.py +147 -0
  39. otterden_lms_interface-0.7.3/uv.lock +628 -0
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # If pyproject version changes, ensure uv.lock is staged in the same commit.
5
+ if git diff --cached -- pyproject.toml | grep -Eq '^[+-][[:space:]]*version[[:space:]]*='; then
6
+ if ! git diff --cached --name-only | grep -qx 'uv.lock'; then
7
+ echo "ERROR: pyproject version changed but uv.lock is not staged."
8
+ echo "Run: uv lock && git add uv.lock"
9
+ exit 1
10
+ fi
11
+ fi
@@ -0,0 +1,34 @@
1
+ # Pull Request
2
+
3
+ ## Description
4
+ <!-- Provide a brief description of what this PR accomplishes -->
5
+
6
+ ## Changes Made
7
+ <!-- List the key changes made in this PR -->
8
+ -
9
+
10
+ ## Type of Change
11
+ <!-- Mark the appropriate option with an [x] -->
12
+ - [ ] Bug fix (non-breaking change that fixes an issue)
13
+ - [ ] New feature (non-breaking change that adds functionality)
14
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
15
+ - [ ] Documentation update
16
+ - [ ] Refactoring (no functional changes)
17
+
18
+ ## Testing Done
19
+ <!-- Describe the testing you've done to verify your changes -->
20
+
21
+ ## Checklist
22
+ <!-- Mark the appropriate options with an [x] -->
23
+ - [ ] My code follows the style guidelines of this project
24
+ - [ ] I have performed a self-review of my own code
25
+ - [ ] I have commented my code, particularly in hard-to-understand areas
26
+ - [ ] I have updated the documentation accordingly
27
+ - [ ] My changes generate no new warnings
28
+ - [ ] Any dependent changes have been merged and published
29
+
30
+ ## Screenshots (if applicable)
31
+ <!-- Add screenshots here if they help explain your changes -->
32
+
33
+ ## Notes
34
+ <!-- Any additional notes or context about the PR -->
@@ -0,0 +1,81 @@
1
+ name: Build And Publish Release Artifacts
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build Distributions
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Check out repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Set up uv
22
+ uses: astral-sh/setup-uv@v6
23
+
24
+ - name: Verify tag matches project version
25
+ run: |
26
+ tag_version="${GITHUB_REF_NAME#v}"
27
+ project_version="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -n 1)"
28
+ if [[ "${tag_version}" != "${project_version}" ]]; then
29
+ echo "Tag version (${tag_version}) does not match pyproject version (${project_version})."
30
+ exit 1
31
+ fi
32
+
33
+ - name: Build wheel and sdist
34
+ run: uv build
35
+
36
+ - name: Upload distribution artifacts
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/*
41
+
42
+ publish:
43
+ name: Publish to PyPI
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment:
47
+ name: pypi
48
+ permissions:
49
+ id-token: write
50
+ steps:
51
+ - name: Set up uv
52
+ uses: astral-sh/setup-uv@v6
53
+
54
+ - name: Download distribution artifacts
55
+ uses: actions/download-artifact@v4
56
+ with:
57
+ name: dist
58
+ path: dist
59
+
60
+ - name: Publish to PyPI
61
+ run: uv publish
62
+
63
+ release:
64
+ name: Create GitHub Release
65
+ needs: publish
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: write
69
+ steps:
70
+ - name: Download distribution artifacts
71
+ uses: actions/download-artifact@v4
72
+ with:
73
+ name: dist
74
+ path: dist
75
+
76
+ - name: Publish release with artifacts
77
+ uses: softprops/action-gh-release@v2
78
+ with:
79
+ tag_name: ${{ github.ref_name }}
80
+ generate_release_notes: true
81
+ files: dist/*
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ .idea/
3
+ .DS_Store
4
+ *.zip
5
+ .env
6
+ .envrc
7
+ .venv/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ __pycache__/
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ CLAUDE.md
15
+ *.so
16
+ .coverage
17
+ htmlcov/
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ dist/
21
+ build/
22
+ *.egg-info/
@@ -0,0 +1,37 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ `lms_interface/` contains the library code. Key modules include `canvas_interface.py` (Canvas API integration), `backends.py`/`interfaces.py` (backend abstraction), `privacy.py` (FERPA-friendly redaction), and `helpers.py` (CLI helpers such as `cleanup-missing`).
5
+
6
+ Tests live in `lms_interface/tests/` and mirror module behavior (`test_canvas_interface.py`, `test_helpers.py`, `test_privacy.py`). Keep new tests in this directory so vendoring stays clean.
7
+
8
+ Repository utilities live in `scripts/`:
9
+ - `install_git_hooks.sh` installs local hooks and `git bump`.
10
+ - `git_bump.sh` bumps version, runs tests, updates `uv.lock`, and commits.
11
+ - `vendor_into_project.py` vendors this package into downstream tools.
12
+
13
+ ## Build, Test, and Development Commands
14
+ - `uv sync --dev`: install runtime and dev dependencies from `pyproject.toml`/`uv.lock`.
15
+ - `uv run pytest -q`: run the full test suite (default `testpaths` is `lms_interface/tests`).
16
+ - `uv run ruff check lms_interface lms_interface/tests`: run lint/import-order checks.
17
+ - `uv run black lms_interface lms_interface/tests`: format code (88-char lines).
18
+ - `bash scripts/install_git_hooks.sh`: enable repo hooks (`.githooks/pre-commit`).
19
+ - `uv lock`: regenerate lockfile after dependency or version updates.
20
+
21
+ ## Coding Style & Naming Conventions
22
+ Use Python 3.12 and 4-space indentation. Follow Black formatting (`line-length = 88`) and keep imports Ruff-clean (`I` rules enabled). Use `snake_case` for functions/variables, `PascalCase` for classes, and `test_*.py` for test modules.
23
+
24
+ Prefer small, focused functions and explicit names around LMS concepts (`CanvasCourse`, `PrivacyBackend`, etc.).
25
+
26
+ ## Testing Guidelines
27
+ Use `pytest` for all tests. Add regression tests for bug fixes and behavior changes, especially for Canvas request/response handling and privacy redaction paths.
28
+
29
+ Name tests clearly by behavior (for example, `test_cleanup_missing_skips_future_due_assignments`). Run `uv run pytest -q` before opening a PR.
30
+
31
+ ## Commit & Pull Request Guidelines
32
+ Commit messages in this repo are short, imperative, and task-focused (for example, `Add LMS backend abstractions`, `Bump to version 0.4.4`).
33
+
34
+ PRs should follow `.github/pull_request_template.md`: include a clear description, change type, testing performed, and any relevant screenshots/notes. If `pyproject.toml` version changes, stage `uv.lock` in the same commit (enforced by pre-commit hook).
35
+
36
+ ## Security & Configuration Tips
37
+ Store Canvas credentials in environment variables (`CANVAS_API_URL`, `CANVAS_API_KEY`, plus optional `_prod` variants). Never commit secrets or real student-identifying data.
@@ -0,0 +1 @@
1
+ * @samogden
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.5
2
+ Name: otterden-lms-interface
3
+ Version: 0.7.3
4
+ Summary: LMS interface library for teaching tools
5
+ Author-email: Sam Ogden <samuel.s.ogden@gmail.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: canvasapi<4,>=3.2
8
+ Requires-Dist: python-dotenv<2,>=1
9
+ Requires-Dist: pyyaml<7,>=6
10
+ Requires-Dist: requests<3,>=2.32.4
11
+ Provides-Extra: dev
12
+ Requires-Dist: black; extra == 'dev'
13
+ Requires-Dist: flake8; extra == 'dev'
14
+ Requires-Dist: jsonschema; extra == 'dev'
15
+ Requires-Dist: mypy; extra == 'dev'
16
+ Requires-Dist: pytest; extra == 'dev'
17
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,286 @@
1
+ # LMSInterface
2
+
3
+ Lightweight LMS abstraction focused on Canvas. This repo is primarily used
4
+ as a vendored dependency in teaching tools.
5
+
6
+ ## Usage
7
+
8
+ Set Canvas credentials in `~/.env` (or pass `env_path` to `CanvasInterface`):
9
+
10
+ - `CANVAS_API_URL`
11
+ - `CANVAS_API_KEY`
12
+
13
+ For production, use:
14
+
15
+ - `CANVAS_API_URL_prod`
16
+ - `CANVAS_API_KEY_prod`
17
+
18
+ ## Tests
19
+
20
+ Run:
21
+
22
+ ```bash
23
+ pytest
24
+ ```
25
+
26
+ Tests live in `lms_interface/tests/` so they vendor cleanly.
27
+
28
+ ## Cleanup Missing Helper
29
+
30
+ Use `cleanup-missing` to normalize Canvas late-policy status for unsubmitted work:
31
+
32
+ - Past due: set to `missing`
33
+ - Future due: set to `none`
34
+
35
+ The helper can also clear stale placeholder grades (`Incomplete`/`0`) on future-due,
36
+ contentless placeholder submissions.
37
+
38
+ When run in an interactive terminal, the helper now shows a live progress view
39
+ with the current assignment and a running results table. Use `--plain-output` if
40
+ you want the older plain text behavior.
41
+
42
+ Run via module:
43
+
44
+ ```bash
45
+ python -m lms_interface.helpers cleanup-missing --course-id <COURSE_ID> --dry-run
46
+ ```
47
+
48
+ Or via script entry point:
49
+
50
+ ```bash
51
+ lms-interface-helper cleanup-missing --course-id <COURSE_ID> --dry-run
52
+ ```
53
+
54
+ Useful flags:
55
+
56
+ - `--assignment-id <ID>`: scope to one assignment
57
+ - `--clear-placeholder-grade`: clear stale placeholder grades
58
+ - `--include-unpublished`: include unpublished assignments
59
+ - `--debug`: verbose per-student logs
60
+
61
+ Recommended rollout:
62
+
63
+ 1. Run `--dry-run` on one assignment.
64
+ 2. Run live on one assignment and verify in Canvas UI.
65
+ 3. Run course-wide with `--dry-run`.
66
+ 4. Run course-wide live.
67
+
68
+ ## Manual Grade Test Harness
69
+
70
+ Use `test.py` for one-off grading against a real Canvas submission while you
71
+ are validating rubric behavior or feedback formatting.
72
+
73
+ Example:
74
+
75
+ ```bash
76
+ python test.py grade \
77
+ --course-id <COURSE_ID> \
78
+ --assignment-id <ASSIGNMENT_ID> \
79
+ --student-id <STUDENT_ID> \
80
+ --score 8 \
81
+ --comments "Well done." \
82
+ --rubric-assessment '{"criterion 1": 5, "crit2": {"points": 3, "rating_id": "rat3"}}'
83
+ ```
84
+
85
+ Useful notes:
86
+
87
+ - `--rubric-assessment` accepts either inline JSON/YAML or a path to a file
88
+ containing an object.
89
+ - Rubric keys should be the exact criterion names shown in Canvas, such as
90
+ `criterion 1` and `crit2` in the example above.
91
+ - If you omit `--score`, the Canvas wrapper derives the posted grade from the
92
+ rubric points total.
93
+ - This command posts feedback to an existing assignment. It does not create or
94
+ attach rubrics to assignments.
95
+
96
+ ## Course Plan Helper
97
+
98
+ Generate a student-facing calendar (HTML + JSON) from a course plan YAML and
99
+ optionally publish it to Canvas as a page.
100
+
101
+ Dry-run / local generation only:
102
+
103
+ ```bash
104
+ lms-interface-helper plan-course --yaml-path course_plans/cst334_compact.yaml --output-dir build/cst334
105
+ ```
106
+
107
+ Publish to Canvas:
108
+
109
+ ```bash
110
+ lms-interface-helper plan-course --yaml-path course_plans/cst334_compact.yaml --course-id <COURSE_ID> --publish
111
+ ```
112
+
113
+ Useful flags:
114
+
115
+ - `--dry-run`: show Canvas actions without writing when `--publish` is used
116
+ - `--page-title`: override published page title
117
+ - `--module-name`: module to place schedule page link in (default: `Course Schedule`)
118
+ - `--publish-weekly-slides`: create/reuse `Week XX` modules and add slide URL links
119
+ - `--weekly-module-template`: override week module naming (default from plan: `Week {week_number}`)
120
+
121
+ Plan hint:
122
+
123
+ - Set `sync.topics_per_meeting: 2` (or higher) when a single class session covers multiple topic blocks.
124
+ - Set `duration_hours` on a topic when it needs extra in-class time (for example `duration_hours: 3`).
125
+ - Use reusable placeholders to insert spacer blocks without redefining `tbd-*` topics each term.
126
+ - Define `resource_defaults.lecture_slides_base_url` to avoid repeating full slide URLs in each topic.
127
+
128
+ Example:
129
+
130
+ ```yaml
131
+ resource_defaults:
132
+ lecture_slides_base_url: https://github.com/CSUMB-SCD-instructors/CST334/tree/main/slides/pdfs
133
+
134
+ topics:
135
+ - id: process-scheduling-os7
136
+ lecture_slides:
137
+ - OSTEP 07.pdf
138
+ ```
139
+
140
+ Reusable placeholder example:
141
+
142
+ ```yaml
143
+ placeholders:
144
+ tbd:
145
+ title: Buffer / TBD
146
+ new_material: false
147
+
148
+ topics:
149
+ - id: mlfq
150
+ duration_hours: 3
151
+ - placeholder: tbd
152
+ duration_hours: 1
153
+ ```
154
+
155
+ Validate a plan file against the schema:
156
+
157
+ ```bash
158
+ python scripts/validate_course_plan.py course_plans/cst334_compact.yaml
159
+ ```
160
+
161
+ ## Parallel Uploads
162
+
163
+ Question uploads support multi-threading to speed up large quizzes. The default
164
+ behavior uses a small worker pool and limits in-flight requests.
165
+
166
+ Defaults:
167
+
168
+ - `max_workers = 4`
169
+ - `max_in_flight = 8`
170
+
171
+ If any worker receives a Canvas `429`, all workers respect a shared backoff
172
+ window before continuing.
173
+
174
+ You can override these via `CanvasCourse.create_question(...)` or by calling
175
+ the lower-level `_upload_question_payloads(...)`.
176
+
177
+ ## Backend Abstraction (LTI-Ready)
178
+
179
+ New tools should depend on the LMS-agnostic interfaces and adapters instead
180
+ of Canvas directly. This makes it easier to add an LTI backend later.
181
+
182
+ Recommended entry points:
183
+
184
+ - `CanvasBackend` for the current Canvas API
185
+ - `PrivacyBackend` to enforce FERPA-friendly anonymization
186
+ - `CanvasInterface(privacy_mode="id_only")` for quick ID-only redaction
187
+
188
+ By default, student names are redacted. To request real names, pass
189
+ `include_names=True` to:
190
+
191
+ - `CanvasCourse.get_students(...)`
192
+ - `CanvasAssignment.get_submissions(...)`
193
+ - `CanvasQuiz.get_quiz_submissions(...)`
194
+
195
+ Example:
196
+
197
+ ```python
198
+ from lms_interface.backends import CanvasBackend
199
+ from lms_interface.privacy import PrivacyBackend
200
+
201
+ backend = CanvasBackend(prod=False)
202
+ backend = PrivacyBackend(backend, salt="my-course-salt", mode="pseudonymous")
203
+
204
+ course = backend.get_course(12345)
205
+ students = course.get_students()
206
+ ```
207
+
208
+ Privacy modes:
209
+
210
+ - `pseudonymous`: hashed IDs (requires `LMS_PRIVACY_SALT`)
211
+ - `id_only`: uses the real Canvas ID but redacts names (`Student <id>`)
212
+
213
+ ## Vendoring
214
+
215
+ Use the shared script to vendor into another project:
216
+
217
+ ```bash
218
+ python scripts/vendor_into_project.py /path/to/target --top-level
219
+ ```
220
+
221
+ ## GitHub Release Artifacts
222
+
223
+ This repo can publish wheel/sdist artifacts to GitHub Releases on tag pushes.
224
+
225
+ Flow:
226
+
227
+ 1. Bump version and publish commit/tag in one step (for example via `git bump patch`).
228
+ 2. GitHub Actions builds `dist/*` and attaches artifacts to that release.
229
+
230
+ Default one-command option:
231
+
232
+ ```bash
233
+ git bump patch
234
+ ```
235
+
236
+ Optional local-only bump (no push/tag):
237
+
238
+ ```bash
239
+ git bump patch --no-tag --no-push
240
+ ```
241
+
242
+ Tag/version contract:
243
+
244
+ - Tag `vX.Y.Z` must match `project.version = "X.Y.Z"` in `pyproject.toml`.
245
+ - The release workflow fails if they do not match.
246
+
247
+ Downstream projects can pin to a release artifact instead of vendoring:
248
+
249
+ ```toml
250
+ dependencies = [
251
+ "lms-interface @ https://github.com/<org>/LMSInterface/releases/download/v0.4.5/lms_interface-0.4.5-py3-none-any.whl",
252
+ ]
253
+ ```
254
+
255
+ ## Peer-Review Surveys
256
+
257
+ Create one restricted Canvas graded survey per PDF, with a shared YAML rubric and
258
+ randomized student assignments:
259
+
260
+ ```bash
261
+ lms-interface-helper peer-review --course-id <COURSE_ID> \
262
+ --yaml-path example_files/peer-review-questions.yaml --dry-run
263
+ ```
264
+
265
+ Remove `--dry-run` to create the surveys. The beta deliberately creates a new
266
+ batch on every live run. The YAML requires `unlock_at`, `due_at`, and `lock_at`;
267
+ `points_possible` defaults to 1, `published` defaults to `false`, and
268
+ `reviews_per_student` is a per-reviewer cap (default 3). Each PDF receives at least
269
+ three reviews by default, and every eligible reviewer receives at least one distinct
270
+ PDF when possible. Configure `min_reviews_per_submission` and
271
+ `min_reviews_per_student` to override those minimums.
272
+
273
+ For submitted reports, use the source assignment instead of the `pdfs` list:
274
+
275
+ ```bash
276
+ lms-interface-helper peer-review --course-id <COURSE_ID> \
277
+ --source-assignment-id <ASSIGNMENT_ID> --yaml-path peer-review.yaml
278
+ ```
279
+
280
+ The helper takes the first PDF attachment from each source submission and never
281
+ assigns a student their own submission. Local `pdfs` remain available for debug
282
+ runs. Use `--seed` to reproduce a randomized allocation.
283
+
284
+ Peer-review YAML ignores legacy section `note` fields. Use `instructions` on the
285
+ rubric (shown above the PDF) or on a section (shown under that section heading)
286
+ for student-facing guidance.