Usage Guide

This guide walks through the primary features of ConfigGuard using examples.

Core Concepts Recap

Before diving in, remember these key ideas:

  • Schema: A Python dictionary defining your settings (types, defaults, validation, version).

  • ConfigGuard Instance: The main object holding the schema and current values.

  • Handlers: Internal components for reading/writing different file formats (e.g., JSON) and handling encryption.

  • Save Modes: mode='values' (default, saves only values) or mode='full' (saves version, schema, and values).

  • Versioning: Automatic comparison during load(); handles older versions via migration, raises errors for newer versions.

1. Defining Your Schema

Create a Python dictionary. The top level needs __version__. Other keys are your settings.

Example Schema Definition (v1.1.0)
    generate_encryption_key,
    log,
    set_log_level,
)


def run_basic_usage_example() -> None:
    """Executes the main demonstration of ConfigGuard features."""

    # Set log level for more verbose output during example run
    set_log_level("DEBUG")
    log.info("Starting ConfigGuard Basic Usage Example (with Nesting)...")

    # --- Configuration Constants ---
    CONFIG_VERSION = "2.0.0"  # Updated version for schema with nesting
    BASE_FILENAME = "my_app_config_nested"
    SECRET_KEY_FILE = Path("config_nested.secret")
    SCHEMA_DEFINITION_FILE = Path(
        "my_app_schema_nested_definition.json"
    )  # For saving schema definition

    # --- 1. Define Schema with Nested Sections ---
    my_schema: typing.Dict[str, typing.Any] = {
        "__version__": CONFIG_VERSION,
        "server": {
            "type": "section",
            "help": "Web server configuration.",
            "schema": {
                "host": {
                    "type": "str",
                    "default": "127.0.0.1",
                    "help": "Hostname or IP address to bind the server to.",
                },
                "port": {
                    "type": "int",
                    "default": 8080,
                    "min_val": 1024,
                    "max_val": 65535,
                    "help": "The network port the application should listen on.",
                },
                "timeout_seconds": {
                    "type": "float",
                    "default": 30.0,
                    "min_val": 0.5,
                    "help": "Request timeout in seconds.",
                },

See the schema definition keys in the main README or API docs for SettingSchema.

2. Initializing ConfigGuard

Pass the schema (dict or file path) and optionally the config file path and encryption key.

from configguard import ConfigGuard, generate_encryption_key
from pathlib import Path

# Assume my_app_schema is the dictionary defined above
# Assume config_file_path points to "my_settings.json"
# Assume full_state_file_path points to "my_settings_full.json"
# Assume enc_key is a valid Fernet key

# Basic initialization (values mode, no encryption)
config_basic = ConfigGuard(schema=my_app_schema, config_path=config_file_path)

# With encryption
config_encrypted = ConfigGuard(
    schema=my_app_schema,
    config_path="encrypted_settings.bin", # Use appropriate extension
    encryption_key=enc_key
)

# With autosave (saves values automatically on change)
# config_autosave = ConfigGuard(schema=my_app_schema, config_path=config_file_path, autosave=True)

ConfigGuard automatically tries to load() from config_path if it exists.

3. Accessing Settings and Schema

Use attribute or dictionary syntax. Use the sc_ prefix for schema details.

# Access value
port = config_basic.port
log_level = config_basic['log_level']

# Access schema
port_help = config_basic.sc_port.help
is_db_nullable = config_basic['sc_database_uri'].nullable

print(f"Port: {port} (Help: {port_help})")
print(f"Is DB Nullable? {is_db_nullable}")

4. Modifying Settings

Assign values directly. Validation occurs automatically.

config_basic.port = 8443
config_basic['log_level'] = 'WARNING'
config_basic.feature_flags.append('new_feature_x') # Modifying lists works directly

try:
    config_basic.port = 100 # Below min_val
except ValidationError as e:
    print(f"Validation failed: {e}")

5. Saving Configuration

Specify the mode: 'values' or 'full'.

# Save only values to the default path
config_basic.save(mode='values')
print(f"Values saved to {config_basic._config_path}") # Access internal for demo

# Save full state to a different file
config_basic.save(filepath="config_backup.json", mode='full')
print("Full state saved to config_backup.json")

6. Loading and Version Handling

Loading usually happens on initialization. Manual config.load() is possible. Versioning is automatic.

Let’s simulate loading an older version:

Simulating Older Version File Creation

        try:
            config.logging.level = "TRACE"  # Not in options
        except ValidationError as e:
            log.info(f"Caught expected validation error: {e}")

        try:
            config.server.enabled = "yes"  # Invalid type for bool
        except ValidationError as e:
            log.info(f"Caught expected validation error: {e}")

        try:
            config.database.uri = None  # Allowed due to nullable=True
            log.info(
                f"Successfully set database.uri back to None. Value: {config.database.uri}"
            )
        except ValidationError as e:
            log.error(
                f"Caught UNEXPECTED validation error setting nullable field to None: {e}"
            )

        # Reset database_uri for subsequent steps
        config.database.uri = "sqlite:///prod_nested.db"

        # --- 7. Saving Configuration (Values vs Full - Nested) ---
        log.info("\n--- Saving Configuration (Nested) ---")
        # Save only values to the default path
        log.info(f"Saving mode='values' to {config_file_path}...")
        config.save(mode="values")
        log.info(f"Content of {config_file_path} (values only - nested):")
        log.info(
# Assume my_app_schema is the V1.1.0 schema
# Assume older_file path points to the simulated V1.0.0 file

try:
    print("\\nLoading older config into V1.1.0 instance...")
    # Initialize with CURRENT schema, load OLD file
    config_migrated = ConfigGuard(schema=my_app_schema, config_path=older_file)

    print(f"Loaded file version: {config_migrated.loaded_file_version}") # Should be "1.0.0"
    print(f"Instance version: {config_migrated.version}")         # Should be "1.1.0"

    # Values from old file are loaded if key exists in V1.1.0 schema
    print(f"Port loaded from old: {config_migrated.port}") # e.g., 7000

    # New settings get V1.1.0 defaults
    print(f"Timeout (new in V1.1.0): {config_migrated.timeout_seconds}") # e.g., 30.0

    # Settings only in old file are skipped (warning logged)

except SchemaError as e:
    print(f"Schema error (e.g., file version too new): {e}")
except Exception as e:
    print(f"Other loading error: {e}")

7. Encryption

Provide a encryption_key during initialization. Saving and loading become transparently encrypted/decrypted by the handler.

Encryption Example Snippet
    older_config_full_state_structured = {
        "version": older_version,
        "schema": {  # Pretend V1 had *some* structure
            "server": {
                "type": "section",
                "schema": {
                    "port": {"type": "int", "default": 8000},
                    "enabled": {"type": "bool"},
                },
            },
            "database": {
                "type": "section",
                "schema": {
                    "uri": {
                        "type": "str",
                        "nullable": False,
                        "default": "default.db",
                    },
                    "retry_attempts": {"type": "int"},
                },
            },
            "logging_level": {
                "type": "str",
                "default": "WARN",
                "options": ["INFO", "WARN"],
            },  # Flat
            "security_key": {
                "type": "str",
                "nullable": True,
            },  # Flat, maps to 'security'
            "removed_section": {
                "type": "section",
                "schema": {"old_val": {"type": "int"}},
            },
        },
        "values": {
            "server": {
                "port": 7000,
                "enabled": True,
            },
            "database": {
                "uri": "old_db.sqlite",
                "retry_attempts": 5,
            },
            "logging_level": "INFO",  # Maps to logging.level
            "security_key": "secret-v1.0",  # Maps to security
            "removed_section": {"old_val": 123},
        },

8. Import/Export

Export Current State (Schema + Values)

Useful for UIs or sending state over APIs.

current_state = config_basic.export_schema_with_values()
import json
print(json.dumps(current_state, indent=2))

Import Values from Dictionary

Update settings from a dict (e.g., from a web form). Only affects values, ignores schema/version.

update_data = {"port": 9999, "log_level": "ERROR", "unknown": "skipped"}
try:
    config_basic.import_config(update_data, ignore_unknown=True)
    print(f"Port after import: {config_basic.port}")
except Exception as e:
    print(f"Import failed: {e}")

This covers the main workflows for using ConfigGuard effectively. Check the API Reference reference for detailed class and method information.