Metadata-Version: 2.4
Name: envilder
Version: 0.4.0
Summary: Securely load environment variables from AWS SSM Parameter Store or Azure Key Vault.
Project-URL: Homepage, https://github.com/macalbert/envilder
Project-URL: Repository, https://github.com/macalbert/envilder
Project-URL: Documentation, https://github.com/macalbert/envilder/tree/main/src/sdks/python
Project-URL: Issues, https://github.com/macalbert/envilder/issues
Author: macalbert
License: MIT
Keywords: aws,azure,envilder,environment,keyvault,secrets,ssm
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: azure-identity>=1.14
Requires-Dist: azure-keyvault-secrets>=4.7
Requires-Dist: boto3>=1.26
Provides-Extra: dev
Requires-Dist: black>=24.0; extra == 'dev'
Requires-Dist: boto3-stubs[ssm]>=1.26; extra == 'dev'
Requires-Dist: isort>=5.13; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: requests>=2.31; extra == 'dev'
Requires-Dist: testcontainers[localstack]>=4.0; extra == 'dev'
Description-Content-Type: text/markdown

# Envilder Python SDK

[![Coverage Report](https://img.shields.io/badge/coverage-report-green.svg)](https://macalbert.github.io/envilder/python/)
[![PyPI version](https://img.shields.io/pypi/v/envilder.svg)](https://pypi.org/project/envilder/)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/macalbert/envilder/blob/main/LICENSE)

Securely load environment variables from **AWS SSM Parameter Store** or **Azure Key Vault** directly into your Python application.
Zero vendor lock-in — secrets stay in your cloud.

Part of the [Envilder](https://github.com/macalbert/envilder) project.

## Prerequisites

- Python 3.10+
- **AWS provider**: AWS credentials configured (CLI, environment variables, or IAM role)
- **Azure provider**: Azure credentials via `az login`, managed identity, or environment variables

## Install

```bash
uv add envilder
# or
pip install envilder
```

## Quick Start

### One-liner

```python
from envilder import Envilder

# Resolve secrets and inject into os.environ
Envilder.load('secrets-map.json')

import os
print(os.environ['DB_PASSWORD'])
```

### Resolve without injecting

```python
from envilder import Envilder

secrets = Envilder.resolve_file('secrets-map.json')
print(secrets['DB_PASSWORD'])
```

### Fluent builder (with overrides)

Override the map file's `$config` at runtime — useful for switching providers,
profiles, or vault URLs per environment:

```python
from envilder import Envilder, SecretProviderType

# Override provider + vault URL
secrets = (
    Envilder.from_map_file('secrets-map.json')
    .with_provider(SecretProviderType.AZURE)
    .with_vault_url('https://my-vault.vault.azure.net')
    .resolve()
)

# Override AWS profile and inject
(
    Envilder.from_map_file('secrets-map.json')
    .with_profile('staging')
    .inject()
)
```

### Environment-based loading

Route secret loading based on your current environment. Each environment
maps to its own secrets file (or `None` to skip loading):

```python
from envilder import Envilder
import os

env = os.getenv('APP_ENV', 'development')

# Resolve + inject into os.environ
Envilder.load(env, {
    'production': 'prod-secrets.json',
    'development': 'dev-secrets.json',
    'test': None,  # no secrets loaded
})
```

Resolve without injecting:

```python
secrets = Envilder.resolve_file(env, {
    'production': 'prod-secrets.json',
    'development': 'dev-secrets.json',
    'test': None,
})
```

Behaviour:

- If the environment maps to a file path, secrets are loaded from that file.
- If the environment maps to `None` or is not in the mapping, an empty dict
  is returned silently — no errors, no output.
- The environment name is stripped of leading/trailing whitespace before lookup.
- Empty or whitespace-only environment names raise `ValueError`.

### Secret validation

Opt-in validation ensures all resolved secrets have non-empty values:

```python
from envilder import Envilder, validate_secrets

secrets = Envilder.resolve_file('secrets-map.json')
validate_secrets(secrets)  # raises SecretValidationError if any value is empty
```

`validate_secrets()` checks that:

- The dictionary is not empty (raises `SecretValidationError` with empty `missing_keys`)
- Every value is non-None and non-whitespace (raises `SecretValidationError` listing the failing keys)
- Passes silently when all values are present

```python
from envilder import SecretValidationError, validate_secrets

try:
    validate_secrets(secrets)
except SecretValidationError as e:
    print(f"Missing: {', '.join(e.missing_keys)}")
```

### Advanced usage

Implement the `ISecretProvider` protocol to plug in a custom backend
(e.g., HashiCorp Vault, GCP Secret Manager):

```python
from envilder import EnvilderClient, ISecretProvider, MapFileParser


class MyCustomProvider(ISecretProvider):
    def get_secret(self, name: str) -> str | None:
        # fetch from your custom backend
        ...


with open('secrets-map.json', encoding='utf-8') as file:
    map_file = MapFileParser().parse(file.read())

provider = MyCustomProvider()
secrets = EnvilderClient(provider).resolve_secrets(map_file)
EnvilderClient.inject_into_environment(secrets)
```

## API Reference

### Static facade (`Envilder`)

| Method | Description |
|--------|-------------|
| `load(path)` | Resolve secrets and inject into `os.environ` |
| `resolve_file(path)` | Resolve secrets, return as `dict` |
| `load(env, mapping)` | Environment-based resolve + inject |
| `resolve_file(env, mapping)` | Environment-based resolve |
| `from_map_file(path)` | Returns fluent builder for configuration |

### Fluent builder (via `from_map_file()`)

| Method | Description |
|--------|-------------|
| `with_provider(type)` | Override secret provider (AWS/Azure) |
| `with_profile(name)` | Override AWS named profile |
| `with_vault_url(url)` | Override Azure Key Vault URL |
| `resolve()` | Resolve secrets, return as dict |
| `inject()` | Resolve + inject into `os.environ` |

### Validation

| Function | Description |
|----------|-------------|
| `validate_secrets(dict)` | Raises `SecretValidationError` if any value is empty or dict is empty |

## Map File Format

```json
{
  "$schema": "https://envilder.com/schema/map-file.v1.json",
  "$config": {
    "provider": "aws",
    "profile": "my-profile"
  },
  "DB_PASSWORD": "/app/prod/db-password",
  "API_KEY": "/app/prod/api-key"
}
```

Supported providers: `aws` (default), `azure`.

For Azure, add `vaultUrl`:

```json
{
  "$schema": "https://envilder.com/schema/map-file.v1.json",
  "$config": {
    "provider": "azure",
    "vaultUrl": "https://my-vault.vault.azure.net"
  },
  "DB_PASSWORD": "db-password",
  "API_KEY": "api-key"
}
```

## Links

- [Changelog](https://github.com/macalbert/envilder/blob/main/docs/changelogs/sdk-python.md)
- [Official Website](https://envilder.com)

## License

MIT

## Development

### Setup

```bash
# From the repo root
make install-sdk-python
```

### Quality checks

```bash
make check-sdk-python    # black + isort + mypy (no changes)
make format-sdk-python   # auto-format
```

### Running tests

Unit tests run without any external dependencies:

```bash
cd tests/sdks/python
python -m pytest -v -m "not acceptance"
```

Acceptance tests require **Docker** and a **LocalStack auth token**:

```bash
export LOCALSTACK_AUTH_TOKEN=<your-token>
cd tests/sdks/python
python -m pytest -v -m acceptance
```

All tests:

```bash
make test-sdk-python
```
