Skip to main content

Configuration

Tasteful provides a robust configuration system built on top of Pydantic Settings, enabling type-safe configuration management with automatic environment variable loading, validation, and seamless integration with the dependency injection system.

What is Configuration?

Configuration in Tasteful manages application settings, environment variables, and runtime parameters for your flavors. The configuration system is designed around the BaseConfig class, which extends Pydantic’s BaseSettings to provide:
  • Type Safety: Full type hints and validation for all configuration values
  • Environment Variable Integration: Automatic loading from environment variables and .env files
  • Nested Configuration: Support for complex nested configuration structures
  • Dependency Injection: Seamless integration with Tasteful’s dependency injection system
  • Validation: Built-in validation with custom validators and constraints
Configuration objects are injected into Controllers, Services, and Repositories through the dependency injection system, providing a clean way to manage application settings across all layers of your flavor.

BaseConfig Class

The BaseConfig class serves as the foundation for all configuration in Tasteful applications:
from tasteful.base_flavor import BaseConfig

class MyConfig(BaseConfig):
    # Database configuration
    database_url: str = "sqlite:///app.db"
    database_echo: bool = False
    
    # API configuration
    api_key: str
    api_timeout: int = 30
    
    # Feature flags
    enable_caching: bool = True
    debug_mode: bool = False

Configuration Features

Environment Variable Loading

BaseConfig automatically loads configuration from environment variables, supporting multiple naming conventions:
class AppConfig(BaseConfig):
    database_url: str = "sqlite:///default.db"
    api_key: str = ""
    max_connections: int = 10

# Environment variables are automatically mapped:
# DATABASE_URL -> database_url
# API_KEY -> api_key  
# MAX_CONNECTIONS -> max_connections

Nested Configuration

Support for complex nested configuration structures using nested delimiter:
class DatabaseConfig(BaseConfig):
    host: str = "localhost"
    port: int = 5432
    name: str = "myapp"
    
class RedisConfig(BaseConfig):
    host: str = "localhost"
    port: int = 6379
    
class AppConfig(BaseConfig):
    database: DatabaseConfig = DatabaseConfig()
    redis: RedisConfig = RedisConfig()

# Environment variables with nested delimiter:
# DATABASE__HOST=production-db
# DATABASE__PORT=5432
# REDIS__HOST=redis-server

.env File Support

Automatic loading from .env files in the project root:
# .env file
DATABASE_URL=postgresql://user:pass@localhost/myapp
API_KEY=your-secret-key
DEBUG_MODE=true
MAX_CONNECTIONS=20

Type Validation

Built-in type validation and conversion:
from typing import List
from pydantic import Field, validator

class AppConfig(BaseConfig):
    # String with constraints
    api_key: str = Field(..., min_length=10)
    
    # Integer with range validation
    max_connections: int = Field(default=10, ge=1, le=100)
    
    # List of allowed values
    log_level: str = Field(default="INFO", regex="^(DEBUG|INFO|WARNING|ERROR)$")
    
    # List configuration
    allowed_hosts: List[str] = ["localhost", "127.0.0.1"]
    
    @validator('database_url')
    def validate_database_url(cls, v):
        if not v.startswith(('sqlite://', 'postgresql://', 'mysql://')):
            raise ValueError('Invalid database URL scheme')
        return v

Integration with Other Components

Configuration classes integrate seamlessly with all layers of your Tasteful flavor through dependency injection.

Configuration in Services

Services receive configuration objects to access business logic settings:
from tasteful.base_flavor import BaseService

class UserService(BaseService):
    def __init__(self, user_repository: UserRepository, user_config: UserConfig):
        super().__init__()
        self.user_repository = user_repository
        self.user_config = user_config
    
    def create_user(self, user_data: dict) -> dict:
        # Use configuration for business logic
        if len(user_data.get("password", "")) < self.user_config.min_password_length:
            raise ValueError(f"Password must be at least {self.user_config.min_password_length} characters")
        
        # Apply configuration-based defaults
        user_data["max_login_attempts"] = self.user_config.max_login_attempts
        return self.user_repository.create(user_data)

Configuration in Repositories

Repositories use configuration for database connections and settings:
from tasteful.repositories import SQLModelRepository

class UserRepository(SQLModelRepository):
    def __init__(self, app_config: AppConfig):
        super().__init__(
            database_url=app_config.database_url,
            echo=app_config.debug_sql
        )
        self.app_config = app_config
    
    def get_users_with_pagination(self, skip: int = 0, limit: int = 100) -> List[User]:
        # Use configuration for default limits
        safe_limit = min(limit, self.app_config.max_query_limit)
        
        with self.get_session() as session:
            statement = select(User).offset(skip).limit(safe_limit)
            return list(session.exec(statement).all())

Configuration in Controllers

Controllers can access configuration for HTTP-specific settings:
from tasteful.base_flavor import BaseController
from tasteful.decorators.controller import Get, Post

class UserController(BaseController):
    def __init__(self, user_service: UserService, api_config: ApiConfig):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service
        self.api_config = api_config
    
    @Get("/")
    async def list_users(self, skip: int = 0, limit: int = 100):
        # Use configuration for default pagination
        safe_limit = min(limit, self.api_config.max_page_size)
        return self.user_service.list_users(skip=skip, limit=safe_limit)

class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService],
            repositories=[UserRepository],
            config=UserConfig  # ← Configuration registered for injection
        )

Configuration Inheritance

Create specialized configurations by inheriting from base configurations:
class BaseAppConfig(BaseConfig):
    debug: bool = False
    log_level: str = "INFO"
    
class DevelopmentConfig(BaseAppConfig):
    debug: bool = True
    log_level: str = "DEBUG"
    database_url: str = "sqlite:///dev.db"
    
class ProductionConfig(BaseAppConfig):
    database_url: str  # Required in production
    redis_url: str     # Required in production
    
    @validator('debug')
    def no_debug_in_production(cls, v):
        if v:
            raise ValueError('Debug mode not allowed in production')
        return v

Best Practices

1. Use Type Hints

Always provide type hints for configuration fields:
class Config(BaseConfig):
    # Good: Clear type hints
    database_url: str
    max_connections: int
    enable_ssl: bool
    
    # Avoid: No type hints
    # some_setting = "default"

2. Provide Sensible Defaults

Provide defaults for non-critical configuration:
class Config(BaseConfig):
    # Required configuration (no default)
    database_url: str
    api_key: str
    
    # Optional configuration (with defaults)
    timeout: int = 30
    retry_count: int = 3
    debug: bool = False

3. Use Validation

Add validation for critical configuration values:
class Config(BaseConfig):
    database_url: str = Field(..., regex=r'^(sqlite|postgresql|mysql)://')
    port: int = Field(default=8000, ge=1, le=65535)
    
    @validator('api_key')
    def validate_api_key(cls, v):
        if len(v) < 32:
            raise ValueError('API key must be at least 32 characters')
        return v
Use nested configuration for related settings:
class DatabaseConfig(BaseConfig):
    url: str
    pool_size: int = 5
    echo: bool = False

class CacheConfig(BaseConfig):
    backend: str = "memory"
    ttl: int = 3600

class AppConfig(BaseConfig):
    database: DatabaseConfig
    cache: CacheConfig
    app_name: str = "MyApp"

# Usage in repositories and services
class UserRepository(SQLModelRepository):
    def __init__(self, app_config: AppConfig):
        super().__init__(
            database_url=app_config.database.url,
            echo=app_config.database.echo
        )

class CacheService(BaseService):
    def __init__(self, app_config: AppConfig):
        super().__init__()
        self.cache_backend = app_config.cache.backend
        self.cache_ttl = app_config.cache.ttl

Configuration Loading Order

Tasteful loads configuration in the following order (later sources override earlier ones):
  1. Default values defined in the configuration class
  2. Environment variables matching field names
  3. .env file in the project root
  4. Explicit values passed during instantiation
This allows for flexible configuration management across different environments while maintaining sensible defaults.

Common Patterns

Environment-Specific Configuration

Create different configuration classes for different environments:
class BaseAppConfig(BaseConfig):
    app_name: str = "MyApp"
    debug: bool = False
    log_level: str = "INFO"

class DevelopmentConfig(BaseAppConfig):
    debug: bool = True
    log_level: str = "DEBUG"
    database_url: str = "sqlite:///dev.db"

class ProductionConfig(BaseAppConfig):
    database_url: str  # Required in production
    redis_url: str     # Required in production
    
    @validator('debug')
    def no_debug_in_production(cls, v):
        if v:
            raise ValueError('Debug mode not allowed in production')
        return v

class TestingConfig(BaseAppConfig):
    database_url: str = "sqlite:///:memory:"
    testing: bool = True

Feature Flags

Use configuration for feature toggles:
class FeatureConfig(BaseConfig):
    enable_caching: bool = True
    enable_analytics: bool = False
    enable_new_ui: bool = False
    max_file_upload_size: int = 10_000_000  # 10MB

class UserService(BaseService):
    def __init__(self, user_repository: UserRepository, feature_config: FeatureConfig):
        super().__init__()
        self.user_repository = user_repository
        self.feature_config = feature_config
    
    def create_user(self, user_data: dict) -> dict:
        user = self.user_repository.create(user_data)
        
        # Feature flag: conditionally enable analytics
        if self.feature_config.enable_analytics:
            self._track_user_creation(user)
        
        return user

Configuration Validation

Implement complex validation logic:
class ApiConfig(BaseConfig):
    host: str = "localhost"
    port: int = 8000
    workers: int = 1
    max_connections: int = 1000
    
    @validator('port')
    def validate_port(cls, v):
        if not (1 <= v <= 65535):
            raise ValueError('Port must be between 1 and 65535')
        return v
    
    @validator('workers')
    def validate_workers(cls, v):
        if v < 1:
            raise ValueError('Workers must be at least 1')
        return v
    
    @root_validator
    def validate_connection_limits(cls, values):
        workers = values.get('workers', 1)
        max_connections = values.get('max_connections', 1000)
        
        if max_connections < workers * 10:
            raise ValueError('max_connections should be at least 10x the number of workers')
        
        return values

Testing

Configuration classes are easily testable and can be mocked for unit tests:
import pytest
from unittest.mock import Mock

def test_user_service_with_config():
    # Arrange
    mock_repository = Mock(spec=UserRepository)
    mock_repository.create.return_value = {"id": 1, "name": "Test User"}
    
    test_config = UserConfig(
        min_password_length=8,
        max_login_attempts=3
    )
    
    user_service = UserService(
        user_repository=mock_repository,
        user_config=test_config
    )
    
    # Act
    user_data = {"name": "Test User", "password": "validpassword"}
    result = user_service.create_user(user_data)
    
    # Assert
    assert result["id"] == 1
    assert user_data["max_login_attempts"] == 3
    mock_repository.create.assert_called_once()

def test_configuration_validation():
    # Test that invalid configuration raises errors
    with pytest.raises(ValueError, match="Port must be between 1 and 65535"):
        ApiConfig(port=70000)
    
    with pytest.raises(ValueError, match="Workers must be at least 1"):
        ApiConfig(workers=0)

def test_environment_variable_loading():
    # Test configuration loading from environment variables
    import os
    os.environ["DATABASE_URL"] = "postgresql://test:test@localhost/testdb"
    os.environ["DEBUG"] = "true"
    
    config = AppConfig()
    
    assert config.database_url == "postgresql://test:test@localhost/testdb"
    assert config.debug is True
    
    # Clean up
    del os.environ["DATABASE_URL"]
    del os.environ["DEBUG"]
Configuration provides the foundation for managing settings across all components of your Tasteful flavors. By leveraging Pydantic’s validation and Tasteful’s dependency injection, you can create robust, type-safe configuration systems that adapt to different environments while maintaining clean separation of concerns.
I