# PySafeConfigX

**Secure API key and configuration management for Python developers.**

[![CI](https://github.com/yourusername/pysafeconfigx/actions/workflows/ci.yml/badge.svg)](https://github.com/yourusername/pysafeconfigx/actions)
[![PyPI version](https://badge.fury.io/py/pysafeconfigx.svg)](https://badge.fury.io/py/pysafeconfigx)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
[![codecov](https://codecov.io/gh/yourusername/pysafeconfigx/branch/main/graph/badge.svg)](https://codecov.io/gh/yourusername/pysafeconfigx)

---

PySafeConfigX solves the configuration management problem once and for all:

- **Multi-source loading** — .env, JSON, YAML, and OS environment with priority merging
- **AES-256-GCM encryption** — protect sensitive values at rest with one command
- **Multi-environment** — development / staging / production out of the box
- **Required-key validation** — fail fast at startup, not at runtime
- **CLI tool** — `pysafeconfigx init`, `set`, `get`, `encrypt`, `doctor`
- **Source-code scanner** — detect accidentally committed secrets
- **Fully typed** — complete type hints, ships a `py.typed` marker
- **Zero magic** — explicit, auditable, and predictable

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration Sources](#configuration-sources)
- [Encryption](#encryption)
- [Multi-Environment](#multi-environment)
- [CLI Reference](#cli-reference)
- [API Reference](#api-reference)
- [Publishing to PyPI](#publishing-to-pypi)
- [Contributing](#contributing)

---

## Installation

```bash
pip install pysafeconfigx
```

For development (includes linting, type checking, and test tools):

```bash
git clone https://github.com/yourusername/pysafeconfigx
cd pysafeconfigx
pip install -e ".[dev]"
pre-commit install
```

**Dependencies:** `cryptography`, `python-dotenv`, `pyyaml`, `click`, `rich`, `platformdirs`

---

## Quick Start

### 1. Initialise your project

```bash
pysafeconfigx init
```

This creates a `.env` file and generates a master encryption key.  Copy the
key to a safe location (password manager, secret vault).

```
🔑  Master Key
────────────────────────────────────────
Your new master key

abc123XYZ...  ← store this securely!
────────────────────────────────────────
✔  Created .env
```

### 2. Add your secrets

```bash
pysafeconfigx set DATABASE_URL postgres://localhost/mydb
pysafeconfigx set API_KEY sk-prod-very-secret --encrypt   # stored as AES-256 ciphertext
```

### 3. Use in your application

```python
from pysafeconfigx import PySafeConfigX

config = PySafeConfigX(
    environment="production",
    required_keys=["DATABASE_URL", "API_KEY"],
)
config.load()

db_url  = config.get("DATABASE_URL")
api_key = config.get("API_KEY", decrypt=True)   # transparent decryption
workers = config.get_int("MAX_WORKERS", default=4)
debug   = config.get_bool("DEBUG", default=False)
hosts   = config.get_list("ALLOWED_HOSTS")
```

### 4. Diagnose problems

```bash
pysafeconfigx doctor
```

```
┌────────────────────────────────────────┬──────────┬───────────────────────────────┐
│ Check                                  │ Status   │ Detail                        │
├────────────────────────────────────────┼──────────┼───────────────────────────────┤
│ Master key (env var)                   │ ✔ OK     │ Valid key found in environment │
│ .env file (.env)                       │ ✔ OK     │ File found                    │
│ .gitignore                             │ ✔ OK     │ .env is ignored               │
│ Source code scan                       │ ✔ OK     │ No exposed secrets in 12 file  │
└────────────────────────────────────────┴──────────┴───────────────────────────────┘
All checks passed! Your configuration looks healthy.
```

---

## Configuration Sources

PySafeConfigX merges configuration from multiple sources.  **Priority order
(highest → lowest):**

1. OS environment variables
2. Custom loaders (added via `add_loader()`, in reverse order of registration)
3. Environment-specific .env file (e.g. `.env.production`)
4. Base `.env` file

```python
from pysafeconfigx import PySafeConfigX
from pysafeconfigx.core.loaders import DotEnvLoader, JsonLoader, YamlLoader

config = PySafeConfigX()
config.add_json("config/database.json")
config.add_yaml("config/features.yaml")
config.add_loader(DotEnvLoader(".env.local"))
config.load()
```

### .env file

```dotenv
# .env
DATABASE_URL=postgres://localhost/mydb
API_KEY=enc:AQID...   # encrypted blob — auto-decrypted on get()
DEBUG=true
MAX_WORKERS=4
ALLOWED_HOSTS=localhost,127.0.0.1
```

### JSON (nested keys are flattened with `.`)

```json
{
  "redis": { "host": "localhost", "port": 6379 },
  "s3":    { "bucket": "my-bucket", "region": "us-east-1" }
}
```

```python
config.get("redis.host")    # "localhost"
config.get_int("redis.port") # 6379
```

### YAML (same flattening rules)

```yaml
redis:
  host: localhost
  port: 6379
feature_flags:
  beta_signup: true
```

---

## Encryption

PySafeConfigX uses **AES-256-GCM** (authenticated encryption) with PBKDF2-HMAC-SHA256
key derivation.  Each encrypted value carries its own random salt and nonce,
so encrypting the same plaintext twice always produces different ciphertexts.

### Generate a master key

```bash
pysafeconfigx init          # generates a key and writes it to .env
# or:
python -c "from pysafeconfigx import PySafeConfigX; print(PySafeConfigX.generate_master_key())"
```

Store the key in `SAFECONFIG_MASTER_KEY` (env var or CI/CD secret).
**Never commit it.**

### Encrypt from the CLI

```bash
# Encrypt an existing plaintext value in .env (in place):
pysafeconfigx encrypt SECRET_API_KEY

# Set a new value as encrypted:
pysafeconfigx set STRIPE_SECRET sk_live_... --encrypt
```

### Encrypt from Python

```python
config = PySafeConfigX(master_key="your-key")
config.load()

blob = config.encrypt("PLAINTEXT_KEY")      # updates the in-memory store
value = config.get("PLAINTEXT_KEY", decrypt=True)
```

### Encrypted blob format

```
<version:1B> <salt:16B> <nonce:12B> <ciphertext+tag:NB>
```

encoded as URL-safe base64.  Values stored in .env have an `enc:` prefix so
PySafeConfigX auto-decrypts them on `get()`.

---

## Multi-Environment

```python
# Reads APP_ENV from the environment, defaulting to "development"
config = PySafeConfigX(environment="production", config_dir="config/")
config.load()
```

PySafeConfigX automatically loads `.env` **and** `.env.production`.  The
environment-specific file takes precedence.

Supported environments: `development`, `staging`, `production`, `test`, `local`.

---

## CLI Reference

```
Usage: pysafeconfigx [OPTIONS] COMMAND [ARGS]...

Options:
  --env-file TEXT  Path to the .env file  [default: .env]
  -v, --verbose    Enable verbose logging
  --help           Show this message and exit.

Commands:
  init     Scaffold a .env file and generate a master encryption key.
  set      Set KEY to VALUE in the .env file.
  get      Print the value of KEY from the .env file.
  encrypt  Encrypt an existing plaintext value in the .env file.
  doctor   Diagnose common configuration issues.
```

### `pysafeconfigx init`

```bash
pysafeconfigx init
pysafeconfigx init -k DATABASE_URL -k API_KEY   # pre-populate template keys
pysafeconfigx init --force                       # overwrite existing .env
```

### `pysafeconfigx set`

```bash
pysafeconfigx set DATABASE_URL postgres://localhost/mydb
pysafeconfigx set SECRET sk_live_abc123 --encrypt
pysafeconfigx --env-file .env.production set LOG_LEVEL WARNING
```

### `pysafeconfigx get`

```bash
pysafeconfigx get DATABASE_URL
pysafeconfigx get SECRET           # auto-decrypts enc: values
pysafeconfigx get SECRET --raw     # print encrypted blob without decrypting
```

### `pysafeconfigx encrypt`

```bash
pysafeconfigx encrypt SECRET_API_KEY          # encrypt in-place in .env
pysafeconfigx encrypt SECRET --no-in-place   # print blob, don't modify file
```

### `pysafeconfigx doctor`

```bash
pysafeconfigx doctor
pysafeconfigx doctor --scan-dir src/    # scan specific directory for exposed secrets
```

---

## API Reference

### `PySafeConfigX`

```python
PySafeConfigX(
    environment="development",     # or read from APP_ENV
    required_keys=["DB_URL"],      # raise MissingRequiredKeyError if absent
    config_dir=".",                # where to look for .env files
    master_key=None,               # or read from SAFECONFIG_MASTER_KEY
    auto_load_env=True,            # auto-register .env and .env.<environment>
    strict=False,                  # raise KeyNotFoundError instead of returning None
)
```

| Method | Description |
|--------|-------------|
| `.load(force=False)` | Load all sources and validate required keys |
| `.reload()` | Force a full reload |
| `.get(key, default, decrypt=False)` | Get a value |
| `.require(key, decrypt=False)` | Get a value, raise if absent |
| `.get_int(key, default)` | Get as integer |
| `.get_bool(key, default)` | Get as boolean |
| `.get_list(key, default, separator=",")` | Get as list |
| `.set(key, value, encrypt=False)` | Set in-memory value |
| `.encrypt(key)` | Encrypt in-place in the in-memory store |
| `.add_loader(loader)` | Register a custom loader |
| `.add_json(path)` | Add a JSON source |
| `.add_yaml(path)` | Add a YAML source |
| `.all()` | Return all loaded key-value pairs |
| `.keys()` | Return sorted list of keys |
| `PySafeConfigX.generate_master_key()` | Generate a new master key |

### Exceptions

| Exception | Raised when |
|-----------|-------------|
| `ConfigurationError` | Base class for all library errors |
| `KeyNotFoundError` | Key not found (in strict mode or via `.require()`) |
| `MissingRequiredKeyError` | Required key absent after load |
| `EncryptionError` | Encryption failed |
| `DecryptionError` | Decryption failed (wrong key or corrupted value) |
| `InvalidEnvironmentError` | Unknown environment name |
| `MasterKeyError` | Master key missing or malformed |
| `ExposedKeyWarning` | Hard-coded secret detected in source (warning, not error) |

---

## Publishing to PyPI

### Prerequisites

1. Register an account at [pypi.org](https://pypi.org/)
2. Install build tools: `pip install build twine`

### Manual publish

```bash
# 1. Bump version in pyproject.toml
# 2. Build distributions
python -m build

# 3. Upload to TestPyPI first (recommended)
twine upload --repository testpypi dist/*

# 4. Test the installation
pip install --index-url https://test.pypi.org/simple/ pysafeconfigx

# 5. Upload to PyPI
twine upload dist/*
```

### Automated publish via GitHub Actions

The included CI workflow publishes automatically when you push a version tag:

```bash
git tag v1.0.0
git push origin v1.0.0
```

Configure [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
in your PyPI project settings to avoid storing API tokens in GitHub Secrets.

---

## Contributing

```bash
# Clone and set up
git clone https://github.com/yourusername/pysafeconfigx
cd pysafeconfigx
pip install -e ".[dev]"
pre-commit install

# Run tests
pytest

# Run type checker
mypy src/pysafeconfigx/

# Run linter
ruff check src/ tests/
```

PRs are welcome!  Please open an issue first to discuss substantial changes.

---

## License

[MIT](LICENSE) © 2024 Your Name
