Metadata-Version: 2.5
Name: vaultref
Version: 0.1.0
Summary: Environment variables that refers a secret in a vault instead of holding one
Author-email: Xitmer <xitmerdev@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: azure,config,dotenv,env,key-vault,secrets
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Provides-Extra: azure
Requires-Dist: azure-identity>=1.19; extra == 'azure'
Requires-Dist: azure-keyvault-secrets>=4.9; extra == 'azure'
Provides-Extra: dotenv
Requires-Dist: python-dotenv>=1.0; extra == 'dotenv'
Description-Content-Type: text/markdown

# vaultref

Environment variables that refers a secret in a vault instead of holding one.

```ini
# .env — checked in, readable at a glance
APP_ENV=production
APP_MONGO_URL=AZKV_MONGODB-HOST     ← the vault holds the value
APP_SECRET_KEY=AZKV_APP-SIGNING-KEY
APP_PORT=8000

AZURE_KEY_VAULT_NAME=myvault
```

```python
import vaultref
vaultref.load()          # before anything reads config
```

Every value beginning with a registered marker is replaced by the secret it
names, written into `os.environ`, and read from there by whatever you already
use — pydantic-settings, Django settings, `os.environ[...]`. Nothing
downstream needs to know a vault exists.

## Install

```bash
pip install vaultref[azure]
```

## Where to call it

| Project | Put `vaultref.load()` … |
|---|---|
| pydantic-settings | above the first `Settings()` construction |
| Django | at the top of `settings.py`, before `env()` / `os.environ` reads |
| Flask | before `app.config.from_prefixed_env()` |
| Plain script / worker | first lines of `main`, before importing config |

It is idempotent and cached — calling it twice costs nothing.

## Behaviour

- **Only markers are exported.** `.env` is read for markers; the rest is left
  to your own reader. Pass `export_all=True` and it replaces python-dotenv
  entirely, resolving markers on the way through.
- **Real environment variables win** over `.env`, marker or not — matching
  pydantic-settings and django-environ, so a shell override still overrides.
- **A marker that will not resolve is fatal.** Starting on the built-in
  default means a localhost database or a freshly minted signing key: silent,
  total, discovered later. `strict=False` if you need a warning instead.
- **One round trip per secret**, cached for the life of the process. A rotated
  secret reaches the app on its next restart.
- **No marker anywhere ⇒ no vault contacted**, no client built, no credentials
  needed. Local development with plain values needs nothing installed.

## Azure Key Vault (`AZKV_`)

Vault from `AZURE_KEY_VAULT_NAME` (or `AZURE_KEY_VAULT_URL` for sovereign
clouds). Authentication is `DefaultAzureCredential`:

- **On Azure** — a managed identity with the **Key Vault Secrets User** role.
  Nothing in the environment.
- **Locally** — `az login`, with that role on your own account.
- **Elsewhere** — `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET`.
  These are read out of `.env` and exported for the SDK, which cannot see
  `.env` on its own.

Secrets only. A Key Vault *key* has no readable value by design — it signs and
wraps inside the vault and never leaves it.

## Another backend

Three lines, and it works everywhere the Azure one does:

```python
@vaultref.resolver("AWSSM_")
def aws_secrets_manager(name, env):
    return boto3.client("secretsmanager").get_secret_value(
        SecretId=name)["SecretString"]
```

```ini
APP_MONGO_URL=AWSSM_prod/mongo/url
```

Markers coexist — one project can read from two vaults during a migration.

## Tests

```bash
python -m unittest discover -s tests            # 11 tests, no vault needed
AZURE_KEY_VAULT_NAME=myvault   VAULTREF_TEST_SECRET=MY-SECRET python tests/live.py   # against a real vault
```
