Provides yaml file parsing with environment variable resolution
npx @tessl/cli install tessl/pypi-pyaml-env@1.2.0A Python library that parses YAML configuration files and resolves environment variables, enabling secure configuration management by keeping sensitive data like passwords, API keys, and database credentials out of version control. The library supports custom YAML tags, default value handling, multiple environment variables per line, and provides a BaseConfig class for attribute-style configuration access.
pip install pyaml-envfrom pyaml_env import parse_config, BaseConfigIndividual imports:
from pyaml_env import parse_config
from pyaml_env import BaseConfigfrom pyaml_env import parse_config, BaseConfig
# Parse YAML file with environment variable resolution
config = parse_config('path/to/config.yaml')
# Access as dictionary
username = config['database']['username']
# Or use BaseConfig for attribute-style access
config_obj = BaseConfig(config)
username = config_obj.database.usernameExample YAML file with environment variables:
database:
name: test_db
username: !ENV ${DB_USER}
password: !ENV ${DB_PASS}
url: !ENV 'http://${DB_BASE_URL}:${DB_PORT}'With environment variables set:
export DB_USER=my_user
export DB_PASS=my_password
export DB_BASE_URL=localhost
export DB_PORT=5432Parse YAML configuration files with automatic environment variable resolution using custom tags.
def parse_config(
path=None,
data=None,
tag='!ENV',
default_sep=':',
default_value='N/A',
raise_if_na=False,
loader=yaml.SafeLoader,
encoding='utf-8'
):
"""
Load yaml configuration from path or from the contents of a file (data)
and resolve any environment variables. The environment variables
must have the tag e.g. !ENV *before* them and be in this format to be
parsed: ${VAR_NAME}
Args:
path (str, optional): Path to the yaml file
data (str, optional): YAML data itself as a stream
tag (str): Tag to look for environment variables (default: '!ENV'). If None, all env variables will be resolved
default_sep (str): Separator for default values (default: ':')
default_value (str): Default value when no environment variable or default is found (default: 'N/A')
raise_if_na (bool): Raise exception if there is no default value set for the env variable (default: False)
loader (Type[yaml.loader]): YAML loader to use (default: yaml.SafeLoader)
encoding (str): Encoding of the data if a path is specified (default: 'utf-8')
Returns:
dict: Parsed configuration dictionary with resolved environment variables
Raises:
ValueError: If neither path nor data is provided, or if raise_if_na=True and no default value is found for an environment variable
"""Usage Examples:
File-based parsing:
config = parse_config(path='config.yaml')String-based parsing:
yaml_data = '''
database:
host: !ENV ${DB_HOST:localhost}
port: !ENV ${DB_PORT:5432}
'''
config = parse_config(data=yaml_data)Custom tag and defaults:
config = parse_config(
path='config.yaml',
tag='!CUSTOM',
default_sep='|',
default_value='missing',
raise_if_na=True
)Environment Variable Syntax:
Basic environment variable:
username: !ENV ${DB_USER}With default value:
username: !ENV ${DB_USER:default_user}
port: !ENV ${DB_PORT:5432}Multiple variables in one line:
url: !ENV 'http://${HOST:localhost}:${PORT:8080}/api'Type conversion with YAML tags:
port: !ENV tag:yaml.org,2002:int ${DB_PORT:5432}
enabled: !ENV tag:yaml.org,2002:bool ${FEATURE_ENABLED:false}
ratio: !ENV tag:yaml.org,2002:float ${RATIO:1.5}Provides attribute-style access to configuration dictionaries, allowing dot notation for nested configuration access.
class BaseConfig:
"""
A base Config class providing attribute-style access to configuration dictionaries.
Recursively converts nested dictionaries to BaseConfig objects.
"""
def __init__(self, config_dict):
"""
Initialize BaseConfig with a configuration dictionary.
Copies existing class attributes, updates with config_dict,
and recursively converts nested dictionaries to BaseConfig objects.
Args:
config_dict (dict): Configuration dictionary to wrap
"""
def __getattr__(self, field_name: str) -> Any:
"""
Provides attribute-style access to configuration values.
Args:
field_name (str): Name of the configuration field
Returns:
Any: The configuration value
"""
@property
def errors(self):
"""
Returns:
list: List of validation errors
"""
@property
def _is_validated(self):
"""
Returns:
bool: Whether validation has been performed
"""
@property
def _is_valid(self):
"""
Returns:
bool: Whether configuration passed validation
"""
@property
def _errors(self):
"""
Returns:
list: Internal list of validation errors
"""
def validate(self):
"""
Abstract method for configuration validation (must be implemented by subclasses).
Raises:
NotImplementedError: This method must be implemented in subclasses
"""Usage Examples:
Basic attribute access:
from pyaml_env import parse_config, BaseConfig
config = BaseConfig(parse_config('config.yaml'))
# Dot notation access instead of dictionary syntax
host = config.database.host
port = config.database.port
username = config.database.usernameNested configuration access:
# YAML:
# app:
# database:
# primary:
# host: !ENV ${PRIMARY_DB_HOST}
# port: !ENV ${PRIMARY_DB_PORT:5432}
# cache:
# redis_url: !ENV ${REDIS_URL}
config = BaseConfig(parse_config('config.yaml'))
primary_host = config.app.database.primary.host
redis_url = config.app.database.cache.redis_urlCustom validation (subclass implementation):
class DatabaseConfig(BaseConfig):
def validate(self):
if not hasattr(self, 'host'):
self._errors.append("Missing required field: host")
if hasattr(self, 'port') and not isinstance(self.port, int):
self._errors.append("Port must be an integer")
self._is_validated = True
self._is_valid = len(self._errors) == 0
db_config = DatabaseConfig(parse_config('db_config.yaml'))
db_config.validate()
if not db_config._is_valid:
print("Configuration errors:", db_config.errors)Support for different YAML loaders including UnsafeLoader for serialized Python objects:
import yaml
from pyaml_env import parse_config
# Using UnsafeLoader for Python objects
config = parse_config(path='config.yaml', loader=yaml.UnsafeLoader)
# Using FullLoader
config = parse_config(path='config.yaml', loader=yaml.FullLoader)Control behavior when environment variables are missing:
# Strict mode - raise exception if no default provided
try:
config = parse_config('config.yaml', raise_if_na=True)
except ValueError as e:
print(f"Missing environment variable: {e}")
# Permissive mode - use default_value for missing variables
config = parse_config('config.yaml', default_value='MISSING')!ENV) are processed${VARIABLE_NAME} syntax:) for defaults: ${VAR:default}tag:yaml.org,2002:intfrom typing import Any, Type
import yaml
# Type aliases for function parameters
LoaderType = Type[yaml.loader]
ConfigDict = dict