Compiler-oriented RSQL query engine for safe, typed, and extensible Python ORM-backed APIs.
Project description
pyrsql
A compiler-oriented RSQL query engine for safe, typed, and extensible filtering, sorting, and pagination in Python APIs.
pyrsql compiles RSQL query strings into ORM-specific statement objects through a language frontend, semantic binder, logical IR, and pluggable backend lowering - making it easy to expose complex query capabilities in your API without coupling to a specific ORM or framework.
Current backends: SQLAlchemy 2.0
Current framework adapters: FastAPI
Planned: Django ORM, SQLModel, Flask
Why pyrsql?
Most API filtering libraries are tightly coupled to one ORM or one framework.
pyrsql is built as a compiler pipeline - parsing, semantic analysis, and
IR lowering are separate stages. Adding a new ORM backend means implementing
one interface (ORM), not rewriting the parser or query language.
- ORM-neutral core -
Query,Sort,PageRequesthave zero ORM dependencies - Pluggable backends - implement
compile_query/compile_sort/compile_page_requestfor any ORM - Pluggable framework adapters - FastAPI today, Flask/Django tomorrow
- Custom operators - define your own RSQL operators with per-ORM lowering
- Field policies - whitelist, blacklist, aliases at global and per-model level
- Type-safe - strict mypy, Google-style docstrings, immutable value objects
Quickstart
pip install pyrsql[sqlalchemy]
import pyrsql
from sqlalchemy import select
from pyrsql.orms.sqlalchemy import SQLAlchemyORM
orm = SQLAlchemyORM()
stmt = select(User)
# Filter: name equals "demo", company name contains "acme"
stmt = pyrsql.parse("name==demo;company.name==acme*").apply(stmt, User, orm=orm)
# Sort: by name ascending, then company name descending
stmt = pyrsql.Sort.parse("name,asc;company.name,desc").apply(stmt, User, orm=orm)
# Paginate: page 0, 25 items per page
stmt = pyrsql.PageRequest.of(0, 25).apply(stmt, User, orm=orm)
Features
Query (filter)
- 20+ built-in operators:
==,!=,=gt=,=ge=,=lt=,=le=,=in=,=out=,=like=,=ilike=,=bt=,=na=,=nn=, etc. - Logical composition:
;(AND),,(OR) - Grouping with parentheses
- Wildcard matching (
*demo*) and case-insensitive markers (^demo) - Strict equality mode (literal
*and^) - Configurable
LIKEescape character SELECT DISTINCTsupport- Configurable parser limits (query length, depth, argument count)
Sort
- Multi-field:
name,asc;company.name,desc,ic - Ignore-case modifier (
ic) - Function selectors:
@upper[name],asc
Pagination
- Page-number + page-size API:
PageRequest.of(0, 25) - Offset + limit API:
PageRequest.from_offset(offset=50, limit=25) - Configurable max page size
Field Mapping & Access Control
- Aliases:
field_mapping={"username": "user.name"} - Whitelist/blacklist:
field_whitelist,field_blacklist - Per-model policies:
model_field_mapping,model_field_whitelist,model_field_blacklist
Custom Predicates
Define custom operators with ORM-specific lowering:
from pyrsql import CustomPredicateDefinition, QueryOptions
from pyrsql.parsing.operators import ComparisonOperator
all_match = ComparisonOperator(name="all_match", spellings=("=all=",), ...)
options = QueryOptions(
custom_predicates={
"all_match": CustomPredicateDefinition(operator=all_match, argument_type=str),
},
)
Value Conversion
- Built-in:
bool,int,float,Decimal,UUID,date,time,datetime,enum - Custom converters:
ValueConverterRegistry+with_converter - Field-scoped:
field_value_converters={"created_at": my_converter} - Model-scoped:
model_field_value_converters={MyModel: {"field": my_converter}}
Join Hints
from pyrsql import QueryOptions
from pyrsql.core.joins import JoinHint
options = QueryOptions(join_hints={"User.company": JoinHint.LEFT})
PostgreSQL JSON / JSONB
- Whole-document: direct
==,!=,=in=,=out=,=na=,=nn=against JSONB columns - Nested path:
jsonb_path_existsvia PostgreSQLjsonpath - Structured values: arrays and objects passed as
jsonpathvars - Temporal:
JSONOptions(use_datetime=True)for datetime-awarejsonpath - JSON sort: text, integer, float, numeric, boolean, date, time, datetime
- Custom function names:
JSONOptions(path_exists_function=...)
FastAPI Integration
from fastapi import Depends, FastAPI
from pyrsql.adapters.fastapi import criteria_dependency
app = FastAPI()
dependency = criteria_dependency()
@app.get("/items")
def list_items(criteria = Depends(dependency)):
...
- Auto-extracts
filter,sort,page,sizefrom query params HTTP 422with structured diagnostics on parse/semantic errors- OpenAPI examples from configuration
- One-based paging support
- Custom query parameter names
FastAPI + SQLAlchemy Integration
from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration
integration = FastAPISQLAlchemyIntegration()
@app.get("/users")
def list_users(stmt = Depends(integration.select_dependency(User))):
return {"sql": str(stmt)}
select_dependency,count_select_dependency,paginated_select_dependency- Declarative
resource()with auto-generated OpenAPI examples applier_dependencyfor custom base statements
Documentation
Full documentation at wskr00.github.io/pyrsql.
| Section | Description |
|---|---|
| Quickstart | One-minute primer |
| Usage | Filter, sort, page, JSON, FastAPI, custom predicates |
| API Reference | Auto-generated from docstrings |
| Operators | Complete operator table |
| Options | QueryOptions, SortOptions, JSONOptions |
| Architecture | Pipeline, modules, design |
| Extensibility | Adding backends and adapters |
| Contributing | Setup, workflow, standards |
Development Principles
- Object-oriented design
- SOLID principles
- Google Python Style Guide
- Strong typing
- ORM-neutral public API
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 pyrsql-0.0.2.tar.gz.
File metadata
- Download URL: pyrsql-0.0.2.tar.gz
- Upload date:
- Size: 101.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
558a13ac15fcdac28ef32217d280ae042dda8247da1d368793772b2cd231600a
|
|
| MD5 |
c87c7c2aa9f2e3133884570271a7e8a1
|
|
| BLAKE2b-256 |
f006d9aaa56fec215dd11e89d9954479387779e21c883e48d26cd713e6e66a87
|
File details
Details for the file pyrsql-0.0.2-py3-none-any.whl.
File metadata
- Download URL: pyrsql-0.0.2-py3-none-any.whl
- Upload date:
- Size: 92.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b72a79e15db1b04bd7e4ca278cd468a62526c18a3fe84e69a77ad0a6450eb73
|
|
| MD5 |
3a0196434b26b2681c5973e2bc1b3e2c
|
|
| BLAKE2b-256 |
85e0b56cc630c61ec7c0494fd6e387b6e4793b358e25b2aefcb4880c944a4fe7
|