Skip to main content

A robust, schema-driven configuration management library for Python with encryption, versioning, nested sections, and multiple backends.

Project description

ConfigGuard

PyPI version PyPI pyversions PyPI license Downloads Documentation Status

Code style: black


Stop fighting inconsistent, error-prone, and insecure configuration files! 🚀

ConfigGuard transforms your Python application's configuration management from a potential source of bugs and security risks into a robust, reliable, and developer-friendly system. Moving beyond simple dictionaries or basic file parsing, ConfigGuard introduces a schema-driven fortress for your settings, offering unparalleled control and safety.

Leverage a comprehensive suite of features designed for modern applications:

  • Define strict Type Safety and complex Validation Rules (min, max, options, nullable).
  • Protect sensitive data effortlessly with built-in, handler-transparent Encryption.
  • Manage configuration changes across application updates with seamless Versioning (optionally override instance version) and automated Migration.
  • Choose your preferred Storage Format (JSON, YAML, TOML, SQLite included) without altering your core logic.
  • Organize complex configurations intuitively using Nested Sections.
  • Accommodate unpredictable structures with flexible Dynamic Sections.

Why waste time debugging subtle configuration typos or managing insecure secrets manually? ConfigGuard catches errors early, simplifies maintenance, and secures your sensitive data, allowing you to focus on building great features.

Adopt ConfigGuard and configure with confidence!


✨ Key Features

  • 📝 Schema-Driven: Define your configuration's expected structure, types, defaults, and validation rules within a Python dictionary or a JSON file. This acts as the single source of truth, ensuring consistency and enabling static analysis benefits. Schema definitions should ideally include a __version__ key for robust version tracking, but the instance version can also be set explicitly.
  • <0xF0><0x9F><0xA7><0xB1> Nested Configuration: Structure complex settings logically using sections, defined directly within your schema ("type": "section"). Access nested settings intuitively through standard attribute or dictionary notation (e.g., config.database.connection.pool_size, config['server']['ssl']['enabled']), promoting code readability and maintainability.
  • <0xF0><0x9F><0x94><0x91> Dynamic Sections: For scenarios requiring flexibility (like plugin settings or user-defined mappings), define sections with an empty schema ("schema": {}). These sections behave like standard Python dictionaries, allowing the addition, modification, and deletion of arbitrary key-value pairs at runtime, while still benefiting from ConfigGuard's saving, loading, and encryption mechanisms.
  • 🔒 Built-in Encryption: Secure sensitive configuration values (API keys, passwords, tokens) transparently using Fernet symmetric encryption (requires the cryptography library). Encryption is handled automatically by the storage backend during save/load operations, meaning your application code always interacts with plain, decrypted values.
  • 💾 Multiple Backends: Persist your configurations in various formats through an extensible handler system. ConfigGuard automatically detects the desired format based on the file extension (.json, .yaml, .yml, .toml, .db, .sqlite, .sqlite3). Default handlers are provided for JSON, YAML, TOML, and SQLite.
  • 🔄 Versioning & Migration: Embed a version string (e.g., "1.2.0") in your schema's __version__ key or provide it via the instance_version parameter during initialization. When loading configuration files, ConfigGuard compares the file's version with the instance's version. It prevents loading configurations newer than the application expects and gracefully handles older versions by merging existing values, applying new defaults, and skipping settings/sections no longer present in the current schema. This migration logic operates recursively through nested sections.
  • <0xF0><0x9F><0x97><0x84>️ Flexible Save Modes: Control the granularity of saved data:
    • mode='values' (default): Saves only the current configuration key-value pairs. Ideal for runtime updates, preserving the structure (including nesting and dynamic content) according to the chosen handler's capabilities.
    • mode='full': Saves the complete state: the current instance version, the full schema definition (including nested structures and empty schemas for dynamic sections), and the current values. Best for backups, transferring configurations between environments, or providing comprehensive state to external tools or UIs.
  • <0xF0><0x9F><0xA7><0xB1> Supported Types: Define standard settings with common Python types: str, int, float, bool, or list. (Note: Validation of individual element types within lists is not currently implemented). Dynamic sections can store any value that is serializable by the chosen backend handler (typically JSON-serializable types).
  • 🐍 Intuitive Access: Interact with your configuration naturally. Access values using attribute (config.section.setting, config.dynamic_section.key) or dictionary (config['section']['setting'], config['dynamic_section']['key']) syntax. Retrieve schema details for defined settings using the sc_ prefix (config.sc_section.sc_setting). Access schema and current values for specific sections using section.get_schema_dict() and section.get_config_dict().
  • ✔️ Automatic Validation: ConfigGuard automatically validates values against the schema rules (type, nullable, options, min_val, max_val) whenever a standard setting is modified or when data is loaded. This prevents invalid data from entering your configuration state. Values added to dynamic sections bypass this schema validation.
  • 📤 Easy Export/Import:
    • export_schema_with_values(): Get a snapshot of the entire configuration state (schema definition + current values, including dynamic content) as a dictionary, suitable for populating UIs, sending over APIs, or debugging. The exported version reflects the current instance version.
    • import_config(data, ignore_unknown=True): Update the configuration values from a (potentially nested) dictionary. This merges data into the existing structure, applying validation for standard settings and adding/updating keys in dynamic sections. The ignore_unknown flag controls whether unexpected keys cause errors.
  • 🧩 Extensible: Built with a clear StorageHandler interface, allowing developers to easily implement and register support for additional storage backends (e.g., databases, cloud services, environment variables).

🤔 Why Choose ConfigGuard?

ConfigGuard addresses common pain points in configuration management:

  • Eliminate Runtime Config Errors: Instead of discovering a typo in a port number or an invalid logging level only when your application crashes, ConfigGuard catches these errors early – either when the schema is defined or when data is loaded/set – thanks to its strict validation against your predefined rules.
  • Secure Your Secrets with Ease: Stop storing sensitive API keys, database passwords, or tokens in plain text files or insecure environment variables. ConfigGuard's integrated encryption provides a simple, transparent mechanism to protect this data at rest, requiring only a single encryption key and the cryptography library.
  • Future-Proof Your Application: As your application evolves, so will its configuration needs. ConfigGuard's versioning system (using schema __version__ or explicit instance_version) allows you to update your schema confidently. When loading older config files, it automatically attempts to migrate the data, preserving user settings where possible and applying new defaults, significantly reducing the friction of application updates.
  • Improve Code Clarity and Maintainability: Schemas act as self-documentation for your configuration settings. The explicit definition of types, defaults, validation rules, and help strings makes it much easier for developers (including your future self) to understand what each setting does and how to configure it correctly. Nested sections further enhance organization.
  • Manage Complexity Effectively: Modern applications often have numerous configuration options. ConfigGuard allows you to tame this complexity by organizing settings into logical, hierarchical sections (both predefined and dynamic), making the overall configuration easier to navigate and manage.
  • Increase Developer Productivity: Eliminate the need to write repetitive, error-prone boilerplate code for parsing different config file formats, validating data types, checking ranges, handling defaults for missing values, and managing encryption. ConfigGuard handles these common tasks robustly.
  • Gain Storage Freedom: Start with JSON for simplicity, move to YAML for readability, use TOML if preferred, or leverage SQLite for transactional saves – all without changing how your application code interacts with the configuration object. The backend is abstracted away by the handler system.

🚀 Installation

ConfigGuard requires Python 3.8 or later.

Base Installation (includes JSON and SQLite support):

pip install configguard

With Optional Features (Extras):

ConfigGuard uses "extras" to manage dependencies for optional features like encryption and specific file format handlers.

  • Encryption: Requires the cryptography library.

    pip install configguard[encryption]
    
  • YAML Support: Requires the PyYAML library.

    pip install configguard[yaml]
    
  • TOML Support: Requires the toml library.

    pip install configguard[toml]
    
  • (SQLite support uses Python's built-in sqlite3 and needs no extra pip install).

Installing Multiple Extras:

pip install configguard[encryption,yaml,toml]

Installing All Optional Features:

pip install configguard[all]

For Development:

git clone https://github.com/ParisNeo/ConfigGuard.git
cd ConfigGuard
pip install -e .[dev]

This installs ConfigGuard itself, plus tools like pytest, black, ruff, mypy, and the dependencies needed for all built-in handlers, encryption, and the GUI example.


⚡ Quick Start

This example demonstrates defining a schema, initializing ConfigGuard (showing different ways to set the version), accessing/modifying values, and saving.

from configguard import ConfigGuard, ValidationError, generate_encryption_key
from pathlib import Path
import typing # Required for type hinting the schema dict

# 1. Define your schema: Includes version, standard section, dynamic section, top-level setting.
SCHEMA_VERSION = "1.1.0"
my_schema: typing.Dict[str, typing.Any] = {
    "__version__": SCHEMA_VERSION, # Standard way to define version
    "server": { # Standard section
        "type": "section", "help": "Core web server settings.",
        "schema": {
            "host": { "type": "str", "default": "127.0.0.1", "help": "IP address to bind to." },
            "port": { "type": "int", "default": 8080, "min_val": 1024, "max_val": 65535, "help": "Port number." }
        }
    },
    "plugin_data": { # DYNAMIC section
        "type": "section", "help": "Stores runtime data or settings for plugins.",
        "schema": {} # Empty schema marks it as dynamic
    },
    "log_level": { # Standard top-level setting
        "type": "str", "default": "INFO",
        "options": ["DEBUG", "INFO", "WARNING", "ERROR"], "help": "Logging verbosity."
    }
}

# Schema without explicit version
my_schema_no_version = my_schema.copy(); del my_schema_no_version["__version__"]

# 2. Setup file path and optional encryption key
config_file = Path("my_app_config.yaml") # Using YAML handler (requires PyYAML)
encryption_key = generate_encryption_key() # Store securely!

# 3. Initialize ConfigGuard instance (Different Versioning Examples)
try:
    # Option A: Explicitly set instance version (overrides schema version)
    explicit_version = "1.2.0-dev"
    config = ConfigGuard(
        schema=my_schema,
        instance_version=explicit_version, # <-- Explicit version
        config_path=config_file,
        encryption_key=encryption_key
    )
    print(f"Initialized with Explicit Version: {config.version}") # -> 1.2.0-dev

    # Option B: Use version from schema (most common)
    # config = ConfigGuard(schema=my_schema, config_path=config_file, encryption_key=encryption_key)
    # print(f"Initialized with Schema Version: {config.version}") # -> 1.1.0

    # Option C: No version in schema or parameter (defaults to 0.0.0)
    # config = ConfigGuard(schema=my_schema_no_version, config_path=config_file, encryption_key=encryption_key)
    # print(f"Initialized with Default Version: {config.version}") # -> 0.0.0

# Handle missing dependencies for the chosen handler
except ImportError as e:
    print(f"ERROR: Missing dependency for {config_file.suffix} files: {e}")
    exit()
except Exception as e:
    print(f"ERROR: Failed to initialize ConfigGuard: {e}")
    exit()

# 4. Access values (defaults initially, unless file existed)
print(f"Initial Server Host: {config.server.host}") # -> '127.0.0.1'
print(f"Initial Log Level: {config['log_level']}") # -> 'INFO'
print(f"Initial Plugin Data: {config.plugin_data.get_config_dict()}") # -> {}

# 5. Access schema details
print(f"Help for server port: {config.server.sc_port.help}")

# 6. Modify values
try:
    config.server.port = 9090
    config['log_level'] = 'DEBUG'
    config.plugin_data['active_plugin'] = 'analyzer_v2' # Dynamic add
    config.plugin_data.user_prefs = {'theme': 'dark'} # Dynamic add
except ValidationError as e:
    print(f"VALIDATION ERROR: {e}")

print(f"Updated Port: {config.server.port}") # -> 9090
print(f"Active Plugin: {config.plugin_data['active_plugin']}") # -> 'analyzer_v2'

# 7. Save configuration (mode='values' is default, saves to config_file)
# The file will contain the version set during init (explicit_version in this case)
# if saved with mode='full'. Mode='values' does not save version info.
config.save()
print(f"Configuration saved to {config_file} (encrypted).")

# To save with version and schema:
# config.save(mode='full', filepath='my_app_config_full.yaml')

📚 Core Concepts Detailed

  • Schema: The cornerstone of ConfigGuard.

    • Structure: A Python dictionary defining the entire configuration layout.
    • Versioning: Determined by the instance_version parameter passed to the constructor, which takes precedence. If not provided, it falls back to the __version__ key within the schema dictionary. If neither is present, it defaults to "0.0.0". The version must be parseable by the packaging library.
    • Settings: Keys in the schema dictionary represent setting names. Each setting has a definition dictionary specifying its properties.
    • Sections ("type": "section"): Allows hierarchical grouping. Requires a nested "schema" dictionary which defines the contents of the section.
    • Dynamic Sections ("schema": {}): A special type of section defined with an empty schema dictionary. These sections allow runtime addition/modification/deletion of arbitrary key-value pairs without schema validation for those pairs.
    • Setting Definition Keys: (type, default, help, nullable, options, min_val, max_val). See previous sections for details.
  • ConfigGuard Object: The main object you interact with.

    • Initialization: Created with the schema definition, optionally instance_version, config_path, encryption_key, autosave, and handler. The instance version is determined based on parameter/schema precedence. Automatically attempts to load data from config_path if provided.
    • version Attribute: Stores the determined instance version (config.version).
    • Access: Provides attribute (config.setting) and dictionary (config['setting']) access to top-level settings and sections.
    • Schema Access: Use config.sc_setting or config['sc_setting'] to get the SettingSchema object for a defined setting, or the schema dictionary for a section.
    • Bulk Access: Use get_instance_schema_definition() for the entire schema definition, get_config_dict() for all current values, and export_schema_with_values() for a combined snapshot.
  • ConfigSection Object: Represents a nested section defined in the schema.

    • Access: Provides the same attribute (section.nested_setting) and dictionary (section['nested_setting']) access for items defined within its schema, or for dynamic keys if it's a dynamic section.
    • Modification Rules: Interact with the contents of a ConfigSection. You cannot replace the section itself via assignment.
    • Introspection: Use section.get_schema_dict() to retrieve the schema definition for the items within that section, and section.get_config_dict() (or its alias section.get_dict()) to get a dictionary of the current values within the section, including nested sections and dynamic keys.
  • Dynamic Sections ("schema": {}):

    • Purpose: Flexibility for unknown keys (plugins, runtime data).
    • Behavior: Acts like a nested dictionary. Add/update/delete keys freely.
    • Trade-off: No schema validation for dynamic content.
    • Integration: Dynamic content included in save/load/export/encryption.
  • Storage Handlers: Abstraction layer for persistence.

    • Selection: Automatic based on config_path suffix (.json, .yaml, .yml, .toml, .db, .sqlite, .sqlite3, .bin, .enc).
    • Encryption: Managed transparently if encryption_key is provided.
    • Structure Handling: JSON/YAML/TOML preserve nesting. SQLite flattens keys (server.port) and stores values as encrypted JSON strings.
  • Save Modes (values vs full):

    • mode='values': Saves current values only (structured per handler). No version/schema info.
    • mode='full': Saves a complete snapshot: the current instance_version, the schema definition, and current values.
  • Versioning & Migration: Facilitates managing configuration changes.

    • Mechanism: Compares loaded file's version (if present) with the config.version (instance version).
    • Loading Newer: Raises SchemaError.
    • Loading Older: Merges data recursively (loads matching, applies new defaults, skips removed).
    • Logging: Warnings logged for skipped keys/coercion failures.
  • Encryption: Provides confidentiality using Fernet.

    • Key Management: Use configguard.generate_encryption_key(). Store the key securely.
    • Transparency: Handled by the storage handler. Application code sees plain data.

📖 Detailed Usage

1. Defining the Schema

(See Core Concepts section for key details)

# More complex schema example
import typing
CONFIG_VERSION = "3.0.0"

complex_schema: typing.Dict[str, typing.Any] = {
    "__version__": CONFIG_VERSION,
    "network": {
        "type": "section", "help": "Network settings",
        "schema": {
            "hostname": {"type": "str", "default": "auto", "help": "System hostname (or 'auto')"},
            "port": {"type": "int", "default": 8000, "min_val": 1, "max_val": 65535},
            "allowed_ips": {"type": "list", "default": ["127.0.0.1", "::1"], "help": "List of allowed client IPs"},
        }
    },
    "performance": {
        "type": "section", "help": "Performance tuning",
        "schema": {
            "worker_threads": {"type": "int", "default": 4, "min_val": 1, "max_val": 64},
            "cache": {
                "type": "section", "help": "Caching options",
                "schema": {
                    "enabled": {"type": "bool", "default": True},
                    "max_size_mb": {"type": "int", "default": 1024, "min_val": 0},
                    "strategy": {"type": "str", "default": "LRU", "options": ["LRU", "FIFO", "LFU"]},
                }
            }
        }
    },
    "user_scripts": { # Dynamic section
        "type": "section", "help": "Paths to user-provided scripts.",
        "schema": {}
    },
    "enable_analytics": { "type": "bool", "default": False, "help": "Enable anonymous usage analytics."}
}

# Optionally save schema to JSON for reuse or distribution
# import json
# with open("app_schema_v3.json", "w") as f:
#     json.dump(complex_schema, f, indent=2)

2. Initializing ConfigGuard

Load schema from dictionary or file path. Provide config_path, encryption_key, and optionally instance_version.

from configguard import ConfigGuard, generate_encryption_key, SchemaError, HandlerError, EncryptionError
from pathlib import Path

schema_source = complex_schema # Or Path("app_schema_v3.json")
config_file = Path("app_config_v3.db") # Using SQLite
enc_key = generate_encryption_key() # Store this securely!
instance_v = "3.1.0" # Explicit version

try:
    # Initialize with explicit version overriding any schema version
    config = ConfigGuard(
        schema=schema_source,
        config_path=config_file,
        encryption_key=enc_key,
        instance_version=instance_v # Provide the version here
    )
    print(f"ConfigGuard initialized with version: {config.version}")

    # Or, initialize using schema version (if instance_version=None)
    # config = ConfigGuard(schema=schema_source, config_path=config_file, ...)
    # print(f"ConfigGuard initialized with version: {config.version}") # Would use schema's __version__

except FileNotFoundError as e: print(f"Schema file error: {e}")
except SchemaError as e: print(f"Schema/Version error: {e}") # Catches invalid version format
except (HandlerError, ImportError) as e: print(f"Configuration handler error: {e}")
except EncryptionError as e: print(f"Encryption error: {e}")
except Exception as e: print(f"Unexpected initialization error: {e}")

3. Accessing Settings and Schema

Attribute or dictionary item access works for both top-level and nested settings. Use the sc_ prefix for schema details.

# Access values
hostname = config.network.hostname
port = config['network']['port']
cache_enabled = config.performance.cache.enabled
worker_threads = config['performance']['worker_threads']

print(f"Hostname: {hostname}")
print(f"Port: {port}")
print(f"Cache Enabled: {cache_enabled}")
print(f"Worker Threads: {worker_threads}")

# Access schema details
port_schema = config.network.sc_port
print(f"Port Help: {port_schema.help}")
print(f"Port Default: {port_schema.default_value}")
cache_strategy_options = config.performance.cache.sc_strategy.options
print(f"Cache Strategy Options: {cache_strategy_options}")

4. Modifying Settings

Use attribute or dictionary item assignment. ConfigGuard handles validation automatically. Dynamic sections allow adding arbitrary keys.

from configguard import ValidationError

try:
    config.network.port = 8080
    config['performance']['worker_threads'] = 8
    config.performance.cache.enabled = False

    # Add dynamic content
    config.user_scripts['on_startup'] = '/opt/scripts/startup.sh'
    config.user_scripts['on_shutdown'] = '/opt/scripts/shutdown.sh'
    config.user_scripts['report_generator'] = {'type': 'python', 'path': '~/reports/gen.py'}

    # Attempt invalid assignment
    # config.network.port = 99999 # Will raise ValidationError (max_val)

except ValidationError as e:
    print(f"ERROR setting value: {e}")

print(f"Updated port: {config.network.port}")
print(f"Startup script: {config.user_scripts.on_startup}")

5. Working with Sections: Introspection

You can inspect the schema or retrieve the current values for a specific section directly.

  • section.get_schema_dict() -> dict: Returns a copy of the schema dictionary that defines the items within this section. For dynamic sections, this will return {}.
  • section.get_config_dict() -> dict (alias: section.get_dict()): Returns a dictionary representing the current values of all items within this section. This includes values from nested sections (obtained recursively) and any key-value pairs added to dynamic sections.
# Assuming 'config' is initialized with 'complex_schema' from above

# Get the 'performance' section object
perf_section = config.performance

# 1. Get the schema definition FOR THE CONTENTS of the 'performance' section
perf_schema_def = perf_section.get_schema_dict()
print("\n--- Schema Definition for 'performance' section ---")
# print(json.dumps(perf_schema_def, indent=2)) # Pretty print
# Expected output:
# {
#   "worker_threads": {"type": "int", "default": 4, ...},
#   "cache": {
#     "type": "section", "help": "Caching options",
#     "schema": { ... } # Note: This is the nested schema *definition*
#   }
# }

# 2. Get the dictionary of CURRENT VALUES within the 'performance' section
perf_values_dict = perf_section.get_config_dict()
print("\n--- Current Values within 'performance' section ---")
# print(json.dumps(perf_values_dict, indent=2)) # Pretty print
# Expected output (after modifications in step 4):
# {
#   "worker_threads": 8,
#   "cache": {
#     "enabled": False,
#     "max_size_mb": 1024,
#     "strategy": "LRU" # Assuming it wasn't changed from default
#   }
# }

# 3. Example with a dynamic section
dynamic_section = config.user_scripts
dynamic_schema_def = dynamic_section.get_schema_dict()
dynamic_values_dict = dynamic_section.get_config_dict()

print("\n--- Schema Definition for 'user_scripts' (Dynamic) ---")
print(dynamic_schema_def) # -> {}

print("\n--- Current Values within 'user_scripts' (Dynamic) ---")
# print(json.dumps(dynamic_values_dict, indent=2))
# Expected output (after modifications in step 4):
# {
#   "on_startup": "/opt/scripts/startup.sh",
#   "on_shutdown": "/opt/scripts/shutdown.sh",
#   "report_generator": { "type": "python", "path": "~/reports/gen.py" }
# }

6. Saving & Loading

save() persists the current state. load() reads from disk. mode='full' saves the current instance version.

# Save current values (encrypted to SQLite DB)
config.save() # Defaults to mode='values'

# Save a full backup (will include instance_version="3.1.0" in this example)
try:
    backup_path = Path("config_v3.1_backup.json")
    config.save(filepath=backup_path, mode='full')
    print(f"Full backup saved to {backup_path}")
except Exception as e:
    print(f"Failed to save backup: {e}")

# Manual Load
try:
    config.load() # Reloads from the path config was initialized with
    print("Reload complete.")
except Exception as e:
    print(f"Error during manual load: {e}")

7. Versioning & Migration

Handled automatically during load based on config.version vs file version. Check logs for warnings about skipped keys or sections from older files.

# --- Simulation ---
# Imagine current schema is V2.0.0
# Load a config file saved with schema V1.0.0:
# config_v1_path = Path("old_config_v1.json")
# try:
#    # Instance needs V2 schema, attempts to load V1 data
#    config_v2_instance = ConfigGuard(schema=schema_v2_dict, config_path=config_v1_path)
#    # ConfigGuard logs warnings for settings in v1 file not in v2 schema.
#    # Settings in v2 schema but not v1 file get v2 defaults.
#    # Matching settings have their values loaded from v1 file.
#    # Dynamic section content (if section exists in both) is loaded from v1 file.
# except SchemaError as e: # e.g. if v1 file version > v2 instance version
#    print(f"Version mismatch: {e}")

8. Encryption

Provide encryption_key at init. Generation and storage are key.

from configguard import generate_encryption_key, ConfigGuard

# Generate key (DO THIS ONCE and store securely!)
# new_key = generate_encryption_key()
# print(f"Store this key safely: {new_key.decode()}")

# Use stored key
stored_key = b'YOUR_SECURELY_STORED_32_BYTE_URLSAFE_BASE64_KEY'

secure_config = ConfigGuard(
    schema=complex_schema,
    config_path="secure_app.bin", # Use .bin or .enc for encrypted files
    encryption_key=stored_key
)

# Modify sensitive and non-sensitive data
secure_config.network.hostname = "prod.server.local"
secure_config.user_scripts.deploy_key = "ssh-rsa AAA..." # Dynamic sensitive data

# Save - data is encrypted on disk
secure_config.save()

# Loading automatically decrypts
# loaded_config = ConfigGuard(...)
# print(loaded_config.user_scripts.deploy_key) # -> Prints plain key

9. Handling Nested Configurations

Define sections within sections in your schema. Access follows the structure naturally. Modification rules apply at each level.

# Accessing deeply nested setting (from complex_schema)
cache_size = config.performance.cache.max_size_mb
print(f"Cache size: {cache_size}")

# Modifying deeply nested setting
config.performance.cache.enabled = False
config['performance']['cache']['strategy'] = 'FIFO' # Item access also works

# Cannot assign to nested section
# config.performance.cache = {"enabled": False} # INVALID

10. Import/Export

export_schema_with_values() provides a full snapshot. import_config() merges value updates.

# --- Export ---
full_state = config.export_schema_with_values()

# Example structure of full_state['settings']:
# {
#   "network": {
#     "schema": { "type": "section", "help": "Network settings", "schema": { ... } },
#     "value": { "hostname": "auto", "port": 8080, "allowed_ips": [...] }
#   },
#   "performance": {
#     "schema": { "type": "section", "help": "Performance tuning", "schema": { ... } },
#     "value": {
#       "worker_threads": 8,
#       "cache": { "enabled": False, "max_size_mb": 1024, "strategy": "FIFO" } # Value is nested
#     }
#   },
#   "user_scripts": { # Dynamic section
#      "schema": { "type": "section", "help": "Paths to user-provided scripts.", "schema": {} },
#      "value": { # Value contains the dynamic keys
#          "on_startup": "/opt/scripts/startup.sh",
#          "on_shutdown": "/opt/scripts/shutdown.sh",
#          "report_generator": { "type": "python", "path": "~/reports/gen.py" }
#      }
#   },
#   "enable_analytics": { "schema": { "type": "bool", ... }, "value": False }
# }

import json
# print(json.dumps(full_state, indent=2))

# --- Import ---
update_data = {
    "performance": {
        "cache": {
            "max_size_mb": 2048, # Update nested standard setting
            "unknown_cache_param": True # Ignored if ignore_unknown=True
        }
    },
    "user_scripts": { # Add/update dynamic keys
        "new_report_script": "/usr/local/bin/report.py",
        "report_generator": { "type": "rust", "path": "/opt/bin/process_rs" } # Update dynamic value
    },
    "unknown_section": True # Ignored if ignore_unknown=True
}

try:
    # Merge updates, ignore keys not in schema (unless in dynamic section)
    config.import_config(update_data, ignore_unknown=True)
    print(f"Cache size after import: {config.performance.cache.max_size_mb}") # -> 2048
    print(f"Report generator after import: {config.user_scripts.report_generator}")
except SettingNotFoundError as e:
     print(f"Import failed (ignore_unknown=False): {e}")
except Exception as e:
    print(f"Import failed: {e}")

💡 Use Cases

  • Robust Application Settings: Define and manage essential parameters like server ports, file paths, feature flags, logging levels with guaranteed type safety and validation. Organize settings by application component (e.g., server, database, ui, tasks) using nested sections.
  • Secure Credential Storage: Store sensitive data like API keys, database connection strings with passwords, OAuth tokens, or encryption keys within specific sections (e.g., credentials.database, credentials.external_api). Enable encryption (encryption_key) to protect this data at rest transparently.
  • User Preferences: Manage user-specific application settings like themes, language choices, layout configurations, notification preferences. A standard section can enforce known preference keys, while a dynamic section could store UI state or less critical, user-defined preferences.
  • Microservice Configuration: Each service can have its own ConfigGuard schema defining its unique requirements (database connections, message queue endpoints, cache settings, service discovery URLs). Shared settings could potentially be managed through includes or layering if needed (though not a built-in feature).
  • Multi-Environment Deployment: Maintain consistency across development, staging, and production environments by using the same schema but different configuration files (dev.yaml, staging.db, prod.toml). Use encryption for production secrets. Versioning helps manage updates across environments.
  • Plugin and Extension Systems: Use dynamic sections ("schema": {}) to allow plugins or extensions to store their own configuration data without requiring modifications to the core application schema. The core app can load/save the dynamic section content, while the plugin interprets its own keys/values.
  • Generating Configuration UIs: Use the output of export_schema_with_values() to dynamically generate web forms or GUI elements for editing configurations. The schema provides field types, help text, options (for dropdowns), and validation rules (min/max) to build intelligent editors.
  • Complex Workflow/Pipeline Configuration: Define parameters for multi-step processes, data pipelines, or scientific workflows, potentially using nested sections for different stages and dynamic sections for stage-specific parameters.

🔧 Advanced Topics

  • Custom Storage Handlers: Extend ConfigGuard's capabilities by creating your own storage backend.
    1. Subclass configguard.handlers.StorageHandler.
    2. Implement the abstract load(self, filepath) and save(self, filepath, data, mode) methods.
      • Your load must return a LoadResult dictionary ({'version': Optional[str], 'schema': Optional[dict], 'values': dict}).
      • Your save must handle the data payload ({'instance_version', 'schema_definition', 'config_values'}) and the mode ('values' or 'full').
      • If your handler should support encryption, use self._encrypt(bytes) and self._decrypt(bytes) internally, which leverage the Fernet instance passed during __init__.
      • Consider how your format represents nested structures and dynamic section content.
    3. Register your handler by adding its file extension(s) and class to the configguard.handlers.HANDLER_MAP dictionary, or provide an instance directly during ConfigGuard initialization using the handler argument.
  • (Potential Future) Custom Migration Functions: For complex schema changes between versions (e.g., renaming keys, splitting sections, complex type transformations), a future enhancement could allow users to register custom Python functions to handle specific version-to-version migrations beyond the default key matching and default filling.
  • (Potential Future) Schema Includes/Composition: For very large configurations, a mechanism to include or compose schemas from multiple files could be considered.

🤝 Contributing

Contributions are highly welcome and appreciated! Help make ConfigGuard even better.

  1. Found a Bug or Have an Idea? Check the Issue Tracker to see if it's already reported. If not, please open a new issue, providing as much detail as possible (code examples, error messages, expected vs. actual behavior).
  2. Ready to Contribute Code?
    • Fork the Repository: Create your own fork on GitHub.
    • Create a Branch: Make a new branch in your fork for your changes (e.g., feature/add-new-handler, bugfix/fix-validation-edge-case).
    • Develop: Write your code, ensuring it adheres to the project's quality standards:
      • Style: Follow PEP 8 guidelines. Use Black for code formatting (black .).
      • Linting: Use Ruff for linting (ruff check .). Address reported issues.
      • Typing: Add Type Hints (typing) to all functions and methods. Check with Mypy (mypy configguard).
      • Docstrings: Write clear, informative docstrings (Google style preferred) for all public modules, classes, functions, and methods. Explain parameters, return values, raised exceptions, and usage.
    • Testing: Add unit tests using pytest in the tests/ directory for any new features or bug fixes. Ensure existing tests pass. Aim for high test coverage (pytest --cov=configguard).
    • Commit: Write clear, concise commit messages explaining your changes.
    • Push & Pull Request: Push your branch to your fork and open a Pull Request against the main branch of the original ParisNeo/ConfigGuard repository. Describe your changes in the PR description and link any relevant issues.
  3. Code of Conduct: Please note that this project is released with a Contributor Code of Conduct. By participating, you are expected to uphold this code. (A formal CODE_OF_CONDUCT.md file may be added later).

📜 License

ConfigGuard is distributed under the terms of the Apache License 2.0.

This means you are free to use, modify, and distribute the software for commercial or non-commercial purposes, but you must include the original copyright notice and license text. See the LICENSE file in the repository for the full license text.


Built with ❤️ by ParisNeo with the help of Gemini 2.5

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

configguard-0.5.3.tar.gz (81.1 kB view details)

Uploaded Source

Built Distribution

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

configguard-0.5.3-py3-none-any.whl (65.3 kB view details)

Uploaded Python 3

File details

Details for the file configguard-0.5.3.tar.gz.

File metadata

  • Download URL: configguard-0.5.3.tar.gz
  • Upload date:
  • Size: 81.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.0.1 CPython/3.12.4

File hashes

Hashes for configguard-0.5.3.tar.gz
Algorithm Hash digest
SHA256 b717f059a1a19b68fbda645ff0f36778587ea0bf912ea07321e3ec654d318a94
MD5 9ea381a8065f2c6432d997a11e0d148b
BLAKE2b-256 abfe3f048dcfec56d8b68bf4ee29a600aaf59a1008b652164b3af8625ceb9899

See more details on using hashes here.

File details

Details for the file configguard-0.5.3-py3-none-any.whl.

File metadata

  • Download URL: configguard-0.5.3-py3-none-any.whl
  • Upload date:
  • Size: 65.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.0.1 CPython/3.12.4

File hashes

Hashes for configguard-0.5.3-py3-none-any.whl
Algorithm Hash digest
SHA256 af640e1902d5dfb6d1647e0ff8059cd45b5bcc859e8331520a9233b7de14d9b5
MD5 7db97ef186d46f038cfb7e08e9c7a661
BLAKE2b-256 b89b1646e19a9570569b9db8bf3e0c86b88bb1a633a29ae3fd52afbabd3145f2

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