ConfZ Configuration Management

repository·main·Indexed 42 days ago

https://github.com/zuehlke/confz

ConfZ is a configuration management library for Python version 2.1.0 built on top of Pydantic. It enables loading, transforming, and validating configuration from various sources—including files (JSON, YAML, TOML), environment variables, and CLI arguments—into type-safe, immutable Python objects. It features a singleton mechanism via BaseConfig, support for multiple configuration origins, and a context manager for temporarily changing configuration values during testing.

Tokens
10K
Snippets
32
Records
49
Agent score
52%

What's inside ConfZ

  1. Overview of ConfZ configuration management

    main

    ConfZ is a configuration management library for Python built on top of pydantic. It provides a structured way to manage application settings by:

    • Loading data from multiple sources: Supports config files, environment variables, command line arguments, and more.
    • Transformation and Validation: Transforms loaded raw data into a desired format and validates it using Pydantic.
    • Type-safe access: Provides configuration as Python dataclass-like objects, enabling full IDE support (autocompletion and type checking).

    Common use cases supported include managing multiple environments, implementing singletons with lazy loading, applying configuration changes for unit tests, and creating custom configuration sources.

  2. Use different configuration sources in ConfZ

    main

    ConfZ allows you to load configuration from multiple origins using different ConfigSource implementations. You can compose these sources to build a layered configuration. The available source types include:

    • FileSource: Loads configuration from files (supports various formats via FileFormat).
    • EnvSource: Loads configuration from environment variables.
    • CLArgSource: Loads configuration from command-line arguments.
    • DataSource: A base class for custom data sources.

    All sources are managed via the ConfigSources data object.

  3. Available Config Sources in ConfZ

    main

    ConfZ uses ConfigSource objects to instruct Loader instances on how to retrieve configuration data. You can use several built-in source types depending on your requirements:

    • FileSource: Loads data from files. Supports yaml, json, and toml formats. The filename can be provided directly, via an environment variable, or via a command line argument.
    • EnvSource: Loads data from environment variables and .env files. You can filter variables using allow-lists or deny-lists, apply an optional prefix, and define a custom separator for nested variables. Variable names can be inferred from the config name or explicitly mapped.
    • CLArgSource: Loads data from command line arguments. Supports an optional prefix and custom separators for nested arguments. Argument names can be inferred or explicitly mapped.
    • DataSource: Defines constant configuration data. This is particularly useful for providing static values during unit tests.
  4. Configure the Loading Order of Config Sources

    main

    When defining configuration sources in a ConfZ config class, you can provide a single source or a list of sources.

    Priority Rules:

    1. Constructor Arguments: If you provide keyword arguments directly to the constructor of your config class, these are read first and have the lowest priority. They will be overridden by any defined ConfigSource.
    2. Source List Order: When providing a list of sources, they are loaded in the order they appear in the list. Later sources in the list override values from earlier sources.
  5. Use CONFIG_SOURCES for singleton configuration

    main

    To avoid manually passing config instances throughout your application, define CONFIG_SOURCES as a class variable in your BaseConfig subclass.

    When CONFIG_SOURCES is set:

    1. The class becomes a singleton.
    2. Loading is lazy: the configuration is only loaded the first time you instantiate the class.
    3. Successive calls to the constructor return the same cached instance.

    Note: If CONFIG_SOURCES is defined, you cannot pass keyword arguments to the constructor.

    from confz import BaseConfig, FileSource
    
    class APIConfig(BaseConfig):
        host: str
        port: int
        CONFIG_SOURCES = FileSource(file="/path/to/config.yaml")
    
    # Accessing config anywhere in your code
    print(APIConfig().port)
    
    # Verification of singleton behavior
    print(APIConfig() is APIConfig())  # True
  6. Handle Pydantic v2 compatibility in ConfZ

    main
    ConfZ v2 supports Pydantic v2. Once initialized, a BaseConfig class behaves similarly to a standard Pydantic BaseModel. Because Pydantic 2 introduced significant changes, you should review the official Pydantic migration guide if your code relies on specific Pydantic functionalities that may have changed.
  7. Extend ConfZ with custom loaders

    main
    ConfZ allows you to extend its configuration loading capabilities by implementing the Loader base class or using the register_loader function. This enables the library to fetch configuration data from arbitrary sources (e.g., remote APIs, specialized databases, or custom file formats) that are not supported out-of-the-box.
  8. Temporarily change config values for testing

    main

    To change configuration values temporarily (e.g., during unit tests) without modifying the global CONFIG_SOURCES, use the change_config_sources context manager provided by your BaseConfig class. You can pass a DataSource containing a dictionary of the new values.

    from confz import BaseConfig, FileSource, DataSource
    
    class MyConfig(BaseConfig):
        number: int
        CONFIG_SOURCES = FileSource(file="/path/to/config.yml")
    
    # Use the context manager to override values
    new_source = DataSource(data={'number': 42})
    with MyConfig.change_config_sources(new_source):
        print(MyConfig().number)  # prints '42'
    
    # Values revert back after the context manager exits
    print(MyConfig().number)      # prints original value from file
  9. Temporarily change config values using a context manager

    main

    If you need to override configuration values temporarily—for example during unit tests—you cannot directly modify the CONFIG_SOURCES class variable. Instead, use the change_config_sources context manager provided by your BaseConfig subclass.

    When entering the context, the manager backs up the existing configuration source definition and the singleton instance, replaces them with the new source, and deletes the old instance. Upon exiting the context, it restores the original configuration source and singleton instance exactly as they were.

    from confz import BaseConfig, FileSource, DataSource
    
    class MyConfig(BaseConfig):
        number: int
        CONFIG_SOURCES = FileSource(file="/path/to/config.yml")
    
    # Temporarily override with a DataSource
    new_source = DataSource(data={"number": 42})
    with MyConfig.change_config_sources(new_source):
        print(MyConfig().number)  # Output: 42
    
    # Automatically reverts to original source
    print(MyConfig().number)  # Output: 1
  10. Configure multiple config sources

    main

    ConfZ allows you to define multiple sources for your configuration. You can use a single source or a list of sources to combine data from different origins like files, environment variables, and command-line arguments.

    Environment-based file selection

    Use folder and file_from_env to pick a config file based on an environment variable:

    CONFIG_SOURCES = FileSource(
        folder='/path/to/config/folder',
        file_from_env='ENVIRONMENT'
    )

    Combining Environment and CLI arguments

    You can provide a list of sources. For example, to read from a .env file and then override with command-line arguments:

    from confz import BaseConfig, EnvSource, CLArgSource
    
    class MyConfig(BaseConfig):
        CONFIG_SOURCES = [
            EnvSource(allow_all=True, file=".env.local"),
            CLArgSource(prefix='conf_')
        ]

    Note: For CLArgSource with prefix conf_, a nested attribute like db.user can be set via the command line argument --conf_db.user.

    from confz import BaseConfig, EnvSource, CLArgSource
    
    class MyConfig(BaseConfig):
        # ... fields ...
        CONFIG_SOURCES = [
            EnvSource(allow_all=True, file=".env.local"),
            CLArgSource(prefix='conf_')
        ]
  11. Quick Start: Define and use configuration with BaseConfig

    main

    To use ConfZ, inherit from BaseConfig and define your configuration schema using Pydantic types. Specify your data sources by setting the CONFIG_SOURCES class variable.

    ConfZ uses a singleton mechanism with lazy loading: the configuration is automatically loaded and cached the first time you instantiate the class. Subsequent calls return the same immutable instance.

    from confz import BaseConfig, FileSource
    from pydantic import SecretStr, AnyUrl
    
    class DBConfig(BaseConfig):
        user: str
        password: SecretStr
    
    class APIConfig(BaseConfig):
        host: AnyUrl
        port: int
        db: DBConfig
    
        CONFIG_SOURCES = FileSource(file='/path/to/config.yml')
    
    # Accessing the config anywhere in your app
    print(f"Serving API at {APIConfig().host}, port {APIConfig().port}.")
  12. Extend ConfZ with custom sources and loaders

    main

    ConfZ is modular, allowing you to define and register your own ConfigSource and Loader to inject dynamic data into your configuration.

    To extend ConfZ, follow these steps:

    1. Define a ConfigSource: Create a dataclass inheriting from ConfigSource. The fields in this class represent the keys that will be written into your configuration dictionary.
    2. Define a Loader: Create a class inheriting from Loader. Implement the @classmethod populate_config(cls, config: dict, config_source: CustomSource) method. This method receives the current configuration dictionary and your custom source instance. Use cls.update_dict_recursively(config, config_update) to merge new values into the existing config.
    3. Register the Loader: Use register_loader(CustomSource, CustomLoader) to link your source to your loader.
    4. Use the Source: Pass an instance of your CustomSource to the config_sources argument of any BaseConfig class.
    import sys
    from dataclasses import dataclass
    from confz import ConfigSource, BaseConfig, register_loader
    from confz.loaders import Loader
    
    # 1. Define the source
    @dataclass
    class CustomSource(ConfigSource):
        platform: str = None    # The value of this field will be the key in the config
        version: str = None     # The value of this field will be the key in the config
    
    # 2. Define the loader
    class CustomLoader(Loader):
        @classmethod
        def populate_config(cls, config: dict, config_source: CustomSource):
            config_update = {
                config_source.platform: sys.platform,
                config_source.version: f"{sys.version_info[0]}.{sys.version_info[1]}"
            }
            cls.update_dict_recursively(config, config_update)
    
    # 3. Register
    register_loader(CustomSource, CustomLoader)
    
    # 4. Use in a config class
    class MyConfig(BaseConfig):
        attr1: str
        attr2: str
    
    config = MyConfig(config_sources=CustomSource(
        platform="attr1",
        version="attr2"
    ))
    print(config)
    # Output: MyConfig(attr1='win32' attr2='3.9')