Simplified environment variable parsing with type casting, validation, and framework integration
—
Docker-style secret file support for reading sensitive configuration from mounted files instead of environment variables, enabling secure handling of passwords, API keys, and certificates in containerized environments.
Extended environment reader that supports reading values from files when corresponding _FILE environment variables are set.
class FileAwareEnv(Env):
def __init__(self, *, file_suffix="_FILE", eager=True, expand_vars=False, prefix=None):
"""
Initialize FileAwareEnv with file reading capabilities.
Parameters:
- file_suffix: str, suffix appended to variable names for file paths (default: "_FILE")
- eager: bool, whether to raise validation errors immediately (default: True)
- expand_vars: bool, whether to expand ${VAR} syntax (default: False)
- prefix: str, global prefix for all variable names (default: None)
Behavior:
When parsing variable "SECRET", first checks for "SECRET_FILE" environment variable.
If "SECRET_FILE" exists and points to readable file, returns file contents.
Otherwise falls back to standard "SECRET" environment variable.
"""Usage examples:
import os
from environs import FileAwareEnv
from pathlib import Path
# Create temporary secret files for demonstration
Path("/tmp/db_password").write_text("super_secret_password")
Path("/tmp/api_key").write_text("sk_live_123456789abcdef")
# Set up file-based environment variables
os.environ["DATABASE_PASSWORD_FILE"] = "/tmp/db_password"
os.environ["API_KEY_FILE"] = "/tmp/api_key"
os.environ["REGULAR_CONFIG"] = "not_from_file"
# Initialize FileAwareEnv
file_env = FileAwareEnv()
# Read from files
db_password = file_env.str("DATABASE_PASSWORD") # Reads from /tmp/db_password
api_key = file_env.str("API_KEY") # Reads from /tmp/api_key
regular = file_env.str("REGULAR_CONFIG") # Reads from environment variable
print(f"Database password: {db_password}") # => "super_secret_password"
print(f"API key: {api_key}") # => "sk_live_123456789abcdef"
print(f"Regular config: {regular}") # => "not_from_file"
# Clean up
Path("/tmp/db_password").unlink()
Path("/tmp/api_key").unlink()Customize the suffix used for file environment variables to match your naming conventions.
import os
from environs import FileAwareEnv
from pathlib import Path
# Custom suffix example
Path("/tmp/secret").write_text("my_secret_value")
os.environ["API_SECRET_PATH"] = "/tmp/secret"
# Initialize with custom suffix
custom_env = FileAwareEnv(file_suffix="_PATH")
# Read using custom suffix
secret = custom_env.str("API_SECRET") # Looks for API_SECRET_PATH
print(f"Secret: {secret}") # => "my_secret_value"
# Clean up
Path("/tmp/secret").unlink()Integrate with Docker Swarm secrets or Kubernetes mounted secrets for production deployments.
import os
from environs import FileAwareEnv
# Docker Swarm secrets are typically mounted at /run/secrets/
os.environ["DATABASE_PASSWORD_FILE"] = "/run/secrets/db_password"
os.environ["SSL_CERT_FILE"] = "/run/secrets/ssl_certificate"
os.environ["SSL_KEY_FILE"] = "/run/secrets/ssl_private_key"
# Kubernetes secrets mounted as files
os.environ["JWT_SECRET_FILE"] = "/etc/secrets/jwt_secret"
os.environ["OAUTH_CLIENT_SECRET_FILE"] = "/etc/secrets/oauth_client_secret"
file_env = FileAwareEnv()
# Read secrets from mounted files
try:
db_password = file_env.str("DATABASE_PASSWORD")
ssl_cert = file_env.str("SSL_CERT")
ssl_key = file_env.str("SSL_KEY")
jwt_secret = file_env.str("JWT_SECRET")
oauth_secret = file_env.str("OAUTH_CLIENT_SECRET")
print("All secrets loaded successfully from files")
except Exception as e:
print(f"Error loading secrets: {e}")FileAwareEnv gracefully falls back to regular environment variables when file variables are not set.
import os
from environs import FileAwareEnv
from pathlib import Path
file_env = FileAwareEnv()
# Set up mixed configuration
Path("/tmp/file_secret").write_text("from_file")
os.environ["SECRET_FROM_FILE_FILE"] = "/tmp/file_secret"
os.environ["SECRET_FROM_ENV"] = "from_environment"
# FileAwareEnv will read from file when _FILE variable exists
secret1 = file_env.str("SECRET_FROM_FILE") # => "from_file"
# Falls back to environment variable when no _FILE variable
secret2 = file_env.str("SECRET_FROM_ENV") # => "from_environment"
# Default values work as expected
secret3 = file_env.str("MISSING_SECRET", "default") # => "default"
print(f"From file: {secret1}")
print(f"From env: {secret2}")
print(f"Default: {secret3}")
# Clean up
Path("/tmp/file_secret").unlink()All standard environs type casting works with file-based values.
import os
from environs import FileAwareEnv
from pathlib import Path
# Create files with different data types
Path("/tmp/port_number").write_text("8080")
Path("/tmp/debug_flag").write_text("true")
Path("/tmp/allowed_hosts").write_text("localhost,127.0.0.1,example.com")
Path("/tmp/config_json").write_text('{"timeout": 30, "retries": 3}')
# Set up file environment variables
os.environ["PORT_FILE"] = "/tmp/port_number"
os.environ["DEBUG_FILE"] = "/tmp/debug_flag"
os.environ["ALLOWED_HOSTS_FILE"] = "/tmp/allowed_hosts"
os.environ["CONFIG_FILE"] = "/tmp/config_json"
file_env = FileAwareEnv()
# Type casting works with file contents
port = file_env.int("PORT") # => 8080
debug = file_env.bool("DEBUG") # => True
hosts = file_env.list("ALLOWED_HOSTS") # => ["localhost", "127.0.0.1", "example.com"]
config = file_env.json("CONFIG") # => {"timeout": 30, "retries": 3}
print(f"Port: {port} (type: {type(port)})")
print(f"Debug: {debug} (type: {type(debug)})")
print(f"Hosts: {hosts}")
print(f"Config: {config}")
# Clean up
for file_path in ["/tmp/port_number", "/tmp/debug_flag", "/tmp/allowed_hosts", "/tmp/config_json"]:
Path(file_path).unlink()Handle file-related errors appropriately with detailed error messages.
import os
from environs import FileAwareEnv, EnvValidationError
file_env = FileAwareEnv()
# File doesn't exist
os.environ["MISSING_FILE_FILE"] = "/path/that/does/not/exist"
try:
value = file_env.str("MISSING_FILE")
except ValueError as e:
print(f"File not found: {e}")
# Permission denied
os.environ["RESTRICTED_FILE_FILE"] = "/root/restricted_file"
try:
value = file_env.str("RESTRICTED_FILE")
except ValueError as e:
print(f"Permission error: {e}")
# Directory instead of file
os.environ["DIRECTORY_FILE"] = "/tmp"
try:
value = file_env.str("DIRECTORY")
except ValueError as e:
print(f"Directory error: {e}")
# Invalid file content for type casting
from pathlib import Path
Path("/tmp/invalid_int").write_text("not_a_number")
os.environ["INVALID_INT_FILE"] = "/tmp/invalid_int"
try:
value = file_env.int("INVALID_INT")
except EnvValidationError as e:
print(f"Type casting error: {e}")
# Clean up
Path("/tmp/invalid_int").unlink()Complete example showing how to use FileAwareEnv in a production Django application with Docker secrets.
# settings.py for production Django app
import os
from environs import FileAwareEnv
# Initialize FileAwareEnv for production secrets
env = FileAwareEnv()
# Load .env file if it exists (for local development)
env.read_env()
# Basic settings
DEBUG = env.bool("DEBUG", False)
SECRET_KEY = env.str("SECRET_KEY") # Will read from SECRET_KEY_FILE if available
# Database configuration (password from file in production)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env.str("DB_NAME"),
'USER': env.str("DB_USER"),
'PASSWORD': env.str("DB_PASSWORD"), # From DB_PASSWORD_FILE in production
'HOST': env.str("DB_HOST", "localhost"),
'PORT': env.int("DB_PORT", 5432),
}
}
# Redis configuration (password from file)
REDIS_PASSWORD = env.str("REDIS_PASSWORD", "") # From REDIS_PASSWORD_FILE
# Email configuration (SMTP password from file)
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", "")
# SSL certificate paths (certificate content from files)
SSL_CERTIFICATE = env.str("SSL_CERTIFICATE", "") # From SSL_CERTIFICATE_FILE
SSL_PRIVATE_KEY = env.str("SSL_PRIVATE_KEY", "") # From SSL_PRIVATE_KEY_FILE
# API keys (from files in production)
STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY", "")
AWS_SECRET_ACCESS_KEY = env.str("AWS_SECRET_ACCESS_KEY", "")Corresponding Docker Compose configuration:
# docker-compose.yml
version: '3.8'
services:
web:
image: myapp:latest
environment:
- DEBUG=false
- SECRET_KEY_FILE=/run/secrets/django_secret_key
- DB_PASSWORD_FILE=/run/secrets/db_password
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
- STRIPE_SECRET_KEY_FILE=/run/secrets/stripe_secret_key
secrets:
- django_secret_key
- db_password
- redis_password
- stripe_secret_key
secrets:
django_secret_key:
external: true
db_password:
external: true
redis_password:
external: true
stripe_secret_key:
external: trueUse file-based secrets in local development for consistency with production.
# Local development setup
import os
from environs import FileAwareEnv
from pathlib import Path
# Create local secrets directory
secrets_dir = Path("./secrets")
secrets_dir.mkdir(exist_ok=True)
# Write development secrets to files
(secrets_dir / "db_password").write_text("dev_password_123")
(secrets_dir / "api_key").write_text("dev_api_key_456")
(secrets_dir / "jwt_secret").write_text("dev_jwt_secret_789")
# Set environment to use local secret files
os.environ["DATABASE_PASSWORD_FILE"] = str(secrets_dir / "db_password")
os.environ["API_KEY_FILE"] = str(secrets_dir / "api_key")
os.environ["JWT_SECRET_FILE"] = str(secrets_dir / "jwt_secret")
# Use FileAwareEnv as in production
env = FileAwareEnv()
db_password = env.str("DATABASE_PASSWORD")
api_key = env.str("API_KEY")
jwt_secret = env.str("JWT_SECRET")
print("Local development secrets loaded from files")from pathlib import Path
from typing import Union
FilePath = Union[str, Path]Install with Tessl CLI
npx tessl i tessl/pypi-environs