PyBlackboard-LMS 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 (53) hide show
  1. pyblackboard_lms-0.1.0/.env.example +4 -0
  2. pyblackboard_lms-0.1.0/CHANGELOG.md +18 -0
  3. pyblackboard_lms-0.1.0/LICENSE +21 -0
  4. pyblackboard_lms-0.1.0/MANIFEST.in +18 -0
  5. pyblackboard_lms-0.1.0/PKG-INFO +175 -0
  6. pyblackboard_lms-0.1.0/README.md +144 -0
  7. pyblackboard_lms-0.1.0/pyproject.toml +75 -0
  8. pyblackboard_lms-0.1.0/setup.cfg +4 -0
  9. pyblackboard_lms-0.1.0/src/PyBlackboard_LMS.egg-info/SOURCES.txt +50 -0
  10. pyblackboard_lms-0.1.0/src/blackboard_api/__init__.py +27 -0
  11. pyblackboard_lms-0.1.0/src/blackboard_api/api_quota.py +59 -0
  12. pyblackboard_lms-0.1.0/src/blackboard_api/auth.py +77 -0
  13. pyblackboard_lms-0.1.0/src/blackboard_api/client.py +228 -0
  14. pyblackboard_lms-0.1.0/src/blackboard_api/config.py +55 -0
  15. pyblackboard_lms-0.1.0/src/blackboard_api/errors.py +26 -0
  16. pyblackboard_lms-0.1.0/src/blackboard_api/facades/__init__.py +19 -0
  17. pyblackboard_lms-0.1.0/src/blackboard_api/facades/api_quota.py +19 -0
  18. pyblackboard_lms-0.1.0/src/blackboard_api/facades/courses.py +96 -0
  19. pyblackboard_lms-0.1.0/src/blackboard_api/facades/enrollments.py +247 -0
  20. pyblackboard_lms-0.1.0/src/blackboard_api/facades/resources.py +56 -0
  21. pyblackboard_lms-0.1.0/src/blackboard_api/facades/terms.py +42 -0
  22. pyblackboard_lms-0.1.0/src/blackboard_api/facades/users.py +81 -0
  23. pyblackboard_lms-0.1.0/src/blackboard_api/identifiers.py +81 -0
  24. pyblackboard_lms-0.1.0/src/blackboard_api/resources/__init__.py +15 -0
  25. pyblackboard_lms-0.1.0/src/blackboard_api/resources/courses.py +150 -0
  26. pyblackboard_lms-0.1.0/src/blackboard_api/resources/enrollment_roles.py +16 -0
  27. pyblackboard_lms-0.1.0/src/blackboard_api/resources/enrollments.py +151 -0
  28. pyblackboard_lms-0.1.0/src/blackboard_api/resources/nodes.py +99 -0
  29. pyblackboard_lms-0.1.0/src/blackboard_api/resources/terms.py +71 -0
  30. pyblackboard_lms-0.1.0/src/blackboard_api/resources/users.py +145 -0
  31. pyblackboard_lms-0.1.0/src/blackboard_api/services/__init__.py +5 -0
  32. pyblackboard_lms-0.1.0/src/blackboard_api/services/courses.py +60 -0
  33. pyblackboard_lms-0.1.0/src/blackboard_api/services/enrollments.py +239 -0
  34. pyblackboard_lms-0.1.0/src/blackboard_api/services/users.py +28 -0
  35. pyblackboard_lms-0.1.0/src/blackboard_api/transport.py +141 -0
  36. pyblackboard_lms-0.1.0/src/blackboard_cli/__init__.py +1 -0
  37. pyblackboard_lms-0.1.0/src/blackboard_cli/__main__.py +5 -0
  38. pyblackboard_lms-0.1.0/src/blackboard_cli/application/__init__.py +1 -0
  39. pyblackboard_lms-0.1.0/src/blackboard_cli/cli.py +596 -0
  40. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/__init__.py +30 -0
  41. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/common.py +25 -0
  42. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/courses.py +15 -0
  43. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/enrollments.py +15 -0
  44. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/generic.py +34 -0
  45. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/nodes.py +25 -0
  46. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/roles.py +15 -0
  47. pyblackboard_lms-0.1.0/src/blackboard_cli/converters/users.py +15 -0
  48. pyblackboard_lms-0.1.0/src/blackboard_cli/encoding.py +14 -0
  49. pyblackboard_lms-0.1.0/src/blackboard_cli/output/__init__.py +8 -0
  50. pyblackboard_lms-0.1.0/src/blackboard_cli/output/csv.py +35 -0
  51. pyblackboard_lms-0.1.0/src/blackboard_cli/output/dataframe.py +12 -0
  52. pyblackboard_lms-0.1.0/src/blackboard_cli/output/excel.py +23 -0
  53. pyblackboard_lms-0.1.0/src/blackboard_cli/output/table.py +15 -0
@@ -0,0 +1,4 @@
1
+ # Blackboard connection settings
2
+ APP_KEY=replace-me
3
+ APP_SECRET=replace-me
4
+ BB_INSTANCE_URL=https://blackboard.example.com
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to PyBlackboard-LMS are documented in this file.
4
+
5
+ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+ Version numbers have the form `MAJOR.MINOR.PATCH`:
7
+
8
+ - Increment `MAJOR` for incompatible public API or CLI changes.
9
+ - Increment `MINOR` for backwards-compatible functionality.
10
+ - Increment `PATCH` for backwards-compatible bug fixes.
11
+
12
+ The single authoritative version is `project.version` in `pyproject.toml`.
13
+
14
+ ## [0.1.0] - 2026-08-30
15
+
16
+ ### Added
17
+
18
+ - Initial public package structure for the Blackboard API and CLI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Amo
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,18 @@
1
+ include README.md
2
+ include CHANGELOG.md
3
+ include LICENSE
4
+ include .env.example
5
+ include pyproject.toml
6
+ graft src
7
+ prune src/*.egg-info
8
+
9
+ prune audit
10
+ prune data
11
+ prune devdocs
12
+ prune docs
13
+ prune tests
14
+ global-exclude .env
15
+ global-exclude .env.*
16
+ include .env.example
17
+ global-exclude __pycache__
18
+ global-exclude *.py[cod]
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyBlackboard-LMS
3
+ Version: 0.1.0
4
+ Summary: Python API and command-line interface for Blackboard Learn.
5
+ Author-email: Alex Amo <hello@alejandroamo.eu>
6
+ License-Expression: MIT
7
+ Keywords: blackboard,blackboard-learn,cli,education,lms
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Education
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests<3,>=2.31
21
+ Requires-Dist: pandas<3,>=2.0
22
+ Requires-Dist: XlsxWriter<4,>=3.1
23
+ Provides-Extra: dev
24
+ Requires-Dist: black<27,>=24.0; extra == "dev"
25
+ Requires-Dist: build<2,>=1.2; extra == "dev"
26
+ Requires-Dist: check-wheel-contents<1,>=0.6; extra == "dev"
27
+ Requires-Dist: mypy<2,>=1.10; extra == "dev"
28
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
29
+ Requires-Dist: twine<7,>=5.1; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # PyBlackboard-LMS
33
+
34
+ Python and command-line interfaces for Blackboard LMS API.
35
+
36
+ The Python API (`blackboard_api`) provides:
37
+
38
+ - Resilient HTTP transport and OAuth authentication.
39
+ - API quota tracking.
40
+ - Transparent pagination for object collections.
41
+ - Iterators for consuming large result sets without accumulating every item in memory.
42
+ - An atomic operations layer for individual requests (`resources`).
43
+ - A service layer for combinations of operations, such as upserts (`services`).
44
+ - Public facades with validated, human-oriented methods.
45
+
46
+ The `blackboard-cli` command exposes every public facade method except iterators,
47
+ and can export data as JSON, CSV, or Microsoft Excel files. It is intended as a
48
+ data exploration and management tool for Blackboard administrators.
49
+
50
+ ## Installation
51
+
52
+ PyBlackboard-LMS requires Python 3.10 or later.
53
+
54
+ ```text
55
+ python -m pip install PyBlackboard-LMS
56
+ ```
57
+
58
+ The package installs the `blackboard-cli` command and the `blackboard_api` module.
59
+
60
+ ## Configuration with explicit parameter values (API only)
61
+
62
+ In a Python script:
63
+
64
+ ```python
65
+ from blackboard_api import BlackboardAPI
66
+ client = BlackboardAPI(
67
+ url="https://blackboard.example.com",
68
+ client_id="...",
69
+ client_secret="...",
70
+ )
71
+ ```
72
+
73
+ `url` is the URL of the Blackboard instance. `client_id` is the value called
74
+ `APP_KEY` in the [Blackboard Developer Portal](https://developer.blackboard.com/portal/applications),
75
+ and `client_secret` is its corresponding `APP_SECRET` value.
76
+
77
+ ## Configuration with .env files (API and CLI)
78
+
79
+ When creating `BlackboardAPI` without direct credentials, `env_file` is required.
80
+ Both the API and CLI use dotenv-style files as their configuration source.
81
+
82
+ 1. Copy the `.env.example` template file to `.env.production.local` and configure it with
83
+ API credentials and Blackboard instance URL of your **PRODUCTION** Blackboard instance.
84
+
85
+ 2. Copy the `.env.example` template file to `.env.test.local` and configure it with
86
+ API credentials and Blackboard instance URL of your **TEST** Blackboard instance,
87
+ if you have any.
88
+
89
+ You can then specify the ENV-file path with:
90
+
91
+ ```text
92
+ blackboard-cli --env-file .env.production.local <command>
93
+ ```
94
+
95
+ or use the API directly:
96
+
97
+ ```python
98
+ from blackboard_api import BlackboardAPI
99
+
100
+ client = BlackboardAPI(env_file=".env.production.local")
101
+ ```
102
+
103
+ The optional `BB_REQUEST_CONNECT_TIMEOUT` and `BB_REQUEST_READ_TIMEOUT` ENV
104
+ settings are positive integer durations in seconds. They default to `10` and
105
+ `60`, respectively. You can add them to env files if you want to tweak them.
106
+
107
+ ## Writes are disabled by default
108
+
109
+ To reduce the risk of accidental changes, mutating operations are disabled by
110
+ default. The API blocks `POST`, `PUT`, `PATCH`, and `DELETE` before they reach
111
+ Blackboard unless writes are explicitly enabled.
112
+
113
+ You can enable writing operations by passing
114
+ `enable_write=True` to `BlackboardAPI`, or `--enable-write` to the CLI.
115
+
116
+ ## API reference
117
+
118
+ See [the API reference](docs/api_reference.md) for the public interface and
119
+ internal layers.
120
+
121
+ ## CLI command reference
122
+
123
+ The CLI exposes one command for every public API facade method except iterators.
124
+ Iterators are intended for progressive data consumption in Python scripts and
125
+ do not provide a useful CLI abstraction.
126
+
127
+ See [the CLI reference](docs/cli_reference.md) for the complete command list.
128
+
129
+ Use `--command-help` to view every available command.
130
+
131
+ ## Developers only
132
+
133
+ ### Development setup
134
+
135
+ Install the project and development tools in editable mode:
136
+
137
+ ```text
138
+ python -m pip install -e ".[dev]"
139
+ ```
140
+
141
+ ### Unit tests
142
+
143
+ Run automated tests with:
144
+
145
+ ```text
146
+ python -m unittest discover -s tests/unit -v
147
+ ```
148
+
149
+ Unit tests are 100% offline and never require Blackboard credentials or an
150
+ environment file.
151
+
152
+ ### Live integration tests
153
+
154
+ Run live integration tests with:
155
+
156
+ ```text
157
+ python -m tests.integration.run
158
+ ```
159
+
160
+ While unit tests are fully offline, live integration tests connect to Blackboard
161
+ and create, modify, and delete test objects. Use an isolated **test tenant** and
162
+ ensure that `.env.test.local` contains only its credentials. The test runner requires
163
+ that file, but cannot verify that its URL is truly a non-production instance.
164
+ If test environment dot-env file is missing or invalid, live integration tests fail.
165
+
166
+ The test runner writes the same messages to standard output and to a timestamped log
167
+ under `data/test-artifacts/logs/`. Use `--log-level DEBUG` for HTTP and
168
+ fixture lifecycle diagnostics. It never logs credentials, OAuth tokens, or
169
+ passwords.
170
+
171
+ The integration test suite uses randomized, easily recognizable identifiers to avoid
172
+ collisions with existing data. Its final test explicitly verifies cleanup,
173
+ and emergency cleanup is attempted when the process exits.
174
+ Failures outside the process or API failures may still require manual review of the
175
+ test tenant.
@@ -0,0 +1,144 @@
1
+ # PyBlackboard-LMS
2
+
3
+ Python and command-line interfaces for Blackboard LMS API.
4
+
5
+ The Python API (`blackboard_api`) provides:
6
+
7
+ - Resilient HTTP transport and OAuth authentication.
8
+ - API quota tracking.
9
+ - Transparent pagination for object collections.
10
+ - Iterators for consuming large result sets without accumulating every item in memory.
11
+ - An atomic operations layer for individual requests (`resources`).
12
+ - A service layer for combinations of operations, such as upserts (`services`).
13
+ - Public facades with validated, human-oriented methods.
14
+
15
+ The `blackboard-cli` command exposes every public facade method except iterators,
16
+ and can export data as JSON, CSV, or Microsoft Excel files. It is intended as a
17
+ data exploration and management tool for Blackboard administrators.
18
+
19
+ ## Installation
20
+
21
+ PyBlackboard-LMS requires Python 3.10 or later.
22
+
23
+ ```text
24
+ python -m pip install PyBlackboard-LMS
25
+ ```
26
+
27
+ The package installs the `blackboard-cli` command and the `blackboard_api` module.
28
+
29
+ ## Configuration with explicit parameter values (API only)
30
+
31
+ In a Python script:
32
+
33
+ ```python
34
+ from blackboard_api import BlackboardAPI
35
+ client = BlackboardAPI(
36
+ url="https://blackboard.example.com",
37
+ client_id="...",
38
+ client_secret="...",
39
+ )
40
+ ```
41
+
42
+ `url` is the URL of the Blackboard instance. `client_id` is the value called
43
+ `APP_KEY` in the [Blackboard Developer Portal](https://developer.blackboard.com/portal/applications),
44
+ and `client_secret` is its corresponding `APP_SECRET` value.
45
+
46
+ ## Configuration with .env files (API and CLI)
47
+
48
+ When creating `BlackboardAPI` without direct credentials, `env_file` is required.
49
+ Both the API and CLI use dotenv-style files as their configuration source.
50
+
51
+ 1. Copy the `.env.example` template file to `.env.production.local` and configure it with
52
+ API credentials and Blackboard instance URL of your **PRODUCTION** Blackboard instance.
53
+
54
+ 2. Copy the `.env.example` template file to `.env.test.local` and configure it with
55
+ API credentials and Blackboard instance URL of your **TEST** Blackboard instance,
56
+ if you have any.
57
+
58
+ You can then specify the ENV-file path with:
59
+
60
+ ```text
61
+ blackboard-cli --env-file .env.production.local <command>
62
+ ```
63
+
64
+ or use the API directly:
65
+
66
+ ```python
67
+ from blackboard_api import BlackboardAPI
68
+
69
+ client = BlackboardAPI(env_file=".env.production.local")
70
+ ```
71
+
72
+ The optional `BB_REQUEST_CONNECT_TIMEOUT` and `BB_REQUEST_READ_TIMEOUT` ENV
73
+ settings are positive integer durations in seconds. They default to `10` and
74
+ `60`, respectively. You can add them to env files if you want to tweak them.
75
+
76
+ ## Writes are disabled by default
77
+
78
+ To reduce the risk of accidental changes, mutating operations are disabled by
79
+ default. The API blocks `POST`, `PUT`, `PATCH`, and `DELETE` before they reach
80
+ Blackboard unless writes are explicitly enabled.
81
+
82
+ You can enable writing operations by passing
83
+ `enable_write=True` to `BlackboardAPI`, or `--enable-write` to the CLI.
84
+
85
+ ## API reference
86
+
87
+ See [the API reference](docs/api_reference.md) for the public interface and
88
+ internal layers.
89
+
90
+ ## CLI command reference
91
+
92
+ The CLI exposes one command for every public API facade method except iterators.
93
+ Iterators are intended for progressive data consumption in Python scripts and
94
+ do not provide a useful CLI abstraction.
95
+
96
+ See [the CLI reference](docs/cli_reference.md) for the complete command list.
97
+
98
+ Use `--command-help` to view every available command.
99
+
100
+ ## Developers only
101
+
102
+ ### Development setup
103
+
104
+ Install the project and development tools in editable mode:
105
+
106
+ ```text
107
+ python -m pip install -e ".[dev]"
108
+ ```
109
+
110
+ ### Unit tests
111
+
112
+ Run automated tests with:
113
+
114
+ ```text
115
+ python -m unittest discover -s tests/unit -v
116
+ ```
117
+
118
+ Unit tests are 100% offline and never require Blackboard credentials or an
119
+ environment file.
120
+
121
+ ### Live integration tests
122
+
123
+ Run live integration tests with:
124
+
125
+ ```text
126
+ python -m tests.integration.run
127
+ ```
128
+
129
+ While unit tests are fully offline, live integration tests connect to Blackboard
130
+ and create, modify, and delete test objects. Use an isolated **test tenant** and
131
+ ensure that `.env.test.local` contains only its credentials. The test runner requires
132
+ that file, but cannot verify that its URL is truly a non-production instance.
133
+ If test environment dot-env file is missing or invalid, live integration tests fail.
134
+
135
+ The test runner writes the same messages to standard output and to a timestamped log
136
+ under `data/test-artifacts/logs/`. Use `--log-level DEBUG` for HTTP and
137
+ fixture lifecycle diagnostics. It never logs credentials, OAuth tokens, or
138
+ passwords.
139
+
140
+ The integration test suite uses randomized, easily recognizable identifiers to avoid
141
+ collisions with existing data. Its final test explicitly verifies cleanup,
142
+ and emergency cleanup is attempted when the process exits.
143
+ Failures outside the process or API failures may still require manual review of the
144
+ test tenant.
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "PyBlackboard-LMS"
7
+ version = "0.1.0"
8
+ description = "Python API and command-line interface for Blackboard Learn."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Alex Amo", email = "hello@alejandroamo.eu" },
15
+ ]
16
+ keywords = ["blackboard", "blackboard-learn", "cli", "education", "lms"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Education",
27
+ ]
28
+ dependencies = [
29
+ "requests>=2.31,<3",
30
+ "pandas>=2.0,<3",
31
+ "XlsxWriter>=3.1,<4",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "black>=24.0,<27",
37
+ "build>=1.2,<2",
38
+ "check-wheel-contents>=0.6,<1",
39
+ "mypy>=1.10,<2",
40
+ "ruff>=0.6,<1",
41
+ "twine>=5.1,<7",
42
+ ]
43
+
44
+ [project.scripts]
45
+ blackboard-cli = "blackboard_cli.cli:main"
46
+
47
+ [tool.setuptools]
48
+ package-dir = { "" = "src" }
49
+ include-package-data = false
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+ include = ["blackboard_api*", "blackboard_cli*"]
54
+ namespaces = false
55
+
56
+ [tool.black]
57
+ line-length = 88
58
+ target-version = ["py310"]
59
+ extend-exclude = "^/(build|dist)/"
60
+
61
+ [tool.ruff]
62
+ line-length = 88
63
+ target-version = "py310"
64
+ extend-exclude = ["build", "dist"]
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "F", "I", "W"]
68
+
69
+ [tool.mypy]
70
+ python_version = "3.10"
71
+ files = ["src"]
72
+ check_untyped_defs = true
73
+ warn_return_any = true
74
+ warn_unused_ignores = true
75
+ exclude = "^(build|dist)/"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,50 @@
1
+ .env.example
2
+ CHANGELOG.md
3
+ LICENSE
4
+ MANIFEST.in
5
+ README.md
6
+ pyproject.toml
7
+ src/blackboard_api/__init__.py
8
+ src/blackboard_api/api_quota.py
9
+ src/blackboard_api/auth.py
10
+ src/blackboard_api/client.py
11
+ src/blackboard_api/config.py
12
+ src/blackboard_api/errors.py
13
+ src/blackboard_api/identifiers.py
14
+ src/blackboard_api/transport.py
15
+ src/blackboard_api/facades/__init__.py
16
+ src/blackboard_api/facades/api_quota.py
17
+ src/blackboard_api/facades/courses.py
18
+ src/blackboard_api/facades/enrollments.py
19
+ src/blackboard_api/facades/resources.py
20
+ src/blackboard_api/facades/terms.py
21
+ src/blackboard_api/facades/users.py
22
+ src/blackboard_api/resources/__init__.py
23
+ src/blackboard_api/resources/courses.py
24
+ src/blackboard_api/resources/enrollment_roles.py
25
+ src/blackboard_api/resources/enrollments.py
26
+ src/blackboard_api/resources/nodes.py
27
+ src/blackboard_api/resources/terms.py
28
+ src/blackboard_api/resources/users.py
29
+ src/blackboard_api/services/__init__.py
30
+ src/blackboard_api/services/courses.py
31
+ src/blackboard_api/services/enrollments.py
32
+ src/blackboard_api/services/users.py
33
+ src/blackboard_cli/__init__.py
34
+ src/blackboard_cli/__main__.py
35
+ src/blackboard_cli/cli.py
36
+ src/blackboard_cli/encoding.py
37
+ src/blackboard_cli/application/__init__.py
38
+ src/blackboard_cli/converters/__init__.py
39
+ src/blackboard_cli/converters/common.py
40
+ src/blackboard_cli/converters/courses.py
41
+ src/blackboard_cli/converters/enrollments.py
42
+ src/blackboard_cli/converters/generic.py
43
+ src/blackboard_cli/converters/nodes.py
44
+ src/blackboard_cli/converters/roles.py
45
+ src/blackboard_cli/converters/users.py
46
+ src/blackboard_cli/output/__init__.py
47
+ src/blackboard_cli/output/csv.py
48
+ src/blackboard_cli/output/dataframe.py
49
+ src/blackboard_cli/output/excel.py
50
+ src/blackboard_cli/output/table.py
@@ -0,0 +1,27 @@
1
+ import logging
2
+
3
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
4
+
5
+ from .client import BlackboardAPI
6
+ from .identifiers import InvalidIdentifierError
7
+ from .errors import (
8
+ BlackboardAPIError,
9
+ AuthenticationError,
10
+ NotFoundError,
11
+ QuotaExhaustedError,
12
+ WriteNotEnabledError,
13
+ ResponseFormatError,
14
+ TransportError,
15
+ )
16
+
17
+ __all__ = [
18
+ "BlackboardAPI",
19
+ "BlackboardAPIError",
20
+ "AuthenticationError",
21
+ "NotFoundError",
22
+ "QuotaExhaustedError",
23
+ "WriteNotEnabledError",
24
+ "ResponseFormatError",
25
+ "TransportError",
26
+ "InvalidIdentifierError",
27
+ ]
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+ import requests
5
+
6
+ from .errors import QuotaExhaustedError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @dataclass
12
+ class ApiQuotaState:
13
+ max_requests_per_day: Optional[int] = None
14
+ remaining: Optional[int] = None
15
+ retry_after: Optional[int] = None
16
+
17
+ def update_from_response(self, response: requests.Response) -> None:
18
+ quota_limit = response.headers.get("X-Rate-Limit-Limit")
19
+ remaining = response.headers.get("X-Rate-Limit-Remaining")
20
+ retry_after = response.headers.get("Retry-After")
21
+ if remaining is None and (
22
+ self.max_requests_per_day is not None or self.remaining is not None
23
+ ):
24
+ logger.warning(
25
+ "Blackboard did not send the remaining-requests header; "
26
+ "preserving the last known value."
27
+ )
28
+ parsed_limit = _parse_int(quota_limit)
29
+ parsed_remaining = _parse_int(remaining)
30
+ parsed_retry_after = _parse_int(retry_after)
31
+ if quota_limit is not None and parsed_limit is None:
32
+ logger.warning(
33
+ "Invalid X-Rate-Limit-Limit header; preserving the prior value."
34
+ )
35
+ if remaining is not None and parsed_remaining is None:
36
+ logger.warning(
37
+ "Invalid X-Rate-Limit-Remaining header; preserving the prior "
38
+ "value."
39
+ )
40
+ if retry_after is not None and parsed_retry_after is None:
41
+ logger.warning(
42
+ "Invalid Retry-After header; preserving the prior value."
43
+ )
44
+ if parsed_limit is not None:
45
+ self.max_requests_per_day = parsed_limit
46
+ if parsed_remaining is not None:
47
+ self.remaining = parsed_remaining
48
+ if parsed_retry_after is not None:
49
+ self.retry_after = parsed_retry_after
50
+ if self.remaining == 0:
51
+ logger.error("Blackboard reported zero remaining API requests")
52
+ raise QuotaExhaustedError("Blackboard API quota is exhausted")
53
+
54
+
55
+ def _parse_int(value: str | None) -> int | None:
56
+ try:
57
+ return int(value)
58
+ except (TypeError, ValueError):
59
+ return None
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ import logging
5
+
6
+ import requests
7
+
8
+ from .errors import AuthenticationError, TransportError
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AuthService:
14
+ def __init__(
15
+ self, base_url: str, client_id: str, client_secret: str, transport: Any
16
+ ) -> None:
17
+ self.base_url = base_url.rstrip("/")
18
+ self.client_id = client_id
19
+ self.client_secret = client_secret
20
+ self._transport = transport
21
+ self.token = None
22
+ self.token_expires_at = 0
23
+
24
+ def is_token_expired(self, now: float) -> bool:
25
+ return self.token is None or now >= self.token_expires_at
26
+
27
+ def request_access_token(self) -> dict[str, Any]:
28
+ logger.debug("Requesting OAuth token from Blackboard")
29
+ try:
30
+ response = self._transport.request(
31
+ "POST",
32
+ f"{self.base_url}/learn/api/public/v1/oauth2/token",
33
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
34
+ data={"grant_type": "client_credentials"},
35
+ auth=(self.client_id, self.client_secret),
36
+ track_api_quota=False,
37
+ )
38
+ except TransportError as exc:
39
+ logger.warning("Could not reach the authentication endpoint")
40
+ raise AuthenticationError(
41
+ "Could not reach the authentication endpoint"
42
+ ) from exc
43
+ try:
44
+ response.raise_for_status()
45
+ data = response.json()
46
+ except requests.exceptions.HTTPError as exc:
47
+ logger.warning("Blackboard rejected OAuth authentication")
48
+ raise AuthenticationError(
49
+ "Blackboard rejected authentication credentials"
50
+ ) from exc
51
+ except ValueError as exc:
52
+ logger.warning("Blackboard returned invalid JSON during authentication")
53
+ raise AuthenticationError("Authentication JSON response is invalid") from exc
54
+ if not isinstance(data, dict) or not data.get("access_token"):
55
+ logger.warning("OAuth response does not contain a valid access_token")
56
+ raise AuthenticationError(
57
+ "Authentication response does not contain access_token"
58
+ )
59
+ return data
60
+
61
+ def authenticate(self, now: float) -> str:
62
+ if not self.is_token_expired(now):
63
+ logger.debug("Reusing valid OAuth token")
64
+ return self.token
65
+ data = self.request_access_token()
66
+ self.token = data["access_token"]
67
+ try:
68
+ expires_in = max(1, int(data.get("expires_in", 3600)))
69
+ except (TypeError, ValueError) as exc:
70
+ raise AuthenticationError(
71
+ "Authentication response contains invalid expires_in"
72
+ ) from exc
73
+ # Prevent very short-lived tokens from expiring immediately.
74
+ skew = min(60, max(0, expires_in // 10))
75
+ self.token_expires_at = now + expires_in - skew
76
+ logger.debug("OAuth token obtained; expiry uses a safety margin")
77
+ return self.token