A structured query builder for FastAPI and SQLModel
Project description
๐ QueryMate
A powerful query builder for FastAPI and SQLModel โ with full support for:
- โ Filtering
- โ Sorting
- โ Pagination (limit/offset)
- โ Field selection
- โ Grouping (with date granularity and timezone support)
- โ Query parameter parsing
- โ Async database support
- โ Built-in serialization
Built for teams that want to build robust APIs with FastAPI and SQLModel.
โจ Key Features
| Feature | Description |
|---|---|
| ๐ Query Parameter Parsing | Parse and validate query parameters with ease |
| ๐ฏ Filtering | Build complex filters with a simple interface |
| ๐ Sorting | Sort results by multiple fields |
| ๐ Pagination | Limit and offset support for efficient data retrieval |
| ๐จ Field Selection | Select specific fields to return |
| ๐๏ธ Query Building | Build SQL queries programmatically |
| โก Async Support | Full support for async database operations |
| ๐ฆ Serialization | Built-in serialization with support for relationships |
| ๐ Grouping | Group results by field with date granularity and timezone support |
๐ Quick Start
Installation
pip install querymate
For async support, you'll also need to install the appropriate async database driver:
# For SQLite
pip install aiosqlite
# For PostgreSQL
pip install asyncpg
# For MySQL
pip install aiomysql
Basic Usage
- Define your SQLModel:
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
id: int = Field(primary_key=True)
name: str
email: str
age: int
posts: list["Post"] = Relationship(back_populates="author")
class Post(SQLModel, table=True):
id: int = Field(primary_key=True)
title: str
content: str
author_id: int = Field(foreign_key="user.id")
author: User = Relationship(back_populates="posts")
- Use QueryMate in your FastAPI route (Synchronous):
from fastapi import FastAPI, Depends
from sqlmodel import Session
from querymate import QueryMate
app = FastAPI()
@app.get("/users")
def get_users(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: Session = Depends(get_db)
):
# Returns serialized results as a list
if query.include_pagination:
return query.run_paginated(db, User)
return query.run(db, User)
@app.get("/users/raw")
def get_users_raw(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: Session = Depends(get_db)
):
# Returns raw model instances
return query.run_raw(db, User)
- Use QueryMate with Async Database (Asynchronous):
from fastapi import FastAPI, Depends
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from querymate import QueryMate
app = FastAPI()
# Create async database engine
engine = create_async_engine("sqlite+aiosqlite:///example.db")
# Create async session factory
async_session = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
# Database dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
finally:
await session.close()
@app.get("/users")
async def get_users(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: AsyncSession = Depends(get_db)
):
# Returns serialized results
if query.include_pagination:
return await query.run_async_paginated(db, User)
return await query.run_async(db, User)
@app.get("/users/raw")
async def get_users_raw(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: AsyncSession = Depends(get_db)
):
# Returns raw model instances
return await query.run_raw_async(db, User)
Advanced Usage
# Example query parameters
# ?q={"filter": {"age": {"gt": 18}}, "sort": ["-name", "age"], "limit": 10, "offset": 0, "select": ["id", "name", {"posts": ["title"]}]}
@app.get("/users")
async def get_users(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: AsyncSession = Depends(get_db)
):
# The query will be built and executed automatically
# Results will be serialized according to the fields
return await query.run_async(db, User)
Logical Filters (AND/OR)
Combine conditions explicitly with and and or in the filter block โ fully backward compatible with field-based filters:
-
OR on the same property (e.g., status = 1 OR status = 2):
/users?q={"filter":{"or":[{"status":{"eq":1}},{"status":{"eq":2}}]}}Or using
in:/users?q={"filter":{"status":{"in":[1,2]}}} -
Mixing AND and OR:
/users?q={ "filter":{ "and":[ {"or":[{"age":{"gt":18}},{"age":{"eq":18}}]}, {"name":{"cont":"J"}} ] } }
Direct equality without an operator remains supported, e.g. {"filter":{"status": 1}}.
Sorting
Basic sorting:
# Ascending by name
Querymate(sort=["name"]).run_raw(db, User)
# Descending by age, then ascending by name
Querymate(sort=["-age", "name"]).run_raw(db, User)
Custom value order (e.g., status pipelines):
# Bring these status values first in this order; others later
Querymate(sort=[{"status": ["pending", "active", "inactive"]}]).run_raw(db, Ticket)
# Equivalent explicit form
Querymate(sort=[{"status": {"values": ["pending", "active", "inactive"]}}]).run_raw(db, Ticket)
# Combine with secondary sort to order remaining values
Querymate(sort=[{"status": ["pending", "active", "inactive"]}, "-created_at"]).run_raw(db, Ticket)
# Custom order on related field using dot notation
Querymate(sort=[{"posts.visibility": ["private", "internal", "public"]}]).run_raw(db, User)
Pagination Metadata Response
In addition to plain lists, you can include pagination metadata alongside items. Use the dedicated paginated methods:
# Sync paginated response
result = query.run_paginated(db, User)
# Async paginated response
result = await query.run_async_paginated(db, User)
# Response shape (PaginatedResponse object)
# {
# "items": [{"id": 1, "name": "John"}, ...],
# "pagination": {
# "total": 57,
# "page": 2,
# "size": 10,
# "pages": 6,
# "previous_page": 1,
# "next_page": 3
# }
# }
The standard run and run_async methods always return a plain list of items:
# Always returns a list[dict[str, Any]]
result = query.run(db, User)
Grouping
Group query results by any field, including dates with configurable granularity and timezone support.
Basic Grouping by Field
# Group users by status
querymate = Querymate(
select=["id", "name", "status"],
group_by="status",
limit=10, # Per-group limit
)
result = querymate.run_grouped(db, User)
# Or async:
# result = await querymate.run_grouped_async(db, User)
Query parameter example:
/users?q={"select":["id","name","status"],"group_by":"status","limit":10}
Date Grouping with Granularity
Group by date fields with configurable granularity: year, month, day, hour, or minute.
# Group by month
querymate = Querymate(
select=["id", "title", "created_at"],
group_by={"field": "created_at", "granularity": "month"},
limit=10,
)
result = querymate.run_grouped(db, Post)
Query parameter examples:
# Group by year
/posts?q={"group_by":{"field":"created_at","granularity":"year"}}
# Group by day
/posts?q={"group_by":{"field":"created_at","granularity":"day"}}
# Group by hour
/posts?q={"group_by":{"field":"created_at","granularity":"hour"}}
Timezone Support
Apply timezone offset to date grouping using numeric offset or IANA timezone names.
# Using numeric offset (UTC-3)
querymate = Querymate(
select=["id", "title", "created_at"],
group_by={
"field": "created_at",
"granularity": "day",
"tz_offset": -3
},
limit=10,
)
# Using IANA timezone name
querymate = Querymate(
select=["id", "title", "created_at"],
group_by={
"field": "created_at",
"granularity": "day",
"timezone": "America/Sao_Paulo"
},
limit=10,
)
Query parameter examples:
# With numeric offset
/posts?q={"group_by":{"field":"created_at","granularity":"day","tz_offset":-3}}
# With IANA timezone
/posts?q={"group_by":{"field":"created_at","granularity":"day","timezone":"America/Sao_Paulo"}}
Supported IANA timezones include: UTC, America/New_York, America/Los_Angeles, America/Sao_Paulo, Europe/London, Europe/Paris, Asia/Tokyo, Asia/Shanghai, Australia/Sydney, and more.
Grouped Response Structure
{
"groups": [
{
"key": "active", # or "2024-01" for month grouping
"items": [
{"id": 1, "name": "Alice", "status": "active"},
{"id": 2, "name": "Bob", "status": "active"}
],
"pagination": {
"total": 15,
"page": 1,
"size": 10,
"pages": 2,
"previous_page": null,
"next_page": 2
}
},
{
"key": "inactive",
"items": [...],
"pagination": {...}
}
],
"truncated": false # true if MAX_LIMIT was reached
}
Pagination Behavior
limitapplies per group (each group returns up tolimititems)MAX_LIMIT(default 200) caps the total items across all groups combined- Groups are ordered naturally: alphabetically for strings, chronologically for dates
# 10 items per group, but total won't exceed MAX_LIMIT (200)
querymate = Querymate(
select=["id", "name", "status"],
group_by="status",
limit=10,
sort=["-created_at"], # Sorting applies within each group
)
Combining with Filters and Sorting
# Group active users by status, sorted by age within each group
querymate = Querymate(
select=["id", "name", "status", "age"],
filter={"is_active": True},
group_by="status",
sort=["-age"],
limit=10,
)
result = querymate.run_grouped(db, User)
Serialization
QueryMate includes built-in serialization capabilities that transform query results into dictionaries containing only the requested fields. This helps reduce payload size and improve performance.
Features:
- Direct field selection
- Nested relationships
- Both list and non-list relationships
- Automatic handling of null values
Example:
# Returns serialized results with only the requested fields
results = query.run(db, User)
# [
# {
# "id": 1,
# "name": "John",
# "posts": [
# {"id": 1, "title": "Post 1"},
# {"id": 2, "title": "Post 2"}
# ]
# }
# ]
# Returns raw model instances
raw_results = query.run_raw(db, User)
# [User(id=1, name="John", posts=[Post(id=1, title="Post 1"), Post(id=2, title="Post 2")])]
Exclude related items by status
To exclude related rows where a field does not match a value, filter on the related field using dot notation. This filters the joined rows while keeping the root records that still have matching related rows.
# Keep only posts with status == "published"
querymate = Querymate(
select=["id", "name", {"posts": ["id", "title", "status"]}],
filter={"posts.status": {"eq": "published"}},
)
results = querymate.run(db, User)
# Exclude posts where status != "archived" (keep all except archived)
querymate = Querymate(
select=["id", "name", {"posts": ["id", "title", "status"]}],
filter={"posts.status": {"ne": "archived"}},
)
results = querymate.run(db, User)
Note: QueryMate currently uses inner joins for relationships. Root rows without any matching related rows will be filtered out. If you need to include root rows with an empty related list, left outer joins are not yet configurable.
๐ ๏ธ Development Guide
Project Setup
# Clone the repository
git clone https://github.com/yourusername/querymate.git
cd querymate
# Set up the development environment
make setup
# Activate the virtual environment
source .venv/bin/activate
Project Structure
querymate/
โโโ core/ # Core functionality
โ โโโ querymate.py # Main QueryMate class
โ โโโ filter.py # Filter handling
โ โโโ query_builder.py # Query building
โ โโโ grouping.py # Grouping functionality
โ โโโ __init__.py # Package initialization
โโโ tests/ # Test suite
โโโ docs/ # Documentation
โ โโโ source/ # Sphinx documentation source
โ โ โโโ api/ # API documentation
โ โ โโโ examples/ # Usage examples
โ โ โโโ usage/ # Usage guides
โ โโโ conf.py # Sphinx configuration
โโโ examples/ # Example usage
Development Workflow
-
Create a new feature branch:
git checkout -b feature/your-feature-name
-
Make your changes and run tests:
make test
-
Run code quality checks:
make lint make format python -m mypy .
-
Update documentation:
make docs -
Submit a pull request
Testing
# Run all tests
make test
# Run tests with coverage
python -m pytest --cov=querymate
Documentation
# Build the documentation
make docs
# View the documentation
open docs/_build/html/index.html
๐ Documentation
For detailed documentation, visit banduk.github.io/querymate.
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
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 querymate-0.6.9.tar.gz.
File metadata
- Download URL: querymate-0.6.9.tar.gz
- Upload date:
- Size: 47.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
937bb687d7ce184a99211b207bbc7b1444642d0fdc15055ade78ad797b77cee3
|
|
| MD5 |
019785b725499efd8fb597a5206ecf61
|
|
| BLAKE2b-256 |
b406dca5166d90c2fd99e741fdb907a69cb8deb92ecbe5499f0139a7b6ad8dc5
|
File details
Details for the file querymate-0.6.9-py3-none-any.whl.
File metadata
- Download URL: querymate-0.6.9-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00d8c93cabba841d6d705d71e45300ec0dc619066b82d51042e10018e0cea048
|
|
| MD5 |
8baf7fd3304b4aa8a71fab769cf97ab2
|
|
| BLAKE2b-256 |
256e98da461cea6b55e20c70e6aa7ce810b66ed86314b7a9de383ebeeda3c66b
|