537 lines
14 KiB
Markdown
537 lines
14 KiB
Markdown
# Settings Classes
|
|
|
|
Learn how Python settings classes validate, structure, and organize your application configuration. The boilerplate uses Pydantic's `BaseSettings` for type-safe configuration management.
|
|
|
|
## Settings Architecture
|
|
|
|
The main `Settings` class inherits from multiple specialized setting groups:
|
|
|
|
```python
|
|
# src/app/core/config.py
|
|
class Settings(
|
|
AppSettings,
|
|
PostgresSettings,
|
|
CryptSettings,
|
|
FirstUserSettings,
|
|
RedisCacheSettings,
|
|
ClientSideCacheSettings,
|
|
RedisQueueSettings,
|
|
RedisRateLimiterSettings,
|
|
DefaultRateLimitSettings,
|
|
EnvironmentSettings,
|
|
):
|
|
pass
|
|
|
|
# Single instance used throughout the app
|
|
settings = Settings()
|
|
```
|
|
|
|
## Built-in Settings Groups
|
|
|
|
### Application Settings
|
|
Basic app metadata and configuration:
|
|
|
|
```python
|
|
class AppSettings(BaseSettings):
|
|
APP_NAME: str = "FastAPI"
|
|
APP_DESCRIPTION: str = "A FastAPI project"
|
|
APP_VERSION: str = "0.1.0"
|
|
CONTACT_NAME: str = "Your Name"
|
|
CONTACT_EMAIL: str = "your.email@example.com"
|
|
LICENSE_NAME: str = "MIT"
|
|
```
|
|
|
|
### Database Settings
|
|
PostgreSQL connection configuration:
|
|
|
|
```python
|
|
class PostgresSettings(BaseSettings):
|
|
POSTGRES_USER: str
|
|
POSTGRES_PASSWORD: str
|
|
POSTGRES_SERVER: str = "localhost"
|
|
POSTGRES_PORT: int = 5432
|
|
POSTGRES_DB: str
|
|
|
|
@computed_field
|
|
@property
|
|
def DATABASE_URL(self) -> str:
|
|
return (
|
|
f"postgresql+asyncpg://{self.POSTGRES_USER}:"
|
|
f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:"
|
|
f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
)
|
|
```
|
|
|
|
### Security Settings
|
|
JWT and authentication configuration:
|
|
|
|
```python
|
|
class CryptSettings(BaseSettings):
|
|
SECRET_KEY: str
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
|
|
|
@field_validator("SECRET_KEY")
|
|
@classmethod
|
|
def validate_secret_key(cls, v: str) -> str:
|
|
if len(v) < 32:
|
|
raise ValueError("SECRET_KEY must be at least 32 characters")
|
|
return v
|
|
```
|
|
|
|
### Redis Settings
|
|
Separate Redis instances for different services:
|
|
|
|
```python
|
|
class RedisCacheSettings(BaseSettings):
|
|
REDIS_CACHE_HOST: str = "localhost"
|
|
REDIS_CACHE_PORT: int = 6379
|
|
|
|
class RedisQueueSettings(BaseSettings):
|
|
REDIS_QUEUE_HOST: str = "localhost"
|
|
REDIS_QUEUE_PORT: int = 6379
|
|
|
|
class RedisRateLimiterSettings(BaseSettings):
|
|
REDIS_RATE_LIMIT_HOST: str = "localhost"
|
|
REDIS_RATE_LIMIT_PORT: int = 6379
|
|
```
|
|
|
|
### Rate Limiting Settings
|
|
Default rate limiting configuration:
|
|
|
|
```python
|
|
class DefaultRateLimitSettings(BaseSettings):
|
|
DEFAULT_RATE_LIMIT_LIMIT: int = 10
|
|
DEFAULT_RATE_LIMIT_PERIOD: int = 3600 # 1 hour
|
|
```
|
|
|
|
### Admin User Settings
|
|
First superuser account creation:
|
|
|
|
```python
|
|
class FirstUserSettings(BaseSettings):
|
|
ADMIN_NAME: str = "Admin"
|
|
ADMIN_EMAIL: str
|
|
ADMIN_USERNAME: str = "admin"
|
|
ADMIN_PASSWORD: str
|
|
|
|
@field_validator("ADMIN_EMAIL")
|
|
@classmethod
|
|
def validate_admin_email(cls, v: str) -> str:
|
|
if "@" not in v:
|
|
raise ValueError("ADMIN_EMAIL must be a valid email")
|
|
return v
|
|
```
|
|
|
|
## Creating Custom Settings
|
|
|
|
### Basic Custom Settings
|
|
|
|
Add your own settings group:
|
|
|
|
```python
|
|
class CustomSettings(BaseSettings):
|
|
CUSTOM_API_KEY: str = ""
|
|
CUSTOM_TIMEOUT: int = 30
|
|
ENABLE_FEATURE_X: bool = False
|
|
MAX_UPLOAD_SIZE: int = 10485760 # 10MB
|
|
|
|
@field_validator("MAX_UPLOAD_SIZE")
|
|
@classmethod
|
|
def validate_upload_size(cls, v: int) -> int:
|
|
if v < 1024: # 1KB minimum
|
|
raise ValueError("MAX_UPLOAD_SIZE must be at least 1KB")
|
|
if v > 104857600: # 100MB maximum
|
|
raise ValueError("MAX_UPLOAD_SIZE cannot exceed 100MB")
|
|
return v
|
|
|
|
# Add to main Settings class
|
|
class Settings(
|
|
AppSettings,
|
|
PostgresSettings,
|
|
# ... other settings ...
|
|
CustomSettings, # Add your custom settings
|
|
):
|
|
pass
|
|
```
|
|
|
|
### Advanced Custom Settings
|
|
|
|
Settings with complex validation and computed fields:
|
|
|
|
```python
|
|
class EmailSettings(BaseSettings):
|
|
SMTP_HOST: str = ""
|
|
SMTP_PORT: int = 587
|
|
SMTP_USERNAME: str = ""
|
|
SMTP_PASSWORD: str = ""
|
|
SMTP_USE_TLS: bool = True
|
|
EMAIL_FROM: str = ""
|
|
EMAIL_FROM_NAME: str = ""
|
|
|
|
@computed_field
|
|
@property
|
|
def EMAIL_ENABLED(self) -> bool:
|
|
return bool(self.SMTP_HOST and self.SMTP_USERNAME)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_email_config(self) -> "EmailSettings":
|
|
if self.SMTP_HOST and not self.EMAIL_FROM:
|
|
raise ValueError("EMAIL_FROM required when SMTP_HOST is set")
|
|
if self.SMTP_USERNAME and not self.SMTP_PASSWORD:
|
|
raise ValueError("SMTP_PASSWORD required when SMTP_USERNAME is set")
|
|
return self
|
|
```
|
|
|
|
### Feature Flag Settings
|
|
|
|
Organize feature toggles:
|
|
|
|
```python
|
|
class FeatureSettings(BaseSettings):
|
|
# Core features
|
|
ENABLE_CACHING: bool = True
|
|
ENABLE_RATE_LIMITING: bool = True
|
|
ENABLE_BACKGROUND_JOBS: bool = True
|
|
|
|
# Optional features
|
|
ENABLE_ANALYTICS: bool = False
|
|
ENABLE_EMAIL_NOTIFICATIONS: bool = False
|
|
ENABLE_FILE_UPLOADS: bool = False
|
|
|
|
# Experimental features
|
|
ENABLE_EXPERIMENTAL_API: bool = False
|
|
ENABLE_BETA_FEATURES: bool = False
|
|
|
|
@model_validator(mode="after")
|
|
def validate_feature_dependencies(self) -> "FeatureSettings":
|
|
if self.ENABLE_EMAIL_NOTIFICATIONS and not self.ENABLE_BACKGROUND_JOBS:
|
|
raise ValueError("Email notifications require background jobs")
|
|
return self
|
|
```
|
|
|
|
## Settings Validation
|
|
|
|
### Field Validation
|
|
|
|
Validate individual fields:
|
|
|
|
```python
|
|
class DatabaseSettings(BaseSettings):
|
|
DB_POOL_SIZE: int = 20
|
|
DB_MAX_OVERFLOW: int = 30
|
|
DB_TIMEOUT: int = 30
|
|
|
|
@field_validator("DB_POOL_SIZE")
|
|
@classmethod
|
|
def validate_pool_size(cls, v: int) -> int:
|
|
if v < 1:
|
|
raise ValueError("Pool size must be at least 1")
|
|
if v > 100:
|
|
raise ValueError("Pool size should not exceed 100")
|
|
return v
|
|
|
|
@field_validator("DB_TIMEOUT")
|
|
@classmethod
|
|
def validate_timeout(cls, v: int) -> int:
|
|
if v < 5:
|
|
raise ValueError("Timeout must be at least 5 seconds")
|
|
return v
|
|
```
|
|
|
|
### Model Validation
|
|
|
|
Validate across multiple fields:
|
|
|
|
```python
|
|
class SecuritySettings(BaseSettings):
|
|
ENABLE_HTTPS: bool = False
|
|
SSL_CERT_PATH: str = ""
|
|
SSL_KEY_PATH: str = ""
|
|
FORCE_SSL: bool = False
|
|
|
|
@model_validator(mode="after")
|
|
def validate_ssl_config(self) -> "SecuritySettings":
|
|
if self.ENABLE_HTTPS:
|
|
if not self.SSL_CERT_PATH:
|
|
raise ValueError("SSL_CERT_PATH required when HTTPS enabled")
|
|
if not self.SSL_KEY_PATH:
|
|
raise ValueError("SSL_KEY_PATH required when HTTPS enabled")
|
|
|
|
if self.FORCE_SSL and not self.ENABLE_HTTPS:
|
|
raise ValueError("Cannot force SSL without enabling HTTPS")
|
|
|
|
return self
|
|
```
|
|
|
|
### Environment-Specific Validation
|
|
|
|
Different validation rules per environment:
|
|
|
|
```python
|
|
class EnvironmentSettings(BaseSettings):
|
|
ENVIRONMENT: str = "local"
|
|
DEBUG: bool = True
|
|
|
|
@model_validator(mode="after")
|
|
def validate_environment_config(self) -> "EnvironmentSettings":
|
|
if self.ENVIRONMENT == "production":
|
|
if self.DEBUG:
|
|
raise ValueError("DEBUG must be False in production")
|
|
|
|
if self.ENVIRONMENT not in ["local", "staging", "production"]:
|
|
raise ValueError("ENVIRONMENT must be local, staging, or production")
|
|
|
|
return self
|
|
```
|
|
|
|
## Computed Properties
|
|
|
|
### Dynamic Configuration
|
|
|
|
Create computed values from other settings:
|
|
|
|
```python
|
|
class StorageSettings(BaseSettings):
|
|
STORAGE_TYPE: str = "local" # local, s3, gcs
|
|
|
|
# Local storage
|
|
LOCAL_STORAGE_PATH: str = "./uploads"
|
|
|
|
# S3 settings
|
|
AWS_ACCESS_KEY_ID: str = ""
|
|
AWS_SECRET_ACCESS_KEY: str = ""
|
|
AWS_BUCKET_NAME: str = ""
|
|
AWS_REGION: str = "us-east-1"
|
|
|
|
@computed_field
|
|
@property
|
|
def STORAGE_ENABLED(self) -> bool:
|
|
if self.STORAGE_TYPE == "local":
|
|
return bool(self.LOCAL_STORAGE_PATH)
|
|
elif self.STORAGE_TYPE == "s3":
|
|
return bool(self.AWS_ACCESS_KEY_ID and self.AWS_SECRET_ACCESS_KEY and self.AWS_BUCKET_NAME)
|
|
return False
|
|
|
|
@computed_field
|
|
@property
|
|
def STORAGE_CONFIG(self) -> dict:
|
|
if self.STORAGE_TYPE == "local":
|
|
return {"path": self.LOCAL_STORAGE_PATH}
|
|
elif self.STORAGE_TYPE == "s3":
|
|
return {
|
|
"bucket": self.AWS_BUCKET_NAME,
|
|
"region": self.AWS_REGION,
|
|
"credentials": {
|
|
"access_key": self.AWS_ACCESS_KEY_ID,
|
|
"secret_key": self.AWS_SECRET_ACCESS_KEY,
|
|
}
|
|
}
|
|
return {}
|
|
```
|
|
|
|
## Organizing Settings
|
|
|
|
### Service-Based Organization
|
|
|
|
Group settings by service or domain:
|
|
|
|
```python
|
|
# Authentication service settings
|
|
class AuthSettings(BaseSettings):
|
|
JWT_SECRET_KEY: str
|
|
JWT_ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE: int = 30
|
|
REFRESH_TOKEN_EXPIRE: int = 7200
|
|
PASSWORD_MIN_LENGTH: int = 8
|
|
|
|
# Notification service settings
|
|
class NotificationSettings(BaseSettings):
|
|
EMAIL_ENABLED: bool = False
|
|
SMS_ENABLED: bool = False
|
|
PUSH_ENABLED: bool = False
|
|
|
|
# Email settings
|
|
SMTP_HOST: str = ""
|
|
SMTP_PORT: int = 587
|
|
|
|
# SMS settings (example with Twilio)
|
|
TWILIO_ACCOUNT_SID: str = ""
|
|
TWILIO_AUTH_TOKEN: str = ""
|
|
|
|
# Main settings
|
|
class Settings(
|
|
AppSettings,
|
|
AuthSettings,
|
|
NotificationSettings,
|
|
# ... other settings
|
|
):
|
|
pass
|
|
```
|
|
|
|
### Conditional Settings Loading
|
|
|
|
Load different settings based on environment:
|
|
|
|
```python
|
|
class BaseAppSettings(BaseSettings):
|
|
APP_NAME: str = "FastAPI App"
|
|
DEBUG: bool = False
|
|
|
|
class DevelopmentSettings(BaseAppSettings):
|
|
DEBUG: bool = True
|
|
LOG_LEVEL: str = "DEBUG"
|
|
DATABASE_ECHO: bool = True
|
|
|
|
class ProductionSettings(BaseAppSettings):
|
|
DEBUG: bool = False
|
|
LOG_LEVEL: str = "WARNING"
|
|
DATABASE_ECHO: bool = False
|
|
|
|
def get_settings() -> BaseAppSettings:
|
|
environment = os.getenv("ENVIRONMENT", "local")
|
|
|
|
if environment == "production":
|
|
return ProductionSettings()
|
|
else:
|
|
return DevelopmentSettings()
|
|
|
|
settings = get_settings()
|
|
```
|
|
|
|
## Removing Unused Services
|
|
|
|
### Minimal Configuration
|
|
|
|
Remove services you don't need:
|
|
|
|
```python
|
|
# Minimal setup without Redis services
|
|
class MinimalSettings(
|
|
AppSettings,
|
|
PostgresSettings,
|
|
CryptSettings,
|
|
FirstUserSettings,
|
|
# Removed: RedisCacheSettings
|
|
# Removed: RedisQueueSettings
|
|
# Removed: RedisRateLimiterSettings
|
|
EnvironmentSettings,
|
|
):
|
|
pass
|
|
```
|
|
|
|
### Service Feature Flags
|
|
|
|
Use feature flags to conditionally enable services:
|
|
|
|
```python
|
|
class ServiceSettings(BaseSettings):
|
|
ENABLE_REDIS: bool = True
|
|
ENABLE_CELERY: bool = True
|
|
ENABLE_MONITORING: bool = False
|
|
|
|
class ConditionalSettings(
|
|
AppSettings,
|
|
PostgresSettings,
|
|
CryptSettings,
|
|
ServiceSettings,
|
|
):
|
|
# Add Redis settings only if enabled
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
if self.ENABLE_REDIS:
|
|
# Dynamically add Redis settings
|
|
self.__class__ = type(
|
|
"ConditionalSettings",
|
|
(self.__class__, RedisCacheSettings),
|
|
{}
|
|
)
|
|
```
|
|
|
|
## Testing Settings
|
|
|
|
### Test Configuration
|
|
|
|
Create separate settings for testing:
|
|
|
|
```python
|
|
class TestSettings(BaseSettings):
|
|
# Override database for testing
|
|
POSTGRES_DB: str = "test_database"
|
|
|
|
# Disable external services
|
|
ENABLE_REDIS: bool = False
|
|
ENABLE_EMAIL: bool = False
|
|
|
|
# Speed up tests
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 5
|
|
|
|
# Test-specific settings
|
|
TEST_USER_EMAIL: str = "test@example.com"
|
|
TEST_USER_PASSWORD: str = "testpassword123"
|
|
|
|
# Use in tests
|
|
@pytest.fixture
|
|
def test_settings():
|
|
return TestSettings()
|
|
```
|
|
|
|
### Settings Validation Testing
|
|
|
|
Test your custom settings:
|
|
|
|
```python
|
|
def test_custom_settings_validation():
|
|
# Test valid configuration
|
|
settings = CustomSettings(
|
|
CUSTOM_API_KEY="test-key",
|
|
CUSTOM_TIMEOUT=60,
|
|
MAX_UPLOAD_SIZE=5242880 # 5MB
|
|
)
|
|
assert settings.CUSTOM_TIMEOUT == 60
|
|
|
|
# Test validation error
|
|
with pytest.raises(ValueError, match="MAX_UPLOAD_SIZE cannot exceed 100MB"):
|
|
CustomSettings(MAX_UPLOAD_SIZE=209715200) # 200MB
|
|
|
|
def test_settings_computed_fields():
|
|
settings = StorageSettings(
|
|
STORAGE_TYPE="s3",
|
|
AWS_ACCESS_KEY_ID="test-key",
|
|
AWS_SECRET_ACCESS_KEY="test-secret",
|
|
AWS_BUCKET_NAME="test-bucket"
|
|
)
|
|
|
|
assert settings.STORAGE_ENABLED is True
|
|
assert settings.STORAGE_CONFIG["bucket"] == "test-bucket"
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Organization
|
|
- Group related settings in dedicated classes
|
|
- Use descriptive names for settings groups
|
|
- Keep validation logic close to the settings
|
|
- Document complex validation rules
|
|
|
|
### Security
|
|
- Validate sensitive settings like secret keys
|
|
- Never set default values for secrets in production
|
|
- Use computed fields to derive connection strings
|
|
- Separate test and production configurations
|
|
|
|
### Performance
|
|
- Use `@computed_field` for expensive calculations
|
|
- Cache settings instances appropriately
|
|
- Avoid complex validation in hot paths
|
|
- Use model validators for cross-field validation
|
|
|
|
### Testing
|
|
- Create separate test settings classes
|
|
- Test all validation rules
|
|
- Mock external service settings in tests
|
|
- Use dependency injection for settings in tests
|
|
|
|
The settings system provides type safety, validation, and organization for your application configuration. Start with the built-in settings and extend them as your application grows! |