philiprehberger-config-kit
Layered configuration loader merging env vars, files, and defaults.
Installation
pip install philiprehberger-config-kit
Usage
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")
Dot-Notation Access
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"
Source Priority
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
])
Schema Validation
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
Reload
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
Change Listeners
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
Export Methods
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"}
Config Snapshot Diffing
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"}}}
Typed List Getters
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]
Environment Variables
With prefix="APP_", env vars are mapped:
APP_PORT->portAPP_DATABASE__HOST->database.host(double underscore = nested)
Bool Coercion
get_bool() accepts: true/false, 1/0, yes/no, on/off
In-Memory Dict Source
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
])
Runtime Mutations with 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
Loading Directly from Env Vars with from_env()
Skip building a full source list when you only need env vars. The prefix-matching, lowercasing, and __ -> . rules are identical to Config.env().
from philiprehberger_config_kit import Config
# APP_DB__HOST=localhost APP_DB__PORT=5432
config = Config.from_env("APP_")
config.get("db.host") # "localhost"
config.get_int("db.port") # 5432
# Keep the prefix in the resulting keys
config = Config.from_env("APP_", strip_prefix=False)
config.get("app_db.host") # "localhost"
Slicing a Config with subset()
Return a new Config restricted to keys under a dot-notation prefix. By default the prefix is stripped from the resulting keys.
from philiprehberger_config_kit import Config
config = Config([
Config.dict_source({
"db": {"host": "localhost", "port": 5432},
"cache": {"ttl": 60},
}),
])
db = config.subset("db.")
db.get("host") # "localhost"
db.get_int("port") # 5432
db_full = config.subset("db.", strip_prefix=False)
db_full.get("db.host") # "localhost"
Composing Configs with merge()
Combine two Config instances without re-reading files or env vars. Values from the right-hand config take precedence using the same deep-merge rules as layered sources.
base = Config(sources=[Config.defaults({"db": {"host": "localhost", "port": 5432}})])
overlay = Config(sources=[Config.defaults({"db": {"host": "prod.example.com"}})])
combined = base.merge(overlay)
combined.get("db.host") # "prod.example.com"
combined.get("db.port") # 5432 (inherited from base)
API
| 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.merge(other) |
Return a new Config with this config merged under other |
Config.from_env(prefix, *, strip_prefix=True) |
Classmethod: build a Config from env vars matching prefix |
Config.subset(prefix, *, strip_prefix=True) |
Return a new Config containing only keys under prefix |
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 |
Development
pip install -e .
python -m pytest tests/ -v
Support
If you find this project useful:
License
Release files for philiprehberger-config-kit 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| philiprehberger_config_kit-0.7.0.tar.gz | 195.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| philiprehberger_config_kit-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 207.4 kB
Release files / philiprehberger_config_kit-0.7.0.tar.gz
| Download URL | philiprehberger_config_kit-0.7.0.tar.gz |
|---|---|
| Size | 195.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a6cdd795e24076ab4595f764190ea02e46abf994f6729324d19a757a973dbf4
|
|
BLAKE2b-256 checksum How to use checksums |
03c1a8c0e7aa234cc14ecdc4604f85e7c88f6e0bb47fd84bc50c32ae48d284dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / philiprehberger_config_kit-0.7.0-py3-none-any.whl
| Download URL | philiprehberger_config_kit-0.7.0-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f398de3cc96ba6ca5f46cdeed0c4829ab8bcaa4217b7629f31d59eb764207786
|
|
BLAKE2b-256 checksum How to use checksums |
d90d62af310d33bcb43dec170e4b08190de7c45eea6e8cec70e8e3ab2a38aca8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|