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/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
+ [![PyPI version](https://badge.fury.io/py/django-flex.svg)](https://pypi.org/project/django-flex/)
51
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-flex.svg)](https://pypi.org/project/django-flex/)
52
+ [![Django versions](https://img.shields.io/badge/django-3.2%20%7C%204.x%20%7C%205.x%20%7C%206.x-blue.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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