Modern Python configuration management system with CLI override support
Project description
cfy (Config For You)
A modern, type-safe Python configuration library with support for multiple sources, validation, and environment-specific overrides.
Features
- 🔧 Multi-source configuration: YAML, JSON, TOML, environment variables, CLI arguments
- 🔒 Type-safe validation: Pydantic v2 models with schema enforcement
- 🔐 Secret management: Secure handling with encryption support
- 🔄 Dynamic reloading: Watch files and reload configuration automatically
- 📝 Variable interpolation: Template substitution with
${VAR}syntax - 🚀 FastAPI integration: Built-in dependency injection and middleware
- 🎨 Beautiful CLI: Rich terminal interface for validation and inspection
- ⚡ Performance optimized: Caching, lazy loading, and monitoring
Installation
pip install cfy
For development installation:
git clone https://github.com/yourusername/cfy.git
cd cfy
pip install -e ".[dev]"
Examples
Explore our comprehensive examples to learn PyConfig:
- 📚 Interactive Jupyter Notebooks - Step-by-step tutorials
- 📁 Example Configuration Files - Ready-to-use templates
Quickstart
Basic Usage
from cfy import BaseConfiguration, ConfigurationLoader
from pydantic import Field
from pydantic_settings import SettingsConfigDict
# Define your configuration schema
class AppConfig(BaseConfiguration):
app_name: str = Field(default="MyApp", description="Application name")
debug: bool = Field(default=False, description="Debug mode")
port: int = Field(default=8000, description="Server port")
database_url: str = Field(default="postgresql://localhost/myapp", description="Database connection URL")
# Configure environment variable loading
model_config = SettingsConfigDict(
env_prefix="APP_", # Load from APP_* environment variables
env_file=".env" # Load from .env file
)
# Create loader (app_name is now optional - auto-generates if not provided)
loader = ConfigurationLoader() # Auto-generates app name from context
# Or explicitly specify app_name for production use
# loader = ConfigurationLoader(app_name="myapp")
# Load configuration using the loader
config = loader.load(AppConfig)
# Access configuration
print(f"Starting {config.app_name} on port {config.port}")
print(f"Database: {config.database_url}")
Configuration Files
config.yaml:
app_name: "MyApp"
port: 8000
database_url: "postgresql://localhost/myapp"
config.prod.yaml:
debug: false
database_url: "${DATABASE_URL}" # Use environment variable
.env:
APP_DEBUG=false
DATABASE_URL=postgresql://prod-server/myapp
APP_SECRET_KEY=${file:/run/secrets/app_key} # Load from file
Variable Interpolation
cfy supports multiple interpolation patterns:
from cfy import ConfigInterpolator
# Create interpolator
interpolator = ConfigInterpolator()
# Environment variable substitution
config = {
"database": "${DATABASE_URL}",
"api_key": "${env:API_KEY}",
"timeout": "${TIMEOUT:30}", # With default value
}
# Cross-reference other config values
config = {
"base_url": "https://api.example.com",
"endpoint": "${config:base_url}/v1/users"
}
# File content substitution
config = {
"ssl_cert": "${file:/etc/ssl/cert.pem}",
"secrets": "${file:/run/secrets/app.json}"
}
# Apply interpolation
result = interpolator.interpolate(config)
Nested Configuration
from cfy import BaseConfiguration, ConfigurationLoader
from pydantic import Field
from pydantic_settings import SettingsConfigDict
from typing import List, Optional
class DatabaseConfig(BaseConfiguration):
host: str = Field(default="localhost")
port: int = Field(default=5432)
name: str = Field(default="myapp")
user: str = Field(default="user")
password: str = Field(default="password", exclude=True) # Exclude from serialization
model_config = SettingsConfigDict(env_prefix="DB_")
class ServerConfig(BaseConfiguration):
host: str = Field(default="0.0.0.0")
port: int = Field(default=8000)
workers: int = Field(default=4)
model_config = SettingsConfigDict(env_prefix="SERVER_")
class AppConfig(BaseConfiguration):
name: str = Field(default="MyApp")
version: str = Field(default="1.0.0")
server: ServerConfig = Field(default_factory=ServerConfig)
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
features: List[str] = Field(default_factory=list)
metadata: Optional[dict] = Field(default=None)
model_config = SettingsConfigDict(env_prefix="APP_")
# Create loader and load nested configuration
loader = ConfigurationLoader(config_paths=["nested_config.yaml"]) # app_name is optional
# Or with explicit app_name: ConfigurationLoader(app_name="myapp", config_paths=["nested_config.yaml"])
config = loader.load(AppConfig)
# Access nested values
print(f"Database: {config.database.host}:{config.database.port}")
print(f"Server: {config.server.workers} workers")
Secret Management
from cfy import SecretManager
from pydantic import SecretStr
import os
# Set up test secrets in environment
os.environ['SECRET_API_KEY'] = 'your-api-key-here'
os.environ['SECRET_DB_PASSWORD'] = 'your-db-password-here'
# Initialize secret manager
secrets = SecretManager()
# Load secrets from environment variables
api_key = secrets.get_secret_env('SECRET_API_KEY', required=False)
db_password = secrets.get_secret_env('SECRET_DB_PASSWORD', required=False)
print(f"API Key type: {type(api_key).__name__}") # SecretStr
print(f"Password type: {type(db_password).__name__}") # SecretStr
# Access secret values (only when needed)
print(f"API Key value: {api_key.get_secret_value()}")
# Mask secrets for logging
masked_key = secrets.mask_secret(api_key, show_chars=4)
print(f"Masked API Key: {masked_key}") # "****-here"
Dynamic Configuration Reloading
from cfy import ConfigurationLoader
import asyncio
loader = ConfigurationLoader()
# Watch for configuration changes
async def watch_config():
async for config in loader.watch_config(
AppConfig,
sources=["config.yaml"],
interval=5.0 # Check every 5 seconds
):
print(f"Configuration updated: {config.app_name}")
# Reconfigure your application
# Run the watcher
asyncio.run(watch_config())
CLI Interface
PyConfig includes a rich CLI for configuration management:
# Validate configuration
cfy validate config.yaml --schema AppConfig
# Inspect configuration with syntax highlighting
cfy inspect config.yaml
# Convert between formats
cfy convert config.yaml config.json
# Generate JSON schema
cfy generate-schema AppConfig > schema.json
# Check secrets
cfy secrets check config.yaml
# Monitor performance
cfy performance config.yaml
FastAPI Integration
from fastapi import FastAPI, Depends
from cfy.integrations.fastapi import (
ConfigDependency,
FastAPIConfigMiddleware,
create_config_dependency
)
from cfy import BaseConfiguration, ConfigurationLoader
from pydantic import Field
from pydantic_settings import SettingsConfigDict
class AppConfig(BaseConfiguration):
app_name: str = Field(default="TestApp")
version: str = Field(default="1.0.0")
model_config = SettingsConfigDict(env_prefix="APP_")
app = FastAPI()
# Add configuration middleware
app.add_middleware(FastAPIConfigMiddleware, config_class=AppConfig)
# Create custom loader (optional)
loader = ConfigurationLoader(config_paths=["config.yaml"]) # app_name auto-generated
# Or with explicit app_name: ConfigurationLoader(app_name="myapp", config_paths=["config.yaml"])
# Create configuration dependency
get_config = create_config_dependency(AppConfig, loader=loader)
# Use in endpoints
@app.get("/")
async def root(config: AppConfig = Depends(get_config)):
return {"app": config.app_name, "version": config.version}
# Test the configuration
if __name__ == "__main__":
config = get_config()
print(f"App: {config.app_name} v{config.version}")
Advanced Features
Custom Validation
from pydantic import BaseModel, field_validator, model_validator, Field
class AppConfig(BaseModel):
port: int = Field(default=8080)
workers: int = Field(default=4)
@field_validator("port")
@classmethod
def validate_port(cls, v):
if not 1 <= v <= 65535:
raise ValueError("Port must be between 1 and 65535")
return v
@model_validator(mode="after")
def validate_workers(self):
if self.workers > 10 and self.port < 1024:
raise ValueError("High worker count requires non-privileged port")
return self
# Test validation
config = AppConfig(port=8080, workers=4)
print(f"Valid config: port={config.port}, workers={config.workers}")
# This will raise a ValueError
try:
invalid_config = AppConfig(port=70000, workers=4)
except ValueError as e:
print(f"Validation error: {e}")
Performance Monitoring
from cfy.utils import PerformanceMonitor
monitor = PerformanceMonitor()
# Track configuration operations
with monitor.track("load_config"):
config = loader.load_config(AppConfig, sources=["config.yaml"])
# Get metrics
metrics = monitor.get_metrics()
print(f"Load time: {metrics['load_config']['avg_duration']:.3f}s")
Caching
from cfy.utils import ConfigCache
# Create cache with TTL
cache = ConfigCache(ttl=300, max_size=100)
# Cache configuration
config = cache.get_or_load(
"app_config",
lambda: loader.load_config(AppConfig, sources=["config.yaml"])
)
Configuration Sources Priority
Configuration sources are loaded in the following priority order (highest to lowest):
- CLI arguments
- Environment variables
.envfiles- Environment-specific config files (e.g.,
config.prod.yaml) - Base configuration files (e.g.,
config.yaml) - Default values in configuration class
Environment Variables
PyConfig automatically loads environment variables based on your configuration schema:
from pydantic_settings import SettingsConfigDict
class AppConfig(BaseConfiguration):
model_config = SettingsConfigDict(
env_prefix="APP_", # Prefix for environment variables
env_nested_delimiter="__" # Delimiter for nested fields
)
# These environment variables will be loaded:
# APP_DEBUG=true
# APP_PORT=8080
# APP_DATABASE__HOST=localhost
# APP_DATABASE__PORT=5432
Testing
cfy provides utilities for testing configuration-dependent code:
import pytest
from cfy.testing import config_fixture
@pytest.fixture
def app_config():
return config_fixture(
AppConfig,
overrides={
"debug": True,
"database.host": "test-db"
}
)
def test_my_app(app_config):
assert app_config.debug is True
assert app_config.database.host == "test-db"
Best Practices
- Use type hints: Always define types for configuration fields
- Validate early: Validate configuration at application startup
- Secure secrets: Never commit secrets to version control
- Environment-specific files: Use separate files for different environments
- Document fields: Add descriptions to configuration fields
- Use interpolation: Leverage variable substitution for flexibility
- Monitor performance: Track configuration loading performance
- Cache when appropriate: Cache expensive configuration operations
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For bugs, questions, and feature requests, please open an issue on GitHub.
Acknowledgments
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cfy-0.1.0.post13-py3-none-any.whl.
File metadata
- Download URL: cfy-0.1.0.post13-py3-none-any.whl
- Upload date:
- Size: 58.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a3a0a6a30f22f2ec8212b63ad13a021b030e3afa652900b941c7b0fc59772a1
|
|
| MD5 |
3f530d314a0d0fc6286bc8431e84ec01
|
|
| BLAKE2b-256 |
e6868dd6e2764baecc15403a7bf2890c1f7e5d46c8abcfa2b1c8c666f2cc9b82
|