mongrator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.3
2
+ Name: mongrator
3
+ Version: 0.1.0
4
+ Summary: Lightweight MongoDB schema migration tool
5
+ Author: Sasha Gerrand
6
+ Author-email: Sasha Gerrand <mongrator@sgerrand.dev>
7
+ Requires-Dist: pymongo>=4.10
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ # mongrator
12
+
13
+ [![CI](https://github.com/sgerrand/pymongrator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sgerrand/pymongrator/actions/workflows/ci.yml)
14
+
15
+ Lightweight MongoDB schema migration tool with synchronous and asynchronous PyMongo support.
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ pip install mongrator
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```sh
26
+ # Create config and migrations directory
27
+ mongrator init
28
+
29
+ # Generate a new migration file
30
+ mongrator create add_users_email_index
31
+
32
+ # Check migration status
33
+ mongrator status
34
+
35
+ # Apply pending migrations
36
+ mongrator up
37
+
38
+ # Roll back the last migration
39
+ mongrator down
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ `mongrator init` creates a `mongrator.toml` stub:
45
+
46
+ ```toml
47
+ uri = "mongodb://localhost:27017"
48
+ database = "mydb"
49
+ migrations_dir = "migrations"
50
+ collection = "mongrator_migrations" # optional
51
+ ```
52
+
53
+ Alternatively, configure via environment variables:
54
+
55
+ | Variable | Description | Required |
56
+ |----------|-------------|----------|
57
+ | `MONGRATOR_URI` | MongoDB connection URI | yes |
58
+ | `MONGRATOR_DB` | Database name | yes |
59
+ | `MONGRATOR_MIGRATIONS_DIR` | Path to migrations directory | no (default: `migrations`) |
60
+ | `MONGRATOR_COLLECTION` | Tracking collection name | no (default: `mongrator_migrations`) |
61
+
62
+ ## Writing migrations
63
+
64
+ Migration files are plain Python named `{timestamp}_{slug}.py` (e.g. `20260408_143022_add_users_email_index.py`). Each file must define an `up(db)` function. A `down(db)` function is optional but enables rollback.
65
+
66
+ ### Using the ops helpers (recommended)
67
+
68
+ The `ops` helpers record their own inverses, so `down()` is generated automatically:
69
+
70
+ ```python
71
+ from mongrator import ops
72
+
73
+ def up(db):
74
+ return [
75
+ ops.create_index("users", {"email": 1}, unique=True),
76
+ ops.rename_field("users", "username", "handle"),
77
+ ops.add_field("users", "verified", default_value=False),
78
+ ]
79
+ ```
80
+
81
+ ### Using plain PyMongo
82
+
83
+ For complex logic, write directly against the `db` argument and define `down()` manually:
84
+
85
+ ```python
86
+ def up(db):
87
+ db["orders"].update_many(
88
+ {"status": {"$exists": False}},
89
+ {"$set": {"status": "pending"}},
90
+ )
91
+
92
+ def down(db):
93
+ db["orders"].update_many({}, {"$unset": {"status": ""}})
94
+ ```
95
+
96
+ ### Available ops helpers
97
+
98
+ | Helper | Reversible | Description |
99
+ |--------|-----------|-------------|
100
+ | `ops.create_index(collection, keys, **kwargs)` | yes | Create an index |
101
+ | `ops.drop_index(collection, index_name)` | no | Drop an index by name |
102
+ | `ops.rename_field(collection, old, new, filter=None)` | yes | Rename a field across documents |
103
+ | `ops.add_field(collection, field, default_value, filter=None)` | yes | Add a field with a default value |
104
+
105
+ ## CLI reference
106
+
107
+ ```
108
+ mongrator init create migrations dir and mongrator.toml
109
+ mongrator create <name> generate a new migration file
110
+ mongrator status show applied/pending migrations
111
+ mongrator up [--target ID] apply pending migrations
112
+ mongrator up --async [--target ID] apply using async runner
113
+ mongrator down [--steps N] roll back N migrations (default: 1)
114
+ mongrator down --async [--steps N] roll back using async runner
115
+ mongrator validate verify checksums of applied migrations
116
+ mongrator --config PATH <command> use an alternate config file
117
+ ```
118
+
119
+ ## Async usage
120
+
121
+ Pass `--async` to `up` or `down` to use the async runner (backed by `pymongo.AsyncMongoClient`):
122
+
123
+ ```sh
124
+ mongrator up --async
125
+ ```
126
+
127
+ To use the runners programmatically:
128
+
129
+ ```python
130
+ # Synchronous
131
+ from pathlib import Path
132
+ import pymongo
133
+ from mongrator.config import MigratorConfig
134
+ from mongrator.runner import SyncRunner
135
+
136
+ config = MigratorConfig(uri="mongodb://localhost:27017", database="mydb", migrations_dir=Path("migrations"))
137
+ runner = SyncRunner(pymongo.MongoClient(config.uri), config)
138
+ runner.up()
139
+
140
+ # Asynchronous
141
+ from pymongo import AsyncMongoClient
142
+ from mongrator.runner import AsyncRunner
143
+
144
+ runner = AsyncRunner(AsyncMongoClient(config.uri), config)
145
+ await runner.up()
146
+ ```
147
+
148
+ ## Migration tracking
149
+
150
+ Applied migrations are recorded in the `mongrator_migrations` collection (configurable) within the target database. Each document stores:
151
+
152
+ - `_id` — migration file stem
153
+ - `applied_at` — UTC timestamp
154
+ - `checksum` — SHA-256 of the migration file at time of application
155
+ - `direction` — `"up"` or `"down"`
156
+ - `duration_ms` — execution time in milliseconds
157
+
158
+ Running `mongrator validate` compares current file checksums against recorded values and reports any modifications.
@@ -0,0 +1,148 @@
1
+ # mongrator
2
+
3
+ [![CI](https://github.com/sgerrand/pymongrator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sgerrand/pymongrator/actions/workflows/ci.yml)
4
+
5
+ Lightweight MongoDB schema migration tool with synchronous and asynchronous PyMongo support.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install mongrator
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```sh
16
+ # Create config and migrations directory
17
+ mongrator init
18
+
19
+ # Generate a new migration file
20
+ mongrator create add_users_email_index
21
+
22
+ # Check migration status
23
+ mongrator status
24
+
25
+ # Apply pending migrations
26
+ mongrator up
27
+
28
+ # Roll back the last migration
29
+ mongrator down
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ `mongrator init` creates a `mongrator.toml` stub:
35
+
36
+ ```toml
37
+ uri = "mongodb://localhost:27017"
38
+ database = "mydb"
39
+ migrations_dir = "migrations"
40
+ collection = "mongrator_migrations" # optional
41
+ ```
42
+
43
+ Alternatively, configure via environment variables:
44
+
45
+ | Variable | Description | Required |
46
+ |----------|-------------|----------|
47
+ | `MONGRATOR_URI` | MongoDB connection URI | yes |
48
+ | `MONGRATOR_DB` | Database name | yes |
49
+ | `MONGRATOR_MIGRATIONS_DIR` | Path to migrations directory | no (default: `migrations`) |
50
+ | `MONGRATOR_COLLECTION` | Tracking collection name | no (default: `mongrator_migrations`) |
51
+
52
+ ## Writing migrations
53
+
54
+ Migration files are plain Python named `{timestamp}_{slug}.py` (e.g. `20260408_143022_add_users_email_index.py`). Each file must define an `up(db)` function. A `down(db)` function is optional but enables rollback.
55
+
56
+ ### Using the ops helpers (recommended)
57
+
58
+ The `ops` helpers record their own inverses, so `down()` is generated automatically:
59
+
60
+ ```python
61
+ from mongrator import ops
62
+
63
+ def up(db):
64
+ return [
65
+ ops.create_index("users", {"email": 1}, unique=True),
66
+ ops.rename_field("users", "username", "handle"),
67
+ ops.add_field("users", "verified", default_value=False),
68
+ ]
69
+ ```
70
+
71
+ ### Using plain PyMongo
72
+
73
+ For complex logic, write directly against the `db` argument and define `down()` manually:
74
+
75
+ ```python
76
+ def up(db):
77
+ db["orders"].update_many(
78
+ {"status": {"$exists": False}},
79
+ {"$set": {"status": "pending"}},
80
+ )
81
+
82
+ def down(db):
83
+ db["orders"].update_many({}, {"$unset": {"status": ""}})
84
+ ```
85
+
86
+ ### Available ops helpers
87
+
88
+ | Helper | Reversible | Description |
89
+ |--------|-----------|-------------|
90
+ | `ops.create_index(collection, keys, **kwargs)` | yes | Create an index |
91
+ | `ops.drop_index(collection, index_name)` | no | Drop an index by name |
92
+ | `ops.rename_field(collection, old, new, filter=None)` | yes | Rename a field across documents |
93
+ | `ops.add_field(collection, field, default_value, filter=None)` | yes | Add a field with a default value |
94
+
95
+ ## CLI reference
96
+
97
+ ```
98
+ mongrator init create migrations dir and mongrator.toml
99
+ mongrator create <name> generate a new migration file
100
+ mongrator status show applied/pending migrations
101
+ mongrator up [--target ID] apply pending migrations
102
+ mongrator up --async [--target ID] apply using async runner
103
+ mongrator down [--steps N] roll back N migrations (default: 1)
104
+ mongrator down --async [--steps N] roll back using async runner
105
+ mongrator validate verify checksums of applied migrations
106
+ mongrator --config PATH <command> use an alternate config file
107
+ ```
108
+
109
+ ## Async usage
110
+
111
+ Pass `--async` to `up` or `down` to use the async runner (backed by `pymongo.AsyncMongoClient`):
112
+
113
+ ```sh
114
+ mongrator up --async
115
+ ```
116
+
117
+ To use the runners programmatically:
118
+
119
+ ```python
120
+ # Synchronous
121
+ from pathlib import Path
122
+ import pymongo
123
+ from mongrator.config import MigratorConfig
124
+ from mongrator.runner import SyncRunner
125
+
126
+ config = MigratorConfig(uri="mongodb://localhost:27017", database="mydb", migrations_dir=Path("migrations"))
127
+ runner = SyncRunner(pymongo.MongoClient(config.uri), config)
128
+ runner.up()
129
+
130
+ # Asynchronous
131
+ from pymongo import AsyncMongoClient
132
+ from mongrator.runner import AsyncRunner
133
+
134
+ runner = AsyncRunner(AsyncMongoClient(config.uri), config)
135
+ await runner.up()
136
+ ```
137
+
138
+ ## Migration tracking
139
+
140
+ Applied migrations are recorded in the `mongrator_migrations` collection (configurable) within the target database. Each document stores:
141
+
142
+ - `_id` — migration file stem
143
+ - `applied_at` — UTC timestamp
144
+ - `checksum` — SHA-256 of the migration file at time of application
145
+ - `direction` — `"up"` or `"down"`
146
+ - `duration_ms` — execution time in milliseconds
147
+
148
+ Running `mongrator validate` compares current file checksums against recorded values and reports any modifications.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "mongrator"
3
+ version = "0.1.0"
4
+ description = "Lightweight MongoDB schema migration tool"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Sasha Gerrand", email = "mongrator@sgerrand.dev" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "pymongo>=4.10",
12
+ ]
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ "pytest>=8.3",
17
+ "pytest-asyncio>=0.24",
18
+ "testcontainers[mongodb]>=4.0",
19
+ "ty",
20
+ "ruff>=0.9",
21
+ ]
22
+
23
+ [project.scripts]
24
+ mongrator = "mongrator:main"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.8.20,<0.9.0"]
28
+ build-backend = "uv_build"
29
+
30
+ [tool.pytest.ini_options]
31
+ asyncio_mode = "auto"
32
+
33
+ [tool.ruff]
34
+ line-length = 120
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "F", "I", "UP"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,40 @@
1
+ """Migration: {slug}
2
+
3
+ Generated by mongrator on {timestamp}.
4
+
5
+ Use the ops helpers for common reversible operations, or write plain PyMongo
6
+ calls directly against the `db` argument for anything more complex.
7
+
8
+ Example using ops helpers (auto-rollback supported):
9
+
10
+ from mongrator import ops
11
+
12
+ def up(db):
13
+ return [
14
+ ops.create_index("my_collection", {{"field": 1}}, unique=True),
15
+ ]
16
+
17
+ Example using plain PyMongo (define down() for rollback):
18
+
19
+ def up(db):
20
+ db["my_collection"].update_many({{}}, {{"$set": {{"new_field": None}}}})
21
+
22
+ def down(db):
23
+ db["my_collection"].update_many({{}}, {{"$unset": {{"new_field": ""}}}})
24
+ """
25
+
26
+
27
+ # Uncomment to use the ops helpers:
28
+ # from mongrator import ops
29
+
30
+
31
+ def up(db):
32
+ pass
33
+ # return [
34
+ # ops.create_index("my_collection", {{"field": 1}}),
35
+ # ]
36
+
37
+
38
+ # def down(db):
39
+ # """Optional: define rollback logic here if not using ops helpers."""
40
+ # pass
@@ -0,0 +1,256 @@
1
+ """Command-line interface for mongrator.
2
+
3
+ Subcommands:
4
+ init — create the migrations directory and a mongrator.toml stub
5
+ create — generate a new timestamped migration file
6
+ status — show applied/pending migration table
7
+ up — apply pending migrations
8
+ down — roll back applied migrations
9
+ validate — verify checksums of applied migrations
10
+ """
11
+
12
+ import argparse
13
+ import asyncio
14
+ import sys
15
+ from datetime import UTC, datetime
16
+ from importlib.resources import files
17
+ from pathlib import Path
18
+
19
+ from .config import MigratorConfig
20
+ from .exceptions import MigratorError
21
+
22
+
23
+ def _build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="mongrator",
26
+ description="Lightweight MongoDB schema migration tool",
27
+ )
28
+ parser.add_argument(
29
+ "--config",
30
+ metavar="PATH",
31
+ default="mongrator.toml",
32
+ help="path to config file (default: mongrator.toml)",
33
+ )
34
+
35
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND")
36
+ sub.required = True
37
+
38
+ # init
39
+ sub.add_parser("init", help="create migrations directory and config stub")
40
+
41
+ # create
42
+ p_create = sub.add_parser("create", help="generate a new migration file")
43
+ p_create.add_argument("name", help="short description, e.g. add_users_email_index")
44
+
45
+ # status
46
+ sub.add_parser("status", help="show applied/pending migration table")
47
+
48
+ # up
49
+ p_up = sub.add_parser("up", help="apply pending migrations")
50
+ p_up.add_argument("--target", metavar="ID", help="apply only up to this migration ID")
51
+ p_up.add_argument("--async", dest="use_async", action="store_true", help="use async runner")
52
+
53
+ # down
54
+ p_down = sub.add_parser("down", help="roll back applied migrations")
55
+ p_down.add_argument(
56
+ "--steps", type=int, default=1, metavar="N", help="number of migrations to roll back (default: 1)"
57
+ )
58
+ p_down.add_argument("--async", dest="use_async", action="store_true", help="use async runner")
59
+
60
+ # validate
61
+ sub.add_parser("validate", help="verify checksums of applied migration files")
62
+
63
+ return parser
64
+
65
+
66
+ def _load_config(args: argparse.Namespace) -> MigratorConfig:
67
+ config_path = Path(args.config)
68
+ if config_path.exists():
69
+ return MigratorConfig.from_toml(config_path)
70
+ return MigratorConfig.from_env()
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Subcommand handlers
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ def _cmd_init(args: argparse.Namespace) -> int:
79
+ config_path = Path(args.config)
80
+ if not config_path.exists():
81
+ config_path.write_text(
82
+ '[mongrator]\nuri = "mongodb://localhost:27017"\ndatabase = "mydb"\n'
83
+ 'migrations_dir = "migrations"\ncollection = "mongrator_migrations"\n'
84
+ )
85
+ print(f"Created {config_path}")
86
+
87
+ migrations_dir = Path("migrations")
88
+ migrations_dir.mkdir(exist_ok=True)
89
+ print(f"Created {migrations_dir}/")
90
+ return 0
91
+
92
+
93
+ def _cmd_create(args: argparse.Namespace) -> int:
94
+ config = _load_config(args)
95
+ config.migrations_dir.mkdir(parents=True, exist_ok=True)
96
+
97
+ slug = args.name.strip().replace(" ", "_").lower()
98
+ timestamp = datetime.now(tz=UTC).strftime("%Y%m%d_%H%M%S")
99
+ filename = f"{timestamp}_{slug}.py"
100
+ dest = config.migrations_dir / filename
101
+
102
+ template_text = files("mongrator._templates").joinpath("migration.py.tmpl").read_text(encoding="utf-8")
103
+ content = template_text.format(slug=slug, timestamp=timestamp)
104
+ dest.write_text(content, encoding="utf-8")
105
+ print(f"Created {dest}")
106
+ return 0
107
+
108
+
109
+ def _cmd_status(args: argparse.Namespace) -> int:
110
+ from .runner import SyncRunner
111
+
112
+ config = _load_config(args)
113
+ try:
114
+ import pymongo
115
+ except ImportError:
116
+ print("error: pymongo is required. Install with: pip install pymongo", file=sys.stderr)
117
+ return 1
118
+
119
+ client = pymongo.MongoClient(config.uri)
120
+ runner = SyncRunner(client, config)
121
+ statuses = runner.status()
122
+
123
+ if not statuses:
124
+ print("No migrations found.")
125
+ return 0
126
+
127
+ col_width = max(len(s.id) for s in statuses) + 2
128
+ print(f"{'Migration':<{col_width}} {'Status':<10} {'Applied At'}")
129
+ print("-" * (col_width + 30))
130
+ for s in statuses:
131
+ state = "applied" if s.applied else "pending"
132
+ if s.applied and not s.checksum_ok:
133
+ state = "MODIFIED"
134
+ applied_at = s.applied_at.isoformat() if s.applied_at else "-"
135
+ print(f"{s.id:<{col_width}} {state:<10} {applied_at}")
136
+ return 0
137
+
138
+
139
+ def _cmd_up(args: argparse.Namespace) -> int:
140
+ config = _load_config(args)
141
+ if args.use_async:
142
+ return asyncio.run(_async_up(config, args.target))
143
+ import pymongo
144
+
145
+ from .runner import SyncRunner
146
+
147
+ client = pymongo.MongoClient(config.uri)
148
+ runner = SyncRunner(client, config)
149
+ applied = runner.up(target=args.target)
150
+ if applied:
151
+ for mid in applied:
152
+ print(f" applied {mid}")
153
+ else:
154
+ print("Nothing to apply.")
155
+ return 0
156
+
157
+
158
+ async def _async_up(config: MigratorConfig, target: str | None) -> int:
159
+ from pymongo import AsyncMongoClient
160
+
161
+ from .runner import AsyncRunner
162
+
163
+ client = AsyncMongoClient(config.uri)
164
+ runner = AsyncRunner(client, config)
165
+ applied = await runner.up(target=target)
166
+ if applied:
167
+ for mid in applied:
168
+ print(f" applied {mid}")
169
+ else:
170
+ print("Nothing to apply.")
171
+ return 0
172
+
173
+
174
+ def _cmd_down(args: argparse.Namespace) -> int:
175
+ config = _load_config(args)
176
+ if args.use_async:
177
+ return asyncio.run(_async_down(config, args.steps))
178
+ import pymongo
179
+
180
+ from .runner import SyncRunner
181
+
182
+ client = pymongo.MongoClient(config.uri)
183
+ runner = SyncRunner(client, config)
184
+ rolled_back = runner.down(steps=args.steps)
185
+ if rolled_back:
186
+ for mid in rolled_back:
187
+ print(f" rolled back {mid}")
188
+ else:
189
+ print("Nothing to roll back.")
190
+ return 0
191
+
192
+
193
+ async def _async_down(config: MigratorConfig, steps: int) -> int:
194
+ from pymongo import AsyncMongoClient
195
+
196
+ from .runner import AsyncRunner
197
+
198
+ client = AsyncMongoClient(config.uri)
199
+ runner = AsyncRunner(client, config)
200
+ rolled_back = await runner.down(steps=steps)
201
+ if rolled_back:
202
+ for mid in rolled_back:
203
+ print(f" rolled back {mid}")
204
+ else:
205
+ print("Nothing to roll back.")
206
+ return 0
207
+
208
+
209
+ def _cmd_validate(args: argparse.Namespace) -> int:
210
+ import pymongo
211
+
212
+ from .runner import SyncRunner
213
+
214
+ config = _load_config(args)
215
+ client = pymongo.MongoClient(config.uri)
216
+ runner = SyncRunner(client, config)
217
+ errors = runner.validate()
218
+
219
+ if not errors:
220
+ print("All applied migrations have valid checksums.")
221
+ return 0
222
+
223
+ eg = ExceptionGroup("Checksum mismatches detected", errors)
224
+ print(f"error: {eg}", file=sys.stderr)
225
+ for e in errors:
226
+ print(f" {e}", file=sys.stderr)
227
+ return 1
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Entry point
232
+ # ---------------------------------------------------------------------------
233
+
234
+
235
+ def main() -> None:
236
+ parser = _build_parser()
237
+ args = parser.parse_args()
238
+
239
+ dispatch = {
240
+ "init": _cmd_init,
241
+ "create": _cmd_create,
242
+ "status": _cmd_status,
243
+ "up": _cmd_up,
244
+ "down": _cmd_down,
245
+ "validate": _cmd_validate,
246
+ }
247
+
248
+ try:
249
+ rc = dispatch[args.command](args)
250
+ except MigratorError as e:
251
+ print(f"error: {e}", file=sys.stderr)
252
+ sys.exit(1)
253
+ except KeyboardInterrupt:
254
+ sys.exit(130)
255
+
256
+ sys.exit(rc)
@@ -0,0 +1,61 @@
1
+ import os
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Self
6
+
7
+ from .exceptions import ConfigurationError
8
+
9
+ _DEFAULT_COLLECTION = "mongrator_migrations"
10
+ _DEFAULT_MIGRATIONS_DIR = Path("migrations")
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MigratorConfig:
15
+ """Immutable configuration for a migrator instance."""
16
+
17
+ uri: str
18
+ database: str
19
+ migrations_dir: Path
20
+ collection: str = _DEFAULT_COLLECTION
21
+
22
+ @classmethod
23
+ def from_toml(cls, path: Path) -> Self:
24
+ """Load configuration from a TOML file (e.g. mongrator.toml)."""
25
+ try:
26
+ with open(path, "rb") as f:
27
+ data = tomllib.load(f)
28
+ except FileNotFoundError:
29
+ raise ConfigurationError(f"Config file not found: {path}")
30
+ except tomllib.TOMLDecodeError as e:
31
+ raise ConfigurationError(f"Invalid TOML in {path}: {e}")
32
+
33
+ try:
34
+ uri: str = data["uri"]
35
+ database: str = data["database"]
36
+ except KeyError as e:
37
+ raise ConfigurationError(f"Missing required config key: {e}")
38
+
39
+ migrations_dir = Path(data.get("migrations_dir", str(_DEFAULT_MIGRATIONS_DIR)))
40
+ collection: str = data.get("collection", _DEFAULT_COLLECTION)
41
+ return cls(uri=uri, database=database, migrations_dir=migrations_dir, collection=collection)
42
+
43
+ @classmethod
44
+ def from_env(cls) -> Self:
45
+ """Load configuration from environment variables.
46
+
47
+ Variables:
48
+ MONGRATOR_URI — MongoDB connection URI (required)
49
+ MONGRATOR_DB — database name (required)
50
+ MONGRATOR_MIGRATIONS_DIR — path to migrations directory (default: migrations)
51
+ MONGRATOR_COLLECTION — tracking collection name (default: mongrator_migrations)
52
+ """
53
+ uri = os.environ.get("MONGRATOR_URI")
54
+ database = os.environ.get("MONGRATOR_DB")
55
+ if not uri:
56
+ raise ConfigurationError("MONGRATOR_URI environment variable is not set")
57
+ if not database:
58
+ raise ConfigurationError("MONGRATOR_DB environment variable is not set")
59
+ migrations_dir = Path(os.environ.get("MONGRATOR_MIGRATIONS_DIR", str(_DEFAULT_MIGRATIONS_DIR)))
60
+ collection = os.environ.get("MONGRATOR_COLLECTION", _DEFAULT_COLLECTION)
61
+ return cls(uri=uri, database=database, migrations_dir=migrations_dir, collection=collection)