Layered configuration loader merging env vars, files, and defaults.
pip install philiprehberger-config-kitLayered configuration loader merging env vars, files, and defaults.
pip install philiprehberger-config-kit
from philiprehberger_config_kit import Config
config = Config(
sources=[
Config.defaults({"port": 3000, "debug": False, "log_level": "info"}),
Config.json_file("config.json", optional=True),
Config.env_file(".env", optional=True),
Config.env(prefix="APP_"),
]
)
# Typed access
port = config.get_int("port")
debug = config.get_bool("debug")
db_url = config.get_str("database_url")
timeout = config.get_float("timeout", default=5.0)
hosts = config.get_list("allowed_hosts")
Retrieve nested values using dot-separated keys:
from philiprehberger_config_kit import Config
config = Config(
sources=[
Config.defaults({
"database": {"host": "localhost", "port": 5432},
"cache": {"redis": {"url": "redis://localhost"}},
}),
]
)
host = config.get("database.host") # "localhost"
port = config.get_int("database.port") # 5432
url = config.get_str("cache.redis.url") # "redis://localhost"
Sources are applied in order -- later sources override earlier ones:
from philiprehberger_config_kit import Config
config = Config(sources=[
Config.defaults({...}), # lowest priority
Config.json_file("..."), # overrides defaults
Config.env_file(".env"), # overrides JSON
Config.env(prefix="APP_"), # highest priority
])
Define expected keys, types, and allowed values, then validate:
from philiprehberger_config_kit import Config, ConfigSchema, SchemaError
config = Config(sources=[
Config.defaults({"host": "localhost", "port": 5432, "mode": "dev"}),
])
schema = ConfigSchema()
schema.required("host", str)
schema.required("port", int)
schema.optional("debug", bool)
schema.required("mode", str, choices=["dev", "prod", "test"])
config.validate(schema) # raises SchemaError with all failures listed
Refresh configuration from all sources at runtime:
from philiprehberger_config_kit import Config
config = Config(sources=[Config.env(prefix="APP_")])
port = config.get("port")
# After environment changes...
config.reload()
port = config.get("port") # picks up the new value
Get notified when a value changes after reload():
from philiprehberger_config_kit import Config
config = Config(sources=[Config.json_file("config.json")])
def log_change(key, old, new):
print(f"{key}: {old!r} -> {new!r}")
unsubscribe = config.on_change(log_change)
# Later, after the source file changes on disk:
config.reload()
# log_change is called once per dotted key whose value changed
unsubscribe() # stop listening
from philiprehberger_config_kit import Config
config = Config(sources=[
Config.defaults({"db": {"host": "localhost", "port": 5432}, "debug": True}),
])
# Deep copy as nested dict
data = config.to_dict()
# {"db": {"host": "localhost", "port": 5432}, "debug": True}
# Flat dict with dot-notation keys (string values)
flat = config.flatten()
# {"db.host": "localhost", "db.port": "5432", "debug": "True"}
# Environment variable format (UPPER_SNAKE_CASE, string values)
env = config.to_env(prefix="APP")
# {"APP_DB_HOST": "localhost", "APP_DB_PORT": "5432", "APP_DEBUG": "True"}
Capture config state and compare snapshots to see what changed:
from philiprehberger_config_kit import Config
config = Config(sources=[
Config.defaults({"host": "localhost", "port": 3000}),
Config.env(prefix="APP_"),
])
before = config.snapshot()
# ... environment changes, then reload ...
config.reload()
after = config.snapshot()
diff = before.diff(after)
# {"added": {...}, "removed": {...}, "changed": {"port": {"old": "3000", "new": "8080"}}}
Parse comma-separated values into typed lists:
from philiprehberger_config_kit import Config
config = Config(sources=[
Config.defaults({"ports": "8080,8081,8082", "rates": "1.5,2.0,3.7"}),
])
ports = config.get_int_list("ports") # [8080, 8081, 8082]
rates = config.get_float_list("rates") # [1.5, 2.0, 3.7]
# Custom separator
config2 = Config(sources=[Config.defaults({"ids": "1|2|3"})])
config2.get_int_list("ids", sep="|") # [1, 2, 3]
With prefix="APP_", env vars are mapped:
APP_PORT -> portAPP_DATABASE__HOST -> database.host (double underscore = nested)get_bool() accepts: true/false, 1/0, yes/no, on/off
Layer programmatic overrides on top of file/env sources without writing to disk. Both flat and nested forms work:
from philiprehberger_config_kit import Config
config = Config([
Config.defaults({"port": 8080}),
Config.dict_source({"db": {"host": "x", "port": 5432}}), # nested
Config.json_file("config.json", optional=True), # overrides above
])
set()Config.set(key, value) updates a value at runtime and fires on_change listeners — only when the value actually changes.
config.on_change(lambda key, old, new: print(f"{key}: {old} -> {new}"))
config.set("db.host", "new-host")
# db.host: localhost -> new-host
config.set("db.host", "new-host") # same value, listener does NOT fire
| Function / Class | Description |
|---|---|
Config(sources) | Layered configuration with typed access |
Config.dict_source(values) | Create an in-memory dict source (flat or nested) |
Config.get(key, default) | Get a value by key with dot-notation support |
Config.get_str(key, default) | Get a string value |
Config.get_int(key, default) | Get an integer value |
Config.get_float(key, default) | Get a float value |
Config.get_bool(key, default) | Get a boolean value with coercion |
Config.get_list(key, separator, default) | Get a list by splitting a string value |
Config.get_int_list(key, sep) | Split a string value and convert each element to int |
Config.get_float_list(key, sep) | Split a string value and convert each element to float |
Config.require(*keys) | Raise ConfigError if any keys are missing |
Config.has(key) | Check if a key exists |
Config.set(key, value) | Set a value at runtime; fires on_change listeners on real change |
Config.validate(schema) | Validate config against a ConfigSchema |
Config.reload() | Reload configuration from all sources |
Config.on_change(callback) | Register (key, old, new) listener; returns unsubscribe |
Config.to_dict() | Return a deep copy as a nested dictionary |
Config.to_env(prefix) | Export as UPPER_SNAKE_CASE environment variable pairs |
Config.flatten(prefix) | Export as flat dict with dot-notation keys |
Config.snapshot() | Capture current state as a ConfigSnapshot |
Config.freeze() | Freeze the config to prevent mutation |
ConfigSchema | Define expected keys, types, required/optional, and choices |
ConfigSchema.required(key, type, choices) | Add a required field to the schema |
ConfigSchema.optional(key, type, choices) | Add an optional field to the schema |
ConfigSnapshot | Immutable snapshot of config state |
ConfigSnapshot.diff(other) | Compare two snapshots and return added/removed/changed keys |
ConfigError(missing) | Raised when required config keys are missing |
SchemaError(errors) | Raised when config values fail schema validation |
pip install -e .
python -m pytest tests/ -v
If you find this project useful: