Async repository pattern implementation for SQLAlchemy
Project description
SQLAlchemy Async Repositories
Async repository pattern with hybrid pagination approach.
๐ฏ Features
โ Basic CRUD (Native SQLAlchemy - Fast!)
get_by_id()- Get entity by IDcreate()- Create new entityupdate()- Update existing entitydelete()- Delete entityget_all()- Get all entities with simple paginationexists()- Check if entity existscount()- Count total entities
โก Advanced Pagination (Hybrid Approach)
- FastCRUD Integration: Uses FastCRUD if installed (battle-tested, feature-rich)
- Native Fallback: Falls back to native SQLAlchemy (zero dependencies)
- Type-Safe Filtering:
FilterSpecwith operators (eq, ne, gt, gte, lt, lte, in, like, ilike) - Multi-Field Sorting:
SortSpecwith asc/desc - Rich Metadata: Total count, pages, has_next, has_prev
๐ฆ Installation
Basic (Native SQLAlchemy only):
pip install sqlalchemy-async-repositories
With FastCRUD (Recommended):
pip install sqlalchemy-async-repositories[fastcrud]
๐ Prerequisites
Before using this library, you need to set up your SQLAlchemy 2.0 models and async session.
1. Define Your SQLAlchemy Model
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from datetime import datetime
class Base(DeclarativeBase):
"""Base class for all ORM models."""
pass
class User(Base):
"""Example: User entity."""
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(nullable=False, unique=True)
name: Mapped[str]
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
2. Set Up Async Database Session
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
# Create async engine
engine = create_async_engine(
"postgresql+asyncpg://user:password@localhost/dbname",
echo=True # Set to False in production
)
# Create session factory
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
# Get session
async with async_session() as session:
# Use session here
pass
๐ก Tip: For FastAPI, use dependency injection:
from fastapi import Depends
async def get_session() -> AsyncSession:
async with async_session() as session:
yield session
# In your route
@app.get("/users/{user_id}")
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
repo = UserRepository(session, User)
return await repo.get_by_id(user_id)
๐ Usage
Basic CRUD
from sqlalchemy_async_repositories import BaseRepository
from sqlalchemy.ext.asyncio import AsyncSession
# Step 1: Define repository for your model
class UserRepository(BaseRepository[User]):
"""
Repository for User entity.
Generic type [User]:
- Provides IDE autocomplete and type hints
- Ensures type safety (mypy/pyright)
- No runtime overhead
"""
pass
# Step 2: Initialize repository
# Parameters:
# - session: AsyncSession (from SQLAlchemy)
# - User: Your SQLAlchemy model class
repo = UserRepository(session, User)
# Step 3: Use CRUD methods
# Get by ID
user = await repo.get_by_id(123) # Returns: Optional[User]
# Create
new_user = User(email="user@example.com", name="John Doe")
created = await repo.create(new_user) # Returns: User
# Update
user.is_active = False
updated = await repo.update(user) # Returns: User
# Delete
deleted = await repo.delete(123) # Returns: bool
# Check existence
exists = await repo.exists(123) # Returns: bool
# Count all
total = await repo.count() # Returns: int
Advanced Pagination
from sqlalchemy_async_repositories import FilterSpec, SortSpec
from datetime import datetime, timedelta
yesterday = datetime.utcnow() - timedelta(days=1)
# Paginated query with filters and sorting
result = await repo.find_paginated(
page=1,
page_size=10,
filters=[
FilterSpec(field="is_active", operator="eq", value=True),
FilterSpec(field="created_at", operator="gt", value=yesterday),
FilterSpec(field="email", operator="like", value="example.com")
],
sort=[
SortSpec(field="created_at", direction="desc"),
SortSpec(field="name", direction="asc")
]
)
# Access results
print(f"Total: {result.total}")
print(f"Page: {result.page}/{result.pages}")
print(f"Has next: {result.has_next}")
for user in result.items:
print(f"{user.name} - {user.email}")
Filter Operators
FilterSpec(field="age", operator="eq", value=18) # Equal
FilterSpec(field="age", operator="ne", value=18) # Not equal
FilterSpec(field="age", operator="gt", value=18) # Greater than
FilterSpec(field="age", operator="gte", value=18) # Greater than or equal
FilterSpec(field="age", operator="lt", value=18) # Less than
FilterSpec(field="age", operator="lte", value=18) # Less than or equal
FilterSpec(field="status", operator="in", value=["active", "pending"]) # In list
FilterSpec(field="email", operator="like", value="example.com") # Like (case-sensitive)
FilterSpec(field="email", operator="ilike", value="example.com") # iLike (case-insensitive)
Check FastCRUD Availability
from sqlalchemy_async_repositories import has_fastcrud
if has_fastcrud():
print("Using FastCRUD for pagination")
else:
print("Using native SQLAlchemy for pagination")
๐๏ธ Architecture
Hybrid Approach
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BaseRepository โ
โ โ
โ Basic CRUD: โ
โ โ
Native SQLAlchemy (always) โ
โ - Fast โ
โ - Simple โ
โ - No dependencies โ
โ โ
โ Pagination/Filtering: โ
โ โก FastCRUD (if installed) โ
โ - Battle-tested โ
โ - Feature-rich โ
โ - Advanced joins โ
โ โ
โ ๐ Native SQLAlchemy (fallback) โ
โ - Zero dependencies โ
โ - Simple implementation โ
โ - Type-safe โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why Hybrid?
- Flexibility: Works with or without FastCRUD
- Performance: Native SQLAlchemy for basic operations
- Battle-tested: FastCRUD for complex pagination when available
- Zero Lock-in: Can switch between implementations
- Clean API: Same interface regardless of backend
๐งช Testing
import pytest
from sqlalchemy_async_repositories import BaseRepository, FilterSpec, SortSpec
@pytest.mark.asyncio
async def test_pagination(session):
"""Test pagination with filters."""
repo = UserRepository(session, User)
result = await repo.find_paginated(
page=1,
page_size=5,
filters=[FilterSpec(field="is_active", operator="eq", value=True)]
)
assert result.total >= 0
assert len(result.items) <= 5
assert result.page == 1
๐ Documentation
For complete documentation on the repository pattern and DDD architecture, see:
architecture/01_ARCHITECTURE_BASICS.md- Repository pattern basicsarchitecture/02_ARCHITECTURE_STRUCTURE.md- Where repositories fit
๐ค Related Packages
python-app-exceptions- Exception handlingpython-domain-primitives- Domain entities and value objectspython-cqrs-core- CQRS pattern implementation
๐๏ธ Design Patterns
This library uses 6 complementary design patterns for flexibility and maintainability:
| Pattern | Purpose | Where |
|---|---|---|
| Repository | Abstract data access | base.py |
| CQRS with use-cases | Separate read/write | use_cases/ |
| Strategy | Swap pagination backends | pagination/strategies/ |
| Factory | Create strategies | strategy_factory.py |
| Protocol (PEP 544) | Type-safe interfaces | IPaginationStrategy |
| Composition | Build from components | Handler delegation |
Each pattern solves a specific problem - see ARCHITECTURE.md for detailed explanation, visual diagrams, and trade-off analysis.
Why Multiple Patterns?
โ Repository - Hides SQLAlchemy details โ CQRS - Separate read/write, which is must-have for repositories โ Strategy - Zero lock-in (FastCRUD optional) โ Factory - Testable strategy selection โ Protocol - Type safety without inheritance โ Composition - Flexible, not rigid
Pattern Philosophy: "Use patterns when they serve a purpose, not for pattern's sake."
๐ License
MIT
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 sqlalchemy_async_repositories-0.1.0.tar.gz.
File metadata
- Download URL: sqlalchemy_async_repositories-0.1.0.tar.gz
- Upload date:
- Size: 21.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb7fe6c929b5a5c5c791c97c65b78ec8e1ec123b1d6a72ad06dd781187d2e2ab
|
|
| MD5 |
33c49254d1cffb0e0770e0d5df6b108e
|
|
| BLAKE2b-256 |
391738d7d4cea9c99def82b22fcd839e0f11bd3665a31e63ec6457aaec5f9ba4
|
File details
Details for the file sqlalchemy_async_repositories-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sqlalchemy_async_repositories-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc944ce2762b7c316842f1f1c53afd627c0f9f146adfb8a242dcb68c918e1ad5
|
|
| MD5 |
6a3d266e2b5efb2ab36cee16b420f596
|
|
| BLAKE2b-256 |
491aec37644adc2e582b75d3760a8e0e1bdf5372bea64e9da924f85566300db3
|