Skip to main content

Layered configuration loader merging env vars, files, and defaults.

Project description

philiprehberger-config-kit

Tests PyPI version Last updated

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 -> port
  • APP_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:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

philiprehberger_config_kit-0.7.0.tar.gz (195.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

philiprehberger_config_kit-0.7.0-py3-none-any.whl (11.8 kB view details)

Uploaded Python 3

File details

Details for the file philiprehberger_config_kit-0.7.0.tar.gz.

File metadata

File hashes

Hashes for philiprehberger_config_kit-0.7.0.tar.gz
Algorithm Hash digest
SHA256 2a6cdd795e24076ab4595f764190ea02e46abf994f6729324d19a757a973dbf4
MD5 40a1aef08e01060c93fb17cc09c35ae2
BLAKE2b-256 03c1a8c0e7aa234cc14ecdc4604f85e7c88f6e0bb47fd84bc50c32ae48d284dc

See more details on using hashes here.

File details

Details for the file philiprehberger_config_kit-0.7.0-py3-none-any.whl.

File metadata

File hashes

Hashes for philiprehberger_config_kit-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f398de3cc96ba6ca5f46cdeed0c4829ab8bcaa4217b7629f31d59eb764207786
MD5 54f6dc3ae6bae86e27296913981ce343
BLAKE2b-256 d90d62af310d33bcb43dec170e4b08190de7c45eea6e8cec70e8e3ab2a38aca8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page