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.
- django_aqueduct-0.1.0/.gitignore +38 -0
- django_aqueduct-0.1.0/CHANGELOG.md +0 -0
- django_aqueduct-0.1.0/LICENSE +28 -0
- django_aqueduct-0.1.0/PKG-INFO +326 -0
- django_aqueduct-0.1.0/README.md +291 -0
- django_aqueduct-0.1.0/pyproject.toml +85 -0
- django_aqueduct-0.1.0/src/django_aqueduct/__init__.py +13 -0
- django_aqueduct-0.1.0/src/django_aqueduct/adapter.py +66 -0
- django_aqueduct-0.1.0/src/django_aqueduct/apps.py +10 -0
- django_aqueduct-0.1.0/src/django_aqueduct/codegen/__init__.py +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/codegen/generator.py +155 -0
- django_aqueduct-0.1.0/src/django_aqueduct/discovery/__init__.py +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/discovery/base.py +46 -0
- django_aqueduct-0.1.0/src/django_aqueduct/discovery/envparser.py +132 -0
- django_aqueduct-0.1.0/src/django_aqueduct/discovery/module.py +79 -0
- django_aqueduct-0.1.0/src/django_aqueduct/discovery/type_inference.py +39 -0
- django_aqueduct-0.1.0/src/django_aqueduct/management/__init__.py +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/management/commands/__init__.py +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/management/commands/generate_aqueduct_settings.py +143 -0
- django_aqueduct-0.1.0/src/django_aqueduct/py.typed +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/sources/__init__.py +0 -0
- django_aqueduct-0.1.0/src/django_aqueduct/sources/aws_ssm.py +137 -0
- django_aqueduct-0.1.0/src/django_aqueduct/sources/vault.py +170 -0
|
@@ -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
|
+
[](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/django-aqueduct/)
|
|
44
|
+
[](https://pypi.org/project/django-aqueduct/)
|
|
45
|
+
[](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
|
+
[](https://github.com/mitodl/django-aqueduct/actions/workflows/ci.yml)
|
|
8
|
+
[](https://pypi.org/project/django-aqueduct/)
|
|
9
|
+
[](https://pypi.org/project/django-aqueduct/)
|
|
10
|
+
[](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
|