Skip to main content

pico-sqlalchemy

PyPI Ask DeepWiki License: MIT CI (tox matrix) codecov Quality Gate Status Duplicated Lines (%) Maintainability Rating Docs Interactive Lab

Pico-SQLAlchemy

Pico-SQLAlchemy integrates Pico-IoC with SQLAlchemy, providing a true inversion of control persistence layer with Spring Data-style declarative features.

It brings constructor-based dependency injection, implicit transaction management, and powerful declarative queries using pure Python and SQLAlchemy’s Async ORM.

Requires Python 3.11+ (tested on 3.11, 3.12, 3.13 and 3.14) Async-Native: Built entirely on AsyncSession and create_async_engine. Zero-Boilerplate: Repositories are transactional by default. Declarative Queries: Define SQL or expressions in decorators; the library executes them for you.


Why pico-sqlalchemy?

Most Python apps suffer from manual session handling (async with session...), scattered transaction logic, and verbose repository patterns.

Pico-SQLAlchemy solves this by offering:

Feature SQLAlchemy Default pico-sqlalchemy
Transactions Manual commit() / rollback() Implicit (Auto-managed)
Repositories DIY Classes @repository (Transactional by default)
Queries Manual implementation @query (Declarative execution)
Injection None / Global variables Constructor Injection (IoC)
Pagination Manual calculation Automatic (PageRequest / Page)

Core Features

  • Implicit Transactions: Methods inside @repository are automatically Read-Write transactional.
  • Declarative Queries: Use @query to run SQL or Expressions automatically (defaults to Read-Only).
  • AOP-Based Propagation: REQUIRED, REQUIRES_NEW, MANDATORY, NEVER, etc.
  • Session Lifecycle: Centralized SessionManager handles engine creation and cleanup.
  • Pagination: Built-in support for paged results via @query(paged=True).

Installation

pip install pico-sqlalchemy

You will also need an async database driver:

pip install aiosqlite   # for SQLite
pip install asyncpg     # for PostgreSQL

Quick Example

1. Define Model

from sqlalchemy import Integer, String
from pico_sqlalchemy import AppBase, Mapped, mapped_column

class User(AppBase):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    username: Mapped[str] = mapped_column(String(50))

2. Define Repository (The "Magic" Part)

Notice we don't need @transactional here.

  • save: Automatically runs in a Read-Write transaction.
  • find_by_name: Automatically runs in a Read-Only transaction and executes the query logic.
from pico_sqlalchemy import repository, query, SessionManager, get_session

@repository(entity=User)
class UserRepository:
    def __init__(self, manager: SessionManager):
        self.manager = manager

    # IMPLICIT: Read-Write Transaction
    async def save(self, user: User) -> User:
        session = get_session(self.manager)
        session.add(user)
        return user

    # DECLARATIVE: Read-Only Transaction + Auto-Execution
    @query(expr="username = :username", unique=True)
    async def find_by_name(self, username: str) -> User | None:
        ... # Body is ignored; the library executes the query

3. Define Service

Use @transactional here to define business logic boundaries.

from pico_ioc import component
from pico_sqlalchemy import transactional

@component
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    @transactional
    async def create(self, name: str) -> User:
        # 1. Check existence (Read-Only tx from repo)
        existing = await self.repo.find_by_name(name)
        if existing:
            raise ValueError("User exists")
            
        # 2. Save new user (Joins current transaction)
        return await self.repo.save(User(username=name))

4. Run it

import asyncio
from pico_boot import init
from pico_ioc import configuration, DictSource

config = configuration(DictSource({
    "database": {
        "url": "sqlite+aiosqlite:///:memory:",
        "echo": False
    }
}))

async def main():
    container = init(modules=["__main__"], config=config)
    service = await container.aget(UserService)
    
    user = await service.create("alice")
    print(f"Created: {user.id}")
    
    await container.cleanup_all_async()

if __name__ == "__main__":
    asyncio.run(main())

Transaction Hierarchy & Rules

Pico-SQLAlchemy applies a "Best Effort" strategy to determine transaction configuration. The priority order (highest wins) is:

Priority Decorator Default Mode Use Case
1 (High) @transactional(...) Explicit Config Overriding defaults, Service layer logic.
2 @query(...) Read-Only Efficient data fetching.
3 (Base) @repository Read-Write Default for CRUD (saves, updates, deletes).

Example Scenarios

  1. Plain Method in Repository:

    async def update_user(self): ...
    

    Result: Active Read-Write Transaction (Implicit from @repository).

  2. Query Method:

    @query("SELECT ...")
    async def get_data(self): ...
    

    Result: Active Read-Only Transaction (Implicit from @query).

  3. Manual Override:

    @transactional(read_only=True)
    async def complex_report(self): ...
    

    Result: Active Read-Only Transaction (Explicit override).

Transaction-scoped components (v0.4.0+)

Beyond managing the SQLAlchemy session, the interceptor binds pico-ioc's "transaction" DI scope to the same boundary. A component registered with scope="transaction" is instantiated once per database transaction and torn down (running its @cleanup hooks) when that transaction ends:

@component(scope="transaction")
class UnitOfWorkAudit:
    def __init__(self):
        self.events: list[str] = []

    @cleanup
    def flush(self):
        # runs exactly when the enclosing transaction ends
        ...

A new transaction (REQUIRES_NEW, or REQUIRED with no enclosing transaction) opens a fresh scope; joins reuse the enclosing one — so the session boundary and the DI lifetime are two facets of a single transaction. Requires pico-ioc ≥ 2.2.6.


Declarative Queries in Depth

The @query decorator eliminates boilerplate for common fetches.

Expression Mode (expr)

Requires @repository(entity=Model). Injects the expression into a SELECT * FROM table WHERE ....

@query(expr="age > :min_age", unique=False)
async def find_adults(self, min_age: int) -> list[User]: ...

SQL Mode (sql)

Executes raw SQL. Useful for complex joins or specific DTOs.

@query(sql="SELECT count(*) as cnt FROM users")
async def count_users(self) -> int: ...

Automatic Pagination

Just add paged=True and a page: PageRequest parameter.

from pico_sqlalchemy import Page, PageRequest

@query(expr="active = true", paged=True)
async def find_active(self, page: PageRequest) -> Page[User]: ...

Testing

Testing is simple because you can override the configuration or the components easily using Pico-IoC.

@pytest.mark.asyncio
async def test_service():
    # Setup container with in-memory DB
    container = ... 
    
    service = await container.aget(UserService)
    user = await service.create("test")
    
    assert user.id is not None

Architecture Overview

                 ┌─────────────────────────────┐
                 │          Your App           │
                 └──────────────┬──────────────┘
                                │
                        Constructor Injection
                                │
                 ┌──────────────▼───────────────┐
                 │          Pico-IoC            │
                 └──────────────┬───────────────┘
                                │
                 ┌──────────────▼───────────────┐
                 │       pico-sqlalchemy        │
                 │ 1. Implicit Repo Transactions│
                 │ 2. Declarative @query        │
                 │ 3. Explicit @transactional   │
                 └──────────────┬───────────────┘
                                │
                           SQLAlchemy
                           (Async ORM)

Built for AI-assisted development

pico-sqlalchemy is part of an ecosystem designed for humans and coding agents building software together. Every package ships AGENTS.md working conventions, an llms.txt machine-readable docs index and documented behaviour pinned by regression tests; pico-testing gives agents a verification loop for their own changes, and releases are gated by the whole ecosystem booting together against real infrastructure. The full story: Built for AI-assisted development.

Install the agent skills for Claude Code or OpenAI Codex:

curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash -s -- sqlalchemy
Command Description
/add-repository Add SQLAlchemy entities and repositories with transactions
/add-component Add components, factories, interceptors, settings
/add-tests Generate tests for pico components

All skills: curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash

See pico-skills for details.


Migrations on startup

Point config at your Alembic directory and the container runs upgrade head before anything else touches the database (extra: pico-sqlalchemy[migrations]):

database:
  url: postgresql+asyncpg://user:pass@host/db
  migrations_path: alembic

License

MIT

Release files for pico-sqlalchemy 0.5.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pico-sqlalchemy 0.5.2
File Size Uploaded
pico_sqlalchemy-0.5.2.tar.gz 86.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pico-sqlalchemy 0.5.2
File Interpreter ABI Platform
pico_sqlalchemy-0.5.2-py3-none-any.whl Python 3 none any Details

Total release size: 115.5 kB

Release files / pico_sqlalchemy-0.5.2.tar.gz

Download URL pico_sqlalchemy-0.5.2.tar.gz
Size 86.8 kB
Tags Source
SHA-256 checksum
How to use checksums
7c5a5d274f1da2b72de90d561a7e36c0f3853f927108133d98072fdb9e0a16a8
BLAKE2b-256 checksum
How to use checksums
b808035636af2309a70d8b2475c370e0815b00c86d6ed8e10078e309014d8fde
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / pico_sqlalchemy-0.5.2-py3-none-any.whl

Download URL pico_sqlalchemy-0.5.2-py3-none-any.whl
Size 28.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ffb1c0cee0135386f1f34c3ad6788568ed1f3db87f97fbd202120d07b2c6b20f
BLAKE2b-256 checksum
How to use checksums
b7c59a134d214ebfe0a78a348abdd50289f66aa41f03bcc4a5906a37450ee3f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.2 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page