Enhanced SQLAlchemy query builder with advanced filtering, sorting, pagination, and search capabilities.
Project description
StrapAlchemy
Enhanced SQLAlchemy query builder with advanced filtering, sorting, pagination, and search capabilities.
StrapAlchemy is a powerful query builder library for SQLAlchemy that provides Strapi-style query syntax for building complex database queries with ease.
Features
- Sync & Async Support: Choose between sync and async based on your session type
- Advanced Filtering: Strapi-style operators (
$eq,$in,$contains,$between, etc.) - Nested Relationship Filtering: Filter through related models with dot notation
- Flexible Sorting: Sort by direct fields or relationship fields
- Pagination: Support for both page-based and offset-based pagination
- Full-Text Search: BM25 search with ParadeDB integration and ILIKE fallback
- Field Selection: Select specific fields to optimize query performance
- Relationship Population: Eager load relationships to prevent N+1 queries
- Query Optimization: Built-in caching and optimization for better performance
- Model Serialization: Convert SQLAlchemy models to dictionaries easily
Installation
pip install strapalchemy
Quick Start
For Async Sessions (AsyncEngine + AsyncSession)
from sqlalchemy import select
from strapalchemy import FilterBuilder, SortBuilder, Paginator, SearchEngine, Base
from sqlalchemy import Column, Integer, String
# Define your model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
status = Column(String)
# Build your query
query = select(User)
# Apply filters (sync - no await needed)
filter_builder = FilterBuilder(User)
query = filter_builder.apply_filters(query, {
"name": {"$contains": "John"},
"status": {"$eq": "active"}
})
# Apply sorting (sync - no await needed)
sort_builder = SortBuilder(User)
query = sort_builder.apply_sorting(query, ["name:asc", "created_at:desc"])
# Apply search (sync - no await needed)
search_engine = SearchEngine()
query = search_engine.apply_search(query, User, "search term")
# Apply pagination (async - use with AsyncSession)
paginator = Paginator(session, User)
query, meta = await paginator.apply_pagination(query, {"page": 1, "page_size": 20})
# Execute
result = await session.execute(query)
users = result.scalars().all()
For Sync Sessions (Engine + Session)
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from strapalchemy import FilterBuilder, SortBuilder, SyncPaginator, SearchEngine, Base
from sqlalchemy import Column, Integer, String
# Define your model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
status = Column(String)
# Create sync session
engine = create_engine("sqlite:///database.db")
session = Session(engine)
# Build your query
query = select(User)
# Apply filters (sync - same as async)
filter_builder = FilterBuilder(User)
query = filter_builder.apply_filters(query, {
"name": {"$contains": "John"},
"status": {"$eq": "active"}
})
# Apply sorting (sync - same as async)
sort_builder = SortBuilder(User)
query = sort_builder.apply_sorting(query, ["name:asc", "created_at:desc"])
# Apply search (sync - same as async)
search_engine = SearchEngine()
query = search_engine.apply_search(query, User, "search term")
# Apply pagination (sync - use SyncPaginator with regular Session)
paginator = SyncPaginator(session, User)
query, meta = paginator.apply_pagination(query, {"page": 1, "page_size": 20})
# Execute
result = session.execute(query)
users = result.scalars().all()
Filtering
StrapAlchemy supports Strapi-style filtering operators:
| Operator | Description | Example |
|---|---|---|
$eq |
Equal | {"status": {"$eq": "active"}} |
$ne |
Not equal | {"status": {"$ne": "deleted"}} |
$lt |
Less than | {"age": {"$lt": 18}} |
$lte |
Less than or equal | {"age": {"$lte": 18}} |
$gt |
Greater than | {"age": {"$gt": 18}} |
$gte |
Greater than or equal | {"age": {"$gte": 18}} |
$in |
In list | {"status": {"$in": ["active", "pending"]}} |
$notIn |
Not in list | {"status": {"$notIn": ["deleted"]}} |
$contains |
Contains | {"name": {"$contains": "John"}} |
$containsi |
Contains (case insensitive) | {"name": {"$containsi": "john"}} |
$startsWith |
Starts with | {"email": {"$startsWith": "admin"}} |
$endsWith |
Ends with | {"email": {"$endsWith": "@example.com"}} |
$null |
Is null | {"deleted_at": {"$null": true}} |
$notNull |
Is not null | {"email": {"$notNull": true}} |
$between |
Between | {"created_at": {"$between": ["2024-01-01", "2024-12-31"]}} |
$or |
Logical OR | {"$or": [{"status": {"$eq": "active"}}, {"status": {"$eq": "pending"}}]} |
$and |
Logical AND | {"$and": [{"status": {"$eq": "active"}}, {"verified": {"$eq": true}}]} |
Nested Relationship Filtering
# Filter by relationship fields (sync - no await needed)
query = filter_builder.apply_filters(query, {
"organization": {"slug": {"$eq": "acme"}}
})
# Or use dot notation (sync - no await needed)
query = filter_builder.apply_filters(query, {
"organization.slug": {"$eq": "acme"}
})
Sorting
# Sort by single field (sync - no await needed)
query = sort_builder.apply_sorting(query, "name:asc")
# Sort by multiple fields (sync - no await needed)
query = sort_builder.apply_sorting(query, ["name:asc", "created_at:desc"])
# Sort by relationship field (sync - no await needed)
query = sort_builder.apply_sorting(query, ["organization.name:asc"])
Pagination
Note: Choose
Paginatorfor async sessions andSyncPaginatorfor sync sessions. Both have the same API.
For Async Sessions
from strapalchemy import Paginator
paginator = Paginator(async_session, User)
query, meta = await paginator.apply_pagination(query, {
"page": 1,
"page_size": 20
})
For Sync Sessions
from strapalchemy import SyncPaginator
paginator = SyncPaginator(session, User)
query, meta = paginator.apply_pagination(query, {
"page": 1,
"page_size": 20
})
Response Metadata
Both return the same metadata structure:
# {
# "page": 1,
# "page_size": 20,
# "page_count": 5,
# "total": 100,
# "has_next": True,
# "has_previous": False
# }
Page-based Pagination
# Async
query, meta = await paginator.apply_pagination(query, {"page": 1, "page_size": 20})
# Sync
query, meta = paginator.apply_pagination(query, {"page": 1, "page_size": 20})
Offset-based Pagination
# Async
query, meta = await paginator.apply_pagination(query, {"start": 0, "limit": 20})
# Sync
query, meta = paginator.apply_pagination(query, {"start": 0, "limit": 20})
Field Selection
from strapalchemy import FieldSelector
field_selector = FieldSelector(User)
query = field_selector.apply_field_selection(query, ["id", "name", "email"])
# Select relationship fields (sync - no await needed)
query = field_selector.apply_field_selection(query, ["id", "name", "organization.slug"])
Model Serialization
from strapalchemy import ModelSerializer
# Serialize a single model
data = ModelSerializer.serialize(user, fields=["id", "name", "email"])
# Serialize a list
data = ModelSerializer.serialize(users, fields=["id", "name"])
# Serialize with relationships
data = ModelSerializer.serialize(user, populate="organization")
# Serialize with nested relationships
data = ModelSerializer.serialize(user, populate=["organization", "user.role"])
Search
from strapalchemy import SearchEngine
search_engine = SearchEngine()
# Add searchable fields to your model
class User(Base):
__tablename__ = "users"
__searchable__ = {
"text_fields": ["name", "email", "bio"]
}
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
bio = Column(String)
# Apply search (sync - no await needed)
query = search_engine.apply_search(query, User, "John Doe")
Advanced Usage
Choosing Between Sync and Async
| Class | Sync Version | Async Version | Session Type |
|---|---|---|---|
| Paginator | SyncPaginator |
Paginator |
Session vs AsyncSession |
| QueryOptimizer | SyncQueryOptimizer |
QueryOptimizer |
Session vs AsyncSession |
API Reference
All builders (FilterBuilder, SortBuilder, SearchEngine, etc.) are sync and work with both session types:
| Method | Type | Session Type |
|---|---|---|
FilterBuilder.apply_filters() |
Sync | Works with both |
SortBuilder.apply_sorting() |
Sync | Works with both |
SearchEngine.apply_search() |
Sync | Works with both |
FieldSelector.apply_field_selection() |
Sync | Works with both |
PopulationBuilder.apply_population() |
Sync | Works with both |
SyncPaginator.apply_pagination() |
Sync | For sync Session only |
Paginator.apply_pagination() |
Async | For async AsyncSession only |
SyncQueryOptimizer.execute_optimized_query() |
Sync | For sync Session only |
QueryOptimizer.execute_optimized_query() |
Async | For async AsyncSession only |
Combining Multiple Builders
Async Example
from strapalchemy import FilterBuilder, SortBuilder, Paginator, SearchEngine
async def get_users(async_session, filters=None, sort=None, search=None, page=None):
query = select(User)
if filters:
query = filter_builder.apply_filters(query, filters) # sync
if sort:
query = sort_builder.apply_sorting(query, sort) # sync
if search:
query = search_engine.apply_search(query, User, search) # sync
if page:
paginator = Paginator(async_session, User)
query, meta = await paginator.apply_pagination(query, page) # async
result = await async_session.execute(query)
return result.scalars().all(), meta
Sync Example
from strapalchemy import FilterBuilder, SortBuilder, SyncPaginator, SearchEngine
def get_users(session, filters=None, sort=None, search=None, page=None):
query = select(User)
if filters:
query = filter_builder.apply_filters(query, filters) # sync
if sort:
query = sort_builder.apply_sorting(query, sort) # sync
if search:
query = search_engine.apply_search(query, User, search) # sync
if page:
paginator = SyncPaginator(session, User)
query, meta = paginator.apply_pagination(query, page) # sync
result = session.execute(query)
return result.scalars().all(), meta
Requirements
- Python >= 3.12
- SQLAlchemy >= 2.0.45
- python-dateutil >= 2.9.0
- rich >= 13.0.0
Changelog
0.2.5
- Added
SyncPaginatorfor synchronous SQLAlchemy sessions - Added
SyncQueryOptimizerfor synchronous query execution - Updated README with comprehensive sync/async examples
- Added sync session fixtures for testing
- All builders now work with both sync and async sessions
0.2.4
- Converted
FilterBuilder.apply_filtersto sync (no async overhead needed) - Updated all documentation and examples to reflect sync API
0.2.3
- Fixed import path for
Base- now import fromstrapalchemydirectly - Updated documentation with correct import examples
0.2.2
- Converted
_handle_or_operatorto sync (no async overhead needed) - Converted
_apply_default_paginationto sync (no async overhead needed) - Performance improvements for sync operations
0.2.1
- Converted
SortBuilder.apply_sortingto sync - Converted
SearchEngine.apply_searchto sync - Converted
FieldSelector.apply_field_selectionto sync - Converted
PopulationBuilder.apply_populationto sync - Converted FilterBuilder helper methods to sync
- Updated README with correct async/sync API usage
0.2.0
- Initial release with core features
- Advanced filtering with Strapi-style operators
- Nested relationship filtering
- Flexible sorting
- Pagination (page-based and offset-based)
- Full-text search with BM25/ILIKE fallback
- Field selection
- Relationship population
- Query optimization
- Model serialization
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues and questions, please use the GitHub issue tracker.
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
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 strapalchemy-0.2.5.tar.gz.
File metadata
- Download URL: strapalchemy-0.2.5.tar.gz
- Upload date:
- Size: 69.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
950bda2e415dd53374c4134e6beef4c70a29be8b8ef41c2a0b58001101ad63c5
|
|
| MD5 |
f6077cb3b9a077f1ba7344a7e4c3afcd
|
|
| BLAKE2b-256 |
a311c318957274fdaf2784d80ec42a25a146a6f25a0fbc2a7220d776b1139028
|
File details
Details for the file strapalchemy-0.2.5-py3-none-any.whl.
File metadata
- Download URL: strapalchemy-0.2.5-py3-none-any.whl
- Upload date:
- Size: 39.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bc59463c316345a447fad01f31d7f7ce63a287977307cad5d82570feee18465
|
|
| MD5 |
8ae2184cb17c71448a4c2d92b6378e5a
|
|
| BLAKE2b-256 |
e1c72544742cdfcc90c3df3b1b3a61f5bb1d23524796de914b0c40a2037b012c
|