Configuration Management
ZodiacCore provides a robust utility for managing application settings using .ini files. It is designed to follow the "Base + Override" pattern, making it ideal for multi-environment deployments (Development, Testing, Production).
1. Core Concepts
Environment-Based Loading
The configuration system automatically detects your current environment (via environment variables) and loads files in a specific order:
- Base Config: Files like
app.ini. These are loaded first. - Environment Config: Files like
app.production.ini. These are loaded second, overriding any matching keys from the base config.
Dot-Notation Access
Instead of using dictionary keys (e.g., config['db']['host']), ZodiacCore can convert your settings into a SimpleNamespace, allowing for cleaner dot-notation access (e.g., config.db.host).
2. Setting Up Your Config Folder
A typical production-ready configuration folder structure:
config/
├── app.ini # Default settings (all environments)
├── app.develop.ini # Local development overrides
├── app.testing.ini # Test overrides
└── app.production.ini # Production secrets/tuning
Loading the Config
You can use ConfigManagement to find the correct files and then load them using your preferred library (like configparser or dependency-injector).
from pathlib import Path
from zodiac_core.config import ConfigManagement
# 1. Get the list of files in correct loading order
config_dir = Path(__file__).parent / "config"
config_files = ConfigManagement.get_config_files(
search_paths=[config_dir],
env_var="APPLICATION_ENVIRONMENT", # Default: APPLICATION_ENVIRONMENT
default_env="production" # Default fallback if env_var is missing
)
# For local app templates, you can override the fallback explicitly:
# ConfigManagement.get_config_files(search_paths=[config_dir], default_env="develop")
Integrating with dependency-injector
This is the default integration pattern used by the generated project template.
from pathlib import Path
from dependency_injector import containers, providers
from zodiac_core.config import ConfigManagement
from zodiac_core.utils import strtobool
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
@staticmethod
def initialize():
config_dir = Path(__file__).resolve().parent.parent / "config"
config_files = ConfigManagement.get_config_files(
search_paths=[config_dir],
default_env="develop",
)
container = Container()
for path in config_files:
container.config.from_ini(path, required=True)
return container
container = Container.initialize()
db_url = container.config.db.url()
db_echo = container.config.db.get("echo", as_=strtobool)
Testing Environment
testing is a first-class environment. A common pattern is:
- Keep test overrides in
config/app.testing.ini - Set
APPLICATION_ENVIRONMENT=testingin your test bootstrap - Let
ConfigManagement.get_config_files()loadapp.inifirst, thenapp.testing.ini
3. Configuration Objects
ZodiacCore provides two ways to access your configuration data using ConfigManagement.provide_config:
If you are using the generated template or dependency-injector, prefer providers.Configuration() plus from_ini(...) as your main path. provide_config() is mainly for projects that want ZodiacCore's config helpers without using DI.
Mode A: SimpleNamespace (Quick Access)
This mode is useful for rapid prototyping. It converts the dictionary into a SimpleNamespace, allowing for dot-notation access but without type hints or validation.
raw_data = {"db": {"host": "localhost", "port": 5432}}
config = ConfigManagement.provide_config(raw_data)
print(config.db.host) # 'localhost'
Mode B: Pydantic Model (Recommended)
For production applications, it is highly recommended to use a Pydantic model. This provides:
- Type Safety: Full IDE autocompletion and type checking.
- Validation: Runtime checks to ensure your configuration is valid.
- Defaults: Automatically fill in missing values defined in your schema.
from pydantic import BaseModel
from zodiac_core.config import ConfigManagement
class DbConfig(BaseModel):
host: str
port: int = 5432
class AppConfig(BaseModel):
db: DbConfig
raw_data = {"db": {"host": "localhost"}}
# Pass the model class as the second argument
config = ConfigManagement.provide_config(raw_data, AppConfig)
print(config.db.host) # 'localhost' (with IDE autocomplete!)
print(config.db.port) # 5432 (default value applied)
4. Best Practices with dependency-injector
For the full
providers.ConfigurationAPI, see the official documentation.
When using providers.Configuration from dependency-injector, follow these practices to catch configuration errors early and keep your code type-safe.
Strict Mode
Always enable strict=True on the Configuration provider. Without it, accessing an undefined config key silently returns None instead of raising an error — bugs surface at runtime instead of startup.
# Good — typo or missing key raises immediately
config = providers.Configuration(strict=True)
# Bad — config.db.hoost() silently returns None
config = providers.Configuration()
Required Config Files
Pass required=True to from_ini() for files that must exist. By default, missing files are silently ignored.
Type Conversion
All values from .ini files are strings. Use the built-in helpers or Pydantic models for conversion:
| Need | Approach |
|---|---|
| Integer | config.api.timeout.as_int() |
| Float | config.api.ratio.as_float() |
| Bool | config.db.echo.as_(strtobool) — not as_(bool), since bool("false") is True |
| Custom | config.pi.as_(Decimal) |
| Whole section | ConfigManagement.provide_config(container.config.db(), DbConfig) — Pydantic handles all conversions |
The Pydantic model approach is recommended for sections with multiple fields — it handles type coercion, validation, and defaults in one place, and you don't need to worry about strtobool or as_int().
Use StrictConfig as the base class instead of BaseModel. It adds extra='forbid' (rejects typo keys like ech0) and frozen=True (immutable after creation):
from zodiac_core.config import ConfigManagement, StrictConfig
class DbConfig(StrictConfig):
url: str
echo: bool = False # Pydantic correctly parses "false" → False
db_cfg = ConfigManagement.provide_config(container.config.db(), DbConfig)
db.setup(database_url=db_cfg.url, echo=db_cfg.echo)
Environment Variable Interpolation
.ini files support ${ENV_VAR} and ${ENV_VAR:default} syntax for injecting secrets without hardcoding:
To require that all referenced environment variables are defined (no silent empty substitution), pass envs_required=True:
5. API Reference
StrictConfig
zodiac_core.config.StrictConfig
Bases: BaseModel
Base model for configuration sections.
Enforces two constraints:
- extra='forbid': unknown keys (e.g. typos in .ini) raise ValidationError
instead of being silently ignored.
- frozen=True: config objects are immutable after creation.
Source code in zodiac_core/config.py
Environment Enum
zodiac_core.config.Environment
Bases: str, Enum
Supported application environments.
Source code in zodiac_core/config.py
Configuration Management
zodiac_core.config.ConfigManagement
Configuration management utility for scanning, loading, and converting configuration files.
This utility is designed to work well with configuration loading patterns where
subsequent files override previous ones (e.g., standard configparser or
dependency-injector).
See: https://python-dependency-injector.ets-labs.org/providers/configuration.html
Source code in zodiac_core/config.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
get_config_files(search_paths, env_var='APPLICATION_ENVIRONMENT', default_env='production')
staticmethod
Scans specified directories for configuration files and returns them in loading order.
The scanning strategy follows these rules: 1. Base Config: Files with at most one dot (e.g., 'app.ini'). Loaded first. 2. Env Config: Files matching '{name}.{env}.ini' (e.g., 'app.develop.ini'). Loaded second.
The returned order (Base -> Env) ensures that environment-specific settings override base settings when loaded by configuration providers.
Files not matching the current environment but belonging to a known Environment enum are silently skipped. Files with unknown environment suffixes are logged as debug.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
search_paths
|
List[Union[str, Path]]
|
List of directory paths to search. REQUIRED. |
required |
env_var
|
str
|
Environment variable name to determine current environment. |
'APPLICATION_ENVIRONMENT'
|
default_env
|
str
|
Fallback environment if env_var is not set. |
'production'
|
Returns:
| Type | Description |
|---|---|
List[str]
|
A list of absolute paths to configuration files, sorted by priority (Base -> Env). |
Example
Source code in zodiac_core/config.py
provide_config(config=None, model=None)
staticmethod
Converts a configuration dictionary into a structured object.
Supports two modes: 1. SimpleNamespace mode (default): Returns a SimpleNamespace for dot notation access. 2. Pydantic model mode: Pass a Pydantic model class to get type-safe, validated config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
dict
|
The configuration dictionary to convert. |
None
|
model
|
Type[T]
|
Optional Pydantic model class. If provided, returns an instance of this model. |
None
|
Returns:
| Type | Description |
|---|---|
Union[SimpleNamespace, T]
|
SimpleNamespace if no model provided, otherwise an instance of the model. |
Example
# Mode 1: SimpleNamespace (no type hints, but convenient)
config = ConfigManagement.provide_config({"db": {"host": "localhost"}})
print(config.db.host) # "localhost"
# Mode 2: Pydantic model (full type hints and validation)
from pydantic import BaseModel
class DbConfig(BaseModel):
host: str
port: int = 5432
class AppConfig(BaseModel):
db: DbConfig
config = ConfigManagement.provide_config({"db": {"host": "localhost"}}, AppConfig)
print(config.db.host) # IDE autocomplete works!
print(config.db.port) # 5432 (default value)