ff-ltitoolkit 0.1.0__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.
- ff_ltitoolkit-0.1.0/.gitignore +25 -0
- ff_ltitoolkit-0.1.0/LICENSE +21 -0
- ff_ltitoolkit-0.1.0/PKG-INFO +98 -0
- ff_ltitoolkit-0.1.0/README.md +65 -0
- ff_ltitoolkit-0.1.0/pyproject.toml +90 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/__init__.py +20 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/__init__.py +11 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/brightspace/__init__.py +35 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/brightspace/client.py +176 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/canvas/__init__.py +27 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/canvas/client.py +142 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/advantage/__init__.py +9 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/advantage/service.py +96 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/__init__.py +19 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/actions.py +6 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/assignments_grades.py +300 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/__init__.py +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/__init__.py +5 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/cookie.py +56 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/launch_data_storage/__init__.py +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/launch_data_storage/cache.py +10 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/__init__.py +139 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/admin.py +48 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/apps.py +6 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/0001_initial.py +168 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/__init__.py +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/models.py +185 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/message_launch.py +39 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/oidc_login.py +41 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/redirect.py +34 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/request.py +32 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/session.py +5 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/__init__.py +7 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/cookie.py +34 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/launch_data_storage/__init__.py +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/launch_data_storage/cache.py +9 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/message_launch.py +32 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/oidc_login.py +31 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/redirect.py +34 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/request.py +40 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/session.py +5 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/py.typed +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/cookie.py +17 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/cookies_allowed_check.py +151 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/course_groups.py +115 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deep_link.py +100 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deep_link_resource.py +96 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deployment.py +13 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/exception.py +16 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/grade.py +143 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/__init__.py +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/base.py +75 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/cache.py +43 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/session.py +29 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/lineitem.py +205 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_launch.py +828 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/__init__.py +13 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/abstract.py +25 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/deep_link.py +34 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/privacy_launch.py +40 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/resource_message.py +21 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/submission_review.py +45 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/names_roles.py +97 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/oidc_login.py +275 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/py.typed +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/redirect.py +24 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/registration.py +119 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/request.py +17 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/roles.py +109 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/service_connector.py +144 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/session.py +70 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/__init__.py +4 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/abstract.py +117 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/dict.py +253 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/json_file.py +100 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/py.typed +0 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/core/utils.py +10 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/__init__.py +39 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/models.py +192 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/service.py +156 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/store.py +40 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/tool_conf.py +102 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/exceptions.py +42 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/__init__.py +30 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/cookie.py +53 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/dynamic_registration.py +40 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/message_launch.py +60 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/oidc_login.py +47 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/redirect.py +54 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/request.py +77 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/session.py +13 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/http.py +80 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/token/__init__.py +20 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/token/cache.py +47 -0
- ff_ltitoolkit-0.1.0/src/ltitoolkit/token/service.py +165 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtualenvs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Keys / secrets (never commit tool private keys)
|
|
21
|
+
*.pem
|
|
22
|
+
.env
|
|
23
|
+
|
|
24
|
+
# Reference clone we vendored from (kept locally, not part of the package)
|
|
25
|
+
/pylti1.3/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Dmitry Viskov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ff-ltitoolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic LTI 1.3 Advantage toolkit for Python — connect any app to any LMS.
|
|
5
|
+
Project-URL: Homepage, https://github.com/CNIT-Organization/ltitoolkit
|
|
6
|
+
Project-URL: Repository, https://github.com/CNIT-Organization/ltitoolkit
|
|
7
|
+
Project-URL: Issues, https://github.com/CNIT-Organization/ltitoolkit/issues
|
|
8
|
+
Author: Faisal Fida
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: canvas,education,lms,lti,lti-advantage,lti1.3,moodle,oidc
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Education
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: jwcrypto>=1.5
|
|
25
|
+
Requires-Dist: pyjwt[crypto]<3,>=2.8
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
27
|
+
Requires-Dist: typing-extensions>=4.9
|
|
28
|
+
Provides-Extra: fastapi
|
|
29
|
+
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
|
|
30
|
+
Requires-Dist: itsdangerous>=2.1; extra == 'fastapi'
|
|
31
|
+
Requires-Dist: python-multipart>=0.0.9; extra == 'fastapi'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# ltitoolkit
|
|
35
|
+
|
|
36
|
+
A framework-agnostic **LTI 1.3 Advantage** toolkit for Python. Connect any Python
|
|
37
|
+
application to any LTI 1.3 compliant LMS — Canvas, Moodle, Blackboard, and more —
|
|
38
|
+
with a single dependency.
|
|
39
|
+
|
|
40
|
+
> Status: **early development (v0.1.0)** — the vendored LTI engine is in place;
|
|
41
|
+
> the FastAPI adapter, Dynamic Registration, and token minting are being built.
|
|
42
|
+
> See [`docs/PROJECT.md`](./docs/PROJECT.md) for the design rationale, capability
|
|
43
|
+
> boundaries, and roadmap.
|
|
44
|
+
|
|
45
|
+
## What it does (portable — same code on every LMS)
|
|
46
|
+
|
|
47
|
+
- **Launch & identity** — verified OIDC/JWT launch: who the user is, which course,
|
|
48
|
+
what role.
|
|
49
|
+
- **LTI Advantage**
|
|
50
|
+
- **AGS** — read/write grades for your tool's activities.
|
|
51
|
+
- **NRPS** — fetch the current course roster.
|
|
52
|
+
- **Deep Linking** — let instructors embed your content into a course.
|
|
53
|
+
- **Dynamic Registration** — install on a new LMS by pasting one URL (no credentials).
|
|
54
|
+
- **Client-credentials tokens** — authenticate as the tool with its own key; no user
|
|
55
|
+
login required.
|
|
56
|
+
|
|
57
|
+
## What it deliberately does **not** do
|
|
58
|
+
|
|
59
|
+
LTI is not a remote control for the whole LMS. Listing all courses, browsing/opening
|
|
60
|
+
files, or creating native quizzes require each LMS's **proprietary** REST API and are
|
|
61
|
+
**not** part of this portable core. Put that code in thin, per-LMS adapters (e.g. a
|
|
62
|
+
Canvas adapter) built on top of the toolkit's generic token minting.
|
|
63
|
+
|
|
64
|
+
## Layout
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
src/ltitoolkit/
|
|
68
|
+
├── core/ # vendored LTI 1.3 engine (PyLTI1p3, rebranded) — internal
|
|
69
|
+
├── fastapi/ # FastAPI adapter (Phase 2)
|
|
70
|
+
├── token/ # generic client-credentials token minting (Phase 4)
|
|
71
|
+
└── dynamic_registration/ # single-URL install (Phase 5)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Install (as a dependency)
|
|
75
|
+
|
|
76
|
+
Published via Git (no PyPI required). Pin to a tag:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install "git+https://github.com/CNIT-Organization/ltitoolkit.git@v0.1.0"
|
|
80
|
+
# with the FastAPI adapter:
|
|
81
|
+
pip install "ltitoolkit[fastapi] @ git+https://github.com/CNIT-Organization/ltitoolkit.git@v0.1.0"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Develop (uv)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync # create the env + install runtime, fastapi extra, and dev tools
|
|
88
|
+
uv run pytest # tests
|
|
89
|
+
uv run ruff check src tests
|
|
90
|
+
uv run mypy src
|
|
91
|
+
uv build # build wheel + sdist into dist/
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT. The `core/` engine is a vendored copy of
|
|
97
|
+
[PyLTI1p3](https://github.com/dmitry-viskov/pylti1.3) (MIT); its original license
|
|
98
|
+
is preserved in [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ltitoolkit
|
|
2
|
+
|
|
3
|
+
A framework-agnostic **LTI 1.3 Advantage** toolkit for Python. Connect any Python
|
|
4
|
+
application to any LTI 1.3 compliant LMS — Canvas, Moodle, Blackboard, and more —
|
|
5
|
+
with a single dependency.
|
|
6
|
+
|
|
7
|
+
> Status: **early development (v0.1.0)** — the vendored LTI engine is in place;
|
|
8
|
+
> the FastAPI adapter, Dynamic Registration, and token minting are being built.
|
|
9
|
+
> See [`docs/PROJECT.md`](./docs/PROJECT.md) for the design rationale, capability
|
|
10
|
+
> boundaries, and roadmap.
|
|
11
|
+
|
|
12
|
+
## What it does (portable — same code on every LMS)
|
|
13
|
+
|
|
14
|
+
- **Launch & identity** — verified OIDC/JWT launch: who the user is, which course,
|
|
15
|
+
what role.
|
|
16
|
+
- **LTI Advantage**
|
|
17
|
+
- **AGS** — read/write grades for your tool's activities.
|
|
18
|
+
- **NRPS** — fetch the current course roster.
|
|
19
|
+
- **Deep Linking** — let instructors embed your content into a course.
|
|
20
|
+
- **Dynamic Registration** — install on a new LMS by pasting one URL (no credentials).
|
|
21
|
+
- **Client-credentials tokens** — authenticate as the tool with its own key; no user
|
|
22
|
+
login required.
|
|
23
|
+
|
|
24
|
+
## What it deliberately does **not** do
|
|
25
|
+
|
|
26
|
+
LTI is not a remote control for the whole LMS. Listing all courses, browsing/opening
|
|
27
|
+
files, or creating native quizzes require each LMS's **proprietary** REST API and are
|
|
28
|
+
**not** part of this portable core. Put that code in thin, per-LMS adapters (e.g. a
|
|
29
|
+
Canvas adapter) built on top of the toolkit's generic token minting.
|
|
30
|
+
|
|
31
|
+
## Layout
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
src/ltitoolkit/
|
|
35
|
+
├── core/ # vendored LTI 1.3 engine (PyLTI1p3, rebranded) — internal
|
|
36
|
+
├── fastapi/ # FastAPI adapter (Phase 2)
|
|
37
|
+
├── token/ # generic client-credentials token minting (Phase 4)
|
|
38
|
+
└── dynamic_registration/ # single-URL install (Phase 5)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install (as a dependency)
|
|
42
|
+
|
|
43
|
+
Published via Git (no PyPI required). Pin to a tag:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "git+https://github.com/CNIT-Organization/ltitoolkit.git@v0.1.0"
|
|
47
|
+
# with the FastAPI adapter:
|
|
48
|
+
pip install "ltitoolkit[fastapi] @ git+https://github.com/CNIT-Organization/ltitoolkit.git@v0.1.0"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Develop (uv)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv sync # create the env + install runtime, fastapi extra, and dev tools
|
|
55
|
+
uv run pytest # tests
|
|
56
|
+
uv run ruff check src tests
|
|
57
|
+
uv run mypy src
|
|
58
|
+
uv build # build wheel + sdist into dist/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT. The `core/` engine is a vendored copy of
|
|
64
|
+
[PyLTI1p3](https://github.com/dmitry-viskov/pylti1.3) (MIT); its original license
|
|
65
|
+
is preserved in [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ff-ltitoolkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Framework-agnostic LTI 1.3 Advantage toolkit for Python — connect any app to any LMS."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["lti", "lti1.3", "lti-advantage", "lms", "canvas", "moodle", "oidc", "education"]
|
|
14
|
+
authors = [{ name = "Faisal Fida" }]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Education",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Pinned deliberately (we own the vendored core, so we control its deps).
|
|
30
|
+
# pyjwt[crypto] pulls `cryptography`, required for RS256 sign/verify.
|
|
31
|
+
dependencies = [
|
|
32
|
+
"pyjwt[crypto]>=2.8,<3",
|
|
33
|
+
"jwcrypto>=1.5",
|
|
34
|
+
"requests>=2.31",
|
|
35
|
+
"typing_extensions>=4.9",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Runtime extra: consumers who use the FastAPI adapter install `ltitoolkit[fastapi]`.
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
fastapi = [
|
|
41
|
+
"fastapi>=0.110",
|
|
42
|
+
"itsdangerous>=2.1", # required by Starlette SessionMiddleware
|
|
43
|
+
"python-multipart>=0.0.9", # required to read the form-POSTed id_token
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/CNIT-Organization/ltitoolkit"
|
|
48
|
+
Repository = "https://github.com/CNIT-Organization/ltitoolkit"
|
|
49
|
+
Issues = "https://github.com/CNIT-Organization/ltitoolkit/issues"
|
|
50
|
+
|
|
51
|
+
# Dev-only tooling (PEP 735). Not published to consumers; `uv sync` installs it
|
|
52
|
+
# by default. Tests exercise the FastAPI adapter, so pull that extra in here.
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"pytest>=8.0",
|
|
56
|
+
"pytest-cov>=5.0",
|
|
57
|
+
"ruff>=0.6",
|
|
58
|
+
"mypy>=1.11",
|
|
59
|
+
"httpx>=0.27", # required by fastapi.testclient.TestClient
|
|
60
|
+
"ff-ltitoolkit[fastapi]",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/ltitoolkit"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = ["src/ltitoolkit", "LICENSE", "README.md"]
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Tooling — clean defaults
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 100
|
|
74
|
+
target-version = "py310"
|
|
75
|
+
# The vendored core is upstream code; lint only our own code.
|
|
76
|
+
extend-exclude = ["src/ltitoolkit/core"]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = ["E", "F", "I", "UP", "B", "W"]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.10"
|
|
83
|
+
warn_unused_ignores = true
|
|
84
|
+
ignore_missing_imports = true
|
|
85
|
+
# Don't type-check the vendored core; it has its own (looser) conventions.
|
|
86
|
+
exclude = ["src/ltitoolkit/core/"]
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
testpaths = ["tests"]
|
|
90
|
+
addopts = "-q"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""ltitoolkit — a framework-agnostic LTI 1.3 Advantage toolkit for Python.
|
|
2
|
+
|
|
3
|
+
Connect any Python application to any LTI 1.3 compliant LMS (Canvas, Moodle,
|
|
4
|
+
Blackboard, …) with one dependency:
|
|
5
|
+
|
|
6
|
+
- LTI 1.3 launch + identity (who, which course, what role)
|
|
7
|
+
- LTI Advantage: Assignment & Grade Services (AGS), Names & Role Provisioning
|
|
8
|
+
(NRPS), and Deep Linking — portable across every LMS
|
|
9
|
+
- Dynamic Registration: single-URL, no-credentials tool install
|
|
10
|
+
- Generic client-credentials token minting for LMS service/API calls
|
|
11
|
+
|
|
12
|
+
The portable LTI engine lives in :mod:`ltitoolkit.core` (vendored). Framework
|
|
13
|
+
glue lives in adapters such as :mod:`ltitoolkit.fastapi`. LMS-proprietary REST
|
|
14
|
+
calls (listing files, quizzes, etc.) are intentionally *not* part of the core —
|
|
15
|
+
they belong in thin, separate per-LMS adapters.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Per-LMS adapters for *proprietary* (Layer 3) APIs.
|
|
2
|
+
|
|
3
|
+
Everything in this package is intentionally **outside** the portable LTI core.
|
|
4
|
+
LTI standardises identity, roster (NRPS), and grades (AGS) — but **not** browsing
|
|
5
|
+
course files, listing quizzes, or reading the full gradebook. Those require each
|
|
6
|
+
LMS's own REST API, which differs per vendor and is not portable.
|
|
7
|
+
|
|
8
|
+
Adapters here build on the portable pieces (`ltitoolkit.token` for auth,
|
|
9
|
+
`ltitoolkit.http` for sessions, `ltitoolkit.exceptions` for errors) but call
|
|
10
|
+
vendor-specific endpoints. Use them only against the LMS they target.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Brightspace (D2L) proprietary REST API adapter (Layer 3 — NOT portable).
|
|
2
|
+
|
|
3
|
+
Calls Brightspace's own Valence LE API (course content: modules, topics, files)
|
|
4
|
+
using the tool's OAuth2 client-credentials token minted for a Service User — no
|
|
5
|
+
user login. Requires the registered OAuth client to carry the matching scopes
|
|
6
|
+
(e.g. ``content:modules:read``); the LMS admin approves those once at install.
|
|
7
|
+
|
|
8
|
+
This works **only** on Brightspace. Other LMSs need their own adapter.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import (
|
|
12
|
+
SCOPE_CONTENT_FILE_READ,
|
|
13
|
+
SCOPE_CONTENT_READ,
|
|
14
|
+
SCOPE_CONTENT_TOPICS_READ,
|
|
15
|
+
SCOPE_ENROLLMENT_READ,
|
|
16
|
+
TOPIC_FILE,
|
|
17
|
+
TOPIC_LINK,
|
|
18
|
+
TYPE_MODULE,
|
|
19
|
+
TYPE_TOPIC,
|
|
20
|
+
BrightspaceAPIClient,
|
|
21
|
+
TokenProvider,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BrightspaceAPIClient",
|
|
26
|
+
"TokenProvider",
|
|
27
|
+
"SCOPE_CONTENT_READ",
|
|
28
|
+
"SCOPE_CONTENT_TOPICS_READ",
|
|
29
|
+
"SCOPE_CONTENT_FILE_READ",
|
|
30
|
+
"SCOPE_ENROLLMENT_READ",
|
|
31
|
+
"TYPE_MODULE",
|
|
32
|
+
"TYPE_TOPIC",
|
|
33
|
+
"TOPIC_FILE",
|
|
34
|
+
"TOPIC_LINK",
|
|
35
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""A thin Brightspace (D2L) Valence REST API client — Layer 3, NOT portable.
|
|
2
|
+
|
|
3
|
+
Reads course content (modules / topics / files) from the Brightspace **Learning
|
|
4
|
+
Environment (LE)** API, authenticated by an OAuth2 *client-credentials* Bearer
|
|
5
|
+
token minted for a Brightspace **Service User** — no user login. The token's
|
|
6
|
+
scopes are Brightspace scopes (``content:modules:read`` …) approved once on the
|
|
7
|
+
registered OAuth client; the admin does that at install, the student does nothing.
|
|
8
|
+
|
|
9
|
+
Brightspace's client-credentials assertion (``iss``=``sub``=client_id, ``aud``=
|
|
10
|
+
token endpoint, RS256 + ``kid``) is exactly what
|
|
11
|
+
:class:`ltitoolkit.token.AccessTokenService` already produces — only the token
|
|
12
|
+
endpoint differs (``https://auth.brightspace.com/core/connect/token``).
|
|
13
|
+
|
|
14
|
+
Endpoints (per docs.valence.desire2learn.com):
|
|
15
|
+
|
|
16
|
+
GET /d2l/api/le/{ver}/{orgUnitId}/content/root/
|
|
17
|
+
GET /d2l/api/le/{ver}/{orgUnitId}/content/modules/{moduleId}/structure/
|
|
18
|
+
GET /d2l/api/le/{ver}/{orgUnitId}/content/topics/{topicId}
|
|
19
|
+
GET /d2l/api/le/{ver}/{orgUnitId}/content/topics/{topicId}/file
|
|
20
|
+
|
|
21
|
+
Works **only** on Brightspace. Other LMSs need their own adapter.
|
|
22
|
+
|
|
23
|
+
.. note::
|
|
24
|
+
Response shapes follow the Valence docs. Verify against the client's instance
|
|
25
|
+
and its LE ``version`` (see ``GET /d2l/api/le/versions/``) before the demo.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import typing as t
|
|
31
|
+
|
|
32
|
+
import requests
|
|
33
|
+
|
|
34
|
+
from ...exceptions import ExternalRequestError
|
|
35
|
+
from ...http import build_session
|
|
36
|
+
|
|
37
|
+
# OAuth2 scopes (format: ``<group>:<resource>:<action>``), approved on the
|
|
38
|
+
# registered OAuth client / Service User by the admin once.
|
|
39
|
+
SCOPE_CONTENT_READ = "content:modules:read"
|
|
40
|
+
SCOPE_CONTENT_TOPICS_READ = "content:topics:read"
|
|
41
|
+
SCOPE_CONTENT_FILE_READ = "content:file:read"
|
|
42
|
+
SCOPE_ENROLLMENT_READ = "enrollment:orgunit:read"
|
|
43
|
+
|
|
44
|
+
# ContentObject.Type values.
|
|
45
|
+
TYPE_MODULE = 0
|
|
46
|
+
TYPE_TOPIC = 1
|
|
47
|
+
|
|
48
|
+
# Topic.TopicType values.
|
|
49
|
+
TOPIC_FILE = 1
|
|
50
|
+
TOPIC_LINK = 3
|
|
51
|
+
|
|
52
|
+
# Default LE API version; override per instance.
|
|
53
|
+
_DEFAULT_LE_VERSION = "1.74"
|
|
54
|
+
# Recursion guard against pathological / cyclic module structures.
|
|
55
|
+
_MAX_MODULE_DEPTH = 25
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@t.runtime_checkable
|
|
59
|
+
class TokenProvider(t.Protocol):
|
|
60
|
+
"""Anything that can mint a Bearer token for a set of scopes."""
|
|
61
|
+
|
|
62
|
+
def get_token(self, scopes: t.Sequence[str]) -> str: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BrightspaceAPIClient:
|
|
66
|
+
"""Read-only convenience wrapper over the Brightspace LE content API."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
base_url: str,
|
|
71
|
+
token_provider: TokenProvider,
|
|
72
|
+
scopes: t.Sequence[str],
|
|
73
|
+
*,
|
|
74
|
+
le_version: str = _DEFAULT_LE_VERSION,
|
|
75
|
+
session: requests.Session | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._base = base_url.rstrip("/")
|
|
78
|
+
self._tokens = token_provider
|
|
79
|
+
self._scopes = tuple(scopes)
|
|
80
|
+
self._ver = le_version
|
|
81
|
+
self._session = session if session is not None else build_session()
|
|
82
|
+
|
|
83
|
+
# -- public API --------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def get_content_root(self, org_unit_id: str | int) -> list[dict[str, t.Any]]:
|
|
86
|
+
"""Top-level modules of a course (``Type == TYPE_MODULE``)."""
|
|
87
|
+
return self._get_list(self._le(org_unit_id, "content/root/"))
|
|
88
|
+
|
|
89
|
+
def get_module_structure(
|
|
90
|
+
self, org_unit_id: str | int, module_id: str | int
|
|
91
|
+
) -> list[dict[str, t.Any]]:
|
|
92
|
+
"""Direct children of a module — a mix of submodules and topics."""
|
|
93
|
+
return self._get_list(
|
|
94
|
+
self._le(org_unit_id, f"content/modules/{module_id}/structure/")
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def get_topic(self, org_unit_id: str | int, topic_id: str | int) -> dict[str, t.Any]:
|
|
98
|
+
"""Metadata for a single topic (``Title``, ``TopicType``, ``Url`` …)."""
|
|
99
|
+
return self._get_json(self._le(org_unit_id, f"content/topics/{topic_id}"))
|
|
100
|
+
|
|
101
|
+
def download_topic_file(
|
|
102
|
+
self, org_unit_id: str | int, topic_id: str | int, *, stream: bool = False
|
|
103
|
+
) -> bytes:
|
|
104
|
+
"""Raw bytes of a file-type topic (feed to NeuralMentor's ingestion)."""
|
|
105
|
+
params = {"stream": "true"} if stream else None
|
|
106
|
+
response = self._request(
|
|
107
|
+
self._le(org_unit_id, f"content/topics/{topic_id}/file"), params
|
|
108
|
+
)
|
|
109
|
+
return response.content
|
|
110
|
+
|
|
111
|
+
def list_course_topics(self, org_unit_id: str | int) -> list[dict[str, t.Any]]:
|
|
112
|
+
"""Flat list of every topic in a course (walks the full module tree).
|
|
113
|
+
|
|
114
|
+
Each topic is the raw ContentObject; useful as "the course's lessons" to
|
|
115
|
+
feed into a lesson-generation pipeline. Submodules are traversed; cycles
|
|
116
|
+
and excessive depth are guarded against.
|
|
117
|
+
"""
|
|
118
|
+
topics: list[dict[str, t.Any]] = []
|
|
119
|
+
seen: set[t.Any] = set()
|
|
120
|
+
|
|
121
|
+
def walk(entries: list[dict[str, t.Any]], depth: int) -> None:
|
|
122
|
+
if depth > _MAX_MODULE_DEPTH:
|
|
123
|
+
return
|
|
124
|
+
for obj in entries:
|
|
125
|
+
if obj.get("Type") == TYPE_TOPIC:
|
|
126
|
+
topics.append(obj)
|
|
127
|
+
elif obj.get("Type") == TYPE_MODULE:
|
|
128
|
+
module_id = obj.get("Id")
|
|
129
|
+
if module_id is None or module_id in seen:
|
|
130
|
+
continue
|
|
131
|
+
seen.add(module_id)
|
|
132
|
+
walk(self.get_module_structure(org_unit_id, module_id), depth + 1)
|
|
133
|
+
|
|
134
|
+
walk(self.get_content_root(org_unit_id), 0)
|
|
135
|
+
return topics
|
|
136
|
+
|
|
137
|
+
# -- internals ---------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _le(self, org_unit_id: str | int, path: str) -> str:
|
|
140
|
+
return f"{self._base}/d2l/api/le/{self._ver}/{org_unit_id}/{path}"
|
|
141
|
+
|
|
142
|
+
def _headers(self) -> dict[str, str]:
|
|
143
|
+
token = self._tokens.get_token(self._scopes)
|
|
144
|
+
return {"Authorization": f"Bearer {token}", "Accept": "application/json"}
|
|
145
|
+
|
|
146
|
+
def _request(
|
|
147
|
+
self, url: str, params: dict[str, t.Any] | None = None
|
|
148
|
+
) -> requests.Response:
|
|
149
|
+
try:
|
|
150
|
+
response = self._session.get(url, params=params, headers=self._headers())
|
|
151
|
+
except requests.Timeout as exc:
|
|
152
|
+
raise ExternalRequestError(
|
|
153
|
+
f"Timed out calling Brightspace: {exc}", url=url, is_timeout=True
|
|
154
|
+
) from exc
|
|
155
|
+
except requests.RequestException as exc:
|
|
156
|
+
raise ExternalRequestError(
|
|
157
|
+
f"Error calling Brightspace: {exc}", url=url
|
|
158
|
+
) from exc
|
|
159
|
+
|
|
160
|
+
if not response.ok:
|
|
161
|
+
raise ExternalRequestError(
|
|
162
|
+
"Brightspace API request failed",
|
|
163
|
+
status_code=response.status_code,
|
|
164
|
+
url=url,
|
|
165
|
+
response_text=response.text,
|
|
166
|
+
)
|
|
167
|
+
return response
|
|
168
|
+
|
|
169
|
+
def _get_json(self, url: str) -> dict[str, t.Any]:
|
|
170
|
+
return self._request(url).json()
|
|
171
|
+
|
|
172
|
+
def _get_list(self, url: str) -> list[dict[str, t.Any]]:
|
|
173
|
+
body = self._request(url).json()
|
|
174
|
+
if not isinstance(body, list):
|
|
175
|
+
raise ExternalRequestError("Expected a list response from Brightspace", url=url)
|
|
176
|
+
return body
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Canvas LMS proprietary REST API adapter (Layer 3 — NOT portable).
|
|
2
|
+
|
|
3
|
+
Calls Canvas's own REST API (files, quizzes, …) using the tool's LTI
|
|
4
|
+
client-credentials token — no user login. Requires the tool's developer key to
|
|
5
|
+
carry the matching Canvas API scopes (e.g. ``url:GET|/api/v1/courses/:id/files``);
|
|
6
|
+
the LMS admin approves those once at install.
|
|
7
|
+
|
|
8
|
+
This works **only** on Canvas. Other LMSs need their own adapter.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import (
|
|
12
|
+
SCOPE_GET_FILE,
|
|
13
|
+
SCOPE_GET_FILE_PUBLIC_URL,
|
|
14
|
+
SCOPE_LIST_COURSE_FILES,
|
|
15
|
+
SCOPE_LIST_QUIZZES,
|
|
16
|
+
CanvasAPIClient,
|
|
17
|
+
TokenProvider,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CanvasAPIClient",
|
|
22
|
+
"TokenProvider",
|
|
23
|
+
"SCOPE_LIST_COURSE_FILES",
|
|
24
|
+
"SCOPE_GET_FILE",
|
|
25
|
+
"SCOPE_GET_FILE_PUBLIC_URL",
|
|
26
|
+
"SCOPE_LIST_QUIZZES",
|
|
27
|
+
]
|