Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ is already reachable at that address and skips container management if so.
Set `PGDEVKIT_SKIP_CONTAINER=1` to always assume it's already there and skip
that check too.

To point at a local Postgres install instead of the podman container —
useful when podman isn't available, or you'd rather use peer authentication
as the current OS user — set `PGDEVKIT_TESTDB_HOST` to the unix socket
directory (e.g. `/var/run/postgresql`) and `PGDEVKIT_TESTDB_PASSWORD=""`.
The role named by `PGDEVKIT_TESTDB_USER` must exist and match your OS user
(`CREATE ROLE <user> SUPERUSER LOGIN;`) and `pg_hba.conf` must allow `peer`
auth for local connections (Debian/Ubuntu Postgres ships this by default).

## `pgdevkit.db` — helpers for application code

Install with the `db` extra: `pip install pgdevkit[db]`.
Expand Down
4 changes: 2 additions & 2 deletions pgdevkit/testdb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=10"
return constants.conninfo("postgres", connect_timeout=10)


def _db_dsn(db_name: str) -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{db_name}?connect_timeout=10"
return constants.conninfo(db_name, connect_timeout=10)


def _resolve(project_root: Path | None) -> tuple[ProjectConfig, str]:
Expand Down
17 changes: 17 additions & 0 deletions pgdevkit/testdb/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,27 @@

import os

from psycopg.conninfo import make_conninfo

CONTAINER_NAME = "pgdevkit-postgres"
IMAGE = "pgvector/pgvector:pg18-trixie"
HOST = os.environ.get("PGDEVKIT_TESTDB_HOST", "localhost")
PORT = int(os.environ.get("PGDEVKIT_TESTDB_PORT", "54322"))
USER = os.environ.get("PGDEVKIT_TESTDB_USER", "postgres")
PASSWORD = os.environ.get("PGDEVKIT_TESTDB_PASSWORD", "testpwd")
PG_SPEED_FLAGS = ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]


def conninfo(dbname: str, *, connect_timeout: int | None = None) -> str:
"""Build a libpq conninfo string from HOST/PORT/USER/PASSWORD.

PASSWORD is omitted when empty (PGDEVKIT_TESTDB_PASSWORD="") so HOST can
be pointed at a unix socket directory (e.g. /var/run/postgresql) and
authenticate via peer auth as the current OS user instead of a password.
"""
params: dict[str, str | int] = {"host": HOST, "port": PORT, "user": USER, "dbname": dbname}
if PASSWORD:
params["password"] = PASSWORD
if connect_timeout is not None:
params["connect_timeout"] = connect_timeout
return make_conninfo(**params)
10 changes: 2 additions & 8 deletions pgdevkit/testdb/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ def _available(timeout: float = 3.0) -> bool:
# libpq's connect_timeout is whole seconds; anything below 1 means "wait
# indefinitely" instead of a short timeout, so it's clamped up to 1.
connect_timeout = max(1, round(timeout))
dsn = (
f"postgresql://{constants.USER}:{constants.PASSWORD}"
f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout={connect_timeout}"
)
dsn = constants.conninfo("postgres", connect_timeout=connect_timeout)
try:
with psycopg.connect(dsn):
return True
Expand Down Expand Up @@ -62,10 +59,7 @@ def _create_container() -> None:

def _wait_ready(timeout: float = 30.0) -> None:
deadline = time.monotonic() + timeout
dsn = (
f"postgresql://{constants.USER}:{constants.PASSWORD}"
f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=2"
)
dsn = constants.conninfo("postgres", connect_timeout=2)
last_error: Exception | None = None
while time.monotonic() < deadline:
try:
Expand Down
14 changes: 10 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@

import psycopg
import pytest
from psycopg.conninfo import make_conninfo

from pgdevkit.testdb import ensure_testdb


@pytest.fixture(scope="session")
def postgres_dsn() -> str:
env = ensure_testdb()
return (
f"postgresql://{env['PGDEVKIT_POSTGRES_USER']}:{env['PGDEVKIT_POSTGRES_PASSWORD']}"
f"@{env['PGDEVKIT_POSTGRES_HOST']}:{env['PGDEVKIT_POSTGRES_PORT']}/{env['PGDEVKIT_POSTGRES_DB']}"
)
params = {
"host": env["PGDEVKIT_POSTGRES_HOST"],
"port": env["PGDEVKIT_POSTGRES_PORT"],
"user": env["PGDEVKIT_POSTGRES_USER"],
"dbname": env["PGDEVKIT_POSTGRES_DB"],
}
if env["PGDEVKIT_POSTGRES_PASSWORD"]:
params["password"] = env["PGDEVKIT_POSTGRES_PASSWORD"]
return make_conninfo(**params)


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/db/test_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


class Widget(PostgresTableModel):
Expand All @@ -49,7 +49,7 @@ async def pool(monkeypatch: pytest.MonkeyPatch):
with psycopg.connect(_admin_dsn(), autocommit=True) as con:
con.execute(f'DROP DATABASE IF EXISTS "{TEST_DB}"')
con.execute(f'CREATE DATABASE "{TEST_DB}"')
with psycopg.connect(f"{_admin_dsn().rsplit('/', 1)[0]}/{TEST_DB}", autocommit=True) as con:
with psycopg.connect(constants.conninfo(TEST_DB), autocommit=True) as con:
con.execute("CREATE TABLE widget (id serial PRIMARY KEY, name text NOT NULL)")

monkeypatch.setenv(f"{ENV_PREFIX}HOST", constants.HOST)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_fetch_missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


def _db_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}"
return constants.conninfo(TEST_DB)


def test_layer_folder_for_matches_stripped_sort_prefix(tmp_path: Path):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_fetch_missing_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


def _db_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}"
return constants.conninfo(TEST_DB)


@requires_podman
Expand Down
2 changes: 1 addition & 1 deletion tests/testdb/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


@requires_podman
Expand Down
5 changes: 1 addition & 4 deletions tests/testdb/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ def _fail(*a, **k):


def _admin_dsn() -> str:
return (
f"postgresql://{constants.USER}:{constants.PASSWORD}"
f"@{constants.HOST}:{constants.PORT}/postgres?connect_timeout=5"
)
return constants.conninfo("postgres", connect_timeout=5)


@requires_podman
Expand Down
4 changes: 2 additions & 2 deletions tests/testdb/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ def test_split_statements_handles_tagged_dollar_quotes():


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


def _db_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}"
return constants.conninfo(TEST_DB)


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/testdb/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@


def _admin_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/postgres"
return constants.conninfo("postgres")


def _db_dsn() -> str:
return f"postgresql://{constants.USER}:{constants.PASSWORD}@{constants.HOST}:{constants.PORT}/{TEST_DB}"
return constants.conninfo(TEST_DB)


@pytest.fixture
Expand Down
Loading