Add Postgres table adapter with TableRepositoryInterface.
PR Title Check / check-title (pull_request) Successful in 7s
Code Quality Pipeline / code-quality (pull_request) Failing after 19s
Test Python Package / unit-tests (pull_request) Successful in 47s
Test Python Package / integration-tests (pull_request) Successful in 44s
Test Python Package / coverage-report (pull_request) Successful in 11s

Introduce PostgresAdapter for dict-based row CRUD via psycopg3, including config, lazy exports, unit/integration tests with testcontainers, and an example UserTableRepository.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Brian Bjarke Jensen
2026-07-11 14:57:34 +02:00
co-authored by Cursor
parent f28d3c4844
commit dc6f8a3e89
24 changed files with 1580 additions and 16 deletions
+47 -7
View File
@@ -9,7 +9,7 @@ Subclass an adapter in your own repository to add domain-specific methods while
| Layer | Responsibility |
| ---------------- | ----------------------------------------------------------------- |
| **Interfaces** | Abstract contracts for connection, context, and CRUD |
| **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`) |
| **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`, `PostgresAdapter`) |
| **Your project** | Subclass an adapter and add domain methods |
Each public interface is a `@runtime_checkable` `Protocol` with `@abstractmethod` members. **Subclass an adapter** when you need connection management and shared behavior — explicit subclasses get runtime instantiation guards and inherited default methods (e.g. `scan_keys`). **Type-annotate against an interface** when you want loose coupling — any object with the right methods satisfies the contract for mypy and `isinstance()` checks, without inheriting from this package.
@@ -29,7 +29,8 @@ Install with the extras you need:
```bash
uv add python-repositories[redis]
uv add python-repositories[minio]
uv add python-repositories[redis,minio]
uv add python-repositories[postgres]
uv add python-repositories[redis,minio,postgres]
```
### Redis (`JsonRepositoryInterface`)
@@ -63,16 +64,33 @@ for key in repo.scan_keys("user:*"):
| `MINIO_SECURE` | Use HTTPS (`true`/`false`; default: `true`) |
| `MINIO_CREATE_BUCKET_IF_MISSING` | Auto-create `MINIO_BUCKET` on connect (`true`/`false`; default: `false`) |
Copy [`.env.example`](.env.example) to `.env` for local development. `RedisConfig.from_env()` and `MinioConfig.from_env()` load `.env` automatically when resolving configuration from the environment.
For production, it is recommended to leave `MINIO_CREATE_BUCKET_IF_MISSING` unset so that `connect()` fails fast if the expected bucket is missing. For local development, you will often want `MINIO_SECURE=false` and `MINIO_CREATE_BUCKET_IF_MISSING=true`.
### Postgres (`TableRepositoryInterface`)
Requires PostgreSQL 10 or later; tested against PostgreSQL 16 in CI. Tables are owned by your migrations — the adapter verifies the configured table exists on connect.
| Environment variable | Description |
| ---------------------- | --------------------------------------- |
| `POSTGRES_URI` | PostgreSQL connection URL |
| `POSTGRES_TABLE` | Table name for CRUD operations |
| `POSTGRES_PRIMARY_KEY` | Primary key column name (default: `id`) |
Copy [`.env.example`](.env.example) to `.env` for local development. `RedisConfig.from_env()`, `MinioConfig.from_env()`, and `PostgresConfig.from_env()` load `.env` automatically when resolving configuration from the environment.
## Configuration injection
Adapters accept optional `config` and `client` keyword arguments for explicit setup and testing:
```python
from python_repositories import RedisAdapter, RedisConfig, MinioAdapter, MinioConfig
from python_repositories import (
MinioAdapter,
MinioConfig,
PostgresAdapter,
PostgresConfig,
RedisAdapter,
RedisConfig,
)
redis = RedisAdapter(config=RedisConfig(uri="redis://localhost:6379"))
minio = MinioAdapter(
@@ -85,6 +103,13 @@ minio = MinioAdapter(
# create_bucket_if_missing=True, # convenient for local dev
)
)
postgres = PostgresAdapter(
config=PostgresConfig(
uri="postgresql://localhost/mydb",
table="users",
primary_key="user_id",
)
)
```
When both `config` and `client` are provided, `connect()` skips client creation (the caller owns the client lifecycle). `config` is required whenever `client` is injected.
@@ -97,7 +122,7 @@ from python_repositories import load_dotenv
load_dotenv() # optional — from_env() also loads .env by default
```
Calling `RedisAdapter()` or `MinioAdapter()` with no arguments still loads configuration from environment variables (and `.env` if present).
Calling `RedisAdapter()`, `MinioAdapter()`, or `PostgresAdapter()` with no arguments still loads configuration from environment variables (and `.env` if present).
## Quick start
@@ -126,6 +151,17 @@ with ArtifactObjectRepository() as repo:
data = repo.get_artifact("report-1")
```
### Relational rows with Postgres
```python
from python_repositories.examples.user_table_repository import UserTableRepository
with UserTableRepository() as repo:
repo.save_user("alice", {"name": "Alice", "email": "[email protected]"})
user = repo.get_user("alice")
repo.delete_user("alice")
```
### Subclassing in your own project
```python
@@ -154,8 +190,11 @@ from python_repositories import (
MinioAdapter,
MinioConfig,
ObjectRepositoryInterface,
PostgresAdapter,
PostgresConfig,
RedisAdapter,
RedisConfig,
TableRepositoryInterface,
load_dotenv,
)
```
@@ -169,10 +208,11 @@ uv run pytest tests/unit/ -v # fast, no Docker
uv run pytest -m "not integration" -v # all non-Docker tests
uv run pytest tests/integration/redis/ -v # Redis container only
uv run pytest tests/integration/minio/ -v # MinIO container only
uv run pytest tests/integration/postgres/ -v # Postgres container only
uv run pytest -v # full suite (requires Docker)
```
Integration tests are marked with `@pytest.mark.integration` and require Docker (testcontainers). Backend-specific markers (`needs_redis`, `needs_minio`) let you run only the containers a test module needs. Run unit tests alone for quick local feedback.
Integration tests are marked with `@pytest.mark.integration` and require Docker (testcontainers). Backend-specific markers (`needs_redis`, `needs_minio`, `needs_postgres`) let you run only the containers a test module needs. Run unit tests alone for quick local feedback.
### Test organization