Basic Usage¶
Variable Expansion¶
envresolve supports variable expansion using ${VAR} and $VAR syntax.
Simple Variable Expansion¶
Multiple Variables¶
You can reference multiple variables in a single string:
from envresolve import expand_variables
env = {
"VAULT_NAME": "my-vault",
"SECRET_NAME": "db-password"
}
result = expand_variables("akv://${VAULT_NAME}/${SECRET_NAME}", env)
print(result) # Output: akv://my-vault/db-password
Nested Variable Expansion¶
Variables can reference other variables:
from envresolve import expand_variables
env = {
"ENVIRONMENT": "prod",
"VAULT_NAME": "${ENVIRONMENT}-vault",
"SECRET_URI": "akv://${VAULT_NAME}/api-key"
}
result = expand_variables(env["SECRET_URI"], env)
print(result) # Output: akv://prod-vault/api-key
Using Environment Variables¶
With os.environ¶
Use EnvExpander to expand variables from the current environment:
import os
from envresolve import EnvExpander
# Set environment variable
os.environ["VAULT_NAME"] = "production-vault"
expander = EnvExpander()
result = expander.expand("akv://${VAULT_NAME}/secret")
print(result) # Output: akv://production-vault/secret
Snapshot Behavior
EnvExpander takes a snapshot of os.environ at initialization time. Changes to environment variables after initialization won't be reflected.
With .env Files¶
Use DotEnvExpander to expand variables from a .env file:
from envresolve import DotEnvExpander
# Contents of .env:
# VAULT_NAME=my-company-vault
# DB_PASSWORD=akv://${VAULT_NAME}/db-password
# API_KEY=akv://${VAULT_NAME}/api-key
expander = DotEnvExpander(".env")
db_password_uri = expander.expand("${DB_PASSWORD}")
api_key_uri = expander.expand("${API_KEY}")
print(db_password_uri) # Output: akv://my-company-vault/db-password
print(api_key_uri) # Output: akv://my-company-vault/api-key
Secret Resolution¶
envresolve can resolve secrets referenced with akv:// URIs. Provider
registration is explicit so you only pay the dependency cost when you opt in.
1. Install the Azure extra (once per environment)¶
2. Register the provider¶
register_azure_kv_provider() is idempotent—you can call it during application
startup without worrying about duplicate work.
3. Resolve a secret URI¶
import envresolve
envresolve.register_azure_kv_provider()
# Plain strings are returned unchanged (idempotent behaviour)
print(envresolve.resolve_secret("db-password")) # db-password
# Secret URIs fetch values from Azure Key Vault
password = envresolve.resolve_secret("akv://corp-vault/db-password")
print(password)
Custom Provider Configuration¶
For advanced scenarios like testing or custom authentication, you can inject a custom provider instance:
import envresolve
from envresolve.providers.azure_kv import AzureKVProvider
from azure.identity import ManagedIdentityCredential
# Create custom provider with specific credential
custom_provider = AzureKVProvider(
credential=ManagedIdentityCredential(client_id="your-client-id")
)
# Register the custom provider
envresolve.register_azure_kv_provider(provider=custom_provider)
# Now use envresolve as normal
secret = envresolve.resolve_secret("akv://vault/secret")
This is particularly useful for:
- Testing: Inject mock providers without patching internal implementation details
- Custom authentication: Use specific Azure credentials (service principal, managed identity with client ID, etc.)
- Provider configuration: Pre-configure providers with custom settings before registration
Iterative resolution¶
resolve_secret() keeps resolving until the returned value is stable. This lets
you chain indirections or mix URI results with variable expansion:
import os
import envresolve
envresolve.register_azure_kv_provider()
os.environ["ENVIRONMENT"] = "prod"
# akv://config/service → "akv://vault-${ENVIRONMENT}/service"
secret = envresolve.resolve_secret("akv://config/service")
print(secret) # Resolved value from akv://vault-prod/service
Loading and exporting from .env¶
import os
import envresolve
envresolve.register_azure_kv_provider()
# .env may contain plain values, variable references, and akv:// URIs
# By default, searches for .env in current directory and exports to os.environ
resolved = envresolve.load_env()
print(resolved["DB_PASSWORD"])
print(os.environ["DB_PASSWORD"]) # Exported unless override=False and already set
Use export=False when you only need the resolved dictionary, or set
override=True if you want to intentionally replace existing os.environ
values.
Complete python-dotenv compatibility
For exact python-dotenv search behavior (searching from calling script location instead of current working directory), use this pattern:
from dotenv import load_dotenv
import envresolve
# Use python-dotenv's search behavior
load_dotenv()
# Resolve secrets in os.environ
envresolve.register_azure_kv_provider()
envresolve.resolve_os_environ()
This preserves python-dotenv's exact search semantics while adding secret resolution capabilities.
Resolving Existing Environment Variables¶
Use resolve_os_environ() to resolve secret URIs that are already set in os.environ. This is useful when environment variables are passed from parent shells or container orchestrators:
import os
import envresolve
envresolve.register_azure_kv_provider()
# Environment variables set by parent process or container
os.environ["API_KEY"] = "akv://prod-vault/api-key"
os.environ["DB_PASSWORD"] = "akv://prod-vault/db-password"
# Resolve all environment variables containing secret URIs
resolved = envresolve.resolve_os_environ()
print(resolved["API_KEY"]) # Resolved secret value
print(os.environ["API_KEY"]) # os.environ is updated by default
Filtering by Specific Keys¶
Resolve only specific environment variables:
import os
import envresolve
envresolve.register_azure_kv_provider()
os.environ["API_KEY"] = "akv://prod-vault/api-key"
os.environ["DB_PASSWORD"] = "akv://prod-vault/db-password"
os.environ["PLAIN_CONFIG"] = "some-value"
# Resolve only API_KEY and DB_PASSWORD
resolved = envresolve.resolve_os_environ(keys=["API_KEY", "DB_PASSWORD"])
# PLAIN_CONFIG is not processed
assert "PLAIN_CONFIG" not in resolved
Filtering by Prefix¶
Resolve variables with a specific prefix and strip the prefix from output:
import os
import envresolve
envresolve.register_azure_kv_provider()
# Different environments using prefixes
os.environ["DEV_API_KEY"] = "akv://dev-vault/api-key"
os.environ["DEV_DB_URL"] = "akv://dev-vault/db-url"
os.environ["PROD_API_KEY"] = "akv://prod-vault/api-key"
os.environ["PROD_DB_URL"] = "akv://prod-vault/db-url"
# Resolve only DEV_ variables and strip the prefix
resolved = envresolve.resolve_os_environ(prefix="DEV_")
# Results have prefix stripped
print(resolved["API_KEY"]) # Resolved from DEV_API_KEY
print(resolved["DB_URL"]) # Resolved from DEV_DB_URL
# os.environ is updated with stripped keys
print(os.environ["API_KEY"]) # Resolved value
assert "DEV_API_KEY" not in os.environ # Old key removed
Prefix Stripping Behavior
When using prefix, the resolved values are stored in os.environ with the prefix stripped, and the original prefixed keys are removed.
Without Updating os.environ¶
Use overwrite=False to get resolved values without modifying os.environ:
import os
import envresolve
envresolve.register_azure_kv_provider()
os.environ["API_KEY"] = "akv://prod-vault/api-key"
# Get resolved values without updating os.environ
resolved = envresolve.resolve_os_environ(overwrite=False)
print(resolved["API_KEY"]) # Resolved secret value
print(os.environ["API_KEY"]) # Still the original URI
assert os.environ["API_KEY"] == "akv://prod-vault/api-key"
Continuing on Errors¶
Use stop_on_expansion_error=False and stop_on_resolution_error=False to control error handling granularly:
import os
import envresolve
envresolve.register_azure_kv_provider()
os.environ["GOOD_KEY"] = "akv://prod-vault/valid-secret"
os.environ["BAD_KEY"] = "akv://prod-vault/missing-secret" # Doesn't exist
os.environ["MISSING_VAR"] = "${UNDEFINED}"
os.environ["PLAIN"] = "plain-value"
# Skip variables with missing references, but still raise on secret resolution errors
resolved = envresolve.resolve_os_environ(stop_on_expansion_error=False)
# Skip variables with secret resolution errors, but still raise on undefined variables
resolved = envresolve.resolve_os_environ(stop_on_resolution_error=False)
# Skip both types of errors
resolved = envresolve.resolve_os_environ(
stop_on_expansion_error=False,
stop_on_resolution_error=False
)
# Successfully resolved variables are in the result
print(resolved["GOOD_KEY"]) # Resolved value
print(resolved["PLAIN"]) # plain-value
assert "BAD_KEY" not in resolved # Skipped due to secret resolution error
assert "MISSING_VAR" not in resolved # Skipped due to expansion error
CircularReferenceError is always raised
CircularReferenceError is always raised regardless of these flags, as it indicates a configuration error that cannot be resolved.
Ignoring Specific Variables¶
Use ignore_keys to skip variable expansion and secret resolution for specific keys. This is useful when certain variables should be preserved as-is:
import os
import envresolve
os.environ["PS1"] = "${USER}@${HOST}$ " # Shell prompt template
os.environ["API_KEY"] = "akv://vault/api-key"
# Skip expansion for PS1
resolved = envresolve.resolve_os_environ(ignore_keys=["PS1"])
print(resolved["PS1"]) # Output: ${USER}@${HOST}$ (unchanged)
print(resolved["API_KEY"]) # Resolved secret value
Ignored variables are included in the result as-is without any processing.
When to use ignore_keys:
- Skip specific variables while maintaining strict error checking for others
- Preserve variables containing literal
$characters that shouldn't be expanded - Temporarily exclude problematic variables during debugging
Difference from stop_on_expansion_error=False:
ignore_keys: Selectively excludes specific variables by namestop_on_expansion_error=False: Suppresses all expansion errors globally
Use ignore_keys when you know exactly which variables to skip, and stop_on_expansion_error=False when you want lenient error handling across all variables.
Ignoring Variables by Pattern¶
Use ignore_patterns to skip variables using glob-style pattern matching. This is useful when you want to exclude groups of related variables:
import os
import envresolve
# Shell prompt variables
os.environ["PS1"] = "${USER}@${HOST}$ "
os.environ["PS2"] = "> "
os.environ["PS4"] = "+ "
os.environ["PROMPT"] = "${PWD}$ "
os.environ["API_KEY"] = "akv://vault/api-key"
# Ignore all prompt-related variables using patterns
resolved = envresolve.resolve_os_environ(ignore_patterns=["PS*", "PROMPT*"])
print(resolved["PS1"]) # Output: ${USER}@${HOST}$ (unchanged)
print(resolved["PS2"]) # Output: > (unchanged)
print(resolved["PROMPT"]) # Output: ${PWD}$ (unchanged)
print(resolved["API_KEY"]) # Resolved secret value
Supported wildcards:
*- matches any characters (e.g.,PS*matchesPS1,PS2,PROMPT)?- matches single character (e.g.,PS?matchesPS1, but notPS10)[seq]- matches any character in seq (e.g.,PS[12]matchesPS1,PS2)
Common use cases:
# System shell variables
ignore_patterns=["PS*", "PROMPT*", "BASH_*"]
# Temporary variables
ignore_patterns=["TEMP_*", "TMP_*"]
# Debug flags
ignore_patterns=["DEBUG_*", "TRACE_*"]
Best practices:
Avoid overly broad patterns
Be specific with your patterns to avoid accidentally excluding variables you need:
Combining exact match and patterns
Use both ignore_keys and ignore_patterns for maximum flexibility:
Execution order:
- Check
ignore_keys(exact match - fast path) - If not matched, check
ignore_patterns(pattern match) - If neither matched, perform resolution
When to use ignore_patterns vs ignore_keys:
ignore_patterns: Exclude groups of related variables (e.g., allPS*shell prompts)ignore_keys: Exclude specific individual variables by exact name
Error Handling¶
When working with external services, it's important to handle potential errors like missing dependencies, incorrect configuration, or network issues.
Provider and Resolution Errors¶
Here is a robust example of how to handle errors during provider registration and secret resolution:
import envresolve
from envresolve.exceptions import ProviderRegistrationError, SecretResolutionError
try:
# This might fail if 'envresolve[azure]' is not installed
envresolve.register_azure_kv_provider()
# This might fail due to permissions, network issues, or if the secret doesn't exist
secret_value = envresolve.resolve_secret("akv://corp-vault/db-password")
print(secret_value)
except ProviderRegistrationError as e:
print(f"Provider setup failed: {e}")
# Example: Provider setup failed: Azure Key Vault provider requires: azure-identity, azure-keyvault-secrets. Install with: pip install envresolve[azure]
except SecretResolutionError as e:
print(f"Failed to fetch secret: {e}")
This pattern ensures that your application can gracefully handle both setup-time (missing dependencies) and run-time (secret access) errors.
Environment Variable Resolution Errors¶
When load_env() or resolve_os_environ() fails, EnvironmentVariableResolutionError indicates which variable was being processed:
import envresolve
try:
envresolve.load_env(dotenv_path=".env", export=False)
except envresolve.EnvironmentVariableResolutionError as e:
print(f"Failed variable: {e.context_key}")
print(f"Cause: {e.original_error}")
# Access original error details
if isinstance(e.original_error, envresolve.VariableNotFoundError):
print(f"Missing: {e.original_error.variable_name}")
elif isinstance(e.original_error, envresolve.SecretResolutionError):
print(f"Failed URI: {e.original_error.uri}")
The exception has two attributes: context_key (variable name) and original_error (underlying exception). CircularReferenceError is not wrapped
Circular Reference Detection¶
envresolve automatically detects circular references and raises a clear error:
from envresolve import expand_variables
from envresolve.exceptions import CircularReferenceError
env = {
"A": "${B}",
"B": "${A}"
}
try:
result = expand_variables(env["A"], env)
except CircularReferenceError as e:
print(f"Error: {e}")
# Error: Circular reference detected: B -> A -> B
# Inspect the exact cycle if you need more detail
print(e.chain) # ['B', 'A', 'B']
Missing Variable Error¶
If a referenced variable doesn't exist, VariableNotFoundError is raised:
from envresolve import expand_variables
from envresolve.exceptions import VariableNotFoundError
env = {"A": "value"}
try:
result = expand_variables("${MISSING}", env)
except VariableNotFoundError as e:
print(f"Error: {e}")
# Error: Variable 'MISSING' not found
Mutually Exclusive Arguments¶
Some API functions have mutually exclusive parameters. For example, resolve_os_environ() cannot accept both keys and prefix parameters:
import envresolve
from envresolve.exceptions import MutuallyExclusiveArgumentsError
envresolve.register_azure_kv_provider()
try:
# This will raise an error - cannot specify both
envresolve.resolve_os_environ(
keys=["API_KEY"],
prefix="DEV_"
)
except MutuallyExclusiveArgumentsError as e:
print(f"Error: {e}")
# Error: Arguments 'keys' and 'prefix' are mutually exclusive.
# Specify either 'keys' or 'prefix', but not both.
# Access argument names programmatically
print(f"Conflicting arguments: {e.arg1} and {e.arg2}")
# Conflicting arguments: keys and prefix
TypeError Compatibility
MutuallyExclusiveArgumentsError also inherits from TypeError, so you can catch it with standard exception handling:
try:
envresolve.resolve_os_environ(keys=["API_KEY"], prefix="DEV_")
except TypeError as e:
print(f"Invalid argument combination: {e}")
Advanced Use Cases¶
Building Secret URIs Dynamically¶
from envresolve import expand_variables
# Define vault and environment once
env = {
"ENVIRONMENT": "production",
"VAULT": "${ENVIRONMENT}-keyvault",
# Define all secrets using the vault
"DB_HOST_URI": "akv://${VAULT}/db-host",
"DB_USER_URI": "akv://${VAULT}/db-user",
"DB_PASS_URI": "akv://${VAULT}/db-password",
"API_KEY_URI": "akv://${VAULT}/api-key",
}
# Expand each URI
for key in ["DB_HOST_URI", "DB_USER_URI", "DB_PASS_URI", "API_KEY_URI"]:
expanded = expand_variables(env[key], env)
print(f"{key}: {expanded}")
# Output:
# DB_HOST_URI: akv://production-keyvault/db-host
# DB_USER_URI: akv://production-keyvault/db-user
# DB_PASS_URI: akv://production-keyvault/db-password
# API_KEY_URI: akv://production-keyvault/api-key
Plain Text Pass-Through¶
Text without variable references is returned unchanged:
from envresolve import expand_variables
result = expand_variables("plain text with $100 price", {"VAR": "value"})
print(result) # Output: plain text with $100 price
Note: A lone $ followed by non-variable characters (like digits) is preserved.
Secret Resolution Errors¶
Azure-specific failures (missing vaults, permission issues, network errors)
raise SecretResolutionError. The exception carries the failing URI, making it
easy to log or surface to users:
import envresolve
from envresolve.exceptions import SecretResolutionError
envresolve.register_azure_kv_provider()
try:
envresolve.resolve_secret("akv://missing-vault/api-key")
except SecretResolutionError as exc:
print(exc) # Human-readable message
print(exc.uri) # akv://missing-vault/api-key
print(exc.original_error) # Underlying Azure exception (if available)