CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-environs

Simplified environment variable parsing with type casting, validation, and framework integration

Pending
Overview
Eval results
Files

file-secrets.mddocs/

File-Based Secrets

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.

Capabilities

FileAwareEnv Class

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

Custom File Suffix

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

Docker Secrets Integration

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}")

Fallback Behavior

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

Type Casting with File Secrets

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

Error Handling

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

Production Deployment Example

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

Local Development with Files

Use 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")

Types

from pathlib import Path
from typing import Union

FilePath = Union[str, Path]

Install with Tessl CLI

npx tessl i tessl/pypi-environs

docs

advanced-data.md

configuration.md

core-parsing.md

custom-parsers.md

file-secrets.md

framework-integration.md

index.md

specialized-types.md

validation.md

tile.json