Configuration Loaders
Rick provides flexible configuration loading utilities for managing application settings from multiple sources. The configuration module supports environment variables, JSON files, TOML files, and hybrid configurations with validation and type conversion.
Location: rick.resource.config
Overview
Configuration management in Rick provides:
- Environment Variable Loading - Load settings from environment variables with type conversion
- File-Based Configuration - Load from JSON or TOML files
- Default Values - Define fallback values for missing configuration
- Validation - Custom validation functions to ensure configuration correctness
- Type Conversion - Automatic type conversion based on default values
- Hybrid Loading - Auto-detect file format or combine multiple sources
- StrOrFile Support - Load values from files when needed
Available Configuration Loaders
EnvironmentConfig
Load configuration from environment variables with automatic type conversion.
Location: rick.resource.config.EnvironmentConfig
JsonFileConfig
Load configuration from JSON files with validation support.
Location: rick.resource.config.JsonFileConfig
TomlFileConfig
Load configuration from TOML files with validation support.
Location: rick.resource.config.TomlFileConfig
HybridFileConfig
Auto-detect file format (JSON or TOML) based on file extension.
Location: rick.resource.config.HybridFileConfig
EnvironmentConfig
Overview
EnvironmentConfig loads configuration from environment variables with automatic type conversion. Class attributes
defined in uppercase are mapped to environment variables and converted to lowercase keys in the resulting configuration.
Features
- Automatic type conversion (str, int, bool, list, dict)
- Environment variable override of default values
- Optional validation functions
- Prefix support for namespacing
- StrOrFile wrapper for loading values from files
Basic Usage
from rick.resource.config import EnvironmentConfig
class DatabaseConfig(EnvironmentConfig):
# Define configuration with default values
DB_HOST = 'localhost'
DB_PORT = 5432
DB_NAME = 'myapp'
DB_USERNAME = 'postgres'
DB_PASSWORD = 'password'
DB_SSL = False
# Build configuration (environment variables override defaults)
config = DatabaseConfig().build()
# Access configuration (keys are lowercase)
print(config.db_host) # 'localhost' or value from DB_HOST env var
print(config.db_port) # 5432 or value from DB_PORT env var
print(config.db_ssl) # False or value from DB_SSL env var
Type Conversion
Environment variables are automatically converted to the type of the default value:
from rick.resource.config import EnvironmentConfig
class AppConfig(EnvironmentConfig):
# Type hints via default values
API_KEY = None # str - None defaults to string
DEBUG_MODE = False # bool - converted from env var
MAX_WORKERS = 4 # int - parsed from env string
ALLOWED_HOSTS = [] # list - split by comma separator
FEATURE_FLAGS = {} # dict - parsed as JSON
# Set environment variables:
# export DEBUG_MODE=true
# export MAX_WORKERS=8
# export ALLOWED_HOSTS=localhost,127.0.0.1,example.com
# export FEATURE_FLAGS='{"new_ui": true, "beta_features": false}'
config = AppConfig().build()
print(config.debug_mode) # True (bool)
print(config.max_workers) # 8 (int)
print(config.allowed_hosts) # ['localhost', '127.0.0.1', 'example.com']
print(config.feature_flags) # {'new_ui': True, 'beta_features': False}
Supported Types
| Default Type | Environment Value | Result Type | Example |
|---|---|---|---|
None |
Any string | str |
"value" |
"" |
Any string | str |
"hello" |
0 |
Numeric string | int |
42 |
False |
Bool string | bool |
True |
[] |
Comma-separated | list |
['a', 'b'] |
{} |
JSON string | dict |
{'key': 'val'} |
Custom List Separator
from rick.resource.config import EnvironmentConfig
class CustomConfig(EnvironmentConfig):
# Change list separator from comma to semicolon
list_separator = ";"
SERVERS = []
# export SERVERS=server1;server2;server3
config = CustomConfig().build()
print(config.servers) # ['server1', 'server2', 'server3']
Validation
Add custom validation by defining methods that start with validate_:
from rick.resource.config import EnvironmentConfig
class ValidatedConfig(EnvironmentConfig):
DB_HOST = 'localhost'
DB_PORT = 5432
API_KEY = None
MAX_CONNECTIONS = 10
def validate_database(self, data: dict):
"""Validate database configuration"""
if not data.get('db_host'):
raise ValueError("Database host is required")
port = data.get('db_port', 0)
if not (1 <= port <= 65535):
raise ValueError("Database port must be between 1 and 65535")
def validate_api_key(self, data: dict):
"""Validate API key format"""
api_key = data.get('api_key')
if not api_key:
raise ValueError("API key is required")
if len(api_key) < 32:
raise ValueError("API key must be at least 32 characters")
def validate_connections(self, data: dict):
"""Validate connection pool settings"""
max_conn = data.get('max_connections', 0)
if max_conn <= 0:
raise ValueError("Max connections must be positive")
# Will raise ValueError if validation fails
config = ValidatedConfig().build()
Prefix Support
Use prefixes to namespace environment variables:
from rick.resource.config import EnvironmentConfig
class PrefixedConfig(EnvironmentConfig):
DB_HOST = 'localhost'
DB_PORT = 5432
API_KEY = None
# Set environment variables with prefix:
# export MYAPP_DB_HOST=production-server
# export MYAPP_DB_PORT=3306
# export MYAPP_API_KEY=secret123
# Build with prefix
config = PrefixedConfig().build(prefix="MYAPP_")
print(config.db_host) # 'production-server'
print(config.db_port) # 3306
StrOrFile Wrapper
Load values from files when environment variable points to a file path:
from rick.resource.config import EnvironmentConfig, StrOrFile
class SecureConfig(EnvironmentConfig):
API_KEY = StrOrFile(None)
DB_PASSWORD = StrOrFile(None)
# Set environment variables:
# export API_KEY=/secrets/api-key.txt
# export DB_PASSWORD=plaintext_password
# Build configuration
config = SecureConfig().build()
# If API_KEY starts with '/' or './', content is read from file
# Otherwise, the value is used as-is
print(config.api_key) # Content of /secrets/api-key.txt
print(config.db_password) # "plaintext_password"
StrOrFile Rules:
- If value starts with
/or./, it's treated as a file path - File content is read and whitespace is stripped
- If file doesn't exist,
ValueErroris raised (unlesssilent=True) - Use
StrOrFile(value, silent=True)to return value as-is if file missing
Complete Example
import os
from rick.resource.config import EnvironmentConfig, StrOrFile
class ProductionConfig(EnvironmentConfig):
# Database settings
DB_HOST = 'localhost'
DB_PORT = 5432
DB_NAME = 'production'
DB_USER = 'postgres'
DB_PASSWORD = StrOrFile(None)
# Application settings
DEBUG = False
SECRET_KEY = StrOrFile(None)
ALLOWED_HOSTS = []
# Feature flags
ENABLE_CACHING = True
ENABLE_MONITORING = True
MAX_UPLOAD_SIZE = 10485760 # 10MB in bytes
# API configuration
API_RATE_LIMIT = 100
API_TIMEOUT = 30
def validate_database(self, data: dict):
"""Ensure database is properly configured"""
required = ['db_host', 'db_name', 'db_user', 'db_password']
for field in required:
if not data.get(field):
raise ValueError(f"Database configuration missing: {field}")
def validate_security(self, data: dict):
"""Ensure security settings are production-ready"""
if data.get('debug'):
raise ValueError("DEBUG must be False in production")
if not data.get('secret_key'):
raise ValueError("SECRET_KEY is required")
if len(data.get('secret_key', '')) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters")
def validate_rate_limit(self, data: dict):
"""Validate rate limiting configuration"""
rate_limit = data.get('api_rate_limit', 0)
if rate_limit <= 0:
raise ValueError("API rate limit must be positive")
# Set environment variables
os.environ['DB_PASSWORD'] = '/secrets/db-password.txt'
os.environ['SECRET_KEY'] = '/secrets/secret-key.txt'
os.environ['ALLOWED_HOSTS'] = 'api.example.com,www.example.com'
# Build and validate configuration
config = ProductionConfig().build()
# Use configuration
print(f"Connecting to {config.db_host}:{config.db_port}/{config.db_name}")
JsonFileConfig
Overview
JsonFileConfig loads configuration from JSON files with support for default values, validation, and runtime
overrides.
Basic Usage
from rick.resource.config import JsonFileConfig
class DatabaseConfig(JsonFileConfig):
# Default values
db_host = "localhost"
db_port = 5432
db_name = "myapp"
api_key = None
debug = False
# Load from file (values in file override defaults)
config = DatabaseConfig("config.json").build()
print(config.db_host) # Value from config.json or default
print(config.db_port) # Value from config.json or default
Example JSON File
config.json:
{
"db_host": "production-server.example.com",
"db_port": 3306,
"db_name": "production_db",
"api_key": "prod_api_key_1234567890abcdef",
"debug": false,
"features": {
"enable_caching": true,
"max_connections": 100
}
}
With Validation
from rick.resource.config import JsonFileConfig
class ValidatedConfig(JsonFileConfig):
db_host = "localhost"
db_port = 5432
api_key = None
def validate_database(self, data: dict):
"""Validate database configuration"""
if not data.get('db_host'):
raise ValueError("Database host is required")
port = data.get('db_port', 0)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
def validate_api_key(self, data: dict):
"""Validate API key"""
api_key = data.get('api_key')
if not api_key:
raise ValueError("API key is required")
if len(api_key) < 16:
raise ValueError("API key must be at least 16 characters")
# Raises ValueError if validation fails
config = ValidatedConfig("config.json").build()
Runtime Overrides
from rick.resource.config import JsonFileConfig
class AppConfig(JsonFileConfig):
debug = False
port = 8000
host = "0.0.0.0"
# Load from file with runtime overrides
config = AppConfig("config.json").build(override_data={
'debug': True,
'port': 9000
})
print(config.debug) # True (overridden)
print(config.port) # 9000 (overridden)
print(config.host) # Value from file or default
Reload Configuration
from rick.resource.config import JsonFileConfig
class ReloadableConfig(JsonFileConfig):
setting1 = "default"
setting2 = 42
config_loader = ReloadableConfig("config.json")
config = config_loader.build()
# Modify config.json...
# Reload from disk
config = config_loader.reload()
TomlFileConfig
Overview
TomlFileConfig loads configuration from TOML files. Requires Python 3.11+ (built-in tomllib) or the tomli package
for older versions.
Installation
For Python < 3.11:
Basic Usage
from rick.resource.config import TomlFileConfig
class AppConfig(TomlFileConfig):
# Default values
app_name = "MyApp"
version = "1.0.0"
debug = False
# Nested structures are supported
database = {}
logging = {}
# Load from TOML file
config = AppConfig("config.toml").build()
print(config.app_name) # Value from config.toml or default
print(config.database) # Nested dict from TOML
Example TOML File
config.toml:
app_name = "Production App"
version = "2.1.0"
debug = false
[database]
host = "db.example.com"
port = 5432
name = "production_db"
pool_size = 20
[logging]
level = "INFO"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers = ["console", "file"]
[api]
base_url = "https://api.example.com"
timeout = 30
retry_attempts = 3
[[services]]
name = "auth"
url = "https://auth.example.com"
enabled = true
[[services]]
name = "billing"
url = "https://billing.example.com"
enabled = false
With Validation
from rick.resource.config import TomlFileConfig
class ValidatedTomlConfig(TomlFileConfig):
app_name = "MyApp"
database = {}
logging = {}
def validate_database(self, data: dict):
"""Validate database configuration"""
db = data.get('database', {})
if not db.get('host'):
raise ValueError("Database host is required")
port = db.get('port', 0)
if not (1 <= port <= 65535):
raise ValueError("Database port must be between 1 and 65535")
def validate_logging(self, data: dict):
"""Validate logging configuration"""
logging = data.get('logging', {})
level = logging.get('level', 'INFO')
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if level not in valid_levels:
raise ValueError(f"Invalid log level: {level}")
config = ValidatedTomlConfig("config.toml").build()
HybridFileConfig
Overview
HybridFileConfig automatically detects the file format (JSON or TOML) based on the file extension and loads
accordingly.
Supported Extensions
.json- Loaded as JSON.toml- Loaded as TOML.tml- Loaded as TOML
Basic Usage
from rick.resource.config import HybridFileConfig
class FlexibleConfig(HybridFileConfig):
debug = False
port = 8000
host = "localhost"
def validate_port(self, data: dict):
port = data.get('port', 0)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
# Works with either format
config1 = FlexibleConfig("config.json").build()
config2 = FlexibleConfig("config.toml").build()
config3 = FlexibleConfig("settings.tml").build()
Use Case
Useful for applications that need to support multiple configuration formats:
import sys
from rick.resource.config import HybridFileConfig
class AppConfig(HybridFileConfig):
# Defaults
app_name = "MyApp"
debug = False
port = 8000
# Load configuration from command line argument
config_file = sys.argv[1] if len(sys.argv) > 1 else "config.json"
try:
config = AppConfig(config_file).build()
print(f"Loaded configuration from {config_file}")
except Exception as e:
print(f"Error loading config: {e}")
sys.exit(1)
Convenience Functions
Rick provides simple functions for quick configuration loading without defining classes:
json_config_file()
Load JSON configuration file directly:
from rick.resource.config import json_config_file
# Simple loading
config = json_config_file("config.json")
print(config.db_host)
print(config.api_key)
toml_config_file()
Load TOML configuration file directly:
from rick.resource.config import toml_config_file
# Simple loading
config = toml_config_file("config.toml")
print(config.app_name)
print(config.database)
config_file()
Auto-detect and load configuration file:
from rick.resource.config import config_file
# Auto-detect format based on extension
config = config_file("config.json") # or config.toml
print(config.debug)
print(config.port)
json_file() (Legacy)
Simple JSON file loader (legacy function):
Error Handling
FileConfigError
All file configuration classes raise FileConfigError for configuration-related errors:
from rick.resource.config import JsonFileConfig, FileConfigError
class MyConfig(JsonFileConfig):
debug = False
try:
config = MyConfig("nonexistent.json").build()
except FileConfigError as e:
print(f"Configuration error: {e}")
# Handle error: use defaults, exit, etc.
Validation Errors
Validation functions should raise ValueError for validation failures:
from rick.resource.config import EnvironmentConfig
class StrictConfig(EnvironmentConfig):
API_KEY = None
def validate_api_key(self, data: dict):
if not data.get('api_key'):
raise ValueError("API_KEY is required")
try:
config = StrictConfig().build()
except ValueError as e:
print(f"Validation failed: {e}")
Best Practices
1. Use Validation for Critical Settings
from rick.resource.config import JsonFileConfig
class ProductionConfig(JsonFileConfig):
secret_key = None
database_url = None
def validate_production(self, data: dict):
"""Ensure production-critical settings are present"""
if not data.get('secret_key'):
raise ValueError("SECRET_KEY is required in production")
if not data.get('database_url'):
raise ValueError("DATABASE_URL is required in production")
config = ProductionConfig("production.json").build()
2. Provide Sensible Defaults
from rick.resource.config import EnvironmentConfig
class AppConfig(EnvironmentConfig):
# Good: sensible defaults for development
DEBUG = True
LOG_LEVEL = 'DEBUG'
MAX_WORKERS = 4
CACHE_TTL = 300
# Override in production via environment variables
3. Use Type Hints via Default Values
from rick.resource.config import EnvironmentConfig
class TypedConfig(EnvironmentConfig):
# Type is inferred from default value
PORT = 8000 # int
DEBUG = False # bool
ALLOWED_HOSTS = [] # list
DATABASE_CONFIG = {} # dict
API_KEY = None # str (None defaults to str)
4. Separate Concerns with Multiple Configs
from rick.resource.config import JsonFileConfig
class DatabaseConfig(JsonFileConfig):
host = "localhost"
port = 5432
def validate_database(self, data):
# Database-specific validation
pass
class CacheConfig(JsonFileConfig):
redis_host = "localhost"
redis_port = 6379
def validate_cache(self, data):
# Cache-specific validation
pass
# Load separate configs
db_config = DatabaseConfig("database.json").build()
cache_config = CacheConfig("cache.json").build()
5. Use StrOrFile for Secrets
from rick.resource.config import EnvironmentConfig, StrOrFile
class SecureConfig(EnvironmentConfig):
# Load from files in production, plain values in development
DB_PASSWORD = StrOrFile(None)
API_SECRET = StrOrFile(None)
JWT_KEY = StrOrFile(None)
# Development: export DB_PASSWORD=dev_password
# Production: export DB_PASSWORD=/secrets/db-password
config = SecureConfig().build()
6. Environment-Specific Configuration
import os
from rick.resource.config import JsonFileConfig
class AppConfig(JsonFileConfig):
debug = False
port = 8000
# Load config based on environment
env = os.getenv('APP_ENV', 'development')
config_files = {
'development': 'config.dev.json',
'staging': 'config.staging.json',
'production': 'config.prod.json'
}
config_file = config_files.get(env, 'config.json')
config = AppConfig(config_file).build()
7. Combine Environment and File Configuration
from rick.resource.config import EnvironmentConfig, JsonFileConfig
# Load base config from file
class BaseConfig(JsonFileConfig):
app_name = "MyApp"
port = 8000
file_config = BaseConfig("config.json").build()
# Override with environment variables
class EnvConfig(EnvironmentConfig):
APP_NAME = file_config.app_name
PORT = file_config.port
DEBUG = False # Override in env
final_config = EnvConfig().build()
Complete Example
import os
import sys
from rick.resource.config import HybridFileConfig, EnvironmentConfig, StrOrFile
class ApplicationConfig(HybridFileConfig):
"""File-based configuration with defaults"""
# Application settings
app_name = "MyApplication"
version = "1.0.0"
debug = False
# Server settings
host = "0.0.0.0"
port = 8000
workers = 4
# Database settings
database = {
"host": "localhost",
"port": 5432,
"name": "myapp",
"pool_size": 10
}
# Cache settings
cache = {
"enabled": True,
"backend": "redis",
"ttl": 300
}
# Validation
def validate_server(self, data: dict):
port = data.get('port', 0)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
workers = data.get('workers', 0)
if workers <= 0:
raise ValueError("Workers must be positive")
def validate_database(self, data: dict):
db = data.get('database', {})
if not db.get('host'):
raise ValueError("Database host is required")
class RuntimeConfig(EnvironmentConfig):
"""Environment-based runtime overrides"""
# Sensitive values loaded from environment
SECRET_KEY = StrOrFile(None)
DATABASE_PASSWORD = StrOrFile(None)
API_KEY = StrOrFile(None)
# Runtime overrides
DEBUG = False
PORT = 8000
WORKERS = 4
def validate_secrets(self, data: dict):
if not data.get('secret_key'):
raise ValueError("SECRET_KEY is required")
if len(data.get('secret_key', '')) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters")
def load_configuration():
"""Load application configuration"""
# Determine config file
env = os.getenv('APP_ENV', 'development')
config_file = f"config.{env}.json" # or .toml
try:
# Load base configuration from file
file_config = ApplicationConfig(config_file).build()
# Load runtime configuration from environment
runtime_config = RuntimeConfig().build()
# Merge configurations
final_config = file_config.asdict()
final_config.update(runtime_config.asdict())
from rick.base import ShallowContainer
return ShallowContainer(final_config)
except Exception as e:
print(f"Failed to load configuration: {e}", file=sys.stderr)
sys.exit(1)
# Use in application
if __name__ == "__main__":
config = load_configuration()
print(f"Starting {config.app_name} v{config.version}")
print(f"Server: {config.host}:{config.port}")
print(f"Workers: {config.workers}")
print(f"Debug: {config.debug}")
print(f"Database: {config.database['host']}:{config.database['port']}")
Related Topics
- Redis Cache - Use configuration to set up Redis caching
- Serializers - JSON serialization used in config files
- Validators - Validation patterns similar to config validation