Skip to main content

A library for managing Pydantic settings objects

Project description

pydantic-settings-manager

A modern, thread-safe library for managing Pydantic settings with support for multiple configurations and runtime overrides.

Features

  • Unified API: Single SettingsManager class handles both simple and complex configurations
  • Thread-safe: Built-in thread safety for concurrent applications
  • Type-safe: Full type hints and Pydantic validation
  • Flexible: Support for single settings or multiple named configurations
  • Runtime overrides: Command-line arguments and dynamic configuration changes
  • Easy migration: Simple upgrade path from configuration files and environment variables

Installation

pip install pydantic-settings-manager

Quick Start

Basic Usage

from pydantic_settings import BaseSettings
from pydantic_settings_manager import SettingsManager

# 1. Define your settings
class AppSettings(BaseSettings):
    app_name: str = "MyApp"
    debug: bool = False
    max_connections: int = 100

# 2. Create a settings manager
manager = SettingsManager(AppSettings)

# 3. Use your settings
settings = manager.settings
print(f"App: {settings.app_name}, Debug: {settings.debug}")
# Output: App: MyApp, Debug: False

Loading from Configuration

# Load from a dictionary (JSON, YAML, etc.)
manager.user_config = {
    "app_name": "ProductionApp",
    "debug": False,
    "max_connections": 500
}

settings = manager.settings
print(f"App: {settings.app_name}")  # Output: App: ProductionApp

Runtime Overrides

# Override settings at runtime (e.g., from command line)
manager.cli_args = {"debug": True, "max_connections": 50}

settings = manager.settings
print(f"Debug: {settings.debug}")  # Output: Debug: True
print(f"Connections: {settings.max_connections}")  # Output: Connections: 50

Multiple Configurations

For applications that need different settings for different environments or contexts:

# Enable multi-configuration mode
manager = SettingsManager(AppSettings, multi=True)

# Configure multiple environments
manager.user_config = {
    "development": {
        "app_name": "MyApp-Dev",
        "debug": True,
        "max_connections": 10
    },
    "production": {
        "app_name": "MyApp-Prod", 
        "debug": False,
        "max_connections": 1000
    },
    "testing": {
        "app_name": "MyApp-Test",
        "debug": True,
        "max_connections": 5
    }
}

# Switch between configurations
manager.active_key = "development"
dev_settings = manager.settings
print(f"Dev: {dev_settings.app_name}, Debug: {dev_settings.debug}")

manager.active_key = "production"
prod_settings = manager.settings
print(f"Prod: {prod_settings.app_name}, Debug: {prod_settings.debug}")

# Get all configurations
all_settings = manager.all_settings
for env, settings in all_settings.items():
    print(f"{env}: {settings.app_name}")

Advanced Usage

Thread Safety

The SettingsManager is fully thread-safe and can be used in multi-threaded applications:

import threading
from concurrent.futures import ThreadPoolExecutor

manager = SettingsManager(AppSettings, multi=True)
manager.user_config = {
    "worker1": {"app_name": "Worker1", "max_connections": 10},
    "worker2": {"app_name": "Worker2", "max_connections": 20}
}

def worker_function(worker_id: int):
    # Each thread can safely switch configurations
    manager.active_key = f"worker{worker_id}"
    settings = manager.settings
    print(f"Worker {worker_id}: {settings.app_name}")

# Run multiple workers concurrently
with ThreadPoolExecutor(max_workers=5) as executor:
    futures = [executor.submit(worker_function, i) for i in range(1, 3)]
    for future in futures:
        future.result()

Dynamic Configuration Updates

# Update individual CLI arguments
manager.set_cli_args("debug", True)
manager.set_cli_args("nested.value", "test")  # Supports nested keys

# Update entire CLI args
manager.cli_args = {"debug": False, "max_connections": 200}

# Get specific settings by key (multi mode)
dev_settings = manager.get_settings_by_key("development")
prod_settings = manager.get_settings_by_key("production")

Complex Settings with Nested Configuration

from typing import Dict, List
from pydantic import Field

class DatabaseSettings(BaseSettings):
    host: str = "localhost"
    port: int = 5432
    username: str = "user"
    password: str = "password"

class APISettings(BaseSettings):
    base_url: str = "https://api.example.com"
    timeout: int = 30
    retries: int = 3

class AppSettings(BaseSettings):
    app_name: str = "MyApp"
    debug: bool = False
    database: DatabaseSettings = Field(default_factory=DatabaseSettings)
    api: APISettings = Field(default_factory=APISettings)
    features: List[str] = Field(default_factory=list)
    metadata: Dict[str, str] = Field(default_factory=dict)

manager = SettingsManager(AppSettings, multi=True)
manager.user_config = {
    "development": {
        "app_name": "MyApp-Dev",
        "debug": True,
        "database": {
            "host": "dev-db.example.com",
            "port": 5433
        },
        "api": {
            "base_url": "https://dev-api.example.com",
            "timeout": 10
        },
        "features": ["debug_toolbar", "hot_reload"],
        "metadata": {"environment": "dev", "version": "1.0.0-dev"}
    },
    "production": {
        "app_name": "MyApp-Prod",
        "debug": False,
        "database": {
            "host": "prod-db.example.com",
            "port": 5432,
            "username": "prod_user"
        },
        "api": {
            "base_url": "https://api.example.com",
            "timeout": 30,
            "retries": 5
        },
        "features": ["monitoring", "caching"],
        "metadata": {"environment": "prod", "version": "1.0.0"}
    }
}

# Use nested configuration
manager.active_key = "development"
settings = manager.settings
print(f"DB Host: {settings.database.host}")
print(f"API URL: {settings.api.base_url}")
print(f"Features: {settings.features}")

Project Structure for Large Applications

For complex applications with multiple modules:

your_project/
├── settings/
│   ├── __init__.py
│   ├── app.py
│   ├── database.py
│   └── api.py
├── modules/
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   └── service.py
│   └── billing/
│       ├── __init__.py
│       ├── settings.py
│       └── service.py
├── config/
│   ├── base.yaml
│   ├── development.yaml
│   └── production.yaml
├── bootstrap.py
└── main.py

Module-based Settings

# settings/app.py
from pydantic_settings import BaseSettings
from pydantic_settings_manager import SettingsManager

class AppSettings(BaseSettings):
    name: str = "MyApp"
    debug: bool = False
    secret_key: str = "dev-secret"

app_settings_manager = SettingsManager(AppSettings, multi=True)

# modules/auth/settings.py
from pydantic_settings import BaseSettings
from pydantic_settings_manager import SettingsManager

class AuthSettings(BaseSettings):
    jwt_secret: str = "jwt-secret"
    token_expiry: int = 3600
    max_login_attempts: int = 5

auth_settings_manager = SettingsManager(AuthSettings, multi=True)

Bootstrap Configuration

# bootstrap.py
import yaml
import importlib
from pathlib import Path
from pydantic_settings_manager import SettingsManager

def load_config(config_path: str) -> dict:
    """Load configuration from YAML file"""
    with open(config_path) as f:
        return yaml.safe_load(f)

def bootstrap_settings(environment: str = "development"):
    """Bootstrap all settings managers with configuration"""
    
    # Load base configuration
    base_config = load_config("config/base.yaml")
    env_config = load_config(f"config/{environment}.yaml")
    
    # Merge configurations (env overrides base)
    config = {**base_config, **env_config}
    
    # Configure each module's settings manager
    settings_managers = [
        ("settings.app", "app_settings_manager"),
        ("modules.auth.settings", "auth_settings_manager"),
        ("modules.billing.settings", "billing_settings_manager"),
    ]
    
    for module_name, manager_name in settings_managers:
        try:
            module = importlib.import_module(module_name)
            manager = getattr(module, manager_name, None)
            
            if isinstance(manager, SettingsManager):
                # Set configuration for this module
                if module_name.split('.')[-1] in config:
                    manager.user_config = config[module_name.split('.')[-1]]
                    manager.active_key = environment
                    
        except ImportError:
            print(f"Warning: Could not import {module_name}")

# main.py
from bootstrap import bootstrap_settings
from settings.app import app_settings_manager
from modules.auth.settings import auth_settings_manager

def main():
    # Bootstrap all settings
    bootstrap_settings("production")
    
    # Use settings throughout the application
    app_settings = app_settings_manager.settings
    auth_settings = auth_settings_manager.settings
    
    print(f"App: {app_settings.name}")
    print(f"JWT Expiry: {auth_settings.token_expiry}")

if __name__ == "__main__":
    main()

Configuration Files

# config/base.yaml
app:
  name: "MyApp"
  debug: false

auth:
  token_expiry: 3600
  max_login_attempts: 5

billing:
  currency: "USD"
  tax_rate: 0.08
# config/development.yaml
app:
  debug: true
  secret_key: "dev-secret-key"

auth:
  jwt_secret: "dev-jwt-secret"
  token_expiry: 7200  # Longer expiry for development

billing:
  mock_payments: true
# config/production.yaml
app:
  secret_key: "${SECRET_KEY}"  # From environment variable

auth:
  jwt_secret: "${JWT_SECRET}"
  max_login_attempts: 3  # Stricter in production

billing:
  mock_payments: false
  stripe_api_key: "${STRIPE_API_KEY}"

CLI Integration

Integrate with command-line tools for runtime configuration:

# cli.py
import click
from bootstrap import bootstrap_settings
from settings.app import app_settings_manager

@click.command()
@click.option("--environment", "-e", default="development", 
              help="Environment to run in")
@click.option("--debug/--no-debug", default=None, 
              help="Override debug setting")
@click.option("--max-connections", type=int, 
              help="Override max connections")
def main(environment: str, debug: bool, max_connections: int):
    """Run the application with specified settings"""
    
    # Bootstrap with environment
    bootstrap_settings(environment)
    
    # Apply CLI overrides
    cli_overrides = {}
    if debug is not None:
        cli_overrides["debug"] = debug
    if max_connections is not None:
        cli_overrides["max_connections"] = max_connections
    
    if cli_overrides:
        app_settings_manager.cli_args = cli_overrides
    
    # Run application
    settings = app_settings_manager.settings
    print(f"Running {settings.name} in {environment} mode")
    print(f"Debug: {settings.debug}")

if __name__ == "__main__":
    main()

Usage:

# Run with defaults
python cli.py

# Run in production with debug enabled
python cli.py --environment production --debug

# Override specific settings
python cli.py --max-connections 500

Migration Guide

Migrating from v0.x to v1.x

Starting from v1.0.0, the library provides a unified SettingsManager class that replaces the previous three separate classes. The old classes are deprecated and will be removed in v2.0.0.

From SingleSettingsManager

# OLD (deprecated)
from pydantic_settings_manager import SingleSettingsManager

manager = SingleSettingsManager(MySettings)
manager.user_config = {"name": "app", "value": 42}
manager.cli_args["value"] = 100
settings = manager.settings

# NEW (recommended)
from pydantic_settings_manager import SettingsManager

manager = SettingsManager(MySettings)  # multi=False is default
manager.user_config = {"name": "app", "value": 42}
manager.cli_args = {"value": 100}  # Note: now a dict assignment, not dict access
settings = manager.settings

From MappedSettingsManager

# OLD (deprecated)
from pydantic_settings_manager import MappedSettingsManager

manager = MappedSettingsManager(MySettings)
manager.user_config = {
    "map": {
        "dev": {"name": "development", "value": 42},
        "prod": {"name": "production", "value": 100}
    }
}
manager.set_cli_args("dev")
settings = manager.settings

# NEW (recommended)
from pydantic_settings_manager import SettingsManager

manager = SettingsManager(MySettings, multi=True)
manager.user_config = {
    "dev": {"name": "development", "value": 42},
    "prod": {"name": "production", "value": 100}
}
manager.active_key = "dev"  # More intuitive property-based API
settings = manager.settings

From BaseSettingsManager

# OLD (deprecated)
from pydantic_settings_manager import BaseSettingsManager

class MyManager(BaseSettingsManager[MySettings]):
    def __init__(self, settings_cls):
        super().__init__(settings_cls)
        # ... custom implementation
    
    @property
    def settings(self):
        # ... custom logic
        pass
    
    def clear(self):
        # ... custom logic
        pass

# NEW (recommended)
from pydantic_settings_manager import SettingsManager

# Use the unified manager directly
manager = SettingsManager(MySettings)  # or SettingsManager(MySettings, multi=True)

# If you need custom behavior, consider composition over inheritance:
class MyCustomManager:
    def __init__(self, settings_cls):
        self._manager = SettingsManager(settings_cls)
        # ... additional setup
    
    @property
    def settings(self):
        # ... custom logic using self._manager
        return self._manager.settings

Key Differences

  1. Unified API: One class handles both single and multiple configurations
  2. Property-based: manager.active_key = "prod" instead of manager.set_cli_args("prod")
  3. Dict assignment: manager.cli_args = {...} instead of manager.cli_args[key] = value
  4. Simplified config: No need for "map" wrapper in multi mode
  5. Thread-safe: Built-in thread safety for concurrent access

Deprecation Timeline

  • v1.0.0: Old classes deprecated with warnings
  • v2.0.0: Old classes will be removed

Suppressing Deprecation Warnings

If you need to suppress deprecation warnings temporarily:

import warnings

with warnings.catch_warnings():
    warnings.simplefilter("ignore", DeprecationWarning)
    # Your code using deprecated classes
    manager = SingleSettingsManager(MySettings)

Related Tools

pydantic-config-builder

For complex projects with multiple configuration files, you might want to use pydantic-config-builder to merge and build your YAML configuration files:

pip install pydantic-config-builder

This tool allows you to:

  • Merge multiple YAML files into a single configuration
  • Use base configurations with overlay files
  • Build different configurations for different environments
  • Support glob patterns and recursive merging

Example workflow:

# pydantic_config_builder.yml
development:
  input:
    - base/*.yaml
    - dev-overrides.yaml
  output:
    - config/dev.yaml

production:
  input:
    - base/*.yaml
    - prod-overrides.yaml
  output:
    - config/prod.yaml

Then use the generated configurations with your settings manager:

import yaml
from your_app import settings_manager

# Load the built configuration
with open("config/dev.yaml") as f:
    config = yaml.safe_load(f)

settings_manager.user_config = config

Development

This project uses modern Python development tools:

  • uv: Fast Python package manager with PEP 735 dependency groups support
  • ruff: Fast linter and formatter (replaces black, isort, and flake8)
  • mypy: Static type checking
  • pytest: Testing framework with coverage reporting

Setup

# Install all development dependencies
uv sync --group dev

# Or install specific dependency groups
uv sync --group test    # Testing tools only
uv sync --group lint    # Linting tools only

# Format code
uv run ruff check --fix .

# Run linting
uv run ruff check .
uv run mypy .

# Run tests
uv run pytest --cov=pydantic_settings_manager tests/

# Build and test everything
make build

Development Workflow

# Quick setup for testing
uv sync --group test
make test

# Quick setup for linting
uv sync --group lint
make lint

# Full development environment
uv sync --group dev
make build

API Reference

SettingsManager

The main class for managing Pydantic settings.

class SettingsManager(Generic[T]):
    def __init__(self, settings_cls: type[T], *, multi: bool = False)

Parameters

  • settings_cls: The Pydantic settings class to manage
  • multi: Whether to enable multi-configuration mode (default: False)

Properties

  • settings: T - Get the current active settings
  • all_settings: dict[str, T] - Get all settings (multi mode)
  • user_config: dict[str, Any] - Get/set user configuration
  • cli_args: dict[str, Any] - Get/set CLI arguments
  • active_key: str | None - Get/set active key (multi mode only)

Methods

  • clear() -> None - Clear cached settings
  • get_settings_by_key(key: str) -> T - Get settings by specific key
  • set_cli_args(target: str, value: Any) -> None - Set individual CLI argument

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Documentation

For more detailed documentation and examples, please see the GitHub repository.

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

pydantic_settings_manager-1.0.0.tar.gz (55.1 kB view details)

Uploaded Source

Built Distribution

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

pydantic_settings_manager-1.0.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_settings_manager-1.0.0.tar.gz.

File metadata

File hashes

Hashes for pydantic_settings_manager-1.0.0.tar.gz
Algorithm Hash digest
SHA256 438f313abf4cb78397fed2aaaa63f97d82b2c0b864afdd4a3efd7ec1594789f7
MD5 a0ccbbdc8897f68a4e4ac3542248b13a
BLAKE2b-256 e3754d2ac51a104c823860740c7aa694a8c8650512010d6629b54500d2cdfbdb

See more details on using hashes here.

File details

Details for the file pydantic_settings_manager-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_settings_manager-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bba1b0cdf2ed6abd7b9ad60641979e1732e6873902ee8e68c67173f7cd766d6d
MD5 8efd1032cf50b549a1fec99b07634634
BLAKE2b-256 b43b16cd27c14b2595d651fd25a2ca9d08a03435f307de45d066f15cdb18fd88

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