django-flex 26.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-flex might be problematic. Click here for more details.
- django_flex/__init__.py +92 -0
- django_flex/conf.py +92 -0
- django_flex/decorators.py +182 -0
- django_flex/fields.py +234 -0
- django_flex/filters.py +207 -0
- django_flex/middleware.py +143 -0
- django_flex/permissions.py +365 -0
- django_flex/query.py +290 -0
- django_flex/response.py +212 -0
- django_flex/views.py +236 -0
- django_flex-26.1.0.dist-info/METADATA +300 -0
- django_flex-26.1.0.dist-info/RECORD +15 -0
- django_flex-26.1.0.dist-info/WHEEL +5 -0
- django_flex-26.1.0.dist-info/licenses/LICENSE +21 -0
- django_flex-26.1.0.dist-info/top_level.txt +1 -0
django_flex/views.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-Flex Views
|
|
3
|
+
|
|
4
|
+
Provides Django class-based views for handling flexible queries.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- FlexQueryView for easy integration
|
|
8
|
+
- Automatic permission handling
|
|
9
|
+
- Configurable per-view settings
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from django.http import JsonResponse
|
|
13
|
+
from django.views import View
|
|
14
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
15
|
+
from django.utils.decorators import method_decorator
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
from django_flex.query import FlexQuery
|
|
20
|
+
from django_flex.response import FlexResponse
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@method_decorator(csrf_exempt, name="dispatch")
|
|
24
|
+
class FlexQueryView(View):
|
|
25
|
+
"""
|
|
26
|
+
Generic view for handling flexible queries.
|
|
27
|
+
|
|
28
|
+
Subclass this view and configure the model and permissions
|
|
29
|
+
to create an endpoint for flexible queries.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
# views.py
|
|
33
|
+
from django_flex.views import FlexQueryView
|
|
34
|
+
from myapp.models import Entry
|
|
35
|
+
|
|
36
|
+
class EntryQueryView(FlexQueryView):
|
|
37
|
+
model = Entry
|
|
38
|
+
|
|
39
|
+
# Optional: custom permissions
|
|
40
|
+
flex_permissions = {
|
|
41
|
+
'staff': {
|
|
42
|
+
'rows': lambda user: Q(),
|
|
43
|
+
'fields': ['*', 'blog.*'],
|
|
44
|
+
'filters': ['rating', 'rating.gte', 'blog.name.icontains'],
|
|
45
|
+
'order_by': ['pub_date', '-pub_date'],
|
|
46
|
+
'ops': ['get', 'query'],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# urls.py
|
|
51
|
+
from django.urls import path
|
|
52
|
+
from myapp.views import EntryQueryView
|
|
53
|
+
|
|
54
|
+
urlpatterns = [
|
|
55
|
+
path('api/entries/', EntryQueryView.as_view(), name='entry-query'),
|
|
56
|
+
]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Required: the model to query
|
|
60
|
+
model = None
|
|
61
|
+
|
|
62
|
+
# Optional: custom permissions (uses settings.DJANGO_FLEX.PERMISSIONS if not set)
|
|
63
|
+
flex_permissions = None
|
|
64
|
+
|
|
65
|
+
# Optional: require authentication
|
|
66
|
+
require_auth = True
|
|
67
|
+
|
|
68
|
+
# Optional: allowed actions
|
|
69
|
+
allowed_actions = ["get", "query"]
|
|
70
|
+
|
|
71
|
+
def get_model(self):
|
|
72
|
+
"""Get the model to query. Override for dynamic model selection."""
|
|
73
|
+
return self.model
|
|
74
|
+
|
|
75
|
+
def get_permissions(self):
|
|
76
|
+
"""Get permissions configuration. Override for dynamic permissions."""
|
|
77
|
+
return self.flex_permissions
|
|
78
|
+
|
|
79
|
+
def get_user(self, request):
|
|
80
|
+
"""Get the user for permission checking. Override for custom auth."""
|
|
81
|
+
return request.user if hasattr(request, "user") else None
|
|
82
|
+
|
|
83
|
+
def check_auth(self, request):
|
|
84
|
+
"""Check authentication. Override for custom auth logic."""
|
|
85
|
+
if not self.require_auth:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
user = self.get_user(request)
|
|
89
|
+
return user and user.is_authenticated
|
|
90
|
+
|
|
91
|
+
def get_query_spec(self, request):
|
|
92
|
+
"""
|
|
93
|
+
Extract query specification from request.
|
|
94
|
+
|
|
95
|
+
Override to customize how queries are parsed from requests.
|
|
96
|
+
Default: parse JSON body for POST, query params for GET.
|
|
97
|
+
"""
|
|
98
|
+
if request.method == "POST":
|
|
99
|
+
try:
|
|
100
|
+
return json.loads(request.body)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
return None
|
|
103
|
+
else:
|
|
104
|
+
# Parse from query params
|
|
105
|
+
spec = {}
|
|
106
|
+
if "fields" in request.GET:
|
|
107
|
+
spec["fields"] = request.GET["fields"]
|
|
108
|
+
if "filters" in request.GET:
|
|
109
|
+
try:
|
|
110
|
+
spec["filters"] = json.loads(request.GET["filters"])
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
spec["filters"] = {}
|
|
113
|
+
if "limit" in request.GET:
|
|
114
|
+
try:
|
|
115
|
+
spec["limit"] = int(request.GET["limit"])
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
if "offset" in request.GET:
|
|
119
|
+
try:
|
|
120
|
+
spec["offset"] = int(request.GET["offset"])
|
|
121
|
+
except ValueError:
|
|
122
|
+
pass
|
|
123
|
+
if "order_by" in request.GET:
|
|
124
|
+
spec["order_by"] = request.GET["order_by"]
|
|
125
|
+
if "id" in request.GET:
|
|
126
|
+
try:
|
|
127
|
+
spec["id"] = int(request.GET["id"])
|
|
128
|
+
except ValueError:
|
|
129
|
+
spec["id"] = request.GET["id"]
|
|
130
|
+
return spec
|
|
131
|
+
|
|
132
|
+
def post(self, request, *args, **kwargs):
|
|
133
|
+
"""Handle POST request for queries."""
|
|
134
|
+
return self.handle_query(request)
|
|
135
|
+
|
|
136
|
+
def get(self, request, *args, **kwargs):
|
|
137
|
+
"""Handle GET request for queries."""
|
|
138
|
+
return self.handle_query(request)
|
|
139
|
+
|
|
140
|
+
def handle_query(self, request):
|
|
141
|
+
"""
|
|
142
|
+
Main query handler.
|
|
143
|
+
|
|
144
|
+
Validates authentication, parses query spec, and executes query.
|
|
145
|
+
"""
|
|
146
|
+
# Check authentication
|
|
147
|
+
if not self.check_auth(request):
|
|
148
|
+
return JsonResponse(
|
|
149
|
+
FlexResponse.error("PERMISSION_DENIED", "Authentication required").to_dict(),
|
|
150
|
+
status=401,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Get model
|
|
154
|
+
model = self.get_model()
|
|
155
|
+
if model is None:
|
|
156
|
+
return JsonResponse(
|
|
157
|
+
FlexResponse.error("MODEL_NOT_FOUND", "Model not configured").to_dict(),
|
|
158
|
+
status=500,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Get query spec
|
|
162
|
+
query_spec = self.get_query_spec(request)
|
|
163
|
+
if query_spec is None:
|
|
164
|
+
return JsonResponse(
|
|
165
|
+
FlexResponse.error("INVALID_FILTER", "Invalid query specification").to_dict(),
|
|
166
|
+
status=400,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Determine action
|
|
170
|
+
action = "get" if "id" in query_spec else "query"
|
|
171
|
+
if action not in self.allowed_actions:
|
|
172
|
+
return JsonResponse(
|
|
173
|
+
FlexResponse.error("PERMISSION_DENIED", f"Action '{action}' not allowed").to_dict(),
|
|
174
|
+
status=403,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Execute query
|
|
178
|
+
user = self.get_user(request)
|
|
179
|
+
permissions = self.get_permissions()
|
|
180
|
+
|
|
181
|
+
query = FlexQuery(model)
|
|
182
|
+
if permissions:
|
|
183
|
+
query.set_permissions(self._build_permissions_dict(permissions))
|
|
184
|
+
|
|
185
|
+
result = query.execute(query_spec, user=user, action=action)
|
|
186
|
+
|
|
187
|
+
# Determine HTTP status
|
|
188
|
+
if result.success:
|
|
189
|
+
status = 200
|
|
190
|
+
elif result.code == "NOT_FOUND":
|
|
191
|
+
status = 404
|
|
192
|
+
elif result.code == "PERMISSION_DENIED":
|
|
193
|
+
status = 403
|
|
194
|
+
else:
|
|
195
|
+
status = 400
|
|
196
|
+
|
|
197
|
+
return JsonResponse(result.to_dict(), status=status)
|
|
198
|
+
|
|
199
|
+
def _build_permissions_dict(self, flex_permissions):
|
|
200
|
+
"""Build a permissions dict compatible with the permission system."""
|
|
201
|
+
model_name = self.get_model().__name__.lower()
|
|
202
|
+
return {model_name: flex_permissions}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class FlexModelView(FlexQueryView):
|
|
206
|
+
"""
|
|
207
|
+
Model-specific view with URL-based model selection.
|
|
208
|
+
|
|
209
|
+
Allows querying multiple models from a single endpoint based on
|
|
210
|
+
the URL parameter.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
# urls.py
|
|
214
|
+
urlpatterns = [
|
|
215
|
+
path('api/<str:model_name>/', FlexModelView.as_view(), name='flex-query'),
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
# Requests:
|
|
219
|
+
# POST /api/entry/ -> query Entry model
|
|
220
|
+
# POST /api/author/ -> query Author model
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
# List of allowed model names (security measure)
|
|
224
|
+
allowed_models = []
|
|
225
|
+
|
|
226
|
+
def get_model(self):
|
|
227
|
+
"""Get model from URL."""
|
|
228
|
+
model_name = self.kwargs.get("model_name", "")
|
|
229
|
+
|
|
230
|
+
# Security check: only allow configured models
|
|
231
|
+
if self.allowed_models and model_name.lower() not in [m.lower() for m in self.allowed_models]:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
from django_flex.query import get_model_by_name
|
|
235
|
+
|
|
236
|
+
return get_model_by_name(model_name)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-flex
|
|
3
|
+
Version: 26.1.0
|
|
4
|
+
Summary: A flexible query language for Django - enable frontends to dynamically construct database queries
|
|
5
|
+
Author: Nehemiah Jacob
|
|
6
|
+
Maintainer: Nehemiah Jacob
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/your-org/django-flex
|
|
9
|
+
Project-URL: Documentation, https://github.com/your-org/django-flex#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/your-org/django-flex.git
|
|
11
|
+
Project-URL: Issues, https://github.com/your-org/django-flex/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/your-org/django-flex/blob/main/CHANGELOG.md
|
|
13
|
+
Keywords: django,query,api,flexible,dynamic,graphql-alternative,rest,orm
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
|
+
Requires-Python: >=3.8
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: django>=3.2
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort>=5.12; extra == "dev"
|
|
43
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
45
|
+
Requires-Dist: django-stubs>=4.0; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Django-Flex
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/django-flex/)
|
|
51
|
+
[](https://pypi.org/project/django-flex/)
|
|
52
|
+
[](https://www.djangoproject.com/)
|
|
53
|
+
|
|
54
|
+
**A flexible query language for Django** — Enable frontends to dynamically construct database queries with built-in security.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- 🔍 **Dynamic Field Selection** — Request only the fields you need
|
|
59
|
+
- 🔬 **Rich Filtering** — Full Django ORM operator support
|
|
60
|
+
- 📄 **Built-in Pagination** — Limit/offset with smart cursor support
|
|
61
|
+
- 🔐 **Layered Security** — Row, field, filter, and operation-level access control
|
|
62
|
+
- ⚡ **N+1 Prevention** — Automatic `select_related` optimization
|
|
63
|
+
- 🎯 **Django Native** — Uses Django's built-in auth (groups, permissions)
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install django-flex
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# settings.py
|
|
73
|
+
INSTALLED_APPS = [
|
|
74
|
+
...
|
|
75
|
+
'django_flex',
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
Using Django's official documentation models: Blog, Author, Entry.
|
|
82
|
+
|
|
83
|
+
### 1. Create a View
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# views.py
|
|
87
|
+
from datetime import date
|
|
88
|
+
from django.db.models import Q
|
|
89
|
+
from django_flex import FlexQueryView
|
|
90
|
+
from .models import Entry
|
|
91
|
+
|
|
92
|
+
class EntryQueryView(FlexQueryView):
|
|
93
|
+
model = Entry
|
|
94
|
+
|
|
95
|
+
flex_permissions = {
|
|
96
|
+
'staff': {
|
|
97
|
+
'rows': lambda user: Q(), # All entries
|
|
98
|
+
'fields': ['*', 'blog.*'],
|
|
99
|
+
'filters': ['id', 'rating.gte', 'blog.id', 'headline.icontains'],
|
|
100
|
+
'order_by': ['-pub_date', 'rating'],
|
|
101
|
+
'ops': ['get', 'query'],
|
|
102
|
+
},
|
|
103
|
+
'authenticated': {
|
|
104
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
105
|
+
'fields': ['id', 'headline', 'pub_date', 'blog.name'],
|
|
106
|
+
'filters': ['id', 'headline.icontains'],
|
|
107
|
+
'order_by': ['-pub_date'],
|
|
108
|
+
'ops': ['get', 'query'],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Add URL Route
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# urls.py
|
|
117
|
+
from .views import EntryQueryView
|
|
118
|
+
|
|
119
|
+
urlpatterns = [
|
|
120
|
+
path('api/entries/', EntryQueryView.as_view()),
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 3. Query from Frontend
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// Query entries
|
|
128
|
+
const response = await fetch('/api/entries/', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {'Content-Type': 'application/json'},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
fields: 'id, headline, pub_date, blog.name',
|
|
133
|
+
filters: { 'rating.gte': 4 },
|
|
134
|
+
order_by: '-pub_date',
|
|
135
|
+
limit: 20
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Response
|
|
140
|
+
{
|
|
141
|
+
"success": true,
|
|
142
|
+
"code": "FLEX_OK_QUERY",
|
|
143
|
+
"pagination": {"offset": 0, "limit": 20, "has_more": false},
|
|
144
|
+
"results": {
|
|
145
|
+
"1": {"id": 1, "headline": "Django 5.0 Released", "pub_date": "2024-01-15", "blog": {"name": "Django News"}},
|
|
146
|
+
"2": {"id": 2, "headline": "Getting Started Guide", "pub_date": "2024-01-14", "blog": {"name": "Tutorials"}}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Query Language
|
|
152
|
+
|
|
153
|
+
### Field Selection
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
fields: 'id, headline' // Specific fields
|
|
157
|
+
fields: '*' // All model fields
|
|
158
|
+
fields: '*, blog.*' // All fields + related
|
|
159
|
+
fields: 'id, blog.name, blog.tagline' // Nested fields
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Filtering
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// Simple equality
|
|
166
|
+
filters: { rating: 5 }
|
|
167
|
+
|
|
168
|
+
// Operators
|
|
169
|
+
filters: { 'rating.gte': 4 } // Greater than or equal
|
|
170
|
+
filters: { 'rating.in': [4, 5] } // In list
|
|
171
|
+
filters: { 'headline.icontains': 'django' } // Case-insensitive search
|
|
172
|
+
filters: { 'pub_date.gte': '2024-01-01' } // Date comparison
|
|
173
|
+
|
|
174
|
+
// Composition
|
|
175
|
+
filters: { or: { rating: 5, 'number_of_comments.gte': 100 } }
|
|
176
|
+
filters: { not: { 'rating.lt': 3 } }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Pagination
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
limit: 20 // Max results
|
|
183
|
+
offset: 40 // Skip first 40
|
|
184
|
+
|
|
185
|
+
// Response includes next cursor
|
|
186
|
+
pagination: {
|
|
187
|
+
offset: 40,
|
|
188
|
+
limit: 20,
|
|
189
|
+
has_more: true,
|
|
190
|
+
next: { fields: '...', limit: 20, offset: 60 }
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Security Configuration
|
|
195
|
+
|
|
196
|
+
Django-Flex uses Django's built-in auth for role resolution:
|
|
197
|
+
|
|
198
|
+
1. `superuser` → bypasses all checks
|
|
199
|
+
2. `staff` → `user.is_staff`
|
|
200
|
+
3. `<group_name>` → first Django group
|
|
201
|
+
4. `authenticated` → logged in, no group
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# settings.py
|
|
205
|
+
DJANGO_FLEX = {
|
|
206
|
+
'DEFAULT_LIMIT': 50,
|
|
207
|
+
'MAX_LIMIT': 200,
|
|
208
|
+
'MAX_RELATION_DEPTH': 2,
|
|
209
|
+
|
|
210
|
+
'PERMISSIONS': {
|
|
211
|
+
'entry': {
|
|
212
|
+
'exclude': ['internal_notes'],
|
|
213
|
+
|
|
214
|
+
'staff': {
|
|
215
|
+
'rows': lambda user: Q(),
|
|
216
|
+
'fields': ['*', 'blog.*'],
|
|
217
|
+
'filters': ['id', 'rating.gte', 'pub_date.gte', 'blog.id'],
|
|
218
|
+
'order_by': ['-pub_date', '-rating'],
|
|
219
|
+
'ops': ['get', 'query', 'create', 'update', 'delete'],
|
|
220
|
+
},
|
|
221
|
+
'authenticated': {
|
|
222
|
+
'rows': lambda user: Q(pub_date__lte=date.today()),
|
|
223
|
+
'fields': ['id', 'headline', 'pub_date', 'rating'],
|
|
224
|
+
'filters': ['id'],
|
|
225
|
+
'order_by': ['-pub_date'],
|
|
226
|
+
'ops': ['get', 'query'],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Usage Patterns
|
|
234
|
+
|
|
235
|
+
### Class-Based View
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from django_flex import FlexQueryView
|
|
239
|
+
|
|
240
|
+
class EntryQueryView(FlexQueryView):
|
|
241
|
+
model = Entry
|
|
242
|
+
flex_permissions = {...}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Decorator
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from django_flex import flex_query
|
|
249
|
+
|
|
250
|
+
@flex_query(
|
|
251
|
+
model=Entry,
|
|
252
|
+
allowed_fields=['id', 'headline', 'blog.name'],
|
|
253
|
+
allowed_filters=['id', 'headline.icontains'],
|
|
254
|
+
allowed_actions=['get', 'query'],
|
|
255
|
+
)
|
|
256
|
+
def entry_query(request, result, query_spec):
|
|
257
|
+
return JsonResponse(result.to_dict())
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Programmatic
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from django_flex import FlexQuery
|
|
264
|
+
|
|
265
|
+
result = FlexQuery(Entry).execute({
|
|
266
|
+
'fields': 'id, headline, blog.name',
|
|
267
|
+
'filters': {'rating.gte': 4},
|
|
268
|
+
}, user=request.user)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Supported Operators
|
|
272
|
+
|
|
273
|
+
| Category | Operators |
|
|
274
|
+
|----------|-----------|
|
|
275
|
+
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range` |
|
|
276
|
+
| Text | `contains`, `icontains`, `startswith`, `endswith`, `regex` |
|
|
277
|
+
| Date/Time | `date`, `year`, `month`, `day`, `hour`, `minute`, `second` |
|
|
278
|
+
| Composition | `and`, `or`, `not` |
|
|
279
|
+
|
|
280
|
+
## Response Codes
|
|
281
|
+
|
|
282
|
+
| Code | Description |
|
|
283
|
+
|------|-------------|
|
|
284
|
+
| `FLEX_OK` | Single object retrieved |
|
|
285
|
+
| `FLEX_OK_QUERY` | Query results returned |
|
|
286
|
+
| `FLEX_LIMIT_CLAMPED` | Results returned, limit was reduced |
|
|
287
|
+
| `FLEX_NOT_FOUND` | Object not found |
|
|
288
|
+
| `FLEX_PERMISSION_DENIED` | Access denied |
|
|
289
|
+
| `FLEX_INVALID_FILTER` | Invalid filter syntax |
|
|
290
|
+
|
|
291
|
+
## Documentation
|
|
292
|
+
|
|
293
|
+
- [Installation Guide](docs/installation.md)
|
|
294
|
+
- [Quick Start](docs/quickstart.md)
|
|
295
|
+
- [Permissions Guide](docs/permissions.md)
|
|
296
|
+
- [API Reference](docs/api_reference.md)
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
django_flex/__init__.py,sha256=m_7w1s_pKA2V6XJmCs2OI2zztIn4Gh7PMXQHY5rnE4s,1929
|
|
2
|
+
django_flex/conf.py,sha256=yZZfu-7UhEAdV6YdDfHHxiR52LoR9OKPn62SO3rM4Ow,2460
|
|
3
|
+
django_flex/decorators.py,sha256=07kUaZ1K0ko5dNmH53krEtzpmZtPswIWA468-jk9BGg,6251
|
|
4
|
+
django_flex/fields.py,sha256=Q-pNpg2xIJ4rUJh9YG2hMjCZQYW7KX-cmRDrvh5Rn4g,7162
|
|
5
|
+
django_flex/filters.py,sha256=XDwDsGP3V0NVZRWma5ZD82f4hWo1n8FGjwWwRTuHPJc,5595
|
|
6
|
+
django_flex/middleware.py,sha256=EcjvagijCW5cZ5XYjt6-6-lfFNr1fyyJYUR6_IFSni8,4232
|
|
7
|
+
django_flex/permissions.py,sha256=4Fyki_3FtTo5QDe9IRIc6MFF2ZvK8ia4D7p46ubF76U,11019
|
|
8
|
+
django_flex/query.py,sha256=nqMmoFygD_t308LpmzbOgMIEcsOp_aKsEDYqcZKfFPM,9380
|
|
9
|
+
django_flex/response.py,sha256=9iTT-FMimAeRTm5X4WyYO6fc_EbL9yw7uJxb8pIDQ70,5496
|
|
10
|
+
django_flex/views.py,sha256=ZHxBtPKyGeQ3I_2jVXSz_F1KuEyqFNy7Laq5wknyPxY,7399
|
|
11
|
+
django_flex-26.1.0.dist-info/licenses/LICENSE,sha256=iMZHDgZuRwKZQjbArgnHtDAWCBr7fzln345UATCMat4,1071
|
|
12
|
+
django_flex-26.1.0.dist-info/METADATA,sha256=37UW4rLUBSOy_AtevCAqEThPI57M_cJGQD3OsfefLBY,8685
|
|
13
|
+
django_flex-26.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
django_flex-26.1.0.dist-info/top_level.txt,sha256=Zgjf1KylVfiV2cXdAQNyPzz5zXSgU4lmKG8aGhSxXuE,12
|
|
15
|
+
django_flex-26.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nehemiah Jacob
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_flex
|