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