first add

This commit is contained in:
2025-08-17 22:18:45 +08:00
commit 093d8efd3b
32 changed files with 3531 additions and 0 deletions

51
examples/01_sync_crud.py Normal file
View File

@@ -0,0 +1,51 @@
"""
Sync CRUD examples using SQLModel and the kit's generic Repository.
Prereq:
- Export SQL_*/PG* env vars to point at your Postgres
- Run this file: uv run python examples/01_sync_crud.py
"""
from typing import Optional, List
from sqlmodel import SQLModel, Field
from sqlmodel_pg_kit import create_all, Repository
from sqlmodel_pg_kit.db import get_session
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: Optional[int] = None
def main():
# Ensure tables exist
create_all()
repo = Repository(Hero)
with get_session() as s:
# Create
h = repo.create(s, {"name": "Alice", "age": 20})
print("Created:", h)
# Read by id
h2 = repo.get(s, h.id)
print("Fetched by id:", h2)
# Update
h3 = repo.update(s, h.id, age=21)
print("Updated:", h3)
# List (pagination)
page = repo.list(s, page=1, size=5)
print("List page=1,size=5 ->", [x.name for x in page])
# Delete
ok = repo.delete(s, h.id)
print("Deleted?", ok)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
"""
Bulk insert and filtering with the generic Repository and SQLModel sessions.
Run: uv run python examples/02_bulk_and_filters.py
"""
from typing import List, Optional
from sqlmodel import select, SQLModel, Field
from sqlmodel_pg_kit import create_all, Repository
from sqlmodel_pg_kit.db import get_session
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: Optional[int] = None
def main():
create_all()
repo = Repository(Hero)
# Clean slate (optional) delete all heroes
with get_session() as s:
s.exec(select(Hero)) # warm up
s.execute(Hero.__table__.delete())
s.commit()
rows = [
{"name": "PG Hero", "age": 1},
{"name": "PG Hero", "age": 2},
{"name": "Bob", "age": 30},
{"name": "Carol", "age": 40},
]
n = repo.bulk_insert(s, rows)
print(f"Bulk inserted: {n}")
# Filter by name using SQLModel/SQLAlchemy expressions
with get_session() as s:
heroes: List[Hero] = s.exec(select(Hero).where(Hero.name == "PG Hero")).all()
print("Filter name='PG Hero' ->", [(h.id, h.age) for h in heroes])
# Range filtering and ordering
with get_session() as s:
res = s.exec(select(Hero).where(Hero.age >= 2).order_by(Hero.age.asc())).all()
print("Age >= 2 asc ->", [(h.name, h.age) for h in res])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,57 @@
"""
Demonstrate relationships and joined reads using SQLModel with generic kit.
Run: uv run python examples/03_relationships.py
"""
from typing import List, Optional
from sqlalchemy.orm import selectinload
from sqlmodel import SQLModel, select, Field, Relationship
from sqlmodel_pg_kit import create_all
from sqlmodel_pg_kit.db import get_session
class Team(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
heroes: List["Hero"] = Relationship(back_populates="team")
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: Optional[int] = None
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
def main():
create_all()
with get_session() as s:
# Clean
s.execute(Hero.__table__.delete())
s.execute(Team.__table__.delete())
s.commit()
# Create Team and Heroes
t = Team(name="Avengers")
s.add(t)
s.commit()
s.refresh(t)
h1 = Hero(name="Thor", age=1500, team_id=t.id)
h2 = Hero(name="Hulk", age=49, team_id=t.id)
s.add(h1)
s.add(h2)
s.commit()
# Query heroes and eager-load team via selectinload
stmt = select(Hero).options(selectinload(Hero.team)).order_by(Hero.id.asc())
heroes: List[Hero] = s.exec(stmt).all()
print([(h.name, h.team.name if h.team else None) for h in heroes])
if __name__ == "__main__":
main()

40
examples/04_async_crud.py Normal file
View File

@@ -0,0 +1,40 @@
"""
Async example using the provided async engine/session and generic AsyncRepository.
Run: uv run python examples/04_async_crud.py
"""
import asyncio
from typing import Optional
from sqlmodel import select, SQLModel, Field
from sqlmodel_pg_kit import create_all, AsyncRepository
from sqlmodel_pg_kit.db import get_async_session
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: Optional[int] = None
async def amain():
# Ensure tables exist (sync helper is fine to call once)
create_all()
repo = AsyncRepository(Hero)
# Clean and insert using async session
async with get_async_session() as s:
await s.execute(Hero.__table__.delete())
await s.commit()
await repo.create(s, {"name": "Async Hero", "age": 7})
res = await s.execute(select(Hero))
heroes = res.scalars().all()
print([h.name for h in heroes])
if __name__ == "__main__":
asyncio.run(amain())

View File

@@ -0,0 +1,173 @@
"""
Cheminformatics example: multi-table schema, dataclass interop, CRUD, joins.
Prereq:
- Export SQL_*/PG* env vars to point at your Postgres
- Run: uv run python examples/05_cheminformatics.py
This example does NOT require RDKit; it stores fields you can compute
externally (smiles, selfies, qed, sa_score). If RDKit is available, you
can compute those before inserting.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
from sqlalchemy.orm import selectinload
from sqlmodel import SQLModel, Field, Relationship, select
from sqlmodel_pg_kit.db import get_session
from sqlmodel_pg_kit import create_all as _create_all # reuse engine + metadata
# --- Models
class MoleculeDataset(SQLModel, table=True):
molecule_id: int = Field(foreign_key="molecule.id", primary_key=True)
dataset_id: int = Field(foreign_key="dataset.id", primary_key=True)
added_at: datetime = Field(default_factory=datetime.utcnow)
class Molecule(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
smiles: str = Field(index=True)
selfies: Optional[str] = Field(default=None)
qed: Optional[float] = Field(default=None, index=True)
sa_score: Optional[float] = Field(default=None, index=True)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
datasets: List["Dataset"] = Relationship(back_populates="molecules", link_model=MoleculeDataset)
class Dataset(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
molecules: List["Molecule"] = Relationship(back_populates="datasets", link_model=MoleculeDataset)
# --- Dataclass DTO (handy for RDKit pipelines)
@dataclass
class MoleculeDTO:
smiles: str
selfies: Optional[str] = None
qed: Optional[float] = None
sa_score: Optional[float] = None
def to_model(self) -> Molecule:
return Molecule(
smiles=self.smiles,
selfies=self.selfies,
qed=self.qed,
sa_score=self.sa_score,
)
def create_all():
# Ensure all tables in this example (plus base kit) exist
_create_all()
def main():
create_all()
# Clean existing data for a repeatable run
with get_session() as s:
s.execute(MoleculeDataset.__table__.delete())
s.execute(Molecule.__table__.delete())
s.execute(Dataset.__table__.delete())
s.commit()
# Create molecules from dataclass (as you would after RDKit computation)
mols = [
MoleculeDTO(smiles="CCO", selfies=None, qed=0.45, sa_score=2.1),
MoleculeDTO(smiles="c1ccccc1", selfies=None, qed=0.76, sa_score=3.5),
MoleculeDTO(smiles="CCN(CC)CC", selfies=None, qed=0.62, sa_score=2.8),
]
with get_session() as s:
for dto in mols:
s.add(dto.to_model())
s.commit()
# Create datasets and link molecules (many-to-many)
with get_session() as s:
ds_train = Dataset(name="train")
ds_holdout = Dataset(name="holdout")
s.add(ds_train)
s.add(ds_holdout)
s.commit()
s.refresh(ds_train)
s.refresh(ds_holdout)
# Link: first two in train, last one in holdout
mol_list: List[Molecule] = s.exec(select(Molecule).order_by(Molecule.id.asc())).all()
links = [
MoleculeDataset(molecule_id=mol_list[0].id, dataset_id=ds_train.id),
MoleculeDataset(molecule_id=mol_list[1].id, dataset_id=ds_train.id),
MoleculeDataset(molecule_id=mol_list[2].id, dataset_id=ds_holdout.id),
]
s.add_all(links)
s.commit()
# CRUD: update a descriptor (e.g., refined QED)
with get_session() as s:
mol = s.exec(select(Molecule).where(Molecule.smiles == "CCO")).one()
mol.qed = 0.50
mol.updated_at = datetime.utcnow()
s.add(mol)
s.commit()
s.refresh(mol)
print("Updated CCO ->", mol.qed)
# Filtering: typical queries
with get_session() as s:
# QED threshold and order by SA score
hi_qed = s.exec(
select(Molecule).where(Molecule.qed >= 0.6).order_by(Molecule.sa_score.asc())
).all()
print("qed>=0.6 order by sa_score:", [(m.smiles, m.qed, m.sa_score) for m in hi_qed])
# Pattern search on SMILES (prefix demo; production use proper search)
starts_with_cc = s.exec(select(Molecule).where(Molecule.smiles.like("CC%"))).all()
print("SMILES like 'CC%':", [m.smiles for m in starts_with_cc])
# Joins: list molecules with dataset name (eager-load relationships)
with get_session() as s:
stmt = (
select(Molecule)
.options(selectinload(Molecule.datasets))
.order_by(Molecule.id.asc())
)
molecules = s.exec(stmt).all()
print("with datasets:", [(m.smiles, [d.name for d in m.datasets]) for m in molecules])
# Join filter: only molecules in 'train'
with get_session() as s:
stmt = (
select(Molecule)
.join(MoleculeDataset, Molecule.id == MoleculeDataset.molecule_id)
.join(Dataset, Dataset.id == MoleculeDataset.dataset_id)
.where(Dataset.name == "train")
.order_by(Molecule.id.asc())
)
train_mols = s.exec(stmt).all()
print("in train:", [m.smiles for m in train_mols])
# Delete: drop a molecule
with get_session() as s:
target = s.exec(select(Molecule).where(Molecule.smiles == "CCN(CC)CC")).one()
s.delete(target)
s.commit()
left = s.exec(select(Molecule).order_by(Molecule.id.asc())).all()
print("after delete:", [m.smiles for m in left])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
"""
Import a CSV into the database by auto-generating a SQLModel class.
Usage:
uv run python examples/06_csv_to_sqlmodel.py --csv path/to/file.csv \
[--class-name MyTable] [--table-name my_table] \
[--sqlite-memory | --sqlite FILE]
If neither --sqlite-* is given, the script uses Postgres via SQL_* / PG* env vars.
"""
from __future__ import annotations
import argparse
from typing import Optional
from sqlmodel_pg_kit import create_all
from sqlmodel_pg_kit.csv_import import build_model_from_csv, insert_rows, create_indexes
from sqlmodel_pg_kit import db
from sqlmodel_pg_kit.db import get_session
def parse_args() -> argparse.Namespace:
ap = argparse.ArgumentParser("csv-to-sqlmodel")
ap.add_argument("--csv", required=True, help="Path to CSV file")
ap.add_argument("--class-name", help="Override generated SQLModel class name (default: CSV stem)")
ap.add_argument("--table-name", help="Override table name (default: snake_case of class name)")
g = ap.add_mutually_exclusive_group()
g.add_argument("--sqlite-memory", action="store_true", help="Use in-memory SQLite")
g.add_argument("--sqlite", help="Use SQLite file path (e.g., ./demo.db)")
ap.add_argument(
"--null",
action="append",
default=[],
help="Add custom null sentinel (repeatable). Default includes '',na,nan,none,null",
)
ap.add_argument(
"--type",
action="append",
default=[],
help="Type override mapping NAME=TYPE (TYPE in bool,int,float,str). Repeatable.",
)
ap.add_argument(
"--rename",
action="append",
default=[],
help="Rename mapping OLD=NEW for columns before sanitization. Repeatable.",
)
ap.add_argument(
"--index",
action="append",
default=[],
help="Create B-Tree index on column (repeatable).",
)
return ap.parse_args()
def maybe_override_engine(args: argparse.Namespace) -> None:
if args.sqlite_memory:
db.engine = db.create_engine("sqlite:///:memory:", echo=False)
elif args.sqlite:
db.engine = db.create_engine(f"sqlite:///{args.sqlite}", echo=False)
def main() -> None:
args = parse_args()
maybe_override_engine(args)
# Parse mappings
def parse_kv(items):
m = {}
for it in items:
if "=" not in it:
raise SystemExit(f"Invalid mapping '{it}', expected NAME=VALUE")
k, v = it.split("=", 1)
m[k.strip()] = v.strip()
return m
rename_map = parse_kv(args.rename)
type_map_raw = parse_kv(args.type)
type_map = {}
for k, v in type_map_raw.items():
v_lower = v.lower()
if v_lower == "bool":
type_map[k] = bool
elif v_lower == "int":
type_map[k] = int
elif v_lower == "float":
type_map[k] = float
elif v_lower == "str":
type_map[k] = str
else:
raise SystemExit(f"Unsupported type '{v}' for column '{k}' (use bool,int,float,str)")
spec, rows = build_model_from_csv(
args.csv,
class_name=args.class_name,
table_name=args.table_name,
null_values=args.null or None,
type_overrides=type_map or None,
rename_map=rename_map or None,
warn_on_nulls=True,
)
# Create table for the generated model
create_all()
# Insert rows
with get_session() as s:
n = insert_rows(spec.model, rows, s)
print(f"Created table '{spec.table_name}' and inserted {n} rows.")
# Create indexes if requested
if args.index:
created = create_indexes(spec.model, args.index, db.engine)
print("Created indexes:", created)
if __name__ == "__main__":
main()