A flexible query language for Django. Enable the frontend to fetch exactly what it needs
Project description
Django-Flex
A flexible query language for Django — let your frontend dynamically construct database queries
Django-Flex enables frontends to send flexible, dynamic queries to your Django backend — think of it as a simpler alternative to GraphQL that feels native to Django.
Features
- 🎯 Field Selection — Request only the fields you need, including nested relations
- 🔍 Dynamic Filtering — Full Django ORM operator support with composable AND/OR/NOT
- 📄 Smart Pagination — Limit/offset with cursor-based continuation
- 🔒 Built-in Security — Row-level, field-level, and operation-level permissions
- ⚡ Automatic Optimization — N+1 prevention with smart
select_related - 🐍 Django-Native — Feels like a natural extension of Django
Installation
pip install django-flex
Add to your Django settings:
# settings.py
INSTALLED_APPS = [
...
'django_flex',
]
# Optional: Configure permissions and defaults
DJANGO_FLEX = {
'DEFAULT_LIMIT': 50,
'MAX_LIMIT': 200,
'PERMISSIONS': {
# See Permission Configuration below
},
}
Quick Start
1. Class-Based View (Recommended)
# views.py
from django_flex import FlexQueryView
from myapp.models import Booking
class BookingQueryView(FlexQueryView):
model = Booking
# Define permissions for this view
flex_permissions = {
'authenticated': {
'rows': lambda user: Q(team__members=user),
'fields': ['id', 'status', 'customer.name', 'customer.email'],
'filters': ['status', 'status.in', 'customer.name.icontains'],
'order_by': ['created_at', '-created_at'],
'ops': ['get', 'query'],
},
}
# urls.py
from django.urls import path
from myapp.views import BookingQueryView
urlpatterns = [
path('api/bookings/', BookingQueryView.as_view()),
path('api/bookings/<int:pk>/', BookingQueryView.as_view()), # Single object by ID
]
2. Make Queries from Frontend
// List bookings with field selection and filtering
const response = await fetch('/api/bookings/', {
method: 'GET',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
fields: 'id, status, customer.name, customer.email',
filters: {
'status.in': ['confirmed', 'completed'],
'customer.name.icontains': 'khan'
},
order_by: '-created_at',
limit: 20
})
});
const data = await response.json();
// {
// "pagination": {"offset": 0, "limit": 20, "has_more": true},
// "results": {
// "1": {"id": 1, "status": "confirmed", "customer": {"name": "Aisha Khan", "email": "aisha@example.com"}},
// "2": {"id": 2, "status": "completed", "customer": {"name": "Omar Khan", "email": "omar@example.com"}}
// }
// }
// Get single object by ID (using URL)
const booking = await fetch('/api/bookings/1/', {
method: 'GET',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
fields: 'id, status, customer.*, address.*'
})
});
// Returns: {"id": 1, "status": "confirmed", "customer": {...}, "address": {...}}
Query Language Reference
Field Selection
// All fields on the model
{ fields: '*' }
// Specific fields
{ fields: 'id, name, email' }
// Nested relation fields (dot notation)
{ fields: 'id, customer.name, customer.email' }
// Relation wildcards
{ fields: 'id, status, customer.*, address.*' }
Filtering
// Simple equality
{ filters: { status: 'confirmed' } }
// With operators
{ filters: { 'price.gte': 100, 'price.lte': 500 } }
// Text search
{ filters: { 'name.icontains': 'khan' } }
// List membership
{ filters: { 'status.in': ['pending', 'confirmed', 'completed'] } }
// OR conditions
{ filters: { or: { status: 'pending', 'customer.vip': true } } }
// NOT conditions
{ filters: { not: { status: 'cancelled' } } }
// Complex composition
{
filters: {
'created_at.gte': '2024-01-01',
or: [
{ status: 'confirmed' },
{ and: { status: 'pending', 'urgent': true } }
]
}
}
Supported Operators:
| Category | Operators |
|---|---|
| Comparison | lt, lte, gt, gte, exact, iexact, in, isnull, range |
| Text | contains, icontains, startswith, istartswith, endswith, iendswith, regex, iregex |
| Date/Time | date, year, month, day, week_day, hour, minute, second |
Pagination
{
limit: 20, // Number of results (default: 50, max: 200)
offset: 0, // Starting position
order_by: '-created_at' // Sort order (prefix with - for descending)
}
Response includes pagination info:
{
"pagination": {
"offset": 0,
"limit": 20,
"has_more": true,
"next": {
"fields": "...",
"filters": {...},
"limit": 20,
"offset": 20
}
}
}
Permission Configuration
Django-Flex uses a deny-by-default security model. You must explicitly grant access.
# settings.py
DJANGO_FLEX = {
'PERMISSIONS': {
'booking': {
# Fields excluded from wildcard expansion (security)
'exclude': ['internal_notes', 'stripe_payment_id'],
# Role-based permissions
'owner': {
# Row-level: which rows can this role see?
'rows': lambda user: Q(created_by=user),
# Field-level: which fields can they access?
'fields': ['*', 'customer.*', 'address.*'],
# Filter-level: which fields can they filter on?
'filters': [
'id', 'status', 'status.in',
'customer.name', 'customer.name.icontains',
'created_at.gte', 'created_at.lte',
],
# Order-level: which fields can they sort by?
'order_by': ['id', '-id', 'created_at', '-created_at', 'customer.name'],
# Operation-level: which actions can they perform?
'ops': ['get', 'query', 'create', 'update', 'delete'],
},
'staff': {
'rows': lambda user: Q(team__members=user),
'fields': ['id', 'status', 'customer.name', 'address.city'],
'filters': ['status', 'status.in'],
'order_by': ['created_at', '-created_at'],
'ops': ['get', 'query'],
},
# Roles not listed have NO ACCESS
},
},
}
Custom Role Resolution
Django-Flex uses Django's built-in groups for role resolution:
from django_flex import FlexPermission
class MyPermission(FlexPermission):
def get_user_role(self, user):
if user.is_superuser:
return 'superuser'
if user.groups.filter(name='Managers').exists():
return 'manager'
return 'staff'
Usage Patterns
1. Class-Based View (Recommended)
from django_flex import FlexQueryView
class BookingQueryView(FlexQueryView):
model = Booking
require_auth = True
allowed_actions = ['get', 'query']
flex_permissions = {...}
2. Function Decorator
from django_flex import flex_query
from django.http import JsonResponse
@flex_query(
model=Booking,
allowed_fields=['id', 'status', 'customer.name'],
allowed_filters=['status', 'status.in'],
)
def booking_list(request, result, query_spec):
return JsonResponse(result.to_dict())
3. Programmatic Usage
from django_flex import FlexQuery
def my_view(request):
result = FlexQuery(Booking).execute({
'fields': 'id, customer.name',
'filters': {'status': 'confirmed'},
'limit': 20,
}, user=request.user)
return JsonResponse(result.to_dict())
4. Middleware (Single Endpoint)
# settings.py
MIDDLEWARE = [
...
'django_flex.middleware.FlexQueryMiddleware',
]
DJANGO_FLEX = {
'MIDDLEWARE_PATH': '/api/',
...
}
Then query any configured model:
fetch('/api/', {
method: 'POST',
body: JSON.stringify({
_model: 'booking',
_action: 'query',
fields: 'id, status',
limit: 20
})
});
Configuration Reference
DJANGO_FLEX = {
# Pagination
'DEFAULT_LIMIT': 50, # Default page size
'MAX_LIMIT': 200, # Maximum page size (hard cap)
# Security
'MAX_RELATION_DEPTH': 2, # Max depth for nested fields/filters
'REQUIRE_AUTHENTICATION': True, # Require auth by default
'AUDIT_QUERIES': False, # Log all queries (for debugging)
# Middleware
'MIDDLEWARE_PATH': '/api/', # Path for middleware endpoint
# Optional: versioned APIs with independent settings
'VERSIONS': {
'v1': {'path': '/api/v1/', 'PERMISSIONS': {...}},
'v2': {'path': '/api/v2/', 'PERMISSIONS': {...}},
},
# Model permissions (see Rate Limiting section below)
'PERMISSIONS': {...},
}
API Versioning
Run unversioned /api/ alongside versioned /api/v1/, /api/v2/ with different settings per version:
DJANGO_FLEX = {
'MIDDLEWARE_PATH': '/api/', # Unversioned endpoint
'PERMISSIONS': {...}, # Top-level = unversioned settings
'MAX_LIMIT': 200,
'VERSIONS': {
'v1': {
'path': '/api/v1/',
'PERMISSIONS': {...}, # v1-specific permissions
'MAX_LIMIT': 100, # v1-specific limit
},
'v2': {
'path': '/api/v2/',
'PERMISSIONS': {...}, # v2-specific permissions
'MAX_LIMIT': 200,
},
},
}
Rate Limiting
Rate limits can be configured at multiple levels (most specific wins):
DJANGO_FLEX = {
'PERMISSIONS': {
'booking': {
# Model-level: integer = same for all ops
'rate_limit': 60,
# OR dict for per-operation limits
# 'rate_limit': {'default': 60, 'query': 30, 'get': 120},
# Anonymous users - very restricted
'anon': {
'fields': ['id', 'status'],
'ops': ['query'],
'rate_limit': 5, # Only 5 requests/minute for anon
},
'authenticated': {
'fields': ['*'],
'ops': ['get', 'query'],
'rate_limit': 50,
},
'staff': {
'fields': ['*'],
'ops': ['get', 'query'],
'rate_limit': 200, # Staff gets higher limits
},
},
},
}
Behind a Proxy
By default, anonymous rate limiting uses REMOTE_ADDR (not spoofable). If you are
behind a trusted reverse proxy that sets X-Forwarded-For, enable:
DJANGO_FLEX = {
'RATE_LIMIT_USE_FORWARDED_IP': True,
}
WARNING: Only enable this if your proxy is properly configured to set
X-Forwarded-For. Otherwise attackers can spoof their IP and bypass limits.
When rate limit is exceeded, returns HTTP 429 with Retry-After header:
{"error": "Rate limit exceeded", "retry_after": 45}
Response Format
Responses use HTTP status codes (200, 400, 401, 403, 404) to indicate success/failure.
Successful Single Object (get) - HTTP 200
{
"id": 1,
"status": "confirmed",
"customer": {
"name": "Aisha Khan",
"email": "aisha@example.com"
}
}
Successful Query (query) - HTTP 200
{
"pagination": {
"offset": 0,
"limit": 20,
"has_more": true,
"next": {...}
},
"results": {
"1": {...},
"2": {...}
}
}
Error Response - HTTP 400/401/403/404
{
"error": "Access denied: field 'secret_field' not accessible"
}
Why Django-Flex?
| Feature | Django-Flex | GraphQL | REST |
|---|---|---|---|
| Learning curve | Low (Django-native) | High | Low |
| Field selection | ✅ | ✅ | ❌ (fixed endpoints) |
| Dynamic filtering | ✅ | ✅ | Limited |
| Built-in security | ✅ | Manual | Manual |
| Django integration | Native | Requires graphene | Native |
| Schema definition | Optional | Required | N/A |
| N+1 prevention | Automatic | Manual | Manual |
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
MIT License — see LICENSE 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 django_flex-26.1.8.tar.gz.
File metadata
- Download URL: django_flex-26.1.8.tar.gz
- Upload date:
- Size: 78.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0abbeadb65d030f92a279008118bb0042c394d8ddc745830a6f90e23be20905f
|
|
| MD5 |
8e950a77838a76c78a5832259d4515b1
|
|
| BLAKE2b-256 |
7aa53c1d75ae5574123a2167df69e304bcd568f900935f076c7e4ef23a90352e
|
File details
Details for the file django_flex-26.1.8-py3-none-any.whl.
File metadata
- Download URL: django_flex-26.1.8-py3-none-any.whl
- Upload date:
- Size: 72.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f5d62324afbb2093e605b29b142941829a2c1e1fc1a9511a3b6164ddf36e0d1
|
|
| MD5 |
563ce89b52887d8abcf106a41a834af9
|
|
| BLAKE2b-256 |
de4bbec5e07bc7b194f67edfcdfaaeee17c321db4aa4d04d419eb57e4964697c
|