Skip to content

Public API

This section covers the main functions intended for direct use in your applications.

Logging Support

All public API functions and EnvResolver methods accept an optional logger parameter for diagnostic logging. See the Logging Guide for details.

import logging
import envresolve

logger = logging.getLogger(__name__)
result = envresolve.resolve_secret("${SECRET}", logger=logger)

envresolve.api

Public API for envresolve.

EnvResolver

Manages provider registration and secret resolution.

This class encapsulates the provider registry and resolver instance, eliminating the need for module-level global variables.

Source code in src/envresolve/api.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
class EnvResolver:
    """Manages provider registration and secret resolution.

    This class encapsulates the provider registry and resolver instance,
    eliminating the need for module-level global variables.
    """

    def __init__(self, logger: logging.Logger | None = None) -> None:
        """Initialize an empty provider registry.

        Args:
            logger: Optional logger for diagnostic messages. If None, no logging occurs.
        """
        self._providers: dict[str, SecretProvider] = {}
        self._resolver: SecretResolver | None = None
        self._logger = logger

    def _get_resolver(self) -> SecretResolver:
        """Get or create the resolver instance.

        Returns:
            SecretResolver instance configured with registered providers
        """
        if self._resolver is None:
            self._resolver = SecretResolver(self._providers)
        return self._resolver

    def register_azure_kv_provider(
        self, provider: "SecretProvider | None" = None
    ) -> None:
        """Register Azure Key Vault provider for akv:// scheme.

        This method is safe to call multiple times (idempotent).

        Args:
            provider: Optional custom provider. If None, uses default AzureKVProvider.

        Raises:
            ProviderRegistrationError: If azure-identity or azure-keyvault-secrets
                is not installed (only when provider is None)
        """
        if provider is None:
            try:
                # Dynamically import the provider module
                provider_module = importlib.import_module(
                    "envresolve.providers.azure_kv"
                )
                provider_class = provider_module.AzureKVProvider
            except ImportError as e:
                # Check which dependency is missing
                missing_deps: list[str] = []
                try:
                    importlib.import_module("azure.identity")
                except ImportError:
                    missing_deps.append("azure-identity")

                try:
                    importlib.import_module("azure.keyvault.secrets")
                except ImportError:
                    missing_deps.append("azure-keyvault-secrets")

                if missing_deps:
                    deps_str = ", ".join(missing_deps)
                    msg = (
                        f"Azure Key Vault provider requires: {deps_str}. "
                        "Install with: pip install envresolve[azure]"
                    )
                else:
                    msg = f"Failed to import Azure Key Vault provider. Error: {e}"
                raise ProviderRegistrationError(msg, original_error=e) from e
            self._providers["akv"] = provider_class()
        else:
            self._providers["akv"] = provider
        # Reset resolver to pick up new providers
        self._resolver = None

    def resolve_secret(self, uri: str, logger: logging.Logger | None = None) -> str:
        """Resolve a secret URI to its value.

        This function supports:
        - Variable expansion: ${VAR} and $VAR syntax using os.environ
        - Secret URI resolution: akv:// scheme
        - Idempotent resolution: Plain strings and non-target URIs pass through

        Args:
            uri: Secret URI or plain string to resolve
            logger: Optional logger for diagnostic messages. If provided, overrides
                the logger set in the constructor.

        Returns:
            Resolved secret value or the original string if not a secret URI

        Raises:
            URIParseError: If the URI format is invalid
            SecretResolutionError: If secret resolution fails
            VariableNotFoundError: If a referenced variable is not found
            InvalidVariableNameError: If a variable name is invalid
            CircularReferenceError: If a circular variable reference is detected
        """
        effective_logger = logger if logger is not None else self._logger
        resolver = self._get_resolver()
        return resolver.resolve(uri, logger=effective_logger)

    def resolve_with_env(
        self, value: str, env: dict[str, str], logger: logging.Logger | None = None
    ) -> str:
        """Expand variables and resolve secret URIs with custom environment.

        Args:
            value: Value to resolve (may contain variables or be a secret URI)
            env: Environment dict for variable expansion
            logger: Optional logger for diagnostic messages. If provided, overrides
                the logger set in the constructor.

        Returns:
            Resolved value
        """
        effective_logger = logger if logger is not None else self._logger
        resolver = self._get_resolver()
        return resolver.resolve(value, env, logger=effective_logger)

    def load_env(
        self,
        dotenv_path: str | Path | None = None,
        *,
        export: bool = True,
        override: bool = False,
        stop_on_expansion_error: bool = True,
        stop_on_resolution_error: bool = True,
        ignore_keys: list[str] | None = None,
        ignore_patterns: list[str] | None = None,
        logger: logging.Logger | None = None,
    ) -> dict[str, str]:
        """Load environment variables from a .env file and resolve secret URIs.

        This function:
        1. Loads variables from the .env file
        2. Expands variable references within values
        3. Resolves secret URIs (akv://) to actual secret values
        4. Optionally exports to os.environ

        Args:
            dotenv_path: Path to .env file. If None, searches for .env in
                current directory. Mimics python-dotenv's load_dotenv() behavior.
                (default: None)
            export: If True, export resolved variables to os.environ
            override: If True, override existing os.environ variables
            stop_on_expansion_error: If False, skip variables with expansion errors
                (e.g., VariableNotFoundError). CircularReferenceError is always
                raised as it indicates a configuration error. (default: True)
            stop_on_resolution_error: If False, skip variables with resolution errors
                (e.g., SecretResolutionError). Useful for resilience against transient
                secret store failures. (default: True)
            ignore_keys: List of keys to skip expansion for. These keys are included
                in the result as-is without variable expansion or secret resolution.
                (default: None)
            ignore_patterns: List of glob patterns to match keys for skipping expansion.
                Keys matching any pattern are included as-is without variable expansion
                or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)
            logger: Optional logger for diagnostic messages. If provided, overrides
                the logger set in the constructor.

        Returns:
            Dictionary of resolved environment variables

        Raises:
            EnvironmentVariableResolutionError: If a variable resolution error occurs
                (wraps VariableNotFoundError or SecretResolutionError with context)
            URIParseError: If a URI format is invalid
            CircularReferenceError: If a circular variable reference is detected
        """
        effective_logger = logger if logger is not None else self._logger

        if effective_logger is not None:
            effective_logger.debug("Loading environment from .env file")

        # Load .env file
        # When dotenv_path is None, use find_dotenv with usecwd=True
        if dotenv_path is None:
            dotenv_path = find_dotenv(usecwd=True)
        # Use interpolate=False to prevent python-dotenv from expanding variables
        # We handle expansion ourselves in resolve_with_env
        env_dict = {
            k: v
            for k, v in dotenv_values(dotenv_path, interpolate=False).items()
            if v is not None
        }

        if effective_logger is not None:
            effective_logger.debug("Environment loaded from .env file")

        # Build complete environment (for variable expansion)
        complete_env = dict(os.environ)
        complete_env.update(env_dict)

        # Resolve each variable
        resolved = self._resolve_env_dict(
            env_dict,
            complete_env,
            stop_on_expansion_error=stop_on_expansion_error,
            stop_on_resolution_error=stop_on_resolution_error,
            ignore_keys=ignore_keys,
            ignore_patterns=ignore_patterns,
            logger=effective_logger,
        )

        # Export to os.environ if requested
        if export:
            for key, value in resolved.items():
                if override or key not in os.environ:
                    os.environ[key] = value

        if effective_logger is not None:
            effective_logger.debug(".env file loading completed")

        return resolved

    def _get_target_environ(
        self, keys: list[str] | None, prefix: str | None
    ) -> dict[str, str]:
        """Get the target environment variables to process."""
        if keys is not None:
            return {k: os.environ[k] for k in keys if k in os.environ}
        if prefix is not None:
            return {k: v for k, v in os.environ.items() if k.startswith(prefix)}
        return dict(os.environ)

    def _should_ignore_key(
        self, key: str, ignore_keys: list[str] | None, ignore_patterns: list[str] | None
    ) -> bool:
        """Check if a key should be ignored based on exact match or pattern match.

        Args:
            key: Environment variable key to check
            ignore_keys: List of keys for exact matching
            ignore_patterns: List of glob patterns for pattern matching

        Returns:
            True if the key should be ignored, False otherwise
        """
        # Check exact match
        if ignore_keys and key in ignore_keys:
            return True

        # Check pattern match
        return bool(
            ignore_patterns
            and any(fnmatch.fnmatch(key, pattern) for pattern in ignore_patterns)
        )

    def _resolve_env_dict(
        self,
        env_dict: dict[str, str],
        complete_env: dict[str, str],
        *,
        stop_on_expansion_error: bool,
        stop_on_resolution_error: bool,
        ignore_keys: list[str] | None = None,
        ignore_patterns: list[str] | None = None,
        logger: logging.Logger | None = None,
    ) -> dict[str, str]:
        """Resolve environment dictionary with error handling.

        Args:
            env_dict: Dictionary of environment variables to resolve
            complete_env: Complete environment for variable expansion
            stop_on_expansion_error: If False, skip variables with expansion errors
            stop_on_resolution_error: If False, skip variables with resolution errors
            ignore_keys: List of keys to skip expansion for
            ignore_patterns: List of glob patterns to match keys for skipping expansion
            logger: Optional logger for diagnostic messages

        Returns:
            Dictionary of resolved environment variables

        Raises:
            EnvironmentVariableResolutionError: If a variable resolution error occurs
        """
        resolved: dict[str, str] = {}
        for key, value in env_dict.items():
            # Skip expansion for ignored keys or patterns
            if self._should_ignore_key(key, ignore_keys, ignore_patterns):
                resolved[key] = value
                continue

            try:
                resolved_value = self._resolve_variable(
                    value,
                    complete_env,
                    stop_on_expansion_error=stop_on_expansion_error,
                    stop_on_resolution_error=stop_on_resolution_error,
                    logger=logger,
                )
            except (VariableNotFoundError, SecretResolutionError) as e:
                msg = f"Failed to resolve environment variable '{key}': {e}"
                raise EnvironmentVariableResolutionError(
                    msg,
                    context_key=key,
                    original_error=e,
                ) from e
            if resolved_value is None:
                continue

            resolved[key] = resolved_value
        return resolved

    def _resolve_and_export_os_environ(
        self,
        target_env: dict[str, str],
        prefix: str | None,
        *,
        overwrite: bool,
        stop_on_expansion_error: bool,
        stop_on_resolution_error: bool,
        ignore_keys: list[str] | None = None,
        ignore_patterns: list[str] | None = None,
        logger: logging.Logger | None = None,
    ) -> dict[str, str]:
        """Resolve os.environ variables and optionally export them.

        Args:
            target_env: Dictionary of environment variables to resolve
            prefix: Prefix to strip from keys (if present)
            overwrite: If True, overwrite existing os.environ variables
            stop_on_expansion_error: If False, skip variables with expansion errors
            stop_on_resolution_error: If False, skip variables with resolution errors
            ignore_keys: List of keys to skip expansion for
            ignore_patterns: List of glob patterns to match keys for skipping expansion
            logger: Optional logger for diagnostic messages

        Returns:
            Dictionary of resolved environment variables

        Raises:
            EnvironmentVariableResolutionError: If a variable resolution error occurs
        """
        resolved: dict[str, str] = {}

        for key, value in target_env.items():
            # Skip expansion for ignored keys or patterns
            if self._should_ignore_key(key, ignore_keys, ignore_patterns):
                resolved[key] = value
                if overwrite:
                    os.environ[key] = value
                continue

            try:
                resolved_value = self._resolve_variable(
                    value,
                    stop_on_expansion_error=stop_on_expansion_error,
                    stop_on_resolution_error=stop_on_resolution_error,
                    logger=logger,
                )
            except (VariableNotFoundError, SecretResolutionError) as e:
                msg = f"Failed to resolve environment variable '{key}': {e}"
                raise EnvironmentVariableResolutionError(
                    msg,
                    context_key=key,
                    original_error=e,
                ) from e
            if resolved_value is None:
                continue

            output_key = (
                key[len(prefix) :] if prefix and key.startswith(prefix) else key
            )
            resolved[output_key] = resolved_value

            if overwrite:
                os.environ[output_key] = resolved_value
                if prefix and key != output_key:
                    del os.environ[key]

        return resolved

    def _resolve_variable(
        self,
        value: str,
        env: dict[str, str] | None = None,
        *,
        stop_on_expansion_error: bool,
        stop_on_resolution_error: bool,
        logger: logging.Logger | None = None,
    ) -> str | None:
        """Resolve a single variable, handling errors granularly.

        Args:
            value: Value to resolve (may contain variables or be a secret URI)
            env: Environment dict for variable expansion. If None, uses os.environ.
            stop_on_expansion_error: If False, return None on VariableNotFoundError
            stop_on_resolution_error: If False, return None on SecretResolutionError
            logger: Optional logger for diagnostic messages

        Returns:
            Resolved value, or None if error occurred and corresponding flag is False
        """
        if env is None:
            env = dict(os.environ)
        try:
            resolver = self._get_resolver()
            return resolver.resolve(value, env, logger=logger)
        except VariableNotFoundError:
            if stop_on_expansion_error:
                raise
            return None
        except SecretResolutionError:
            if stop_on_resolution_error:
                raise
            return None
        # CircularReferenceError is always raised as it's a configuration error.

    def resolve_os_environ(
        self,
        keys: list[str] | None = None,
        prefix: str | None = None,
        *,
        overwrite: bool = True,
        stop_on_expansion_error: bool = True,
        stop_on_resolution_error: bool = True,
        ignore_keys: list[str] | None = None,
        ignore_patterns: list[str] | None = None,
        logger: logging.Logger | None = None,
    ) -> dict[str, str]:
        """Resolve secret URIs in os.environ.

        Args:
            keys: List of specific environment variable keys to resolve
            prefix: Prefix to filter environment variables
            overwrite: If True, overwrite existing os.environ variables
            stop_on_expansion_error: If False, skip variables with expansion errors
            stop_on_resolution_error: If False, skip variables with resolution errors
            ignore_keys: List of keys to skip expansion for
            ignore_patterns: List of glob patterns to match keys for skipping expansion
            logger: Optional logger for diagnostic messages. If provided, overrides
                the logger set in the constructor.

        Returns:
            Dictionary of resolved environment variables

        Raises:
            EnvironmentVariableResolutionError: If a variable resolution error occurs
                (wraps VariableNotFoundError or SecretResolutionError with context)
            MutuallyExclusiveArgumentsError: If both keys and prefix are specified
            URIParseError: If the URI format is invalid
            CircularReferenceError: If a circular variable reference is detected
        """
        effective_logger = logger if logger is not None else self._logger

        if effective_logger is not None:
            effective_logger.debug("Resolving os.environ variables")

        if keys is not None and prefix is not None:
            arg1 = "keys"
            arg2 = "prefix"
            raise MutuallyExclusiveArgumentsError(arg1, arg2)

        target_env = self._get_target_environ(keys, prefix)

        resolved = self._resolve_and_export_os_environ(
            target_env,
            prefix,
            overwrite=overwrite,
            stop_on_expansion_error=stop_on_expansion_error,
            stop_on_resolution_error=stop_on_resolution_error,
            ignore_keys=ignore_keys,
            ignore_patterns=ignore_patterns,
            logger=effective_logger,
        )

        if effective_logger is not None:
            effective_logger.debug("os.environ resolution completed")

        return resolved

__init__

__init__(logger: Logger | None = None) -> None

Initialize an empty provider registry.

Parameters:

Name Type Description Default
logger Logger | None

Optional logger for diagnostic messages. If None, no logging occurs.

None
Source code in src/envresolve/api.py
def __init__(self, logger: logging.Logger | None = None) -> None:
    """Initialize an empty provider registry.

    Args:
        logger: Optional logger for diagnostic messages. If None, no logging occurs.
    """
    self._providers: dict[str, SecretProvider] = {}
    self._resolver: SecretResolver | None = None
    self._logger = logger

register_azure_kv_provider

register_azure_kv_provider(
    provider: SecretProvider | None = None,
) -> None

Register Azure Key Vault provider for akv:// scheme.

This method is safe to call multiple times (idempotent).

Parameters:

Name Type Description Default
provider SecretProvider | None

Optional custom provider. If None, uses default AzureKVProvider.

None

Raises:

Type Description
ProviderRegistrationError

If azure-identity or azure-keyvault-secrets is not installed (only when provider is None)

Source code in src/envresolve/api.py
def register_azure_kv_provider(
    self, provider: "SecretProvider | None" = None
) -> None:
    """Register Azure Key Vault provider for akv:// scheme.

    This method is safe to call multiple times (idempotent).

    Args:
        provider: Optional custom provider. If None, uses default AzureKVProvider.

    Raises:
        ProviderRegistrationError: If azure-identity or azure-keyvault-secrets
            is not installed (only when provider is None)
    """
    if provider is None:
        try:
            # Dynamically import the provider module
            provider_module = importlib.import_module(
                "envresolve.providers.azure_kv"
            )
            provider_class = provider_module.AzureKVProvider
        except ImportError as e:
            # Check which dependency is missing
            missing_deps: list[str] = []
            try:
                importlib.import_module("azure.identity")
            except ImportError:
                missing_deps.append("azure-identity")

            try:
                importlib.import_module("azure.keyvault.secrets")
            except ImportError:
                missing_deps.append("azure-keyvault-secrets")

            if missing_deps:
                deps_str = ", ".join(missing_deps)
                msg = (
                    f"Azure Key Vault provider requires: {deps_str}. "
                    "Install with: pip install envresolve[azure]"
                )
            else:
                msg = f"Failed to import Azure Key Vault provider. Error: {e}"
            raise ProviderRegistrationError(msg, original_error=e) from e
        self._providers["akv"] = provider_class()
    else:
        self._providers["akv"] = provider
    # Reset resolver to pick up new providers
    self._resolver = None

resolve_secret

resolve_secret(
    uri: str, logger: Logger | None = None
) -> str

Resolve a secret URI to its value.

This function supports: - Variable expansion: ${VAR} and $VAR syntax using os.environ - Secret URI resolution: akv:// scheme - Idempotent resolution: Plain strings and non-target URIs pass through

Parameters:

Name Type Description Default
uri str

Secret URI or plain string to resolve

required
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the logger set in the constructor.

None

Returns:

Type Description
str

Resolved secret value or the original string if not a secret URI

Raises:

Type Description
URIParseError

If the URI format is invalid

SecretResolutionError

If secret resolution fails

VariableNotFoundError

If a referenced variable is not found

InvalidVariableNameError

If a variable name is invalid

CircularReferenceError

If a circular variable reference is detected

Source code in src/envresolve/api.py
def resolve_secret(self, uri: str, logger: logging.Logger | None = None) -> str:
    """Resolve a secret URI to its value.

    This function supports:
    - Variable expansion: ${VAR} and $VAR syntax using os.environ
    - Secret URI resolution: akv:// scheme
    - Idempotent resolution: Plain strings and non-target URIs pass through

    Args:
        uri: Secret URI or plain string to resolve
        logger: Optional logger for diagnostic messages. If provided, overrides
            the logger set in the constructor.

    Returns:
        Resolved secret value or the original string if not a secret URI

    Raises:
        URIParseError: If the URI format is invalid
        SecretResolutionError: If secret resolution fails
        VariableNotFoundError: If a referenced variable is not found
        InvalidVariableNameError: If a variable name is invalid
        CircularReferenceError: If a circular variable reference is detected
    """
    effective_logger = logger if logger is not None else self._logger
    resolver = self._get_resolver()
    return resolver.resolve(uri, logger=effective_logger)

resolve_with_env

resolve_with_env(
    value: str,
    env: dict[str, str],
    logger: Logger | None = None,
) -> str

Expand variables and resolve secret URIs with custom environment.

Parameters:

Name Type Description Default
value str

Value to resolve (may contain variables or be a secret URI)

required
env dict[str, str]

Environment dict for variable expansion

required
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the logger set in the constructor.

None

Returns:

Type Description
str

Resolved value

Source code in src/envresolve/api.py
def resolve_with_env(
    self, value: str, env: dict[str, str], logger: logging.Logger | None = None
) -> str:
    """Expand variables and resolve secret URIs with custom environment.

    Args:
        value: Value to resolve (may contain variables or be a secret URI)
        env: Environment dict for variable expansion
        logger: Optional logger for diagnostic messages. If provided, overrides
            the logger set in the constructor.

    Returns:
        Resolved value
    """
    effective_logger = logger if logger is not None else self._logger
    resolver = self._get_resolver()
    return resolver.resolve(value, env, logger=effective_logger)

load_env

load_env(
    dotenv_path: str | Path | None = None,
    *,
    export: bool = True,
    override: bool = False,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: Logger | None = None,
) -> dict[str, str]

Load environment variables from a .env file and resolve secret URIs.

This function: 1. Loads variables from the .env file 2. Expands variable references within values 3. Resolves secret URIs (akv://) to actual secret values 4. Optionally exports to os.environ

Parameters:

Name Type Description Default
dotenv_path str | Path | None

Path to .env file. If None, searches for .env in current directory. Mimics python-dotenv's load_dotenv() behavior. (default: None)

None
export bool

If True, export resolved variables to os.environ

True
override bool

If True, override existing os.environ variables

False
stop_on_expansion_error bool

If False, skip variables with expansion errors (e.g., VariableNotFoundError). CircularReferenceError is always raised as it indicates a configuration error. (default: True)

True
stop_on_resolution_error bool

If False, skip variables with resolution errors (e.g., SecretResolutionError). Useful for resilience against transient secret store failures. (default: True)

True
ignore_keys list[str] | None

List of keys to skip expansion for. These keys are included in the result as-is without variable expansion or secret resolution. (default: None)

None
ignore_patterns list[str] | None

List of glob patterns to match keys for skipping expansion. Keys matching any pattern are included as-is without variable expansion or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)

None
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the logger set in the constructor.

None

Returns:

Type Description
dict[str, str]

Dictionary of resolved environment variables

Raises:

Type Description
EnvironmentVariableResolutionError

If a variable resolution error occurs (wraps VariableNotFoundError or SecretResolutionError with context)

URIParseError

If a URI format is invalid

CircularReferenceError

If a circular variable reference is detected

Source code in src/envresolve/api.py
def load_env(
    self,
    dotenv_path: str | Path | None = None,
    *,
    export: bool = True,
    override: bool = False,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: logging.Logger | None = None,
) -> dict[str, str]:
    """Load environment variables from a .env file and resolve secret URIs.

    This function:
    1. Loads variables from the .env file
    2. Expands variable references within values
    3. Resolves secret URIs (akv://) to actual secret values
    4. Optionally exports to os.environ

    Args:
        dotenv_path: Path to .env file. If None, searches for .env in
            current directory. Mimics python-dotenv's load_dotenv() behavior.
            (default: None)
        export: If True, export resolved variables to os.environ
        override: If True, override existing os.environ variables
        stop_on_expansion_error: If False, skip variables with expansion errors
            (e.g., VariableNotFoundError). CircularReferenceError is always
            raised as it indicates a configuration error. (default: True)
        stop_on_resolution_error: If False, skip variables with resolution errors
            (e.g., SecretResolutionError). Useful for resilience against transient
            secret store failures. (default: True)
        ignore_keys: List of keys to skip expansion for. These keys are included
            in the result as-is without variable expansion or secret resolution.
            (default: None)
        ignore_patterns: List of glob patterns to match keys for skipping expansion.
            Keys matching any pattern are included as-is without variable expansion
            or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)
        logger: Optional logger for diagnostic messages. If provided, overrides
            the logger set in the constructor.

    Returns:
        Dictionary of resolved environment variables

    Raises:
        EnvironmentVariableResolutionError: If a variable resolution error occurs
            (wraps VariableNotFoundError or SecretResolutionError with context)
        URIParseError: If a URI format is invalid
        CircularReferenceError: If a circular variable reference is detected
    """
    effective_logger = logger if logger is not None else self._logger

    if effective_logger is not None:
        effective_logger.debug("Loading environment from .env file")

    # Load .env file
    # When dotenv_path is None, use find_dotenv with usecwd=True
    if dotenv_path is None:
        dotenv_path = find_dotenv(usecwd=True)
    # Use interpolate=False to prevent python-dotenv from expanding variables
    # We handle expansion ourselves in resolve_with_env
    env_dict = {
        k: v
        for k, v in dotenv_values(dotenv_path, interpolate=False).items()
        if v is not None
    }

    if effective_logger is not None:
        effective_logger.debug("Environment loaded from .env file")

    # Build complete environment (for variable expansion)
    complete_env = dict(os.environ)
    complete_env.update(env_dict)

    # Resolve each variable
    resolved = self._resolve_env_dict(
        env_dict,
        complete_env,
        stop_on_expansion_error=stop_on_expansion_error,
        stop_on_resolution_error=stop_on_resolution_error,
        ignore_keys=ignore_keys,
        ignore_patterns=ignore_patterns,
        logger=effective_logger,
    )

    # Export to os.environ if requested
    if export:
        for key, value in resolved.items():
            if override or key not in os.environ:
                os.environ[key] = value

    if effective_logger is not None:
        effective_logger.debug(".env file loading completed")

    return resolved

resolve_os_environ

resolve_os_environ(
    keys: list[str] | None = None,
    prefix: str | None = None,
    *,
    overwrite: bool = True,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: Logger | None = None,
) -> dict[str, str]

Resolve secret URIs in os.environ.

Parameters:

Name Type Description Default
keys list[str] | None

List of specific environment variable keys to resolve

None
prefix str | None

Prefix to filter environment variables

None
overwrite bool

If True, overwrite existing os.environ variables

True
stop_on_expansion_error bool

If False, skip variables with expansion errors

True
stop_on_resolution_error bool

If False, skip variables with resolution errors

True
ignore_keys list[str] | None

List of keys to skip expansion for

None
ignore_patterns list[str] | None

List of glob patterns to match keys for skipping expansion

None
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the logger set in the constructor.

None

Returns:

Type Description
dict[str, str]

Dictionary of resolved environment variables

Raises:

Type Description
EnvironmentVariableResolutionError

If a variable resolution error occurs (wraps VariableNotFoundError or SecretResolutionError with context)

MutuallyExclusiveArgumentsError

If both keys and prefix are specified

URIParseError

If the URI format is invalid

CircularReferenceError

If a circular variable reference is detected

Source code in src/envresolve/api.py
def resolve_os_environ(
    self,
    keys: list[str] | None = None,
    prefix: str | None = None,
    *,
    overwrite: bool = True,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: logging.Logger | None = None,
) -> dict[str, str]:
    """Resolve secret URIs in os.environ.

    Args:
        keys: List of specific environment variable keys to resolve
        prefix: Prefix to filter environment variables
        overwrite: If True, overwrite existing os.environ variables
        stop_on_expansion_error: If False, skip variables with expansion errors
        stop_on_resolution_error: If False, skip variables with resolution errors
        ignore_keys: List of keys to skip expansion for
        ignore_patterns: List of glob patterns to match keys for skipping expansion
        logger: Optional logger for diagnostic messages. If provided, overrides
            the logger set in the constructor.

    Returns:
        Dictionary of resolved environment variables

    Raises:
        EnvironmentVariableResolutionError: If a variable resolution error occurs
            (wraps VariableNotFoundError or SecretResolutionError with context)
        MutuallyExclusiveArgumentsError: If both keys and prefix are specified
        URIParseError: If the URI format is invalid
        CircularReferenceError: If a circular variable reference is detected
    """
    effective_logger = logger if logger is not None else self._logger

    if effective_logger is not None:
        effective_logger.debug("Resolving os.environ variables")

    if keys is not None and prefix is not None:
        arg1 = "keys"
        arg2 = "prefix"
        raise MutuallyExclusiveArgumentsError(arg1, arg2)

    target_env = self._get_target_environ(keys, prefix)

    resolved = self._resolve_and_export_os_environ(
        target_env,
        prefix,
        overwrite=overwrite,
        stop_on_expansion_error=stop_on_expansion_error,
        stop_on_resolution_error=stop_on_resolution_error,
        ignore_keys=ignore_keys,
        ignore_patterns=ignore_patterns,
        logger=effective_logger,
    )

    if effective_logger is not None:
        effective_logger.debug("os.environ resolution completed")

    return resolved

set_logger

set_logger(logger: Logger | None) -> None

Set the default logger for the global facade functions.

This function configures the logger used by the default EnvResolver instance that backs the module-level facade functions (resolve_secret, load_env, etc.).

Parameters:

Name Type Description Default
logger Logger | None

Logger instance for diagnostic messages. If None, logging is disabled.

required

Examples:

>>> import envresolve
>>> import logging
>>> # Set up logging
>>> logger = logging.getLogger(__name__)
>>> envresolve.set_logger(logger)
>>> # Now all module-level functions will use this logger
>>> envresolve.resolve_secret("akv://vault/secret")
>>> # Disable logging
>>> envresolve.set_logger(None)
Source code in src/envresolve/api.py
def set_logger(logger: logging.Logger | None) -> None:
    """Set the default logger for the global facade functions.

    This function configures the logger used by the default EnvResolver instance
    that backs the module-level facade functions (resolve_secret, load_env, etc.).

    Args:
        logger: Logger instance for diagnostic messages. If None, logging is disabled.

    Examples:
        >>> import envresolve
        >>> import logging
        >>> # Set up logging
        >>> logger = logging.getLogger(__name__)
        >>> envresolve.set_logger(logger)
        >>> # Now all module-level functions will use this logger
        >>> envresolve.resolve_secret("akv://vault/secret")  # doctest: +SKIP
        >>> # Disable logging
        >>> envresolve.set_logger(None)
    """
    _default_resolver._logger = logger  # noqa: SLF001

register_azure_kv_provider

register_azure_kv_provider(
    provider: SecretProvider | None = None,
) -> None

Register Azure Key Vault provider for akv:// scheme.

This function should be called before attempting to resolve secrets from Azure Key Vault. It is safe to call multiple times (idempotent).

Parameters:

Name Type Description Default
provider SecretProvider | None

Optional custom provider. If None, uses default AzureKVProvider.

None

Raises:

Type Description
ProviderRegistrationError

If azure-identity or azure-keyvault-secrets is not installed (only when provider is None)

Examples:

>>> import envresolve
>>> # Default behavior
>>> envresolve.register_azure_kv_provider()
>>> # Custom provider (requires Azure SDK imports)
>>> # from envresolve.providers.azure_kv import AzureKVProvider
>>> # from azure.identity import ManagedIdentityCredential
>>> # custom = AzureKVProvider(credential=ManagedIdentityCredential())
>>> # envresolve.register_azure_kv_provider(provider=custom)
>>> # Now you can resolve secrets (requires Azure authentication)
>>> # secret = envresolve.resolve_secret("akv://my-vault/db-password")
Source code in src/envresolve/api.py
def register_azure_kv_provider(provider: "SecretProvider | None" = None) -> None:
    """Register Azure Key Vault provider for akv:// scheme.

    This function should be called before attempting to resolve secrets
    from Azure Key Vault. It is safe to call multiple times (idempotent).

    Args:
        provider: Optional custom provider. If None, uses default AzureKVProvider.

    Raises:
        ProviderRegistrationError: If azure-identity or azure-keyvault-secrets
            is not installed (only when provider is None)

    Examples:
        >>> import envresolve
        >>> # Default behavior
        >>> envresolve.register_azure_kv_provider()
        >>> # Custom provider (requires Azure SDK imports)
        >>> # from envresolve.providers.azure_kv import AzureKVProvider
        >>> # from azure.identity import ManagedIdentityCredential
        >>> # custom = AzureKVProvider(credential=ManagedIdentityCredential())
        >>> # envresolve.register_azure_kv_provider(provider=custom)
        >>> # Now you can resolve secrets (requires Azure authentication)
        >>> # secret = envresolve.resolve_secret("akv://my-vault/db-password")
    """
    _default_resolver.register_azure_kv_provider(provider=provider)

resolve_secret

resolve_secret(
    uri: str, logger: Logger | None = None
) -> str

Resolve a secret URI to its value.

This function supports: - Variable expansion: ${VAR} and $VAR syntax using os.environ - Secret URI resolution: akv:// scheme - Idempotent resolution: Plain strings and non-target URIs pass through unchanged

Parameters:

Name Type Description Default
uri str

Secret URI or plain string to resolve

required
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the global default logger set by set_logger().

None

Returns:

Type Description
str

Resolved secret value or the original string if not a secret URI

Raises:

Type Description
URIParseError

If the URI format is invalid

SecretResolutionError

If secret resolution fails

VariableNotFoundError

If a referenced variable is not found

InvalidVariableNameError

If a variable name is invalid

CircularReferenceError

If a circular variable reference is detected

Examples:

>>> import envresolve
>>> # Idempotent - plain strings pass through
>>> value = envresolve.resolve_secret("just-a-string")
>>> value
'just-a-string'
>>> # Non-target URIs pass through unchanged
>>> uri = envresolve.resolve_secret("postgres://localhost/db")
>>> uri
'postgres://localhost/db'
>>> # Secret URIs require provider registration and authentication
>>> # envresolve.register_azure_kv_provider()
>>> # secret = envresolve.resolve_secret("akv://my-vault/db-password")
Source code in src/envresolve/api.py
def resolve_secret(uri: str, logger: logging.Logger | None = None) -> str:
    """Resolve a secret URI to its value.

    This function supports:
    - Variable expansion: ${VAR} and $VAR syntax using os.environ
    - Secret URI resolution: akv:// scheme
    - Idempotent resolution: Plain strings and non-target URIs pass through unchanged

    Args:
        uri: Secret URI or plain string to resolve
        logger: Optional logger for diagnostic messages. If provided, overrides
            the global default logger set by set_logger().

    Returns:
        Resolved secret value or the original string if not a secret URI

    Raises:
        URIParseError: If the URI format is invalid
        SecretResolutionError: If secret resolution fails
        VariableNotFoundError: If a referenced variable is not found
        InvalidVariableNameError: If a variable name is invalid
        CircularReferenceError: If a circular variable reference is detected

    Examples:
        >>> import envresolve
        >>> # Idempotent - plain strings pass through
        >>> value = envresolve.resolve_secret("just-a-string")
        >>> value
        'just-a-string'
        >>> # Non-target URIs pass through unchanged
        >>> uri = envresolve.resolve_secret("postgres://localhost/db")
        >>> uri
        'postgres://localhost/db'
        >>> # Secret URIs require provider registration and authentication
        >>> # envresolve.register_azure_kv_provider()
        >>> # secret = envresolve.resolve_secret("akv://my-vault/db-password")
    """
    effective_logger = logger if logger is not None else _default_resolver._logger  # noqa: SLF001
    return _default_resolver.resolve_secret(uri, logger=effective_logger)

load_env

load_env(
    dotenv_path: str | Path | None = None,
    *,
    export: bool = True,
    override: bool = False,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: Logger | None = None,
) -> dict[str, str]

Load environment variables from a .env file and resolve secret URIs.

This function: 1. Loads variables from the .env file 2. Expands variable references within values 3. Resolves secret URIs (akv://) to actual secret values 4. Optionally exports to os.environ

Parameters:

Name Type Description Default
dotenv_path str | Path | None

Path to .env file. If None, searches for .env in current directory. Mimics python-dotenv's load_dotenv() behavior. (default: None)

None
export bool

If True, export resolved variables to os.environ (default: True)

True
override bool

If True, override existing os.environ variables (default: False)

False
stop_on_expansion_error bool

If False, skip variables with expansion errors (e.g., VariableNotFoundError). CircularReferenceError is always raised as it indicates a configuration error. (default: True)

True
stop_on_resolution_error bool

If False, skip variables with resolution errors (e.g., SecretResolutionError). Useful for resilience against transient secret store failures. (default: True)

True
ignore_keys list[str] | None

List of keys to skip expansion for. These keys are included in the result as-is without variable expansion or secret resolution. (default: None)

None
ignore_patterns list[str] | None

List of glob patterns to match keys for skipping expansion. Keys matching any pattern are included as-is without variable expansion or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)

None
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the global default logger set by set_logger().

None

Returns:

Type Description
dict[str, str]

Dictionary of resolved environment variables

Raises:

Type Description
EnvironmentVariableResolutionError

If a variable resolution error occurs (wraps VariableNotFoundError or SecretResolutionError with context)

URIParseError

If a URI format is invalid

CircularReferenceError

If a circular variable reference is detected

Examples:

>>> import envresolve
>>> envresolve.register_azure_kv_provider()
>>> # Load and export to os.environ (searches for .env in cwd)
>>> resolved = envresolve.load_env(export=True)
>>> # Load specific file without exporting
>>> resolved = envresolve.load_env("custom.env", export=False)
>>> # Skip expansion for system variables
>>> resolved = envresolve.load_env(ignore_keys=["PS1"])
Source code in src/envresolve/api.py
def load_env(
    dotenv_path: str | Path | None = None,
    *,
    export: bool = True,
    override: bool = False,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: logging.Logger | None = None,
) -> dict[str, str]:
    """Load environment variables from a .env file and resolve secret URIs.

    This function:
    1. Loads variables from the .env file
    2. Expands variable references within values
    3. Resolves secret URIs (akv://) to actual secret values
    4. Optionally exports to os.environ

    Args:
        dotenv_path: Path to .env file. If None, searches for .env in current directory.
            Mimics python-dotenv's load_dotenv() behavior. (default: None)
        export: If True, export resolved variables to os.environ (default: True)
        override: If True, override existing os.environ variables (default: False)
        stop_on_expansion_error: If False, skip variables with expansion errors
            (e.g., VariableNotFoundError). CircularReferenceError is always
            raised as it indicates a configuration error. (default: True)
        stop_on_resolution_error: If False, skip variables with resolution errors
            (e.g., SecretResolutionError). Useful for resilience against transient
            secret store failures. (default: True)
        ignore_keys: List of keys to skip expansion for. These keys are included
            in the result as-is without variable expansion or secret resolution.
            (default: None)
        ignore_patterns: List of glob patterns to match keys for skipping expansion.
            Keys matching any pattern are included as-is without variable expansion
            or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)
        logger: Optional logger for diagnostic messages. If provided, overrides
            the global default logger set by set_logger().

    Returns:
        Dictionary of resolved environment variables

    Raises:
        EnvironmentVariableResolutionError: If a variable resolution error occurs
            (wraps VariableNotFoundError or SecretResolutionError with context)
        URIParseError: If a URI format is invalid
        CircularReferenceError: If a circular variable reference is detected

    Examples:
        >>> import envresolve
        >>> envresolve.register_azure_kv_provider()
        >>> # Load and export to os.environ (searches for .env in cwd)
        >>> resolved = envresolve.load_env(export=True)  # doctest: +SKIP
        >>> # Load specific file without exporting
        >>> resolved = envresolve.load_env("custom.env", export=False)  # doctest: +SKIP
        >>> # Skip expansion for system variables
        >>> resolved = envresolve.load_env(ignore_keys=["PS1"])  # doctest: +SKIP
    """
    effective_logger = logger if logger is not None else _default_resolver._logger  # noqa: SLF001
    return _default_resolver.load_env(
        dotenv_path,
        export=export,
        override=override,
        stop_on_expansion_error=stop_on_expansion_error,
        stop_on_resolution_error=stop_on_resolution_error,
        ignore_keys=ignore_keys,
        ignore_patterns=ignore_patterns,
        logger=effective_logger,
    )

resolve_os_environ

resolve_os_environ(
    keys: list[str] | None = None,
    prefix: str | None = None,
    *,
    overwrite: bool = True,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: Logger | None = None,
) -> dict[str, str]

Resolve secret URIs in os.environ.

This function resolves secret URIs that are already set in environment variables, useful when values are passed from parent shells or container orchestrators.

Parameters:

Name Type Description Default
keys list[str] | None

List of specific keys to resolve. If None, scan all keys. Mutually exclusive with prefix.

None
prefix str | None

Only process keys with this prefix, strip prefix from output. Mutually exclusive with keys.

None
overwrite bool

If True, update os.environ with resolved values (default: True).

True
stop_on_expansion_error bool

If False, skip variables with expansion errors (e.g., VariableNotFoundError). CircularReferenceError is always raised as it indicates a configuration error. (default: True)

True
stop_on_resolution_error bool

If False, skip variables with resolution errors (e.g., SecretResolutionError). Useful for resilience against transient secret store failures. (default: True)

True
ignore_keys list[str] | None

List of keys to skip expansion for. These keys are included in the result as-is without variable expansion or secret resolution. (default: None)

None
ignore_patterns list[str] | None

List of glob patterns to match keys for skipping expansion. Keys matching any pattern are included as-is without variable expansion or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)

None
logger Logger | None

Optional logger for diagnostic messages. If provided, overrides the global default logger set by set_logger().

None

Returns:

Type Description
dict[str, str]

Dictionary of resolved values

Raises:

Type Description
EnvironmentVariableResolutionError

If a variable resolution error occurs (wraps VariableNotFoundError or SecretResolutionError with context)

MutuallyExclusiveArgumentsError

If both keys and prefix are specified

URIParseError

If the URI format is invalid

CircularReferenceError

If a circular variable reference is detected

Examples:

>>> import envresolve
>>> import os
>>> envresolve.register_azure_kv_provider()
>>> # Resolve all environment variables
>>> resolved = envresolve.resolve_os_environ()
>>> # Resolve specific keys only
>>> resolved = envresolve.resolve_os_environ(keys=["API_KEY"])
>>> # Skip expansion for system variables
>>> resolved = envresolve.resolve_os_environ(
...     ignore_keys=["PS1"]
... )
Source code in src/envresolve/api.py
def resolve_os_environ(
    keys: list[str] | None = None,
    prefix: str | None = None,
    *,
    overwrite: bool = True,
    stop_on_expansion_error: bool = True,
    stop_on_resolution_error: bool = True,
    ignore_keys: list[str] | None = None,
    ignore_patterns: list[str] | None = None,
    logger: logging.Logger | None = None,
) -> dict[str, str]:
    """Resolve secret URIs in os.environ.

    This function resolves secret URIs that are already set in environment variables,
    useful when values are passed from parent shells or container orchestrators.

    Args:
        keys: List of specific keys to resolve. If None, scan all keys.
            Mutually exclusive with prefix.
        prefix: Only process keys with this prefix, strip prefix from output.
            Mutually exclusive with keys.
        overwrite: If True, update os.environ with resolved values (default: True).
        stop_on_expansion_error: If False, skip variables with expansion errors
            (e.g., VariableNotFoundError). CircularReferenceError is always
            raised as it indicates a configuration error. (default: True)
        stop_on_resolution_error: If False, skip variables with resolution errors
            (e.g., SecretResolutionError). Useful for resilience against transient
            secret store failures. (default: True)
        ignore_keys: List of keys to skip expansion for. These keys are included
            in the result as-is without variable expansion or secret resolution.
            (default: None)
        ignore_patterns: List of glob patterns to match keys for skipping expansion.
            Keys matching any pattern are included as-is without variable expansion
            or secret resolution. Supports wildcards: *, ?, [seq]. (default: None)
        logger: Optional logger for diagnostic messages. If provided, overrides
            the global default logger set by set_logger().

    Returns:
        Dictionary of resolved values

    Raises:
        EnvironmentVariableResolutionError: If a variable resolution error occurs
            (wraps VariableNotFoundError or SecretResolutionError with context)
        MutuallyExclusiveArgumentsError: If both keys and prefix are specified
        URIParseError: If the URI format is invalid
        CircularReferenceError: If a circular variable reference is detected

    Examples:
        >>> import envresolve
        >>> import os
        >>> envresolve.register_azure_kv_provider()
        >>> # Resolve all environment variables
        >>> resolved = envresolve.resolve_os_environ()  # doctest: +SKIP
        >>> # Resolve specific keys only
        >>> resolved = envresolve.resolve_os_environ(keys=["API_KEY"])  # doctest: +SKIP
        >>> # Skip expansion for system variables
        >>> resolved = envresolve.resolve_os_environ(
        ...     ignore_keys=["PS1"]
        ... )  # doctest: +SKIP
    """
    effective_logger = logger if logger is not None else _default_resolver._logger  # noqa: SLF001
    return _default_resolver.resolve_os_environ(
        keys=keys,
        prefix=prefix,
        overwrite=overwrite,
        stop_on_expansion_error=stop_on_expansion_error,
        stop_on_resolution_error=stop_on_resolution_error,
        ignore_keys=ignore_keys,
        ignore_patterns=ignore_patterns,
        logger=effective_logger,
    )