django-aqueduct 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.
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # uv
19
+ uv.lock
20
+
21
+ # Testing / coverage
22
+ .pytest_cache/
23
+ .coverage
24
+ htmlcov/
25
+ .tox/
26
+
27
+ # mypy
28
+ .mypy_cache/
29
+
30
+ # IDEs
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
File without changes
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, MIT Open Learning Engineering
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-aqueduct
3
+ Version: 0.1.0
4
+ Summary: Structured, typed, auditable Django settings management powered by Pydantic
5
+ Project-URL: Homepage, https://github.com/mitodl/django-aqueduct
6
+ Project-URL: Repository, https://github.com/mitodl/django-aqueduct
7
+ Project-URL: Issues, https://github.com/mitodl/django-aqueduct/issues
8
+ Author-email: MIT Open Learning Engineering <odl-devops@mit.edu>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: configuration,django,kubernetes,pydantic,settings,vault
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.0
15
+ Classifier: Framework :: Django :: 5.1
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: BSD License
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: django>=5.0
26
+ Requires-Dist: pydantic-settings>=2.0
27
+ Requires-Dist: pydantic>=2.0
28
+ Provides-Extra: aws
29
+ Requires-Dist: boto3>=1.26; extra == 'aws'
30
+ Provides-Extra: mitol
31
+ Requires-Dist: mitol-django-common>=2023.1.1; extra == 'mitol'
32
+ Provides-Extra: vault
33
+ Requires-Dist: hvac>=2.0; extra == 'vault'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # django-aqueduct
37
+
38
+ Structured, typed, auditable Django settings management powered by Pydantic.
39
+
40
+ `django-aqueduct` channels configuration from multiple sources — environment variables, YAML files, HashiCorp Vault, AWS SSM Parameter Store — into a single typed, validated model, making settings auditable and K8s-friendly without changing any application code.
41
+
42
+ [![CI](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml/badge.svg)](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/django-aqueduct)](https://pypi.org/project/django-aqueduct/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/django-aqueduct)](https://pypi.org/project/django-aqueduct/)
45
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install django-aqueduct
53
+
54
+ # Optional extras
55
+ pip install django-aqueduct[vault] # HashiCorp Vault support (hvac)
56
+ pip install django-aqueduct[aws] # AWS SSM Parameter Store (boto3)
57
+ pip install django-aqueduct[mitol] # mitol-django-common EnvParser integration
58
+ ```
59
+
60
+ Add to `INSTALLED_APPS`:
61
+
62
+ ```python
63
+ INSTALLED_APPS = [
64
+ ...
65
+ "django_aqueduct",
66
+ ]
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Quickstart
72
+
73
+ ### Step 1 — Generate a scaffold
74
+
75
+ Point `generate_aqueduct_settings` at your existing settings module:
76
+
77
+ ```bash
78
+ python manage.py generate_aqueduct_settings \
79
+ --modules myapp.settings.common \
80
+ --output src/myapp/settings_model.py
81
+ ```
82
+
83
+ This emits a typed `AqueductSettings(BaseSettings)` class with every
84
+ `UPPERCASE` name from your settings module as a Pydantic field, grouped
85
+ under section comments by source module.
86
+
87
+ ### Step 2 — Refine the scaffold
88
+
89
+ Open `settings_model.py` and:
90
+
91
+ - Fix any `# TODO: refine type` annotations
92
+ - Add `model_validator` methods to derive complex objects from primitives:
93
+
94
+ ```python
95
+ from pydantic import model_validator
96
+ import dj_database_url
97
+
98
+ class AqueductSettings(BaseSettings):
99
+ DATABASE_URL: str = Field(default="sqlite:///db.sqlite3")
100
+
101
+ # Derived — populated by the validator below
102
+ DATABASES: dict[str, Any] = Field(default_factory=dict)
103
+
104
+ @model_validator(mode="after")
105
+ def build_databases(self) -> "AqueductSettings":
106
+ self.DATABASES = {"default": dj_database_url.parse(self.DATABASE_URL)}
107
+ return self
108
+ ```
109
+
110
+ ### Step 3 — Wire the shim
111
+
112
+ Replace your host settings file with a thin shim:
113
+
114
+ ```python
115
+ # myapp/settings/production.py
116
+ from django_aqueduct import configure_django_settings
117
+ from myapp.settings_model import AqueductSettings
118
+
119
+ configure_django_settings(AqueductSettings)
120
+ ```
121
+
122
+ That's it. `DJANGO_SETTINGS_MODULE` stays the same. All existing
123
+ `django.conf.settings.FOO` access in application code continues to work
124
+ with zero changes.
125
+
126
+ ---
127
+
128
+ ## Kubernetes deployment pattern
129
+
130
+ In Kubernetes, configuration typically arrives from multiple sources:
131
+
132
+ | Source | Typical content |
133
+ |--------|----------------|
134
+ | Pod environment variables | Non-secret config from ConfigMaps |
135
+ | Vault (Kubernetes SA auth) | Database passwords, API keys |
136
+ | AWS SSM Parameter Store | Secrets in AWS-hosted deployments |
137
+
138
+ Configure all three in your settings model:
139
+
140
+ ```python
141
+ from django_aqueduct import configure_django_settings
142
+ from django_aqueduct.sources.vault import VaultSettingsSource
143
+ from django_aqueduct.sources.aws_ssm import AWSParameterStoreSource
144
+ from pydantic_settings import BaseSettings, SettingsConfigDict
145
+
146
+
147
+ class ProductionSettings(BaseSettings):
148
+ model_config = SettingsConfigDict(extra="allow")
149
+
150
+ SECRET_KEY: str = Field(...)
151
+ DATABASE_URL: str = Field(...)
152
+
153
+ @classmethod
154
+ def settings_customise_sources(cls, settings_cls, **kwargs):
155
+ return (
156
+ # 1. Environment variables (from K8s ConfigMaps)
157
+ kwargs["env_settings"],
158
+ # 2. Vault via Kubernetes SA — reads JWT from default mount path
159
+ # /var/run/secrets/kubernetes.io/serviceaccount/token
160
+ VaultSettingsSource(
161
+ settings_cls,
162
+ vault_url="https://vault.example.com",
163
+ vault_path="myapp/production",
164
+ auth_method="kubernetes",
165
+ role="myapp",
166
+ # Optional: custom JWT path for projected service accounts
167
+ # jwt_path="/var/run/secrets/custom/token",
168
+ ),
169
+ )
170
+
171
+
172
+ # myapp/settings/production.py
173
+ configure_django_settings(ProductionSettings)
174
+ ```
175
+
176
+ ### Vault authentication methods
177
+
178
+ | Method | When to use |
179
+ |--------|-------------|
180
+ | `"token"` | Local dev, CI with a static token |
181
+ | `"oidc"` | Interactive / browser-based login |
182
+ | `"kubernetes"` | Production K8s — uses the pod's service account JWT |
183
+
184
+ ```python
185
+ # Token auth (dev/CI)
186
+ VaultSettingsSource(settings_cls, ..., auth_method="token", vault_token="s.xxx")
187
+
188
+ # OIDC (interactive)
189
+ VaultSettingsSource(settings_cls, ..., auth_method="oidc", role="myapp")
190
+
191
+ # Kubernetes SA (production) — custom JWT path
192
+ VaultSettingsSource(
193
+ settings_cls,
194
+ ...,
195
+ auth_method="kubernetes",
196
+ role="myapp",
197
+ jwt_path="/var/run/secrets/tokens/vault", # projected SA token
198
+ )
199
+ ```
200
+
201
+ ### AWS SSM Parameter Store
202
+
203
+ ```python
204
+ from django_aqueduct.sources.aws_ssm import AWSParameterStoreSource
205
+
206
+ # All parameters under /myapp/production/ are fetched with full pagination.
207
+ # The prefix is stripped: /myapp/production/SECRET_KEY → SECRET_KEY
208
+ AWSParameterStoreSource(
209
+ settings_cls,
210
+ path_prefix="/myapp/production/",
211
+ region_name="us-east-1",
212
+ )
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Adapter modes
218
+
219
+ ### Option A — Shim settings file (recommended)
220
+
221
+ `DJANGO_SETTINGS_MODULE` stays unchanged. The settings file becomes a
222
+ thin shim:
223
+
224
+ ```python
225
+ # myapp/settings/production.py
226
+ from django_aqueduct import configure_django_settings
227
+ from myapp.settings_model import ProductionSettings
228
+
229
+ configure_django_settings(ProductionSettings)
230
+ ```
231
+
232
+ Works with gunicorn, Celery, pytest-django, management commands, and every
233
+ other tool that reads `DJANGO_SETTINGS_MODULE` — no changes required.
234
+
235
+ ### Option B — Programmatic configure (greenfield)
236
+
237
+ For new projects or container-native apps where you control all entry points
238
+ and want no `DJANGO_SETTINGS_MODULE`:
239
+
240
+ ```python
241
+ # manage.py or WSGI/ASGI entry point — call before django.setup()
242
+ from django_aqueduct import configure_django_programmatic
243
+ from myapp.settings_model import AppSettings
244
+
245
+ configure_django_programmatic(AppSettings)
246
+
247
+ import django
248
+ django.setup()
249
+ ```
250
+
251
+ ---
252
+
253
+ ## edx-platform migration walkthrough
254
+
255
+ edx-platform's `lms/envs/production.py` currently loads a YAML file and
256
+ applies hundreds of lines of post-processing. With `django-aqueduct`:
257
+
258
+ 1. **Generate the scaffold** from `common.py`:
259
+
260
+ ```bash
261
+ python manage.py generate_aqueduct_settings \
262
+ --modules lms.envs.common \
263
+ --output lms/envs/settings_model.py
264
+ ```
265
+
266
+ 2. **Review** `settings_model.py` — fix `# TODO: refine type` entries,
267
+ move `derive_settings` logic into `@model_validator` methods.
268
+
269
+ 3. **Replace** `lms/envs/production.py`:
270
+
271
+ ```python
272
+ # lms/envs/production.py
273
+ from django_aqueduct import configure_django_settings
274
+ from lms.envs.settings_model import LMSSettings
275
+
276
+ configure_django_settings(LMSSettings)
277
+ ```
278
+
279
+ 4. Set `DJANGO_SETTINGS_MODULE=lms.envs.production` as before.
280
+ All LMS app code using `from django.conf import settings` is unchanged.
281
+
282
+ ---
283
+
284
+ ## `[mitol]` extra — EnvParser integration
285
+
286
+ If your project uses `mitol-django-common`'s `EnvParser`, install the
287
+ `[mitol]` extra and pass `--include-envparser` to the generator:
288
+
289
+ ```bash
290
+ pip install django-aqueduct[mitol]
291
+
292
+ python manage.py generate_aqueduct_settings \
293
+ --modules myapp.settings \
294
+ --include-envparser
295
+ ```
296
+
297
+ The `EnvParserInspector` reads the global `env._configured_vars` registry
298
+ and emits precisely-typed fields for every `get_string`/`get_bool`/`get_int`
299
+ call, preserving `description`, `required`, and `dev_only` metadata.
300
+
301
+ ---
302
+
303
+ ## Contributing
304
+
305
+ ```bash
306
+ git clone https://github.com/mitodl/django-aqueduct
307
+ cd django-aqueduct
308
+ uv sync
309
+ uv run pytest
310
+ uv run mypy src/django_aqueduct
311
+ ```
312
+
313
+ Install pre-commit hooks with [prek](https://prek.j178.dev):
314
+
315
+ ```bash
316
+ pip install prek
317
+ prek install
318
+ ```
319
+
320
+ Please open an issue before submitting a pull request for significant changes.
321
+
322
+ ---
323
+
324
+ ## License
325
+
326
+ BSD-3-Clause © MIT Open Learning Engineering
@@ -0,0 +1,291 @@
1
+ # django-aqueduct
2
+
3
+ Structured, typed, auditable Django settings management powered by Pydantic.
4
+
5
+ `django-aqueduct` channels configuration from multiple sources — environment variables, YAML files, HashiCorp Vault, AWS SSM Parameter Store — into a single typed, validated model, making settings auditable and K8s-friendly without changing any application code.
6
+
7
+ [![CI](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml/badge.svg)](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/django-aqueduct)](https://pypi.org/project/django-aqueduct/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/django-aqueduct)](https://pypi.org/project/django-aqueduct/)
10
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install django-aqueduct
18
+
19
+ # Optional extras
20
+ pip install django-aqueduct[vault] # HashiCorp Vault support (hvac)
21
+ pip install django-aqueduct[aws] # AWS SSM Parameter Store (boto3)
22
+ pip install django-aqueduct[mitol] # mitol-django-common EnvParser integration
23
+ ```
24
+
25
+ Add to `INSTALLED_APPS`:
26
+
27
+ ```python
28
+ INSTALLED_APPS = [
29
+ ...
30
+ "django_aqueduct",
31
+ ]
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quickstart
37
+
38
+ ### Step 1 — Generate a scaffold
39
+
40
+ Point `generate_aqueduct_settings` at your existing settings module:
41
+
42
+ ```bash
43
+ python manage.py generate_aqueduct_settings \
44
+ --modules myapp.settings.common \
45
+ --output src/myapp/settings_model.py
46
+ ```
47
+
48
+ This emits a typed `AqueductSettings(BaseSettings)` class with every
49
+ `UPPERCASE` name from your settings module as a Pydantic field, grouped
50
+ under section comments by source module.
51
+
52
+ ### Step 2 — Refine the scaffold
53
+
54
+ Open `settings_model.py` and:
55
+
56
+ - Fix any `# TODO: refine type` annotations
57
+ - Add `model_validator` methods to derive complex objects from primitives:
58
+
59
+ ```python
60
+ from pydantic import model_validator
61
+ import dj_database_url
62
+
63
+ class AqueductSettings(BaseSettings):
64
+ DATABASE_URL: str = Field(default="sqlite:///db.sqlite3")
65
+
66
+ # Derived — populated by the validator below
67
+ DATABASES: dict[str, Any] = Field(default_factory=dict)
68
+
69
+ @model_validator(mode="after")
70
+ def build_databases(self) -> "AqueductSettings":
71
+ self.DATABASES = {"default": dj_database_url.parse(self.DATABASE_URL)}
72
+ return self
73
+ ```
74
+
75
+ ### Step 3 — Wire the shim
76
+
77
+ Replace your host settings file with a thin shim:
78
+
79
+ ```python
80
+ # myapp/settings/production.py
81
+ from django_aqueduct import configure_django_settings
82
+ from myapp.settings_model import AqueductSettings
83
+
84
+ configure_django_settings(AqueductSettings)
85
+ ```
86
+
87
+ That's it. `DJANGO_SETTINGS_MODULE` stays the same. All existing
88
+ `django.conf.settings.FOO` access in application code continues to work
89
+ with zero changes.
90
+
91
+ ---
92
+
93
+ ## Kubernetes deployment pattern
94
+
95
+ In Kubernetes, configuration typically arrives from multiple sources:
96
+
97
+ | Source | Typical content |
98
+ |--------|----------------|
99
+ | Pod environment variables | Non-secret config from ConfigMaps |
100
+ | Vault (Kubernetes SA auth) | Database passwords, API keys |
101
+ | AWS SSM Parameter Store | Secrets in AWS-hosted deployments |
102
+
103
+ Configure all three in your settings model:
104
+
105
+ ```python
106
+ from django_aqueduct import configure_django_settings
107
+ from django_aqueduct.sources.vault import VaultSettingsSource
108
+ from django_aqueduct.sources.aws_ssm import AWSParameterStoreSource
109
+ from pydantic_settings import BaseSettings, SettingsConfigDict
110
+
111
+
112
+ class ProductionSettings(BaseSettings):
113
+ model_config = SettingsConfigDict(extra="allow")
114
+
115
+ SECRET_KEY: str = Field(...)
116
+ DATABASE_URL: str = Field(...)
117
+
118
+ @classmethod
119
+ def settings_customise_sources(cls, settings_cls, **kwargs):
120
+ return (
121
+ # 1. Environment variables (from K8s ConfigMaps)
122
+ kwargs["env_settings"],
123
+ # 2. Vault via Kubernetes SA — reads JWT from default mount path
124
+ # /var/run/secrets/kubernetes.io/serviceaccount/token
125
+ VaultSettingsSource(
126
+ settings_cls,
127
+ vault_url="https://vault.example.com",
128
+ vault_path="myapp/production",
129
+ auth_method="kubernetes",
130
+ role="myapp",
131
+ # Optional: custom JWT path for projected service accounts
132
+ # jwt_path="/var/run/secrets/custom/token",
133
+ ),
134
+ )
135
+
136
+
137
+ # myapp/settings/production.py
138
+ configure_django_settings(ProductionSettings)
139
+ ```
140
+
141
+ ### Vault authentication methods
142
+
143
+ | Method | When to use |
144
+ |--------|-------------|
145
+ | `"token"` | Local dev, CI with a static token |
146
+ | `"oidc"` | Interactive / browser-based login |
147
+ | `"kubernetes"` | Production K8s — uses the pod's service account JWT |
148
+
149
+ ```python
150
+ # Token auth (dev/CI)
151
+ VaultSettingsSource(settings_cls, ..., auth_method="token", vault_token="s.xxx")
152
+
153
+ # OIDC (interactive)
154
+ VaultSettingsSource(settings_cls, ..., auth_method="oidc", role="myapp")
155
+
156
+ # Kubernetes SA (production) — custom JWT path
157
+ VaultSettingsSource(
158
+ settings_cls,
159
+ ...,
160
+ auth_method="kubernetes",
161
+ role="myapp",
162
+ jwt_path="/var/run/secrets/tokens/vault", # projected SA token
163
+ )
164
+ ```
165
+
166
+ ### AWS SSM Parameter Store
167
+
168
+ ```python
169
+ from django_aqueduct.sources.aws_ssm import AWSParameterStoreSource
170
+
171
+ # All parameters under /myapp/production/ are fetched with full pagination.
172
+ # The prefix is stripped: /myapp/production/SECRET_KEY → SECRET_KEY
173
+ AWSParameterStoreSource(
174
+ settings_cls,
175
+ path_prefix="/myapp/production/",
176
+ region_name="us-east-1",
177
+ )
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Adapter modes
183
+
184
+ ### Option A — Shim settings file (recommended)
185
+
186
+ `DJANGO_SETTINGS_MODULE` stays unchanged. The settings file becomes a
187
+ thin shim:
188
+
189
+ ```python
190
+ # myapp/settings/production.py
191
+ from django_aqueduct import configure_django_settings
192
+ from myapp.settings_model import ProductionSettings
193
+
194
+ configure_django_settings(ProductionSettings)
195
+ ```
196
+
197
+ Works with gunicorn, Celery, pytest-django, management commands, and every
198
+ other tool that reads `DJANGO_SETTINGS_MODULE` — no changes required.
199
+
200
+ ### Option B — Programmatic configure (greenfield)
201
+
202
+ For new projects or container-native apps where you control all entry points
203
+ and want no `DJANGO_SETTINGS_MODULE`:
204
+
205
+ ```python
206
+ # manage.py or WSGI/ASGI entry point — call before django.setup()
207
+ from django_aqueduct import configure_django_programmatic
208
+ from myapp.settings_model import AppSettings
209
+
210
+ configure_django_programmatic(AppSettings)
211
+
212
+ import django
213
+ django.setup()
214
+ ```
215
+
216
+ ---
217
+
218
+ ## edx-platform migration walkthrough
219
+
220
+ edx-platform's `lms/envs/production.py` currently loads a YAML file and
221
+ applies hundreds of lines of post-processing. With `django-aqueduct`:
222
+
223
+ 1. **Generate the scaffold** from `common.py`:
224
+
225
+ ```bash
226
+ python manage.py generate_aqueduct_settings \
227
+ --modules lms.envs.common \
228
+ --output lms/envs/settings_model.py
229
+ ```
230
+
231
+ 2. **Review** `settings_model.py` — fix `# TODO: refine type` entries,
232
+ move `derive_settings` logic into `@model_validator` methods.
233
+
234
+ 3. **Replace** `lms/envs/production.py`:
235
+
236
+ ```python
237
+ # lms/envs/production.py
238
+ from django_aqueduct import configure_django_settings
239
+ from lms.envs.settings_model import LMSSettings
240
+
241
+ configure_django_settings(LMSSettings)
242
+ ```
243
+
244
+ 4. Set `DJANGO_SETTINGS_MODULE=lms.envs.production` as before.
245
+ All LMS app code using `from django.conf import settings` is unchanged.
246
+
247
+ ---
248
+
249
+ ## `[mitol]` extra — EnvParser integration
250
+
251
+ If your project uses `mitol-django-common`'s `EnvParser`, install the
252
+ `[mitol]` extra and pass `--include-envparser` to the generator:
253
+
254
+ ```bash
255
+ pip install django-aqueduct[mitol]
256
+
257
+ python manage.py generate_aqueduct_settings \
258
+ --modules myapp.settings \
259
+ --include-envparser
260
+ ```
261
+
262
+ The `EnvParserInspector` reads the global `env._configured_vars` registry
263
+ and emits precisely-typed fields for every `get_string`/`get_bool`/`get_int`
264
+ call, preserving `description`, `required`, and `dev_only` metadata.
265
+
266
+ ---
267
+
268
+ ## Contributing
269
+
270
+ ```bash
271
+ git clone https://github.com/mitodl/django-aqueduct
272
+ cd django-aqueduct
273
+ uv sync
274
+ uv run pytest
275
+ uv run mypy src/django_aqueduct
276
+ ```
277
+
278
+ Install pre-commit hooks with [prek](https://prek.j178.dev):
279
+
280
+ ```bash
281
+ pip install prek
282
+ prek install
283
+ ```
284
+
285
+ Please open an issue before submitting a pull request for significant changes.
286
+
287
+ ---
288
+
289
+ ## License
290
+
291
+ BSD-3-Clause © MIT Open Learning Engineering