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.
Files changed (95) hide show
  1. ff_ltitoolkit-0.1.0/.gitignore +25 -0
  2. ff_ltitoolkit-0.1.0/LICENSE +21 -0
  3. ff_ltitoolkit-0.1.0/PKG-INFO +98 -0
  4. ff_ltitoolkit-0.1.0/README.md +65 -0
  5. ff_ltitoolkit-0.1.0/pyproject.toml +90 -0
  6. ff_ltitoolkit-0.1.0/src/ltitoolkit/__init__.py +20 -0
  7. ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/__init__.py +11 -0
  8. ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/brightspace/__init__.py +35 -0
  9. ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/brightspace/client.py +176 -0
  10. ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/canvas/__init__.py +27 -0
  11. ff_ltitoolkit-0.1.0/src/ltitoolkit/adapters/canvas/client.py +142 -0
  12. ff_ltitoolkit-0.1.0/src/ltitoolkit/advantage/__init__.py +9 -0
  13. ff_ltitoolkit-0.1.0/src/ltitoolkit/advantage/service.py +96 -0
  14. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/__init__.py +19 -0
  15. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/actions.py +6 -0
  16. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/assignments_grades.py +300 -0
  17. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/__init__.py +0 -0
  18. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/__init__.py +5 -0
  19. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/cookie.py +56 -0
  20. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/launch_data_storage/__init__.py +0 -0
  21. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/launch_data_storage/cache.py +10 -0
  22. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/__init__.py +139 -0
  23. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/admin.py +48 -0
  24. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/apps.py +6 -0
  25. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/0001_initial.py +168 -0
  26. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/__init__.py +0 -0
  27. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/lti1p3_tool_config/models.py +185 -0
  28. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/message_launch.py +39 -0
  29. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/oidc_login.py +41 -0
  30. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/redirect.py +34 -0
  31. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/request.py +32 -0
  32. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/django/session.py +5 -0
  33. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/__init__.py +7 -0
  34. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/cookie.py +34 -0
  35. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/launch_data_storage/__init__.py +0 -0
  36. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/launch_data_storage/cache.py +9 -0
  37. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/message_launch.py +32 -0
  38. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/oidc_login.py +31 -0
  39. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/redirect.py +34 -0
  40. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/request.py +40 -0
  41. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/flask/session.py +5 -0
  42. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/contrib/py.typed +0 -0
  43. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/cookie.py +17 -0
  44. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/cookies_allowed_check.py +151 -0
  45. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/course_groups.py +115 -0
  46. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deep_link.py +100 -0
  47. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deep_link_resource.py +96 -0
  48. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/deployment.py +13 -0
  49. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/exception.py +16 -0
  50. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/grade.py +143 -0
  51. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/__init__.py +0 -0
  52. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/base.py +75 -0
  53. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/cache.py +43 -0
  54. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/launch_data_storage/session.py +29 -0
  55. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/lineitem.py +205 -0
  56. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_launch.py +828 -0
  57. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/__init__.py +13 -0
  58. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/abstract.py +25 -0
  59. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/deep_link.py +34 -0
  60. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/privacy_launch.py +40 -0
  61. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/resource_message.py +21 -0
  62. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/message_validators/submission_review.py +45 -0
  63. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/names_roles.py +97 -0
  64. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/oidc_login.py +275 -0
  65. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/py.typed +0 -0
  66. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/redirect.py +24 -0
  67. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/registration.py +119 -0
  68. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/request.py +17 -0
  69. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/roles.py +109 -0
  70. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/service_connector.py +144 -0
  71. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/session.py +70 -0
  72. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/__init__.py +4 -0
  73. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/abstract.py +117 -0
  74. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/dict.py +253 -0
  75. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/json_file.py +100 -0
  76. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/tool_config/py.typed +0 -0
  77. ff_ltitoolkit-0.1.0/src/ltitoolkit/core/utils.py +10 -0
  78. ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/__init__.py +39 -0
  79. ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/models.py +192 -0
  80. ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/service.py +156 -0
  81. ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/store.py +40 -0
  82. ff_ltitoolkit-0.1.0/src/ltitoolkit/dynamic_registration/tool_conf.py +102 -0
  83. ff_ltitoolkit-0.1.0/src/ltitoolkit/exceptions.py +42 -0
  84. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/__init__.py +30 -0
  85. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/cookie.py +53 -0
  86. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/dynamic_registration.py +40 -0
  87. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/message_launch.py +60 -0
  88. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/oidc_login.py +47 -0
  89. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/redirect.py +54 -0
  90. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/request.py +77 -0
  91. ff_ltitoolkit-0.1.0/src/ltitoolkit/fastapi/session.py +13 -0
  92. ff_ltitoolkit-0.1.0/src/ltitoolkit/http.py +80 -0
  93. ff_ltitoolkit-0.1.0/src/ltitoolkit/token/__init__.py +20 -0
  94. ff_ltitoolkit-0.1.0/src/ltitoolkit/token/cache.py +47 -0
  95. 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
+ ]