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

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.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.6.0.tar.gz (194.6 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.6.0-py3-none-any.whl (11.1 kB view details)

Uploaded Python 3

File details

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

File metadata

File hashes

Hashes for philiprehberger_config_kit-0.6.0.tar.gz
Algorithm Hash digest
SHA256 7d111b6a3b391e82b9fce88dce9b2029cf7261c32c1dd0066579e1e2a2db7a5f
MD5 d567b784be83c73922e595acc7e4ca5f
BLAKE2b-256 aaa20c815855f0be83875c294f86cbc70067416315b39d0f519c55a107a20994

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for philiprehberger_config_kit-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b7978bb12732207815e064d4faeea71d6dcaf07b77c860e505a484ab9b1bd8b
MD5 78a251e7b55e12b0b67fe110bf901e10
BLAKE2b-256 5acdbacfcc4dfd377141f1d81f96e6599dcd2d8026d32fe74f2ae59d79721e3e

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