Skip to main content

Portable SQLAlchemy repository kit: sync and async CRUD, statement builders, sort/pagination, and SQL helpers.

Project description

sqlphilosophy

Portable SQLAlchemy repository kit: sync and async CRUD, fluent statement builders, sort/pagination, Core SQL helpers, and optional audit listeners.

PyPI sqlphilosophy
GitHub SignalSafeSoftware/sqlphilosophy
Import sqlphilosophy (explicit submodules — no root re-exports)
Python 3.12+
License MIT — see LICENSE

What this package does

  • Repository pattern for a single mapped model (BaseRepository, AsyncBaseRepository).
  • Fluent query builders with pagination/sort (StatementQueryBuilder, ListQuery, SortConfig).
  • SQL helpers for row mapping, partial updates, filters, and developer-defined raw SQL fragments.
  • Optional audit listeners and timestamp mixins.

What this package does not do

  • Migrations, schema design, or connection pooling configuration.
  • Authorization, multi-tenant isolation, or query sandboxing.
  • Automatic commits for normal CRUD — see Transaction ownership below.

Install

pip install sqlphilosophy

Async ORM (AsyncSession) also needs greenlet:

pip install sqlphilosophy[async]

Requires Python 3.12+ and SQLAlchemy 2.x.

Full example (sync model + session)

from sqlalchemy import String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, sessionmaker

from sqlphilosophy.sorting import ListQuery
from sqlphilosophy.sync.repository import BaseRepository


class Base(DeclarativeBase):
    pass


class Widget(Base):
    __tablename__ = "widget"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(64))


engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)

with SessionLocal() as session:
    repo = BaseRepository(Widget, session)
    widget = repo.create(name="alpha")  # stages + flush; does not commit
    session.commit()

    page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
    assert page.total >= 1

Async: swap SessionAsyncSession, BaseRepositoryAsyncBaseRepository from sqlphilosophy.aio.repository, and await repository methods.

Strongly typed repositories (factory pattern)

Domain repositories subclass BaseRepository[Model, RepositoryFactory] (or AsyncBaseRepository for async) and add typed query helpers. A session-scoped factory implements RepositoryFactory to cache repositories and wire statement() / for_repo() across repos on the same session.

from sqlalchemy.orm import Session

from sqlphilosophy.sync.protocols import RepositoryFactory
from sqlphilosophy.sync.repository import BaseRepository


class UserRepository(BaseRepository[User, RepositoryFactory]):
    def __init__(self, session: Session, factory: RepositoryFactory) -> None:
        super().__init__(User, session, factory)

    def get_by_username(self, username: str) -> User | None:
        return self.first(username=username)

    def get_by_email(self, email: str) -> User | None:
        return self.first(email=email)

    def get_active_by_email(self, email: str) -> User | None:
        return (
            self.statement()
            .where(User.email == email, User.is_active.is_(True))
            .scalars()
            .first()
        )

Full runnable examples with models, a multi-repo factory, and cross-repository usage:

For async, swap SessionAsyncSession, BaseRepositoryAsyncBaseRepository from sqlphilosophy.aio.repository, and await repository methods.

Package layout

Module Contents
sqlphilosophy.types Portable typing aliases (RowMapping, PrimaryKey, SqlFilter, …)
sqlphilosophy.sql Row mapping helpers, partial updates, Core table helpers, filter builders
sqlphilosophy.sorting ListQuery, SortConfig, SortSpec, pagination/sort resolution
sqlphilosophy.sync Sync BaseRepository, StatementQueryBuilder, RepositoryFactory protocol
sqlphilosophy.aio Async AsyncBaseRepository, AsyncStatementQueryBuilder, AsyncRepositoryFactory
sqlphilosophy.audit Optional SQLAlchemy audit listeners and timestamp mixins

Sync usage

from sqlalchemy.orm import Session

from sqlphilosophy.sorting import ListQuery, SortConfig, SortSpec
from sqlphilosophy.sql import partial_update_model, row_int
from sqlphilosophy.sync.protocols import RepositoryFactory
from sqlphilosophy.sync.repository import BaseRepository
from sqlphilosophy.sync.query import SqlAlchemyStatementBuilder

repo = BaseRepository(User, session)
rows = repo.statement().where(User.active.is_(True)).mappings().all()

repo = BaseRepository(User, session, factory)
page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
other = repo.for_repo(OrderRepository)

Async usage

from sqlalchemy.ext.asyncio import AsyncSession

from sqlphilosophy.aio.repository import AsyncBaseRepository

repo = AsyncBaseRepository(User, session)
rows = await repo.statement().where(User.active.is_(True)).mappings().all()

Transaction ownership

  • create / update / delete helpers on repositories call session.flush() but do not commit unless documented otherwise.
  • delete_all() executes a bulk delete and does not commit — the caller owns session.commit() / rollback() for the work unit.
  • batched_purge_ids(...) deletes matching rows in batches and commits after each batch — treat it as a destructive, application-level operation you must authorize first.
  • Your application owns session.commit() / rollback() for normal request/work-unit boundaries.

Raw SQL trust boundaries

The following must be developer-defined and must never be built from end-user input:

  • Raw SQL fragments passed to SQL helper functions
  • Literal column names, table names, and ORDER BY expressions
  • Sort field allowlists wired into query builders

User-supplied values must use bind parameters (SQLAlchemy bound values), not string concatenation into SQL text or identifiers. See SECURITY.md.

Destructive helpers

  • delete_all() — removes all rows for the repository model (sync and async variants). Does not commit; caller must commit or roll back.
  • batched_purge_ids(...) — deletes matching rows in batches and commits each batch.

Call only after your application has authorized the operation. These helpers assume the caller understands the data loss impact.

Audit mixins

from sqlphilosophy.audit.context import audit_context
from sqlphilosophy.audit.listener import configure_audit_listeners
from sqlphilosophy.audit.model import TimestampModel

configure_audit_listeners()

with audit_context(actor_id=42):
    session.add(MyModel(name="example"))
    session.flush()
    session.commit()

Audit listeners record changes; they do not enforce access control.

Development

This repo uses uv:

uv sync --extra dev
uv run pytest
uv run flake8 .
uv run python -m build

Security

See SECURITY.md for vulnerability reporting and SQL trust boundaries.

Releasing

See RELEASING.md for GitHub + PyPI trusted publishing. See CHANGELOG.md.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlphilosophy-0.1.8.tar.gz (26.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlphilosophy-0.1.8-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

Details for the file sqlphilosophy-0.1.8.tar.gz.

File metadata

  • Download URL: sqlphilosophy-0.1.8.tar.gz
  • Upload date:
  • Size: 26.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for sqlphilosophy-0.1.8.tar.gz
Algorithm Hash digest
SHA256 d987590b4e9bc4328043ffacd92abeb2fcefc8af541e9fd548f57dc739e9b1cc
MD5 04a9f751ba29112cdcbd909ae7dccfb5
BLAKE2b-256 9dd80ec106f86ba0c01d35d2b25a721d9a8d6684df3bc4077fff92343f6a3462

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlphilosophy-0.1.8.tar.gz:

Publisher: ci.yml on SignalSafeSoftware/sqlphilosophy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sqlphilosophy-0.1.8-py3-none-any.whl.

File metadata

  • Download URL: sqlphilosophy-0.1.8-py3-none-any.whl
  • Upload date:
  • Size: 29.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for sqlphilosophy-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 ed855aa6bdc3d7cfb61164fdf506ffc02568f2ad63bc97e78bdae327c073c201
MD5 31d14294364be1a02ad084529817bf09
BLAKE2b-256 4bf9e8c07b2b8ba81c3dc338973b72b10eeeb30fa3efff06fbbe0dccf21d1d64

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlphilosophy-0.1.8-py3-none-any.whl:

Publisher: ci.yml on SignalSafeSoftware/sqlphilosophy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page