django-flex 26.1.8__tar.gz

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.
Files changed (53) hide show
  1. django_flex-26.1.8/CHANGELOG.md +42 -0
  2. django_flex-26.1.8/LICENSE +21 -0
  3. django_flex-26.1.8/MANIFEST.in +7 -0
  4. django_flex-26.1.8/PKG-INFO +572 -0
  5. django_flex-26.1.8/README.md +525 -0
  6. django_flex-26.1.8/django_flex/__init__.py +92 -0
  7. django_flex-26.1.8/django_flex/conf.py +101 -0
  8. django_flex-26.1.8/django_flex/decorators.py +172 -0
  9. django_flex-26.1.8/django_flex/fields.py +360 -0
  10. django_flex-26.1.8/django_flex/filters.py +207 -0
  11. django_flex-26.1.8/django_flex/middleware.py +378 -0
  12. django_flex-26.1.8/django_flex/permissions.py +494 -0
  13. django_flex-26.1.8/django_flex/query.py +414 -0
  14. django_flex-26.1.8/django_flex/ratelimit.py +188 -0
  15. django_flex-26.1.8/django_flex/response.py +338 -0
  16. django_flex-26.1.8/django_flex/tests/__init__.py +1 -0
  17. django_flex-26.1.8/django_flex/tests/conftest.py +43 -0
  18. django_flex-26.1.8/django_flex/tests/test_fields.py +79 -0
  19. django_flex-26.1.8/django_flex/tests/test_filters.py +168 -0
  20. django_flex-26.1.8/django_flex/tests/test_jsonfield.py +142 -0
  21. django_flex-26.1.8/django_flex/tests/test_middleware.py +244 -0
  22. django_flex-26.1.8/django_flex/tests/test_permissions.py +129 -0
  23. django_flex-26.1.8/django_flex/tests/test_query_crud.py +624 -0
  24. django_flex-26.1.8/django_flex/tests/test_ratelimit.py +242 -0
  25. django_flex-26.1.8/django_flex/tests/test_response.py +304 -0
  26. django_flex-26.1.8/django_flex/tests/test_role_resolver.py +336 -0
  27. django_flex-26.1.8/django_flex/tests/test_security.py +160 -0
  28. django_flex-26.1.8/django_flex/tests/test_security_h1.py +183 -0
  29. django_flex-26.1.8/django_flex/tests/test_security_h2.py +239 -0
  30. django_flex-26.1.8/django_flex/tests/test_security_h3.py +321 -0
  31. django_flex-26.1.8/django_flex/tests/test_security_h4.py +230 -0
  32. django_flex-26.1.8/django_flex/tests/test_security_h5.py +242 -0
  33. django_flex-26.1.8/django_flex/tests/test_security_medium.py +369 -0
  34. django_flex-26.1.8/django_flex/tests/test_utils.py +201 -0
  35. django_flex-26.1.8/django_flex/views.py +254 -0
  36. django_flex-26.1.8/django_flex.egg-info/PKG-INFO +572 -0
  37. django_flex-26.1.8/django_flex.egg-info/SOURCES.txt +51 -0
  38. django_flex-26.1.8/django_flex.egg-info/dependency_links.txt +1 -0
  39. django_flex-26.1.8/django_flex.egg-info/requires.txt +11 -0
  40. django_flex-26.1.8/django_flex.egg-info/top_level.txt +1 -0
  41. django_flex-26.1.8/docs/api_reference.md +411 -0
  42. django_flex-26.1.8/docs/examples/basic_usage.md +388 -0
  43. django_flex-26.1.8/docs/examples/filtering.md +411 -0
  44. django_flex-26.1.8/docs/installation.md +86 -0
  45. django_flex-26.1.8/docs/permissions.md +365 -0
  46. django_flex-26.1.8/docs/quickstart.md +236 -0
  47. django_flex-26.1.8/pyproject.toml +111 -0
  48. django_flex-26.1.8/setup.cfg +4 -0
  49. django_flex-26.1.8/tests/test_fields.py +79 -0
  50. django_flex-26.1.8/tests/test_filters.py +168 -0
  51. django_flex-26.1.8/tests/test_permissions.py +77 -0
  52. django_flex-26.1.8/tests/test_ratelimit.py +239 -0
  53. django_flex-26.1.8/tests/test_response.py +94 -0
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2024-01-10
9
+
10
+ ### Added
11
+
12
+ - Initial release of django-flex
13
+ - Core query engine with field selection and filtering
14
+ - Permission system with row-level, field-level, and operation-level access control
15
+ - `FlexQueryView` class-based view for easy integration
16
+ - `flex_query` decorator for function-based views
17
+ - Optional middleware for centralized endpoint
18
+ - Comprehensive documentation and examples
19
+ - Support for Django 3.2, 4.0, 4.1, 4.2, 5.0, and 6.0
20
+ - Support for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.14
21
+
22
+ ### CRUD Action Names
23
+
24
+ - `get` - Retrieve single object by ID
25
+ - `query` - Query multiple objects with filters/pagination
26
+ - `create` - Create new objects
27
+ - `update` - Update existing objects
28
+ - `delete` - Delete objects
29
+
30
+ ### Features
31
+
32
+ - **Field Selection**: Use comma-separated field strings with dot notation for relations
33
+ - Wildcards: `*` for all fields, `customer.*` for all customer fields
34
+ - Nested relations: `customer.address.city`
35
+ - **Filtering**: Full Django ORM operator support
36
+ - Comparison: `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range`
37
+ - Text: `contains`, `icontains`, `startswith`, `endswith`, `regex`
38
+ - Date/Time: `date`, `year`, `month`, `day`, `hour`, `minute`, `second`
39
+ - Composable: `and`, `or`, `not` for complex conditions
40
+ - **Pagination**: Limit/offset with smart cursor-based continuation
41
+ - **Security**: Principle of least privilege with deny-by-default
42
+ - **Performance**: Automatic `select_related` for N+1 prevention
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 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,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ recursive-include django_flex *.py
5
+ recursive-include docs *.md
6
+ global-exclude __pycache__
7
+ global-exclude *.py[cod]
@@ -0,0 +1,572 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-flex
3
+ Version: 26.1.8
4
+ Summary: A flexible query language for Django. Enable the frontend to fetch exactly what it needs
5
+ Author: Nehemiah Jacob
6
+ Maintainer: Nehemiah Jacob
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/n3h3m/django-flex
9
+ Project-URL: Documentation, https://github.com/n3h3m/django-flex#readme
10
+ Project-URL: Repository, https://github.com/n3h3m/django-flex.git
11
+ Project-URL: Issues, https://github.com/n3h3m/django-flex/issues
12
+ Project-URL: Changelog, https://github.com/n3h3m/django-flex/blob/main/CHANGELOG.md
13
+ Keywords: django,query,api,flexible,dynamic,graphql-alternative,rest,orm
14
+ Classifier: Development Status :: 5 - Production/Stable
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
+ <p align="center">
51
+ <em>A flexible query language for Django — let your frontend dynamically construct database queries</em>
52
+ </p>
53
+
54
+ <p align="center">
55
+ <a href="https://pypi.org/project/django-flex/">
56
+ <img src="https://img.shields.io/pypi/v/django-flex.svg" alt="PyPI version">
57
+ </a>
58
+ <a href="https://pypi.org/project/django-flex/">
59
+ <img src="https://img.shields.io/pypi/pyversions/django-flex.svg" alt="Python versions">
60
+ </a>
61
+ <a href="https://github.com/your-org/django-flex/blob/main/LICENSE">
62
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
63
+ </a>
64
+ </p>
65
+
66
+ ---
67
+
68
+ **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.
69
+
70
+ ## Features
71
+
72
+ - 🎯 **Field Selection** — Request only the fields you need, including nested relations
73
+ - 🔍 **Dynamic Filtering** — Full Django ORM operator support with composable AND/OR/NOT
74
+ - 📄 **Smart Pagination** — Limit/offset with cursor-based continuation
75
+ - 🔒 **Built-in Security** — Row-level, field-level, and operation-level permissions
76
+ - ⚡ **Automatic Optimization** — N+1 prevention with smart `select_related`
77
+ - 🐍 **Django-Native** — Feels like a natural extension of Django
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install django-flex
83
+ ```
84
+
85
+ Add to your Django settings:
86
+
87
+ ```python
88
+ # settings.py
89
+ INSTALLED_APPS = [
90
+ ...
91
+ 'django_flex',
92
+ ]
93
+
94
+ # Optional: Configure permissions and defaults
95
+ DJANGO_FLEX = {
96
+ 'DEFAULT_LIMIT': 50,
97
+ 'MAX_LIMIT': 200,
98
+ 'PERMISSIONS': {
99
+ # See Permission Configuration below
100
+ },
101
+ }
102
+ ```
103
+
104
+ ## Quick Start
105
+
106
+ ### 1. Class-Based View (Recommended)
107
+
108
+ ```python
109
+ # views.py
110
+ from django_flex import FlexQueryView
111
+ from myapp.models import Booking
112
+
113
+ class BookingQueryView(FlexQueryView):
114
+ model = Booking
115
+
116
+ # Define permissions for this view
117
+ flex_permissions = {
118
+ 'authenticated': {
119
+ 'rows': lambda user: Q(team__members=user),
120
+ 'fields': ['id', 'status', 'customer.name', 'customer.email'],
121
+ 'filters': ['status', 'status.in', 'customer.name.icontains'],
122
+ 'order_by': ['created_at', '-created_at'],
123
+ 'ops': ['get', 'query'],
124
+ },
125
+ }
126
+ ```
127
+
128
+ ```python
129
+ # urls.py
130
+ from django.urls import path
131
+ from myapp.views import BookingQueryView
132
+
133
+ urlpatterns = [
134
+ path('api/bookings/', BookingQueryView.as_view()),
135
+ path('api/bookings/<int:pk>/', BookingQueryView.as_view()), # Single object by ID
136
+ ]
137
+ ```
138
+
139
+ ### 2. Make Queries from Frontend
140
+
141
+ ```javascript
142
+ // List bookings with field selection and filtering
143
+ const response = await fetch('/api/bookings/', {
144
+ method: 'GET',
145
+ headers: {'Content-Type': 'application/json'},
146
+ body: JSON.stringify({
147
+ fields: 'id, status, customer.name, customer.email',
148
+ filters: {
149
+ 'status.in': ['confirmed', 'completed'],
150
+ 'customer.name.icontains': 'khan'
151
+ },
152
+ order_by: '-created_at',
153
+ limit: 20
154
+ })
155
+ });
156
+
157
+ const data = await response.json();
158
+ // {
159
+ // "pagination": {"offset": 0, "limit": 20, "has_more": true},
160
+ // "results": {
161
+ // "1": {"id": 1, "status": "confirmed", "customer": {"name": "Aisha Khan", "email": "aisha@example.com"}},
162
+ // "2": {"id": 2, "status": "completed", "customer": {"name": "Omar Khan", "email": "omar@example.com"}}
163
+ // }
164
+ // }
165
+ ```
166
+
167
+ ```javascript
168
+ // Get single object by ID (using URL)
169
+ const booking = await fetch('/api/bookings/1/', {
170
+ method: 'GET',
171
+ headers: {'Content-Type': 'application/json'},
172
+ body: JSON.stringify({
173
+ fields: 'id, status, customer.*, address.*'
174
+ })
175
+ });
176
+ // Returns: {"id": 1, "status": "confirmed", "customer": {...}, "address": {...}}
177
+ ```
178
+
179
+ ## Query Language Reference
180
+
181
+ ### Field Selection
182
+
183
+ ```javascript
184
+ // All fields on the model
185
+ { fields: '*' }
186
+
187
+ // Specific fields
188
+ { fields: 'id, name, email' }
189
+
190
+ // Nested relation fields (dot notation)
191
+ { fields: 'id, customer.name, customer.email' }
192
+
193
+ // Relation wildcards
194
+ { fields: 'id, status, customer.*, address.*' }
195
+ ```
196
+
197
+ ### Filtering
198
+
199
+ ```javascript
200
+ // Simple equality
201
+ { filters: { status: 'confirmed' } }
202
+
203
+ // With operators
204
+ { filters: { 'price.gte': 100, 'price.lte': 500 } }
205
+
206
+ // Text search
207
+ { filters: { 'name.icontains': 'khan' } }
208
+
209
+ // List membership
210
+ { filters: { 'status.in': ['pending', 'confirmed', 'completed'] } }
211
+
212
+ // OR conditions
213
+ { filters: { or: { status: 'pending', 'customer.vip': true } } }
214
+
215
+ // NOT conditions
216
+ { filters: { not: { status: 'cancelled' } } }
217
+
218
+ // Complex composition
219
+ {
220
+ filters: {
221
+ 'created_at.gte': '2024-01-01',
222
+ or: [
223
+ { status: 'confirmed' },
224
+ { and: { status: 'pending', 'urgent': true } }
225
+ ]
226
+ }
227
+ }
228
+ ```
229
+
230
+ **Supported Operators:**
231
+
232
+ | Category | Operators |
233
+ |----------|-----------|
234
+ | Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `iexact`, `in`, `isnull`, `range` |
235
+ | Text | `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `regex`, `iregex` |
236
+ | Date/Time | `date`, `year`, `month`, `day`, `week_day`, `hour`, `minute`, `second` |
237
+
238
+ ### Pagination
239
+
240
+ ```javascript
241
+ {
242
+ limit: 20, // Number of results (default: 50, max: 200)
243
+ offset: 0, // Starting position
244
+ order_by: '-created_at' // Sort order (prefix with - for descending)
245
+ }
246
+ ```
247
+
248
+ Response includes pagination info:
249
+
250
+ ```javascript
251
+ {
252
+ "pagination": {
253
+ "offset": 0,
254
+ "limit": 20,
255
+ "has_more": true,
256
+ "next": {
257
+ "fields": "...",
258
+ "filters": {...},
259
+ "limit": 20,
260
+ "offset": 20
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ ## Permission Configuration
267
+
268
+ Django-Flex uses a **deny-by-default** security model. You must explicitly grant access.
269
+
270
+ ```python
271
+ # settings.py
272
+ DJANGO_FLEX = {
273
+ 'PERMISSIONS': {
274
+ 'booking': {
275
+ # Fields excluded from wildcard expansion (security)
276
+ 'exclude': ['internal_notes', 'stripe_payment_id'],
277
+
278
+ # Role-based permissions
279
+ 'owner': {
280
+ # Row-level: which rows can this role see?
281
+ 'rows': lambda user: Q(created_by=user),
282
+
283
+ # Field-level: which fields can they access?
284
+ 'fields': ['*', 'customer.*', 'address.*'],
285
+
286
+ # Filter-level: which fields can they filter on?
287
+ 'filters': [
288
+ 'id', 'status', 'status.in',
289
+ 'customer.name', 'customer.name.icontains',
290
+ 'created_at.gte', 'created_at.lte',
291
+ ],
292
+
293
+ # Order-level: which fields can they sort by?
294
+ 'order_by': ['id', '-id', 'created_at', '-created_at', 'customer.name'],
295
+
296
+ # Operation-level: which actions can they perform?
297
+ 'ops': ['get', 'query', 'create', 'update', 'delete'],
298
+ },
299
+
300
+ 'staff': {
301
+ 'rows': lambda user: Q(team__members=user),
302
+ 'fields': ['id', 'status', 'customer.name', 'address.city'],
303
+ 'filters': ['status', 'status.in'],
304
+ 'order_by': ['created_at', '-created_at'],
305
+ 'ops': ['get', 'query'],
306
+ },
307
+
308
+ # Roles not listed have NO ACCESS
309
+ },
310
+ },
311
+ }
312
+ ```
313
+
314
+ ### Custom Role Resolution
315
+
316
+ Django-Flex uses Django's built-in groups for role resolution:
317
+
318
+ ```python
319
+ from django_flex import FlexPermission
320
+
321
+ class MyPermission(FlexPermission):
322
+ def get_user_role(self, user):
323
+ if user.is_superuser:
324
+ return 'superuser'
325
+ if user.groups.filter(name='Managers').exists():
326
+ return 'manager'
327
+ return 'staff'
328
+ ```
329
+
330
+ ## Usage Patterns
331
+
332
+ ### 1. Class-Based View (Recommended)
333
+
334
+ ```python
335
+ from django_flex import FlexQueryView
336
+
337
+ class BookingQueryView(FlexQueryView):
338
+ model = Booking
339
+ require_auth = True
340
+ allowed_actions = ['get', 'query']
341
+ flex_permissions = {...}
342
+ ```
343
+
344
+ ### 2. Function Decorator
345
+
346
+ ```python
347
+ from django_flex import flex_query
348
+ from django.http import JsonResponse
349
+
350
+ @flex_query(
351
+ model=Booking,
352
+ allowed_fields=['id', 'status', 'customer.name'],
353
+ allowed_filters=['status', 'status.in'],
354
+ )
355
+ def booking_list(request, result, query_spec):
356
+ return JsonResponse(result.to_dict())
357
+ ```
358
+
359
+ ### 3. Programmatic Usage
360
+
361
+ ```python
362
+ from django_flex import FlexQuery
363
+
364
+ def my_view(request):
365
+ result = FlexQuery(Booking).execute({
366
+ 'fields': 'id, customer.name',
367
+ 'filters': {'status': 'confirmed'},
368
+ 'limit': 20,
369
+ }, user=request.user)
370
+
371
+ return JsonResponse(result.to_dict())
372
+ ```
373
+
374
+ ### 4. Middleware (Single Endpoint)
375
+
376
+ ```python
377
+ # settings.py
378
+ MIDDLEWARE = [
379
+ ...
380
+ 'django_flex.middleware.FlexQueryMiddleware',
381
+ ]
382
+
383
+ DJANGO_FLEX = {
384
+ 'MIDDLEWARE_PATH': '/api/',
385
+ ...
386
+ }
387
+ ```
388
+
389
+ Then query any configured model:
390
+
391
+ ```javascript
392
+ fetch('/api/', {
393
+ method: 'POST',
394
+ body: JSON.stringify({
395
+ _model: 'booking',
396
+ _action: 'query',
397
+ fields: 'id, status',
398
+ limit: 20
399
+ })
400
+ });
401
+ ```
402
+
403
+ ## Configuration Reference
404
+
405
+ ```python
406
+ DJANGO_FLEX = {
407
+ # Pagination
408
+ 'DEFAULT_LIMIT': 50, # Default page size
409
+ 'MAX_LIMIT': 200, # Maximum page size (hard cap)
410
+
411
+ # Security
412
+ 'MAX_RELATION_DEPTH': 2, # Max depth for nested fields/filters
413
+ 'REQUIRE_AUTHENTICATION': True, # Require auth by default
414
+ 'AUDIT_QUERIES': False, # Log all queries (for debugging)
415
+
416
+ # Middleware
417
+ 'MIDDLEWARE_PATH': '/api/', # Path for middleware endpoint
418
+
419
+ # Optional: versioned APIs with independent settings
420
+ 'VERSIONS': {
421
+ 'v1': {'path': '/api/v1/', 'PERMISSIONS': {...}},
422
+ 'v2': {'path': '/api/v2/', 'PERMISSIONS': {...}},
423
+ },
424
+
425
+ # Model permissions (see Rate Limiting section below)
426
+ 'PERMISSIONS': {...},
427
+ }
428
+ ```
429
+
430
+ ### API Versioning
431
+
432
+ Run unversioned `/api/` alongside versioned `/api/v1/`, `/api/v2/` with different settings per version:
433
+
434
+ ```python
435
+ DJANGO_FLEX = {
436
+ 'MIDDLEWARE_PATH': '/api/', # Unversioned endpoint
437
+ 'PERMISSIONS': {...}, # Top-level = unversioned settings
438
+ 'MAX_LIMIT': 200,
439
+
440
+ 'VERSIONS': {
441
+ 'v1': {
442
+ 'path': '/api/v1/',
443
+ 'PERMISSIONS': {...}, # v1-specific permissions
444
+ 'MAX_LIMIT': 100, # v1-specific limit
445
+ },
446
+ 'v2': {
447
+ 'path': '/api/v2/',
448
+ 'PERMISSIONS': {...}, # v2-specific permissions
449
+ 'MAX_LIMIT': 200,
450
+ },
451
+ },
452
+ }
453
+ ```
454
+
455
+ ## Rate Limiting
456
+
457
+ Rate limits can be configured at multiple levels (most specific wins):
458
+
459
+ ```python
460
+ DJANGO_FLEX = {
461
+ 'PERMISSIONS': {
462
+ 'booking': {
463
+ # Model-level: integer = same for all ops
464
+ 'rate_limit': 60,
465
+
466
+ # OR dict for per-operation limits
467
+ # 'rate_limit': {'default': 60, 'query': 30, 'get': 120},
468
+
469
+ # Anonymous users - very restricted
470
+ 'anon': {
471
+ 'fields': ['id', 'status'],
472
+ 'ops': ['query'],
473
+ 'rate_limit': 5, # Only 5 requests/minute for anon
474
+ },
475
+
476
+ 'authenticated': {
477
+ 'fields': ['*'],
478
+ 'ops': ['get', 'query'],
479
+ 'rate_limit': 50,
480
+ },
481
+
482
+ 'staff': {
483
+ 'fields': ['*'],
484
+ 'ops': ['get', 'query'],
485
+ 'rate_limit': 200, # Staff gets higher limits
486
+ },
487
+ },
488
+ },
489
+ }
490
+ ```
491
+
492
+ ### Behind a Proxy
493
+
494
+ By default, anonymous rate limiting uses `REMOTE_ADDR` (not spoofable). If you are
495
+ behind a trusted reverse proxy that sets `X-Forwarded-For`, enable:
496
+
497
+ ```python
498
+ DJANGO_FLEX = {
499
+ 'RATE_LIMIT_USE_FORWARDED_IP': True,
500
+ }
501
+ ```
502
+
503
+ WARNING: Only enable this if your proxy is properly configured to set
504
+ `X-Forwarded-For`. Otherwise attackers can spoof their IP and bypass limits.
505
+
506
+ When rate limit is exceeded, returns HTTP 429 with `Retry-After` header:
507
+
508
+ ```json
509
+ {"error": "Rate limit exceeded", "retry_after": 45}
510
+ ```
511
+
512
+ ## Response Format
513
+
514
+ Responses use HTTP status codes (200, 400, 401, 403, 404) to indicate success/failure.
515
+
516
+ ### Successful Single Object (get) - HTTP 200
517
+
518
+ ```json
519
+ {
520
+ "id": 1,
521
+ "status": "confirmed",
522
+ "customer": {
523
+ "name": "Aisha Khan",
524
+ "email": "aisha@example.com"
525
+ }
526
+ }
527
+ ```
528
+
529
+ ### Successful Query (query) - HTTP 200
530
+
531
+ ```json
532
+ {
533
+ "pagination": {
534
+ "offset": 0,
535
+ "limit": 20,
536
+ "has_more": true,
537
+ "next": {...}
538
+ },
539
+ "results": {
540
+ "1": {...},
541
+ "2": {...}
542
+ }
543
+ }
544
+ ```
545
+
546
+ ### Error Response - HTTP 400/401/403/404
547
+
548
+ ```json
549
+ {
550
+ "error": "Access denied: field 'secret_field' not accessible"
551
+ }
552
+ ```
553
+
554
+ ## Why Django-Flex?
555
+
556
+ | Feature | Django-Flex | GraphQL | REST |
557
+ |---------|-------------|---------|------|
558
+ | Learning curve | Low (Django-native) | High | Low |
559
+ | Field selection | ✅ | ✅ | ❌ (fixed endpoints) |
560
+ | Dynamic filtering | ✅ | ✅ | Limited |
561
+ | Built-in security | ✅ | Manual | Manual |
562
+ | Django integration | Native | Requires graphene | Native |
563
+ | Schema definition | Optional | Required | N/A |
564
+ | N+1 prevention | Automatic | Manual | Manual |
565
+
566
+ ## Contributing
567
+
568
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
569
+
570
+ ## License
571
+
572
+ MIT License — see [LICENSE](LICENSE) for details.