# Python settings revisited

So, earlier I mucked about with pydantic, and then some plain dataclasses for the sake of making a settings object. Why should you? Well.. it's darn snazzy! And further more it helps in that you can compartmentalize your settings as you app grows.

I outlined how you could use dotenv to load secrets into your environment and populate a settings object. This time I'm going to expand on the settings object, and I will show how to add the settings object to django.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In a django application your environment is typically defined either in your kube.yaml file or docker_compose.yaml file.. The first is Podman/kubernetes and the second is podman/docker. And if you are not using containers.. you really should.</div>
</div>

Below is a non-typical django settings structure. Typically, when you start a project and add an application the application will contain a *settings.py* file. This is fine, for the most part. However - I tend to build monoliths, this after years and years of micro services. I've learned my lesson! Monoliths it is (I do of course also use micro services, only they are not so micro any more). Monoliths tend to acquire a lot of applications. Mine does anyway, and therefore I also tend to structure my settings in the way shown below.

```bash
<project>/<app>/settings
├── apps
│   ├── bcentral_settings.py
│   ├── some_settings.py
│   ├── __init__.py
│   ├── sso_settings.py
|   |── logging_settings.py
│   └── other_settings.py
├── base.py
└── __init__.py
```

*base.py* is more or less the just the *settings.py* file. The *\_\_init\_\_.py* file looks like this:

```bash
$ cat __init__.py
from .base import *
from bcentral_settings.py import *
from .apps.some_settings import *
from .apps.sso_settings import *
from .apps.logging_settings import *
from .apps.other_settings import *
```

This means that everything(yeah...) defined in these files will be read into the *settings* namespace. For example, *logging\_settings.py* contains a dictionary

```python
LOGGING = {
    "version": 1, 
    ....
}
```

I just prefer to have the logging in a separate configuration file. There's no magic here.. other than the magic that django introduces by automagically configuring the logger when it encounters **LOGGING**.. still. It's not like it's Galtworth or something.

We are just going to look at *bcentral\_settings.py*

Let's start with some code.

```python
"""A settings class."""
import os
from dataclasses import dataclass, field
from django.core.exceptions import ValidationError

__all__ = ["BCENTRAL"]

PREFIX = "BCENTRAL_"  # the environment variables are prefixed

@dataclass
class BCentralSettings:
    CLIENT_ID: str = field(default="")
    CLIENT_SECRET: str = field(default="")
    TENANT_ID: str = field(default="")
    API_URL: str = field(default="")
    AUTH_ENDPOINT: str = "https://login.microsoftonline.com//oauth2/v2.0/token"
    COMPANY_UUID: str = field(default="")
    SCOPE: list[str] = field(default_factory=lambda: [
        "https://api.businesscentral.dynamics.com/.default"
        ])
```

So far pretty normal. We are going to add a *\_\_post\_init\_\_* magic method to the class. In this *\_\_post\_init\_\_* we will build the mechanics for populating the class with environment variables, and we will add validation mechanics. The validation is no different from what you'd normally do in django (or any other framework / program). [https://docs.djangoproject.com/en/5.0/ref/validators/](https://docs.djangoproject.com/en/5.0/ref/validators/)

```python
    def __post_init__(self):
        for name, _field in self.__dataclass_fields__.items():
            if match := os.getenv(f"{PREFIX}{name}"):
                setattr(self, name, match)
            if method := getattr(self, f"validate_{name}", None):
                setattr(self, name, method(getattr(self, name), field=_field))
```

This code might warrant some explanation. In this implementation of *\_\_post\_init\_\_* we are looping over all the fields in the dataclass and can do whatever we want with them. First, we try to match an environment variable to a name in the dataclass. I tend to keep one flat file of environment variables and distinguish between them I use a prefix that tie into the settings object. In this case **PREFIX = "BCENTRAL\_"**

The second *if* statement looks for a metod named *validate\_NAME* in the class.

```python
    def validate_CLIENT_ID(self, value: str, field=field) -> str:
        if not value:
            raise ValidationError(f"{field.name} is required")
        return value
```

And so on and so forth. You could also make sure *value* is a valid UUID, has fins and lives in the Gobi desert.. if that's your thing.

After all this, make an instance of your object

```python
BCENTRAL = BCentralSettings()
```

Do this at the end of *bcentral\_settings.py*. As you might have noticed *\_\_all\_\_ = \["BCENTRAL"\]* is used. This ensures that **settings** is not polluted with variables from our file.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: <em>__all__</em> affects the <strong>from &lt;module&gt; import * </strong>behavior only<em>. </em>Members that are not mentioned in<em> __all__ are</em> still accessible from outside the module and can be imported with <strong>from &lt;module&gt; import &lt;name&gt;</strong>.</div>
</div>

Access your settings:

```python
$ python <project>/manage.py shell
In [1]: from django.conf import settings
In [2]: settings.BCENTRAL.CLIENT_ID
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
In [3]: settings.PREFIX
.
.
AttributeError: 'Settings' object has no attribute 'PREFIX'
```

That's about it. Easy configuration management for, in this instance, django.

Until next time..
