A modular, reusable configuration system for composable settings with environment variable overrides, file loading, and optional object construction helpers.
pip install dataknobs-config
from dataknobs_config import Config
# Load from dictionary
config = Config({
"database": [
{"name": "primary", "host": "localhost", "port": 5432},
{"name": "secondary", "host": "backup.local", "port": 5433}
],
"cache": [
{"name": "redis", "host": "localhost", "port": 6379}
]
})
# Access configurations
primary_db = config.get("database", "primary")
print(primary_db["host"]) # localhost
# Load from file
config = Config.from_file("config.yaml")
# Load from multiple sources
config = Config("base.yaml", "overrides.json", {"extra": [...]})
Each configuration is an “atomic” unit - a dictionary of settings for a single object:
{
"name": "primary", # Optional, auto-generated if not provided
"type": "database", # Optional, inferred from parent key
"host": "localhost",
"port": 5432,
# ... any other attributes
}
Internally, configurations are organized by type:
{
"database": [ # Type name
{...}, # Atomic config 1
{...} # Atomic config 2
],
"cache": [
{...} # Atomic config
],
"settings": { # Special type for global settings
"config_root": "/app/config",
"default_timeout": 30
}
}
Reference other configurations using the xref format:
config = Config({
"database": [
{"name": "primary", "host": "db.example.com"}
],
"api": [
{
"name": "main",
"database": "xref:database[primary]" # Reference
}
]
})
# Resolve references
api = config.resolve_reference("xref:api[main]")
print(api["database"]["host"]) # db.example.com
xref:type[name]
- Reference by namexref:type[0]
- Reference by indexxref:type[-1]
- Reference last itemxref:type
- Reference first/only itemOverride any configuration value using environment variables:
export DATAKNOBS_DATABASE__PRIMARY__HOST=prod.example.com
export DATAKNOBS_DATABASE__PRIMARY__PORT=5433
export DATAKNOBS_CACHE__REDIS__TTL=7200
config = Config({
"database": [{"name": "primary", "host": "localhost", "port": 5432}],
"cache": [{"name": "redis", "ttl": 3600}]
})
# Environment variables automatically override values
db = config.get("database", "primary")
print(db["host"]) # prod.example.com
print(db["port"]) # 5433 (converted to int)
DATAKNOBS_<TYPE>__<NAME_OR_INDEX>__<ATTRIBUTE>
DATAKNOBS_DATABASE__0__CONNECTION__TIMEOUT
Reference external configuration files using the @
prefix:
# main.yaml
database:
- "@database/primary.yaml" # Load from file
- "@database/secondary.yaml"
settings:
config_root: /app/config # Base path for relative references
Configure global settings and defaults in the special settings
section:
config = Config({
"database": [{"name": "db1"}],
"settings": {
# Paths
"config_root": "/app/config", # Base path for "@"-prefixed config file references
"global_root": "/app", # Base for path resolution (settings.path_resolution_attributes)
"database.global_root": "/app/db", # Type-specific base for path resolution
# Path resolution (supports exact names and regex patterns)
"path_resolution_attributes": [
"config_path", # Exact match for all types
"database.data_dir", # Exact match for database type only
"/.*_path$/", # Regex: all attributes ending with "_path"
"cache./.*_dir$/" # Regex: cache type attributes ending with "_dir"
],
# Defaults
"default_timeout": 30, # Global default
"database.default_pool_size": 10 # Type-specific default
}
})
Automatically resolve relative paths to absolute:
config = Config({
"database": [{
"name": "db1",
"data_dir": "./data", # Relative path
"backup_dir": "/abs/path" # Absolute path unchanged
}],
"settings": {
"global_root": "/app", # Base for path resolution
"path_resolution_attributes": ["data_dir", "backup_dir"]
}
})
db = config.get("database", "db1")
print(db["data_dir"]) # /app/data (resolved)
print(db["backup_dir"]) # /abs/path (unchanged)
Build objects directly from configurations:
# Using class attribute
config = Config({
"database": [{
"name": "primary",
"class": "myapp.database.PostgreSQL",
"host": "localhost",
"port": 5432
}]
})
# Build object
db = config.build_object("xref:database[primary]")
# Returns instance of myapp.database.PostgreSQL
# Using factory pattern
config = Config({
"cache": [{
"name": "redis",
"factory": "myapp.cache.CacheFactory",
"type": "redis",
"host": "localhost"
}]
})
cache = config.build_object("xref:cache[redis]")
from dataknobs_config import ConfigurableBase
class MyDatabase(ConfigurableBase):
def __init__(self, host, port, **kwargs):
self.host = host
self.port = port
@classmethod
def from_config(cls, config):
# Custom configuration logic
return cls(**config)
from dataknobs_config import FactoryBase
class DatabaseFactory(FactoryBase):
def create(self, **config):
db_type = config.pop("type", "postgresql")
if db_type == "postgresql":
return PostgreSQL(**config)
elif db_type == "mysql":
return MySQL(**config)
# Configuration with factory
config = Config({
"database": [{
"name": "primary",
"factory": "myapp.db.DatabaseFactory",
"type": "postgresql",
"host": "localhost"
}]
})
# Get the factory instance (cached)
factory = config.get_factory("database", "primary")
db1 = factory.create(database="app1")
db2 = factory.create(database="app2")
# Or get an instance directly
db = config.get_instance("database", "primary", database="myapp")
class Config:
def __init__(self, *sources, use_env=True)
def from_file(cls, path) -> Config
def from_dict(cls, data) -> Config
# Access
def get_types() -> List[str]
def get_count(type_name: str) -> int
def get_names(type_name: str) -> List[str]
def get(type_name: str, name_or_index: Union[str, int] = 0) -> dict
def set(type_name: str, name_or_index: Union[str, int], config: dict)
# References
def resolve_reference(ref: str) -> dict
def build_reference(type_name: str, name_or_index: Union[str, int]) -> str
# Merging
def merge(other: Config, precedence: str = "first")
# Export
def to_dict() -> dict
def to_file(path: Path, format: str = None)
# Object Construction
def build_object(ref: str, cache: bool = True, **kwargs) -> Any
def clear_object_cache(ref: str = None)
# Lazy Factory Access
def get_factory(type_name: str, name_or_index: Union[str, int] = 0) -> Any
def get_instance(type_name: str, name_or_index: Union[str, int] = 0, **kwargs) -> Any
# base.yaml
database:
- name: primary
host: localhost
port: 5432
# production.yaml
database:
- name: primary
host: prod.db.example.com
pool_size: 50
# Load with overrides
config = Config("base.yaml", "production.yaml")
config = Config({
"services": [
{"name": "auth", "url": "http://auth:8000"},
{"name": "api", "url": "http://api:8080"}
],
"app": [{
"name": "main",
"auth_service": "xref:services[auth]",
"api_service": "xref:services[api]"
}]
})
app = config.resolve_reference("xref:app[main]")
# app["auth_service"]["url"] = "http://auth:8000"
# Development: export DATAKNOBS_DATABASE__PRIMARY__HOST=localhost
# Production: export DATAKNOBS_DATABASE__PRIMARY__HOST=prod.db.aws.com
config = Config.from_file("config.yaml")
db = config.get("database", "primary")
# Automatically uses environment-appropriate host
Run tests with pytest:
pytest tests/
MIT License - see LICENSE file for details.