A Python package that seamlessly integrates PostgreSQL, Jinja templating, and Pydantic for type-safe database queries
Project description
pgjinja
pgjinja is a Python library for running PostgreSQL queries from Jinja2 SQL templates while keeping results type-safe with Pydantic models. It exists to separate SQL from application logic, reduce query string duplication, and provide a single sync/async API with connection pooling through psycopg3.
Tech Stack
- Language: Python 3.11+
- Build backend: Hatchling
- Core libraries:
jinjasql2>=0.1.11,psycopg[binary,pool]>=3.2.6,pydantic>=2.10.6 - Dev tools:
pytest,pytest-asyncio,pytest-cov,ruff,uv
How It Works
PgJinja and PgJinjaAsync are the two main clients. Both accept a PgJinjaSettings object that stores PostgreSQL credentials, template directory, and pool sizing. The sync client uses ConnectionPool and the async client uses AsyncConnectionPool.
When you call query(template, params, model), the library reads the SQL template file from template_dir, renders it with JinjaSql, and executes the prepared SQL with psycopg. For read queries, it returns tuples by default, or Pydantic model instances if you pass a model class.
If a model is provided, pgjinja injects _model_fields_ into template context so templates can select model-defined columns directly. This keeps SQL column lists aligned with your Pydantic schema without hand-maintaining SELECT fields in multiple places.
Both clients lazily open their connection pool on first use and retry failed query execution attempts (default: 2 attempts total), logging pool stats to help diagnose failures.
Getting Started
Prerequisites
- Python
>=3.11 uv- PostgreSQL (local or remote)
Installation
git clone https://github.com/tungph/pgjinja.git
cd pgjinja
uv sync
To install from PyPI instead:
pip install pgjinja
Configuration
The library is configured in Python through PgJinjaSettings:
| Field | Required | Default | Description |
|---|---|---|---|
user |
Yes | - | PostgreSQL username |
password |
Yes | - | PostgreSQL password (SecretStr) |
host |
No | localhost |
PostgreSQL host |
port |
No | 5432 |
PostgreSQL port |
dbname |
No | public |
Database name |
template_dir |
No | current directory | Directory containing SQL template files |
min_size |
No | 4 |
Minimum pool size |
max_size |
No | None |
Maximum pool size (None = unlimited) |
application_name |
No | pgjinja |
Label for PostgreSQL connection logs |
Example setup:
from pathlib import Path
from pydantic import SecretStr
from pgjinja import PgJinjaSettings, PgJinjaAsync
settings = PgJinjaSettings(
user="postgres",
password=SecretStr("postgres"),
host="localhost",
dbname="postgres",
template_dir=Path("./templates"),
)
client = PgJinjaAsync(settings)
Application lifetime (singletons)
If you keep a single PgJinja or PgJinjaAsync instance for the whole process (typical for web apps and workers), its connection pool opens lazily on the first query. You should close the pool when the application shuts down so connections are released cleanly:
- Async:
await client.close()in your teardown path (for example ASGI lifespanshutdown, or afinallyblock inasyncio.run()). - Sync:
client.close()on process exit or in your framework’s shutdown hook.
If the pool was opened and the client is destroyed without closing, you may see a log warning asking you to use await client.close() for async clients.
Example (FastAPI-style lifespan):
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.db = PgJinjaAsync(settings)
yield
await app.state.db.close()
app = FastAPI(lifespan=lifespan)
Running Locally
make run-example
examples/merchant_example.py expects a local examples/config.ini file with a [database] section (user, password, host, dbname) and uses SQL templates in examples/template/.
Verifying It Works
Run tests:
make test
Or run lint + formatting checks:
make lint
External Dependencies
| Name | Purpose | Local Setup | Env Vars |
|---|---|---|---|
| PostgreSQL | Executes rendered SQL templates via psycopg3 pool connections | Run PostgreSQL locally (for example via Docker or system package) and create a database/user reachable by PgJinjaSettings |
None required by library itself (credentials provided in PgJinjaSettings; example script reads examples/config.ini) |
Project Structure
.
├── src/pgjinja/
│ ├── pgjinja.py # Sync client (ConnectionPool + template execution)
│ ├── pgjinja_async.py # Async client (AsyncConnectionPool + template execution)
│ ├── schemas/pgjinja_settings.py # Typed DB and pool configuration model
│ └── shared/
│ ├── common.py # Template reading and model-field helpers
│ └── execution.py # Shared query prep and result mapping helpers
├── tests/ # Unit/integration-style test suite
├── examples/
│ ├── merchant_example.py # End-to-end async usage example
│ └── template/ # Example SQL Jinja templates
├── pyproject.toml # Packaging, dependencies, pytest/ruff config
└── Makefile # Common development and release commands
Common Tasks
| Task | Command |
|---|---|
| Install dependencies | make install |
| Run tests | make test |
| Lint + format | make lint |
| Format only | make format |
| Build package | make build |
| Run example app | make run-example |
| Publish to PyPI | make publish |
Maintainer
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pgjinja-3.1.0.tar.gz.
File metadata
- Download URL: pgjinja-3.1.0.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9ba669c796fc6f505ea7b416abeec20b83e47bb8974b8c2e410b4cc8fbcf8cb
|
|
| MD5 |
ad87471ba828ab72c4a3ffd2a3562466
|
|
| BLAKE2b-256 |
9556aabd7384d4b190fbec72085f202387ed07d5ed4f6072e0dbb9f69c9d3c7b
|
File details
Details for the file pgjinja-3.1.0-py3-none-any.whl.
File metadata
- Download URL: pgjinja-3.1.0-py3-none-any.whl
- Upload date:
- Size: 10.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d9f11541eb56cf4b97e9b55b0caf223b06ef1f320cde0c8a7c8906f29e4071f
|
|
| MD5 |
9ef7a6d522821b6227fa0b54a4d4867d
|
|
| BLAKE2b-256 |
f7b674d2daa03e7ca62168112af96a4b2d943c04822a8682790a1d922499a609
|