isoladb 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.
- isoladb-0.1.0/PKG-INFO +312 -0
- isoladb-0.1.0/README.md +282 -0
- isoladb-0.1.0/pyproject.toml +63 -0
- isoladb-0.1.0/src/isoladb/__init__.py +35 -0
- isoladb-0.1.0/src/isoladb/_compat.py +50 -0
- isoladb-0.1.0/src/isoladb/_pg_proto.py +201 -0
- isoladb-0.1.0/src/isoladb/async_database.py +176 -0
- isoladb-0.1.0/src/isoladb/binary.py +223 -0
- isoladb-0.1.0/src/isoladb/config.py +33 -0
- isoladb-0.1.0/src/isoladb/database.py +183 -0
- isoladb-0.1.0/src/isoladb/exceptions.py +33 -0
- isoladb-0.1.0/src/isoladb/pytest_plugin.py +223 -0
- isoladb-0.1.0/src/isoladb/ramdisk.py +253 -0
- isoladb-0.1.0/src/isoladb/server.py +330 -0
isoladb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: isoladb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ephemeral PostgreSQL instances for unit testing
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: testing,postgresql,database,unit-testing,ephemeral
|
|
7
|
+
Author: Yegor Stepanov
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Provides-Extra: psycopg
|
|
23
|
+
Provides-Extra: pytest
|
|
24
|
+
Provides-Extra: sqlalchemy
|
|
25
|
+
Requires-Dist: psycopg[binary] (>=3.1) ; extra == "psycopg"
|
|
26
|
+
Requires-Dist: pytest (>=7.0) ; extra == "pytest"
|
|
27
|
+
Requires-Dist: sqlalchemy (>=2.0) ; extra == "sqlalchemy"
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# isoladb
|
|
31
|
+
|
|
32
|
+
Ephemeral PostgreSQL instances for unit testing. No pre-installed PostgreSQL required — just Python 3.8+.
|
|
33
|
+
|
|
34
|
+
isoladb downloads pre-built PostgreSQL binaries (or uses your system installation), starts an isolated server, creates per-test databases, and cleans up automatically.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install isoladb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
With pytest support:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install isoladb[pytest]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
With psycopg (PostgreSQL client):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install isoladb[psycopg]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import psycopg
|
|
58
|
+
from isoladb import IsolaDB
|
|
59
|
+
|
|
60
|
+
with IsolaDB() as db:
|
|
61
|
+
with psycopg.connect(db.url) as conn:
|
|
62
|
+
conn.execute("CREATE TABLE users (id serial PRIMARY KEY, name text)")
|
|
63
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
64
|
+
conn.commit()
|
|
65
|
+
result = conn.execute("SELECT name FROM users").fetchone()
|
|
66
|
+
assert result[0] == "Alice"
|
|
67
|
+
# Server and database are cleaned up automatically
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Each `IsolaDB()` context manager creates a fresh, isolated database. The underlying PostgreSQL server is shared and reused across invocations with the same configuration.
|
|
71
|
+
|
|
72
|
+
## Connection Properties
|
|
73
|
+
|
|
74
|
+
The context manager yields an object with these properties:
|
|
75
|
+
|
|
76
|
+
| Property | Description | Example |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `db.url` | Full connection URL | `postgresql://postgres@localhost/isoladb_test_a1b2c3?host=/tmp/pg_xyz&port=54321` |
|
|
79
|
+
| `db.host` | Unix socket directory | `/tmp/pg_xyz` |
|
|
80
|
+
| `db.port` | Server port | `54321` |
|
|
81
|
+
| `db.dbname` | Database name | `isoladb_test_a1b2c3` |
|
|
82
|
+
| `db.user` | Superuser name | `postgres` |
|
|
83
|
+
|
|
84
|
+
Works with any PostgreSQL client library:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# psycopg v3
|
|
88
|
+
conn = psycopg.connect(db.url)
|
|
89
|
+
|
|
90
|
+
# psycopg2
|
|
91
|
+
conn = psycopg2.connect(host=db.host, port=db.port, dbname=db.dbname, user=db.user)
|
|
92
|
+
|
|
93
|
+
# asyncpg
|
|
94
|
+
conn = await asyncpg.connect(host=db.host, port=db.port, database=db.dbname, user=db.user)
|
|
95
|
+
|
|
96
|
+
# SQLAlchemy
|
|
97
|
+
engine = create_engine(db.url)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## PostgreSQL Binary Resolution
|
|
101
|
+
|
|
102
|
+
By default, isoladb looks for PostgreSQL in this order:
|
|
103
|
+
|
|
104
|
+
1. **System PostgreSQL** — detected via `pg_ctl` on `PATH` (e.g., Homebrew, apt)
|
|
105
|
+
2. **Cached download** — previously downloaded binaries in `~/.cache/isoladb`
|
|
106
|
+
3. **Fresh download** — fetched from Maven Central (~50MB, cached for future use)
|
|
107
|
+
|
|
108
|
+
To always use downloaded binaries instead of a system installation:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with IsolaDB(use_system_pg=False) as db:
|
|
112
|
+
...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Schema and Setup
|
|
116
|
+
|
|
117
|
+
Apply a SQL schema file automatically after each database is created:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
with IsolaDB(schema="schema.sql") as db:
|
|
121
|
+
# Tables from schema.sql are already created
|
|
122
|
+
with psycopg.connect(db.url) as conn:
|
|
123
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or point to a directory of `.sql` files — they are sorted by filename and applied in order:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
migrations/
|
|
130
|
+
001_create_users.sql
|
|
131
|
+
002_create_posts.sql
|
|
132
|
+
003_seed_data.sql
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
with IsolaDB(schema="migrations/") as db:
|
|
137
|
+
# All .sql files applied in sorted order
|
|
138
|
+
...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Non-`.sql` files in the directory are ignored.
|
|
142
|
+
|
|
143
|
+
For programmatic initialization (e.g., Alembic migrations), use a setup callable:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def apply_migrations(url):
|
|
147
|
+
from alembic.config import Config
|
|
148
|
+
from alembic import command
|
|
149
|
+
cfg = Config("alembic.ini")
|
|
150
|
+
cfg.set_main_option("sqlalchemy.url", url)
|
|
151
|
+
command.upgrade(cfg, "head")
|
|
152
|
+
|
|
153
|
+
with IsolaDB(setup=apply_migrations) as db:
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Both can be combined — schema is applied first, then setup.
|
|
158
|
+
|
|
159
|
+
## RAM Disk
|
|
160
|
+
|
|
161
|
+
Run the PostgreSQL data directory on a RAM disk for faster I/O:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
with IsolaDB(ram=True) as db:
|
|
165
|
+
...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Uses tmpfs on Linux and hdiutil RAM disk on macOS. Falls back to a regular temp directory if RAM disk creation fails.
|
|
169
|
+
|
|
170
|
+
## Async Support
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from isoladb import AsyncIsolaDB
|
|
174
|
+
|
|
175
|
+
async with AsyncIsolaDB() as db:
|
|
176
|
+
conn = await asyncpg.connect(
|
|
177
|
+
host=db.host, port=db.port, database=db.dbname, user=db.user
|
|
178
|
+
)
|
|
179
|
+
await conn.execute("CREATE TABLE test (id serial PRIMARY KEY)")
|
|
180
|
+
await conn.close()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The async setup callable can be either sync or async:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
async def apply_migrations(url: str) -> None:
|
|
187
|
+
engine = create_async_engine(url)
|
|
188
|
+
async with engine.begin() as conn:
|
|
189
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
190
|
+
await engine.dispose()
|
|
191
|
+
|
|
192
|
+
async with AsyncIsolaDB(setup=apply_migrations) as db:
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Configuration
|
|
197
|
+
|
|
198
|
+
All options can be passed to `IsolaDB()` / `AsyncIsolaDB()`:
|
|
199
|
+
|
|
200
|
+
| Option | Default | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `pg_version` | `"17.2.0"` | PostgreSQL version (for downloaded binaries) |
|
|
203
|
+
| `ram` | `False` | Use RAM disk for the data directory |
|
|
204
|
+
| `ram_size_mb` | `256` | RAM disk size in megabytes |
|
|
205
|
+
| `use_system_pg` | `True` | Prefer system PostgreSQL over downloading |
|
|
206
|
+
| `schema` | `None` | Path to a SQL file or directory of `.sql` files |
|
|
207
|
+
| `setup` | `None` | Callable receiving the connection URL for custom setup |
|
|
208
|
+
| `cache_dir` | `~/.cache/isoladb` | Directory for cached PostgreSQL binaries |
|
|
209
|
+
| `startup_timeout` | `30.0` | Seconds to wait for the server to start |
|
|
210
|
+
| `pg_conf` | `{}` | Extra postgresql.conf settings as `{"key": "value"}` |
|
|
211
|
+
|
|
212
|
+
## Pytest Plugin
|
|
213
|
+
|
|
214
|
+
isoladb includes a pytest plugin that provides fixtures automatically when `isoladb[pytest]` is installed.
|
|
215
|
+
|
|
216
|
+
### Fixtures
|
|
217
|
+
|
|
218
|
+
**`isoladb`** — per-test fixture yielding an `IsolaDBConnection` with `.url`, `.host`, `.port`, `.dbname`:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
def test_users(isoladb):
|
|
222
|
+
with psycopg.connect(isoladb.url) as conn:
|
|
223
|
+
conn.execute("CREATE TABLE users (id serial PRIMARY KEY, name text)")
|
|
224
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
225
|
+
conn.commit()
|
|
226
|
+
result = conn.execute("SELECT count(*) FROM users").fetchone()
|
|
227
|
+
assert result[0] == 1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**`isoladb_engine`** — per-test fixture yielding a SQLAlchemy engine (requires `sqlalchemy`):
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
def test_with_engine(isoladb_engine):
|
|
234
|
+
with isoladb_engine.connect() as conn:
|
|
235
|
+
conn.execute(text("SELECT 1"))
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**`isoladb_async`** — per-test async fixture (requires `pytest-asyncio`):
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
async def test_async(isoladb_async):
|
|
242
|
+
conn = await asyncpg.connect(
|
|
243
|
+
host=isoladb_async.host, port=isoladb_async.port,
|
|
244
|
+
database=isoladb_async.dbname, user="postgres",
|
|
245
|
+
)
|
|
246
|
+
await conn.execute("SELECT 1")
|
|
247
|
+
await conn.close()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**`isoladb_async_engine`** — per-test async SQLAlchemy engine (requires `sqlalchemy[asyncio]`, `asyncpg`):
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
async def test_async_engine(isoladb_async_engine):
|
|
254
|
+
async with isoladb_async_engine.connect() as conn:
|
|
255
|
+
await conn.execute(text("SELECT 1"))
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**`isoladb_server`** — session-scoped fixture exposing the underlying `IsolaDBServer`. Useful for custom fixture composition.
|
|
259
|
+
|
|
260
|
+
**`isoladb_setup`** — session-scoped fixture to override with a custom setup callable:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# conftest.py
|
|
264
|
+
@pytest.fixture(scope="session")
|
|
265
|
+
def isoladb_setup():
|
|
266
|
+
def apply_migrations(url):
|
|
267
|
+
from alembic.config import Config
|
|
268
|
+
from alembic import command
|
|
269
|
+
cfg = Config("alembic.ini")
|
|
270
|
+
cfg.set_main_option("sqlalchemy.url", url)
|
|
271
|
+
command.upgrade(cfg, "head")
|
|
272
|
+
return apply_migrations
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Ini Options
|
|
276
|
+
|
|
277
|
+
Configure in `pyproject.toml`, `pytest.ini`, or `setup.cfg`:
|
|
278
|
+
|
|
279
|
+
```toml
|
|
280
|
+
# pyproject.toml
|
|
281
|
+
[tool.pytest.ini_options]
|
|
282
|
+
isoladb_pg_version = "16.1.0"
|
|
283
|
+
isoladb_ram = true
|
|
284
|
+
isoladb_use_system_pg = false
|
|
285
|
+
isoladb_schema = "tests/schema.sql"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| Option | Default | Description |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `isoladb_pg_version` | latest stable | PostgreSQL version |
|
|
291
|
+
| `isoladb_ram` | `false` | Use RAM disk |
|
|
292
|
+
| `isoladb_use_system_pg` | `true` | Prefer system PostgreSQL |
|
|
293
|
+
| `isoladb_schema` | none | SQL schema file path |
|
|
294
|
+
|
|
295
|
+
The pytest header shows which PostgreSQL binary is being used:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
============================= test session starts ==============================
|
|
299
|
+
platform darwin -- Python 3.13.6
|
|
300
|
+
isoladb: PostgreSQL at /opt/homebrew/Cellar/postgresql@14/14.19
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Requirements
|
|
304
|
+
|
|
305
|
+
- Python 3.8+
|
|
306
|
+
- No pre-installed PostgreSQL needed (downloads automatically if not found)
|
|
307
|
+
- Linux (x86_64, arm64) or macOS (x86_64, arm64)
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT
|
|
312
|
+
|
isoladb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# isoladb
|
|
2
|
+
|
|
3
|
+
Ephemeral PostgreSQL instances for unit testing. No pre-installed PostgreSQL required — just Python 3.8+.
|
|
4
|
+
|
|
5
|
+
isoladb downloads pre-built PostgreSQL binaries (or uses your system installation), starts an isolated server, creates per-test databases, and cleans up automatically.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install isoladb
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With pytest support:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install isoladb[pytest]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
With psycopg (PostgreSQL client):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install isoladb[psycopg]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import psycopg
|
|
29
|
+
from isoladb import IsolaDB
|
|
30
|
+
|
|
31
|
+
with IsolaDB() as db:
|
|
32
|
+
with psycopg.connect(db.url) as conn:
|
|
33
|
+
conn.execute("CREATE TABLE users (id serial PRIMARY KEY, name text)")
|
|
34
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
35
|
+
conn.commit()
|
|
36
|
+
result = conn.execute("SELECT name FROM users").fetchone()
|
|
37
|
+
assert result[0] == "Alice"
|
|
38
|
+
# Server and database are cleaned up automatically
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each `IsolaDB()` context manager creates a fresh, isolated database. The underlying PostgreSQL server is shared and reused across invocations with the same configuration.
|
|
42
|
+
|
|
43
|
+
## Connection Properties
|
|
44
|
+
|
|
45
|
+
The context manager yields an object with these properties:
|
|
46
|
+
|
|
47
|
+
| Property | Description | Example |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `db.url` | Full connection URL | `postgresql://postgres@localhost/isoladb_test_a1b2c3?host=/tmp/pg_xyz&port=54321` |
|
|
50
|
+
| `db.host` | Unix socket directory | `/tmp/pg_xyz` |
|
|
51
|
+
| `db.port` | Server port | `54321` |
|
|
52
|
+
| `db.dbname` | Database name | `isoladb_test_a1b2c3` |
|
|
53
|
+
| `db.user` | Superuser name | `postgres` |
|
|
54
|
+
|
|
55
|
+
Works with any PostgreSQL client library:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# psycopg v3
|
|
59
|
+
conn = psycopg.connect(db.url)
|
|
60
|
+
|
|
61
|
+
# psycopg2
|
|
62
|
+
conn = psycopg2.connect(host=db.host, port=db.port, dbname=db.dbname, user=db.user)
|
|
63
|
+
|
|
64
|
+
# asyncpg
|
|
65
|
+
conn = await asyncpg.connect(host=db.host, port=db.port, database=db.dbname, user=db.user)
|
|
66
|
+
|
|
67
|
+
# SQLAlchemy
|
|
68
|
+
engine = create_engine(db.url)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## PostgreSQL Binary Resolution
|
|
72
|
+
|
|
73
|
+
By default, isoladb looks for PostgreSQL in this order:
|
|
74
|
+
|
|
75
|
+
1. **System PostgreSQL** — detected via `pg_ctl` on `PATH` (e.g., Homebrew, apt)
|
|
76
|
+
2. **Cached download** — previously downloaded binaries in `~/.cache/isoladb`
|
|
77
|
+
3. **Fresh download** — fetched from Maven Central (~50MB, cached for future use)
|
|
78
|
+
|
|
79
|
+
To always use downloaded binaries instead of a system installation:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
with IsolaDB(use_system_pg=False) as db:
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Schema and Setup
|
|
87
|
+
|
|
88
|
+
Apply a SQL schema file automatically after each database is created:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with IsolaDB(schema="schema.sql") as db:
|
|
92
|
+
# Tables from schema.sql are already created
|
|
93
|
+
with psycopg.connect(db.url) as conn:
|
|
94
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or point to a directory of `.sql` files — they are sorted by filename and applied in order:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
migrations/
|
|
101
|
+
001_create_users.sql
|
|
102
|
+
002_create_posts.sql
|
|
103
|
+
003_seed_data.sql
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
with IsolaDB(schema="migrations/") as db:
|
|
108
|
+
# All .sql files applied in sorted order
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Non-`.sql` files in the directory are ignored.
|
|
113
|
+
|
|
114
|
+
For programmatic initialization (e.g., Alembic migrations), use a setup callable:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
def apply_migrations(url):
|
|
118
|
+
from alembic.config import Config
|
|
119
|
+
from alembic import command
|
|
120
|
+
cfg = Config("alembic.ini")
|
|
121
|
+
cfg.set_main_option("sqlalchemy.url", url)
|
|
122
|
+
command.upgrade(cfg, "head")
|
|
123
|
+
|
|
124
|
+
with IsolaDB(setup=apply_migrations) as db:
|
|
125
|
+
...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Both can be combined — schema is applied first, then setup.
|
|
129
|
+
|
|
130
|
+
## RAM Disk
|
|
131
|
+
|
|
132
|
+
Run the PostgreSQL data directory on a RAM disk for faster I/O:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
with IsolaDB(ram=True) as db:
|
|
136
|
+
...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Uses tmpfs on Linux and hdiutil RAM disk on macOS. Falls back to a regular temp directory if RAM disk creation fails.
|
|
140
|
+
|
|
141
|
+
## Async Support
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from isoladb import AsyncIsolaDB
|
|
145
|
+
|
|
146
|
+
async with AsyncIsolaDB() as db:
|
|
147
|
+
conn = await asyncpg.connect(
|
|
148
|
+
host=db.host, port=db.port, database=db.dbname, user=db.user
|
|
149
|
+
)
|
|
150
|
+
await conn.execute("CREATE TABLE test (id serial PRIMARY KEY)")
|
|
151
|
+
await conn.close()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The async setup callable can be either sync or async:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
async def apply_migrations(url: str) -> None:
|
|
158
|
+
engine = create_async_engine(url)
|
|
159
|
+
async with engine.begin() as conn:
|
|
160
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
161
|
+
await engine.dispose()
|
|
162
|
+
|
|
163
|
+
async with AsyncIsolaDB(setup=apply_migrations) as db:
|
|
164
|
+
...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
All options can be passed to `IsolaDB()` / `AsyncIsolaDB()`:
|
|
170
|
+
|
|
171
|
+
| Option | Default | Description |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `pg_version` | `"17.2.0"` | PostgreSQL version (for downloaded binaries) |
|
|
174
|
+
| `ram` | `False` | Use RAM disk for the data directory |
|
|
175
|
+
| `ram_size_mb` | `256` | RAM disk size in megabytes |
|
|
176
|
+
| `use_system_pg` | `True` | Prefer system PostgreSQL over downloading |
|
|
177
|
+
| `schema` | `None` | Path to a SQL file or directory of `.sql` files |
|
|
178
|
+
| `setup` | `None` | Callable receiving the connection URL for custom setup |
|
|
179
|
+
| `cache_dir` | `~/.cache/isoladb` | Directory for cached PostgreSQL binaries |
|
|
180
|
+
| `startup_timeout` | `30.0` | Seconds to wait for the server to start |
|
|
181
|
+
| `pg_conf` | `{}` | Extra postgresql.conf settings as `{"key": "value"}` |
|
|
182
|
+
|
|
183
|
+
## Pytest Plugin
|
|
184
|
+
|
|
185
|
+
isoladb includes a pytest plugin that provides fixtures automatically when `isoladb[pytest]` is installed.
|
|
186
|
+
|
|
187
|
+
### Fixtures
|
|
188
|
+
|
|
189
|
+
**`isoladb`** — per-test fixture yielding an `IsolaDBConnection` with `.url`, `.host`, `.port`, `.dbname`:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
def test_users(isoladb):
|
|
193
|
+
with psycopg.connect(isoladb.url) as conn:
|
|
194
|
+
conn.execute("CREATE TABLE users (id serial PRIMARY KEY, name text)")
|
|
195
|
+
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
|
|
196
|
+
conn.commit()
|
|
197
|
+
result = conn.execute("SELECT count(*) FROM users").fetchone()
|
|
198
|
+
assert result[0] == 1
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**`isoladb_engine`** — per-test fixture yielding a SQLAlchemy engine (requires `sqlalchemy`):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
def test_with_engine(isoladb_engine):
|
|
205
|
+
with isoladb_engine.connect() as conn:
|
|
206
|
+
conn.execute(text("SELECT 1"))
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**`isoladb_async`** — per-test async fixture (requires `pytest-asyncio`):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
async def test_async(isoladb_async):
|
|
213
|
+
conn = await asyncpg.connect(
|
|
214
|
+
host=isoladb_async.host, port=isoladb_async.port,
|
|
215
|
+
database=isoladb_async.dbname, user="postgres",
|
|
216
|
+
)
|
|
217
|
+
await conn.execute("SELECT 1")
|
|
218
|
+
await conn.close()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**`isoladb_async_engine`** — per-test async SQLAlchemy engine (requires `sqlalchemy[asyncio]`, `asyncpg`):
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
async def test_async_engine(isoladb_async_engine):
|
|
225
|
+
async with isoladb_async_engine.connect() as conn:
|
|
226
|
+
await conn.execute(text("SELECT 1"))
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**`isoladb_server`** — session-scoped fixture exposing the underlying `IsolaDBServer`. Useful for custom fixture composition.
|
|
230
|
+
|
|
231
|
+
**`isoladb_setup`** — session-scoped fixture to override with a custom setup callable:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# conftest.py
|
|
235
|
+
@pytest.fixture(scope="session")
|
|
236
|
+
def isoladb_setup():
|
|
237
|
+
def apply_migrations(url):
|
|
238
|
+
from alembic.config import Config
|
|
239
|
+
from alembic import command
|
|
240
|
+
cfg = Config("alembic.ini")
|
|
241
|
+
cfg.set_main_option("sqlalchemy.url", url)
|
|
242
|
+
command.upgrade(cfg, "head")
|
|
243
|
+
return apply_migrations
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Ini Options
|
|
247
|
+
|
|
248
|
+
Configure in `pyproject.toml`, `pytest.ini`, or `setup.cfg`:
|
|
249
|
+
|
|
250
|
+
```toml
|
|
251
|
+
# pyproject.toml
|
|
252
|
+
[tool.pytest.ini_options]
|
|
253
|
+
isoladb_pg_version = "16.1.0"
|
|
254
|
+
isoladb_ram = true
|
|
255
|
+
isoladb_use_system_pg = false
|
|
256
|
+
isoladb_schema = "tests/schema.sql"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Option | Default | Description |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `isoladb_pg_version` | latest stable | PostgreSQL version |
|
|
262
|
+
| `isoladb_ram` | `false` | Use RAM disk |
|
|
263
|
+
| `isoladb_use_system_pg` | `true` | Prefer system PostgreSQL |
|
|
264
|
+
| `isoladb_schema` | none | SQL schema file path |
|
|
265
|
+
|
|
266
|
+
The pytest header shows which PostgreSQL binary is being used:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
============================= test session starts ==============================
|
|
270
|
+
platform darwin -- Python 3.13.6
|
|
271
|
+
isoladb: PostgreSQL at /opt/homebrew/Cellar/postgresql@14/14.19
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Requirements
|
|
275
|
+
|
|
276
|
+
- Python 3.8+
|
|
277
|
+
- No pre-installed PostgreSQL needed (downloads automatically if not found)
|
|
278
|
+
- Linux (x86_64, arm64) or macOS (x86_64, arm64)
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "isoladb"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Ephemeral PostgreSQL instances for unit testing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = ["Yegor Stepanov"]
|
|
8
|
+
keywords = ["testing", "postgresql", "database", "unit-testing", "ephemeral"]
|
|
9
|
+
packages = [{ include = "isoladb", from = "src" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.8",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
"Topic :: Database",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.poetry.dependencies]
|
|
26
|
+
python = ">=3.8"
|
|
27
|
+
psycopg = {version = ">=3.1", extras = ["binary"], optional = true}
|
|
28
|
+
pytest = {version = ">=7.0", optional = true}
|
|
29
|
+
sqlalchemy = {version = ">=2.0", optional = true}
|
|
30
|
+
|
|
31
|
+
[tool.poetry.extras]
|
|
32
|
+
psycopg = ["psycopg"]
|
|
33
|
+
pytest = ["pytest"]
|
|
34
|
+
sqlalchemy = ["sqlalchemy"]
|
|
35
|
+
|
|
36
|
+
[tool.poetry.group.dev.dependencies]
|
|
37
|
+
pytest = ">=7.0"
|
|
38
|
+
sqlalchemy = ">=2.0"
|
|
39
|
+
ruff = "*"
|
|
40
|
+
mypy = "*"
|
|
41
|
+
|
|
42
|
+
[tool.poetry.plugins.pytest11]
|
|
43
|
+
isoladb = "isoladb.pytest_plugin"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["poetry-core"]
|
|
47
|
+
build-backend = "poetry.core.masonry.api"
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
target-version = "py38"
|
|
51
|
+
line-length = 99
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
markers = [
|
|
58
|
+
"integration: tests that download PG binaries and start a real server",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.8"
|
|
63
|
+
strict = true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""isoladb — Ephemeral PostgreSQL instances for unit testing."""
|
|
2
|
+
|
|
3
|
+
from isoladb.async_database import AsyncIsolaDB
|
|
4
|
+
from isoladb.config import IsolaDBConfig
|
|
5
|
+
from isoladb.database import IsolaDB, shutdown
|
|
6
|
+
from isoladb.exceptions import (
|
|
7
|
+
BinaryDownloadError,
|
|
8
|
+
BinaryNotFoundError,
|
|
9
|
+
DatabaseError,
|
|
10
|
+
IsolaDBError,
|
|
11
|
+
RamDiskError,
|
|
12
|
+
ServerStartError,
|
|
13
|
+
ServerStopError,
|
|
14
|
+
UnsupportedPlatformError,
|
|
15
|
+
)
|
|
16
|
+
from isoladb.server import IsolaDBServer
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"IsolaDB",
|
|
22
|
+
"AsyncIsolaDB",
|
|
23
|
+
"IsolaDBConfig",
|
|
24
|
+
"IsolaDBServer",
|
|
25
|
+
"shutdown",
|
|
26
|
+
# Exceptions
|
|
27
|
+
"BinaryDownloadError",
|
|
28
|
+
"BinaryNotFoundError",
|
|
29
|
+
"DatabaseError",
|
|
30
|
+
"IsolaDBError",
|
|
31
|
+
"RamDiskError",
|
|
32
|
+
"ServerStartError",
|
|
33
|
+
"ServerStopError",
|
|
34
|
+
"UnsupportedPlatformError",
|
|
35
|
+
]
|