Metadata-Version: 2.4
Name: migrate-env
Version: 0.3.0
Summary: Django-style migrations for environment (.env) files
Author: player1537-playground
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/player1537-playground/migrate-env
Project-URL: Changelog, https://github.com/player1537-playground/migrate-env/releases
Project-URL: Issues, https://github.com/player1537-playground/migrate-env/issues
Project-URL: CI, https://github.com/player1537-playground/migrate-env/actions
Keywords: dotenv,env,migrations,django
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# migrate-env

[![PyPI](https://img.shields.io/pypi/v/migrate-env.svg)](https://pypi.org/project/migrate-env/)
[![Tests](https://github.com/player1537-playground/migrate-env/actions/workflows/test.yml/badge.svg)](https://github.com/player1537-playground/migrate-env/actions/workflows/test.yml)
[![Changelog](https://img.shields.io/github/v/release/player1537-playground/migrate-env?include_prereleases&label=changelog)](https://github.com/player1537-playground/migrate-env/releases)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/player1537-playground/migrate-env/blob/main/LICENSE)

Django-style migrations for environment files.

Your `.env.example` is the source of truth (like `models.py`). When it changes, `makemigrations` detects what was added and writes a numbered, reviewable migration. Each developer runs `migrate` against their own `.env`, which gains the new blocks and variables **without ever touching the values they already have**.

```
pip install migrate-env
```

## Quick start

```bash
# Detect changes in .env.example and write env_migrations/0001_initial.py
migrate_env makemigrations

# Apply pending migrations to your local .env (creates it if missing)
migrate_env migrate .env

# See what's applied
migrate_env showmigrations .env
```

Later, someone adds a Postgres section to `.env.example`:

```bash
migrate_env makemigrations --name postgres
# Created env_migrations/0002_postgres.py
#   - Create assignment block [WHYHC_POSTGRES_DB, WHYHC_POSTGRES_USER, ...]
```

Everyone else pulls and runs `migrate_env migrate .env`. Their custom secret keys, hosts, and passwords are untouched; the new block is appended.

## The data model

An env file is a sequence of **blocks**, and is byte-for-byte reconstructible from them:

- `CommentBlock` — leading blank lines + comment lines.
- `AssignmentBlock` — leading blank lines + comment lines + assignments.

An `Assignment` is `(Key, Value, Comment | Blank)` — the third element is the inline trailing comment (`'  # like this'`) or `''`. A commented-out assignment such as `# WHYHC_CONN_MAX_AGE=0` is parsed as an assignment with `enabled=False`, representing an **optional** variable. Prose comments that merely mention `FOO=bar` mid-sentence, or use spaces around `=`, remain comments.

```python
from migrate_env import parse

env = parse(open(".env.example").read())
env.render() == open(".env.example").read()  # True, always
```

## Migrations

Migrations are plain Python modules, ordered by numeric prefix with validated dependencies:

```python
# env_migrations/0002_postgres.py
from migrate_env import migrations, ops

class Migration(migrations.Migration):
    dependencies = ['0001_initial']
    operations = [
        ops.CreateAssignmentBlock(
            comments=['# PostgreSQL database connection'],
            assignments=[
                ops.Assignment(key='WHYHC_POSTGRES_DB', value='whyhc'),
                ops.Assignment(key='WHYHC_POSTGRES_PORT', value='5432'),
            ],
        ),
    ]
```

Operations are **additive and idempotent**:

- `CreateAssignmentBlock` / `CreateCommentBlock` — append a block (skipped if its content already exists).
- `AddAssignments` — append variables to an existing block, located by an anchor `key=` or `comment=`. Keys the user already has anywhere in their file are skipped, so values are never overwritten.
- `AddComments` — append documentation comments to an existing block.

Value changes and removals in `.env.example` are _reported as warnings_ by `makemigrations` but never auto-migrated — users own their values, and destructive changes deserve a hand-written migration.

Applied state per target file lives in a sidecar (`.env.migrations`, the analog of Django's `django_migrations` table). Add it to `.gitignore` alongside `.env`.

## Commands

| Command                                                            | Description                                                          |
| ------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `makemigrations [--example PATH] [--name N] [--dry-run] [--dir D]` | Diff the example against replayed migration state; write a migration |
| `migrate TARGET [--plan] [--dir D]`                                | Apply unapplied migrations to `TARGET`                               |
| `showmigrations TARGET [--dir D]`                                  | List migrations with applied status                                  |

## Development

To contribute to this library, first checkout the code. Then create a new virtual environment:

```bash
cd migrate-env
python -m venv venv
source venv/bin/activate
```

Now install the dependencies and test dependencies:

```bash
python -m pip install -e '.[dev]'
```

To run the tests:

```bash
python -m pytest
```

## License

Apache 2.0
