add postgres examples
This commit is contained in:
14
README.md
14
README.md
@@ -14,6 +14,12 @@ Reusable SQLModel + PostgreSQL kit with src layout, sync/async engines, and gene
|
|||||||
- Editable install: `uv pip install -e .`
|
- Editable install: `uv pip install -e .`
|
||||||
- Run tests: `uv pip install pytest && pytest -q`
|
- Run tests: `uv pip install pytest && pytest -q`
|
||||||
|
|
||||||
|
## Local Postgres (Docker)
|
||||||
|
- Start the container: `docker compose -f docker/docker-compose.yml up -d`
|
||||||
|
- Stop when done: `docker compose -f docker/docker-compose.yml down`
|
||||||
|
- Default credentials match `DatabaseConfig`: user `appuser`, password `changeme`, database `appdb`
|
||||||
|
- Export `SQL_SSLMODE=disable` (container does not use TLS by default)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
- Configure environment (either `SQL_*` or Postgres `PG*` vars). Example for container ADDR:
|
- Configure environment (either `SQL_*` or Postgres `PG*` vars). Example for container ADDR:
|
||||||
- `export SQL_HOST=192.168.64.8`
|
- `export SQL_HOST=192.168.64.8`
|
||||||
@@ -142,6 +148,14 @@ with get_session() as s:
|
|||||||
ok = repo.delete(s, h.id)
|
ok = repo.delete(s, h.id)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a container-backed workflow, run `uv run python examples/07_postgres_minimal.py` (paired with `notebooks/07_postgres_minimal.ipynb`). That walkthrough now mirrors a REST-style request cycle:
|
||||||
|
- wipes existing demo rows so each run is deterministic
|
||||||
|
- upserts seed rows via `Repository.bulk_insert`
|
||||||
|
- paginates filtered results with `Repository.list(... where=..., order_by=..., page=..., size=...)`
|
||||||
|
- performs fuzzy search using `select(...).where(Model.name.ilike(...))`
|
||||||
|
- counts inventory with `session.exec(select(func.count(...))).scalar()` (compatible with SQLAlchemy 1.4/2.x)
|
||||||
|
- updates and deletes with the repository helpers you would call from PATCH/DELETE handlers
|
||||||
|
|
||||||
### Bulk insert + filters/pagination
|
### Bulk insert + filters/pagination
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
19
docker/docker-compose.yml
Normal file
19
docker/docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: appdb
|
||||||
|
POSTGRES_USER: appuser
|
||||||
|
POSTGRES_PASSWORD: changeme
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
94
examples/07_postgres_minimal.py
Normal file
94
examples/07_postgres_minimal.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"""Minimal Postgres example using sqlmodel-pg-kit.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
1. Start the dockerized Postgres instance: ``docker compose -f docker/docker-compose.yml up -d``.
|
||||||
|
2. Export env vars so the kit can discover the database::
|
||||||
|
|
||||||
|
export SQL_HOST=localhost
|
||||||
|
export SQL_PORT=5432
|
||||||
|
export SQL_USER=appuser
|
||||||
|
export SQL_PASSWORD=changeme
|
||||||
|
export SQL_DATABASE=appdb
|
||||||
|
export SQL_SSLMODE=disable
|
||||||
|
|
||||||
|
3. Install dependencies (``pip install sqlmodel-pg-kit[async]`` or ``uv pip install .``).
|
||||||
|
|
||||||
|
Run:
|
||||||
|
``python examples/07_postgres_minimal.py``
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlmodel import Field, SQLModel, select
|
||||||
|
|
||||||
|
from sqlmodel_pg_kit import Repository, create_all, get_session
|
||||||
|
|
||||||
|
|
||||||
|
class Widget(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str
|
||||||
|
in_stock: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_all()
|
||||||
|
|
||||||
|
repo = Repository(Widget)
|
||||||
|
|
||||||
|
with get_session() as session:
|
||||||
|
# In web apps it's common to start each request by ensuring you have a clean view of data.
|
||||||
|
session.execute(Widget.__table__.delete())
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create: typical POST handler persisting a single payload.
|
||||||
|
widget = repo.create(session, {"name": "rocket"})
|
||||||
|
print("Created:", widget)
|
||||||
|
|
||||||
|
# Bulk insert: useful for admin imports or seeding catalogs.
|
||||||
|
repo.bulk_insert(
|
||||||
|
session,
|
||||||
|
[
|
||||||
|
{"name": "satellite", "in_stock": True},
|
||||||
|
{"name": "capsule", "in_stock": False},
|
||||||
|
{"name": "probe", "in_stock": True},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read by id: GET /widgets/{id}
|
||||||
|
same = repo.get(session, widget.id)
|
||||||
|
print("Fetched by primary key:", same)
|
||||||
|
|
||||||
|
# Query a paginated listing ordered by newest first, backing a GET /widgets endpoint.
|
||||||
|
page: List[Widget] = repo.list(
|
||||||
|
session,
|
||||||
|
where=Widget.in_stock.is_(True),
|
||||||
|
order_by=[Widget.id.desc()],
|
||||||
|
page=1,
|
||||||
|
size=2,
|
||||||
|
)
|
||||||
|
print("First page of in-stock widgets:", [(w.id, w.name) for w in page])
|
||||||
|
|
||||||
|
# Free-form filtering with SQLModel/SQLAlchemy expressions for search endpoints.
|
||||||
|
search_term = "ro"
|
||||||
|
search_stmt = (
|
||||||
|
select(Widget)
|
||||||
|
.where(Widget.name.ilike(f"%{search_term}%"))
|
||||||
|
.order_by(Widget.name.asc())
|
||||||
|
)
|
||||||
|
matches = session.exec(search_stmt).all()
|
||||||
|
print(f"Search '{search_term}' ->", [(w.id, w.name) for w in matches])
|
||||||
|
|
||||||
|
# Aggregate counts to power dashboards or pagination metadata.
|
||||||
|
count_stmt = select(func.count(Widget.id)).where(Widget.in_stock.is_(True))
|
||||||
|
# scalar() keeps compatibility across SQLAlchemy versions
|
||||||
|
inventory_count = session.exec(count_stmt).scalar() or 0
|
||||||
|
print("In-stock count:", inventory_count)
|
||||||
|
|
||||||
|
# Update: PATCH /widgets/{id}
|
||||||
|
updated = repo.update(session, widget.id, in_stock=False)
|
||||||
|
print("Updated:", updated)
|
||||||
|
|
||||||
|
# Delete: DELETE /widgets/{id}
|
||||||
|
removed = repo.delete(session, widget.id)
|
||||||
|
print("Deleted:", removed)
|
||||||
156
notebooks/07_postgres_minimal.ipynb
Normal file
156
notebooks/07_postgres_minimal.ipynb
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Minimal Postgres Demo\n",
|
||||||
|
"\n",
|
||||||
|
"This notebook connects to the dockerized Postgres instance defined in `docker/docker-compose.yml`.\n",
|
||||||
|
"\n",
|
||||||
|
"Steps before running the cells:\n",
|
||||||
|
"\n",
|
||||||
|
"1. Start Postgres: `docker compose -f docker/docker-compose.yml up -d`.\n",
|
||||||
|
"2. Export environment variables so `sqlmodel_pg_kit` can locate the database (host defaults to `localhost` when run from the host).\n",
|
||||||
|
" ```bash\n",
|
||||||
|
" export SQL_HOST=localhost\n",
|
||||||
|
" export SQL_PORT=5432\n",
|
||||||
|
" export SQL_USER=appuser\n",
|
||||||
|
" export SQL_PASSWORD=changeme\n",
|
||||||
|
" export SQL_DATABASE=appdb\n",
|
||||||
|
" export SQL_SSLMODE=disable\n",
|
||||||
|
" ```\n",
|
||||||
|
"3. `pip install -e .` or `uv pip install .` from the project root.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "44df8c21",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from typing import Optional, List\n",
|
||||||
|
"import os\n",
|
||||||
|
"os.environ[\"SQL_HOST\"] = \"127.0.0.1\"\n",
|
||||||
|
"os.environ[\"SQL_PORT\"] = \"5432\"\n",
|
||||||
|
"os.environ[\"SQL_USER\"] = \"appuser\"\n",
|
||||||
|
"os.environ[\"SQL_PASSWORD\"] = \"changeme\"\n",
|
||||||
|
"os.environ[\"SQL_DATABASE\"] = \"appdb\"\n",
|
||||||
|
"os.environ[\"SQL_SSLMODE\"] = \"disable\"\n",
|
||||||
|
"from sqlalchemy import func\n",
|
||||||
|
"from sqlmodel import Field, SQLModel, select\n",
|
||||||
|
"\n",
|
||||||
|
"from sqlmodel_pg_kit import Repository, create_all, get_session\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"class Widget(SQLModel, table=True):\n",
|
||||||
|
" id: Optional[int] = Field(default=None, primary_key=True)\n",
|
||||||
|
" name: str\n",
|
||||||
|
" in_stock: bool = True\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "23860bb5",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Created: in_stock=True id=6 name='rocket'\n",
|
||||||
|
"Fetched by primary key: in_stock=True name='rocket' id=6\n",
|
||||||
|
"First page of in-stock widgets: [(9, 'probe'), (7, 'satellite')]\n",
|
||||||
|
"Search 'ro' -> [(9, 'probe'), (6, 'rocket')]\n",
|
||||||
|
"In-stock count: 3\n",
|
||||||
|
"Updated: in_stock=False id=6 name='rocket'\n",
|
||||||
|
"Deleted: True\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"create_all()\n",
|
||||||
|
"repo = Repository(Widget)\n",
|
||||||
|
"\n",
|
||||||
|
"with get_session() as session:\n",
|
||||||
|
" session.execute(Widget.__table__.delete())\n",
|
||||||
|
" session.commit()\n",
|
||||||
|
"\n",
|
||||||
|
" widget = repo.create(session, {\"name\": \"rocket\"})\n",
|
||||||
|
" print(\"Created:\", widget)\n",
|
||||||
|
"\n",
|
||||||
|
" repo.bulk_insert(\n",
|
||||||
|
" session,\n",
|
||||||
|
" [\n",
|
||||||
|
" {\"name\": \"satellite\", \"in_stock\": True},\n",
|
||||||
|
" {\"name\": \"capsule\", \"in_stock\": False},\n",
|
||||||
|
" {\"name\": \"probe\", \"in_stock\": True},\n",
|
||||||
|
" ],\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" same = repo.get(session, widget.id)\n",
|
||||||
|
" print(\"Fetched by primary key:\", same)\n",
|
||||||
|
"\n",
|
||||||
|
" page = repo.list(\n",
|
||||||
|
" session,\n",
|
||||||
|
" where=Widget.in_stock.is_(True),\n",
|
||||||
|
" order_by=[Widget.id.desc()],\n",
|
||||||
|
" page=1,\n",
|
||||||
|
" size=2,\n",
|
||||||
|
" )\n",
|
||||||
|
" print(\"First page of in-stock widgets:\", [(w.id, w.name) for w in page])\n",
|
||||||
|
"\n",
|
||||||
|
" search_term = \"ro\"\n",
|
||||||
|
" search_stmt = (\n",
|
||||||
|
" select(Widget)\n",
|
||||||
|
" .where(Widget.name.ilike(f\"%{search_term}%\"))\n",
|
||||||
|
" .order_by(Widget.name.asc())\n",
|
||||||
|
" )\n",
|
||||||
|
" matches = session.exec(search_stmt).all()\n",
|
||||||
|
" print(f\"Search '{search_term}' ->\", [(w.id, w.name) for w in matches])\n",
|
||||||
|
"\n",
|
||||||
|
" count_stmt = select(func.count(Widget.id)).where(Widget.in_stock.is_(True))\n",
|
||||||
|
" # scalar() keeps compatibility across SQLAlchemy versions\n",
|
||||||
|
" inventory_count = session.exec(count_stmt).scalar() or 0\n",
|
||||||
|
" print(\"In-stock count:\", inventory_count)\n",
|
||||||
|
"\n",
|
||||||
|
" updated = repo.update(session, widget.id, in_stock=False)\n",
|
||||||
|
" print(\"Updated:\", updated)\n",
|
||||||
|
"\n",
|
||||||
|
" removed = repo.delete(session, widget.id)\n",
|
||||||
|
" print(\"Deleted:\", removed)\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7637a17d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "sqlmodel",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.12.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user