Skip to main content

cfy (Config For You)

A modern, type-safe Python configuration library with support for multiple sources, validation, and environment-specific overrides.

Python Version License Code Style

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:

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):

  1. CLI arguments
  2. Environment variables
  3. .env files
  4. Environment-specific config files (e.g., config.prod.yaml)
  5. Base configuration files (e.g., config.yaml)
  6. 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

  1. Use type hints: Always define types for configuration fields
  2. Validate early: Validate configuration at application startup
  3. Secure secrets: Never commit secrets to version control
  4. Environment-specific files: Use separate files for different environments
  5. Document fields: Add descriptions to configuration fields
  6. Use interpolation: Leverage variable substitution for flexibility
  7. Monitor performance: Track configuration loading performance
  8. Cache when appropriate: Cache expensive configuration operations

Contributing

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

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

  • Built with Pydantic for validation
  • CLI powered by Typer and Rich
  • Inspired by best practices from the Python community

Metadata

Release files for cfy 0.1.0.post13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for cfy 0.1.0.post13
File Interpreter ABI Platform Actions
cfy-0.1.0.post13-py3-none-any.whl Python 3 none any Details for cfy-0.1.0.post13-py3-none-any.whl

Release files / cfy-0.1.0.post13-py3-none-any.whl

Download URL cfy-0.1.0.post13-py3-none-any.whl
Size 58.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a3a0a6a30f22f2ec8212b63ad13a021b030e3afa652900b941c7b0fc59772a1
BLAKE2b-256 checksum
How to use checksums
e6868dd6e2764baecc15403a7bf2890c1f7e5d46c8abcfa2b1c8c666f2cc9b82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.2

Security for cfy 0.1.0.post13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page