drf-restflow 1.0.0__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.
- drf_restflow-1.0.0/LICENSE +13 -0
- drf_restflow-1.0.0/MANIFEST.in +5 -0
- drf_restflow-1.0.0/PKG-INFO +649 -0
- drf_restflow-1.0.0/README.md +590 -0
- drf_restflow-1.0.0/drf_restflow.egg-info/PKG-INFO +649 -0
- drf_restflow-1.0.0/drf_restflow.egg-info/SOURCES.txt +181 -0
- drf_restflow-1.0.0/drf_restflow.egg-info/dependency_links.txt +1 -0
- drf_restflow-1.0.0/drf_restflow.egg-info/requires.txt +31 -0
- drf_restflow-1.0.0/drf_restflow.egg-info/top_level.txt +1 -0
- drf_restflow-1.0.0/pyproject.toml +279 -0
- drf_restflow-1.0.0/restflow/__init__.py +8 -0
- drf_restflow-1.0.0/restflow/authentication/__init__.py +50 -0
- drf_restflow-1.0.0/restflow/authentication/apps.py +12 -0
- drf_restflow-1.0.0/restflow/authentication/authentication.py +147 -0
- drf_restflow-1.0.0/restflow/authentication/jwt.py +609 -0
- drf_restflow-1.0.0/restflow/authentication/migrations/0001_initial.py +36 -0
- drf_restflow-1.0.0/restflow/authentication/migrations/__init__.py +0 -0
- drf_restflow-1.0.0/restflow/authentication/models.py +39 -0
- drf_restflow-1.0.0/restflow/authentication/serializers.py +20 -0
- drf_restflow-1.0.0/restflow/authentication/simplejwt.py +76 -0
- drf_restflow-1.0.0/restflow/authentication/views.py +115 -0
- drf_restflow-1.0.0/restflow/caching/__init__.py +64 -0
- drf_restflow-1.0.0/restflow/caching/apps.py +16 -0
- drf_restflow-1.0.0/restflow/caching/constants.py +33 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/__init__.py +110 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/asyncio.py +56 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/base.py +70 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/celery.py +88 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/django_q.py +81 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/django_rq.py +74 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/dramatiq.py +92 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/inline.py +31 -0
- drf_restflow-1.0.0/restflow/caching/dispatchers/threadpool.py +81 -0
- drf_restflow-1.0.0/restflow/caching/hashing.py +13 -0
- drf_restflow-1.0.0/restflow/caching/key_constructor.py +363 -0
- drf_restflow-1.0.0/restflow/caching/key_fields.py +499 -0
- drf_restflow-1.0.0/restflow/caching/registry.py +660 -0
- drf_restflow-1.0.0/restflow/caching/rules.py +103 -0
- drf_restflow-1.0.0/restflow/caching/tasks.py +103 -0
- drf_restflow-1.0.0/restflow/caching/wrapper.py +487 -0
- drf_restflow-1.0.0/restflow/exceptions.py +232 -0
- drf_restflow-1.0.0/restflow/filters/__init__.py +28 -0
- drf_restflow-1.0.0/restflow/filters/backends.py +81 -0
- drf_restflow-1.0.0/restflow/filters/fields.py +873 -0
- drf_restflow-1.0.0/restflow/filters/filters.py +949 -0
- drf_restflow-1.0.0/restflow/helpers.py +208 -0
- drf_restflow-1.0.0/restflow/pagination/__init__.py +15 -0
- drf_restflow-1.0.0/restflow/pagination/pagination.py +219 -0
- drf_restflow-1.0.0/restflow/permissions/__init__.py +34 -0
- drf_restflow-1.0.0/restflow/permissions/permissions.py +225 -0
- drf_restflow-1.0.0/restflow/responses/__init__.py +11 -0
- drf_restflow-1.0.0/restflow/responses/streaming.py +97 -0
- drf_restflow-1.0.0/restflow/serializers/__init__.py +28 -0
- drf_restflow-1.0.0/restflow/serializers/fields.py +94 -0
- drf_restflow-1.0.0/restflow/serializers/serializers.py +516 -0
- drf_restflow-1.0.0/restflow/serializers/validated_data.py +94 -0
- drf_restflow-1.0.0/restflow/settings.py +169 -0
- drf_restflow-1.0.0/restflow/spectacular/__init__.py +30 -0
- drf_restflow-1.0.0/restflow/spectacular/extensions.py +0 -0
- drf_restflow-1.0.0/restflow/spectacular/hooks.py +49 -0
- drf_restflow-1.0.0/restflow/spectacular/parameters.py +287 -0
- drf_restflow-1.0.0/restflow/spectacular/schema.py +162 -0
- drf_restflow-1.0.0/restflow/tasks.py +6 -0
- drf_restflow-1.0.0/restflow/test/__init__.py +21 -0
- drf_restflow-1.0.0/restflow/test/client.py +227 -0
- drf_restflow-1.0.0/restflow/test/testcases.py +27 -0
- drf_restflow-1.0.0/restflow/throttling/__init__.py +17 -0
- drf_restflow-1.0.0/restflow/throttling/throttling.py +73 -0
- drf_restflow-1.0.0/restflow/views/__init__.py +72 -0
- drf_restflow-1.0.0/restflow/views/generics.py +194 -0
- drf_restflow-1.0.0/restflow/views/mixins.py +257 -0
- drf_restflow-1.0.0/restflow/views/post_fetch.py +203 -0
- drf_restflow-1.0.0/restflow/views/views.py +453 -0
- drf_restflow-1.0.0/restflow/views/viewsets.py +374 -0
- drf_restflow-1.0.0/setup.cfg +4 -0
- drf_restflow-1.0.0/tests/__init__.py +0 -0
- drf_restflow-1.0.0/tests/conftest.py +143 -0
- drf_restflow-1.0.0/tests/integration/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/api/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/api/conftest.py +16 -0
- drf_restflow-1.0.0/tests/integration/api/test_async_crud.py +296 -0
- drf_restflow-1.0.0/tests/integration/api/test_authentication_matrix.py +280 -0
- drf_restflow-1.0.0/tests/integration/api/test_caching_flow.py +181 -0
- drf_restflow-1.0.0/tests/integration/api/test_coverage_fillins.py +233 -0
- drf_restflow-1.0.0/tests/integration/api/test_filter_pagination_flow.py +236 -0
- drf_restflow-1.0.0/tests/integration/api/test_handler_modes.py +668 -0
- drf_restflow-1.0.0/tests/integration/api/test_jwt_auth_flow.py +298 -0
- drf_restflow-1.0.0/tests/integration/api/test_pagination_matrix.py +215 -0
- drf_restflow-1.0.0/tests/integration/api/test_request_response_serializer_matrix.py +346 -0
- drf_restflow-1.0.0/tests/integration/api/test_response_headers.py +168 -0
- drf_restflow-1.0.0/tests/integration/api/test_response_matrix.py +215 -0
- drf_restflow-1.0.0/tests/integration/api/test_streaming_flow.py +152 -0
- drf_restflow-1.0.0/tests/integration/api/test_sync_crud.py +204 -0
- drf_restflow-1.0.0/tests/integration/api/test_sync_viewset_actions.py +353 -0
- drf_restflow-1.0.0/tests/integration/api/test_sync_viewset_helpers.py +236 -0
- drf_restflow-1.0.0/tests/integration/api/test_throttle_matrix.py +230 -0
- drf_restflow-1.0.0/tests/integration/api/test_throttling_flow.py +187 -0
- drf_restflow-1.0.0/tests/integration/api/test_viewset_actions_flow.py +291 -0
- drf_restflow-1.0.0/tests/integration/authentication/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_async.py +305 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_blacklist_backends.py +206 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_edge_cases.py +469 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_jwt.py +301 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_jwt_security.py +114 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_jwt_views.py +186 -0
- drf_restflow-1.0.0/tests/integration/authentication/test_simplejwt.py +110 -0
- drf_restflow-1.0.0/tests/integration/caching/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/conftest.py +10 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_asyncio.py +155 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_celery.py +278 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_django_q.py +137 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_django_rq.py +125 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_dramatiq.py +158 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_inline.py +149 -0
- drf_restflow-1.0.0/tests/integration/caching/dispatchers/test_threadpool.py +164 -0
- drf_restflow-1.0.0/tests/integration/caching/test_apply_rules.py +447 -0
- drf_restflow-1.0.0/tests/integration/caching/test_async.py +409 -0
- drf_restflow-1.0.0/tests/integration/caching/test_cache_register.py +1316 -0
- drf_restflow-1.0.0/tests/integration/caching/test_decorator_and_keys.py +353 -0
- drf_restflow-1.0.0/tests/integration/caching/test_invalidator.py +498 -0
- drf_restflow-1.0.0/tests/integration/caching/test_redis.py +174 -0
- drf_restflow-1.0.0/tests/integration/exceptions/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/exceptions/test_envelope.py +217 -0
- drf_restflow-1.0.0/tests/integration/exceptions/test_handler.py +298 -0
- drf_restflow-1.0.0/tests/integration/filters/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/filters/test_async.py +303 -0
- drf_restflow-1.0.0/tests/integration/filters/test_backend.py +445 -0
- drf_restflow-1.0.0/tests/integration/filters/test_django_fields.py +730 -0
- drf_restflow-1.0.0/tests/integration/filters/test_field_application.py +208 -0
- drf_restflow-1.0.0/tests/integration/filters/test_field_combinations.py +236 -0
- drf_restflow-1.0.0/tests/integration/filters/test_filters.py +989 -0
- drf_restflow-1.0.0/tests/integration/filters/test_postgres.py +388 -0
- drf_restflow-1.0.0/tests/integration/pagination/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/pagination/test_async.py +426 -0
- drf_restflow-1.0.0/tests/integration/pagination/test_edge_cases.py +236 -0
- drf_restflow-1.0.0/tests/integration/permissions/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/permissions/test_async.py +209 -0
- drf_restflow-1.0.0/tests/integration/permissions/test_combinators.py +214 -0
- drf_restflow-1.0.0/tests/integration/responses/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/responses/test_encoding.py +165 -0
- drf_restflow-1.0.0/tests/integration/responses/test_streaming.py +158 -0
- drf_restflow-1.0.0/tests/integration/serializers/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_async.py +340 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_field_combinations.py +726 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_field_kwargs.py +635 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_hyperlinked.py +95 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_serializers.py +542 -0
- drf_restflow-1.0.0/tests/integration/serializers/test_validated_data.py +468 -0
- drf_restflow-1.0.0/tests/integration/spectacular/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/spectacular/test_filterset_parameters.py +274 -0
- drf_restflow-1.0.0/tests/integration/spectacular/test_schema.py +698 -0
- drf_restflow-1.0.0/tests/integration/test/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/test/test_client.py +351 -0
- drf_restflow-1.0.0/tests/integration/test_apps.py +68 -0
- drf_restflow-1.0.0/tests/integration/throttling/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/throttling/test_async.py +108 -0
- drf_restflow-1.0.0/tests/integration/throttling/test_rate_classes.py +184 -0
- drf_restflow-1.0.0/tests/integration/views/__init__.py +0 -0
- drf_restflow-1.0.0/tests/integration/views/test_action_config.py +480 -0
- drf_restflow-1.0.0/tests/integration/views/test_apiview.py +238 -0
- drf_restflow-1.0.0/tests/integration/views/test_async_helpers.py +493 -0
- drf_restflow-1.0.0/tests/integration/views/test_generics.py +685 -0
- drf_restflow-1.0.0/tests/integration/views/test_post_fetch.py +402 -0
- drf_restflow-1.0.0/tests/integration/views/test_request_response_split.py +199 -0
- drf_restflow-1.0.0/tests/integration/views/test_round_trip.py +580 -0
- drf_restflow-1.0.0/tests/integration/views/test_viewset.py +312 -0
- drf_restflow-1.0.0/tests/models.py +61 -0
- drf_restflow-1.0.0/tests/smoke/__init__.py +0 -0
- drf_restflow-1.0.0/tests/smoke/test_smoke.py +307 -0
- drf_restflow-1.0.0/tests/unit/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/caching/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/caching/dispatchers/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/caching/dispatchers/test_base.py +213 -0
- drf_restflow-1.0.0/tests/unit/caching/test_caching.py +1284 -0
- drf_restflow-1.0.0/tests/unit/caching/test_settings.py +219 -0
- drf_restflow-1.0.0/tests/unit/filters/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/filters/test_fields.py +721 -0
- drf_restflow-1.0.0/tests/unit/serializers/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/serializers/test_fields.py +180 -0
- drf_restflow-1.0.0/tests/unit/test_helpers.py +63 -0
- drf_restflow-1.0.0/tests/unit/views/__init__.py +0 -0
- drf_restflow-1.0.0/tests/unit/views/test_async_dispatch.py +88 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
License
|
|
2
|
+
|
|
3
|
+
Copyright © 2025-present, Khan Asfi Reza. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
10
|
+
|
|
11
|
+
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drf-restflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Declarative library for Django REST Framework
|
|
5
|
+
Author-email: Khan Asfi Reza <mail@khanasfireza.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/khan-asfi-reza/drf-restflow
|
|
8
|
+
Project-URL: Repository, https://github.com/khan-asfi-reza/drf-restflow
|
|
9
|
+
Project-URL: Issues, https://github.com/khan-asfi-reza/drf-restflow/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/khan-asfi-reza/drf-restflow/releases
|
|
11
|
+
Keywords: django,django-rest-framework,drf,filters,queryset,declarative,type-safe,fastapi,rest-api
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 3.2
|
|
16
|
+
Classifier: Framework :: Django :: 4.0
|
|
17
|
+
Classifier: Framework :: Django :: 4.1
|
|
18
|
+
Classifier: Framework :: Django :: 4.2
|
|
19
|
+
Classifier: Framework :: Django :: 5.0
|
|
20
|
+
Classifier: Framework :: Django :: 6.0
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python
|
|
24
|
+
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
30
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
31
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
32
|
+
Classifier: Typing :: Typed
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: django>=3.2
|
|
37
|
+
Requires-Dist: djangorestframework>=3.14
|
|
38
|
+
Requires-Dist: pyjwt>=2.8
|
|
39
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
40
|
+
Provides-Extra: celery
|
|
41
|
+
Requires-Dist: celery>=5.0; extra == "celery"
|
|
42
|
+
Provides-Extra: django-q
|
|
43
|
+
Requires-Dist: django-q2>=1.7; extra == "django-q"
|
|
44
|
+
Provides-Extra: django-rq
|
|
45
|
+
Requires-Dist: django-rq>=2.10; extra == "django-rq"
|
|
46
|
+
Provides-Extra: dramatiq
|
|
47
|
+
Requires-Dist: dramatiq>=1.16; extra == "dramatiq"
|
|
48
|
+
Provides-Extra: postgres
|
|
49
|
+
Requires-Dist: psycopg2-binary>=2.9.10; extra == "postgres"
|
|
50
|
+
Provides-Extra: postgres-psycopg3
|
|
51
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "postgres-psycopg3"
|
|
52
|
+
Provides-Extra: redis
|
|
53
|
+
Requires-Dist: django-redis>=5.4.0; extra == "redis"
|
|
54
|
+
Provides-Extra: simplejwt
|
|
55
|
+
Requires-Dist: djangorestframework-simplejwt>=5.3; extra == "simplejwt"
|
|
56
|
+
Provides-Extra: spectacular
|
|
57
|
+
Requires-Dist: drf-spectacular>=0.27; extra == "spectacular"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
# Restflow
|
|
61
|
+
|
|
62
|
+
A declarative library on top of Django REST Framework. It uses DRF's
|
|
63
|
+
serializer and validation infrastructure and adds declarative classes for
|
|
64
|
+
the parts of an API that turn into boilerplate over time.
|
|
65
|
+
|
|
66
|
+
The library covers caching, filtering, type-driven serializers, async
|
|
67
|
+
authentication, async permissions, a full async view and viewset stack,
|
|
68
|
+
async pagination, async throttling, streaming responses, a unified
|
|
69
|
+
exception handler, OpenAPI schema generation, and an async test client
|
|
70
|
+
and case suite.
|
|
71
|
+
|
|
72
|
+
Inspired by [FastAPI](https://fastapi.tiangolo.com/) and
|
|
73
|
+
[django-filter](https://django-filter.readthedocs.io). Works alongside
|
|
74
|
+
DRF rather than replacing it.
|
|
75
|
+
|
|
76
|
+
Full documentation:
|
|
77
|
+
[https://restflow.khanasfireza.dev/](https://restflow.khanasfireza.dev/)
|
|
78
|
+
|
|
79
|
+
## Table of Contents
|
|
80
|
+
|
|
81
|
+
- [Motivation](#motivation)
|
|
82
|
+
- [Installation](#installation)
|
|
83
|
+
- [Caching](#caching)
|
|
84
|
+
- [Filtering](#filtering)
|
|
85
|
+
- [Serializers](#serializers)
|
|
86
|
+
- [Authentication](#authentication)
|
|
87
|
+
- [Permissions](#permissions)
|
|
88
|
+
- [Views](#views)
|
|
89
|
+
- [Pagination](#pagination)
|
|
90
|
+
- [Throttling](#throttling)
|
|
91
|
+
- [Responses](#responses)
|
|
92
|
+
- [Exception handler](#exception-handler)
|
|
93
|
+
- [Spectacular](#spectacular)
|
|
94
|
+
- [Testing](#testing)
|
|
95
|
+
- [Contributing](#contributing)
|
|
96
|
+
- [License](#license)
|
|
97
|
+
|
|
98
|
+
## Motivation
|
|
99
|
+
|
|
100
|
+
Hi, I am Khan, the author of drf-restflow. This library was born from the
|
|
101
|
+
realities of building APIs in a fast-moving startup environment. Most of
|
|
102
|
+
my work involved large database tables, constantly evolving product
|
|
103
|
+
requirements, and the challenge of exposing clean, reliable REST APIs
|
|
104
|
+
while making sure new developers could onboard quickly and understand the
|
|
105
|
+
codebase and business logic as early as possible.
|
|
106
|
+
|
|
107
|
+
I started with django-filter, which is an excellent and very mature tool.
|
|
108
|
+
But as our product grew (and pivoted repeatedly), the FilterSets became
|
|
109
|
+
harder to maintain. They were getting long, repetitive, and full of
|
|
110
|
+
boilerplate. Some might say this was a skill issue, and honestly, I
|
|
111
|
+
agree. But the truth is, I am a lazy developer. I like writing less
|
|
112
|
+
code. I like being fast. I like tools that let me declare what I want
|
|
113
|
+
instead of wiring everything by hand. Over time, I built small internal
|
|
114
|
+
utilities to reduce repetition and make filtering easier. Those tools
|
|
115
|
+
worked well, so I compiled them into a proper library so I could reuse
|
|
116
|
+
them across projects.
|
|
117
|
+
|
|
118
|
+
The caching layer comes from the same instinct, applied to a different
|
|
119
|
+
problem. In production the part of caching that goes wrong is rarely the
|
|
120
|
+
read or the write; it is the cache-key construction and the
|
|
121
|
+
invalidation. So drf-restflow models the cache key as a declarative
|
|
122
|
+
class made of small, composable fields, and models invalidation as
|
|
123
|
+
rules attached to Django model signals. The function and the rule sit
|
|
124
|
+
side by side in the same file, which makes it much easier to keep them
|
|
125
|
+
in sync as the schema changes.
|
|
126
|
+
|
|
127
|
+
Many of the early internal utilities were built from scratch, which
|
|
128
|
+
brought some inconsistency. Instead of reinventing the wheel everywhere,
|
|
129
|
+
I leaned on what is already battle-tested and borrowed ideas from
|
|
130
|
+
different libraries, including FastAPI, django-filter, and django-ninja.
|
|
131
|
+
That is how drf-restflow took its current shape: a library that does not
|
|
132
|
+
replace Django REST Framework but extends it with declarative classes for
|
|
133
|
+
the parts of an API that turn into boilerplate. There are likely other
|
|
134
|
+
libraries that promise similar things or do more, and feedback,
|
|
135
|
+
contributions, and constructive criticism are very welcome.
|
|
136
|
+
|
|
137
|
+
## Installation
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pip install drf-restflow
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv add drf-restflow
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Restflow ships two Django apps:
|
|
148
|
+
|
|
149
|
+
- `restflow.caching` -- registers post-save and post-delete signal
|
|
150
|
+
handlers that drive cache invalidation. Required for any project that
|
|
151
|
+
uses `@cache_result` with `invalidates_on=[...]`.
|
|
152
|
+
- `restflow.authentication` -- ships the `BlacklistedToken` model used
|
|
153
|
+
by `ModelBlacklistBackend`. Required only when revoking JWTs through
|
|
154
|
+
the model-backed blacklist.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# settings.py
|
|
158
|
+
INSTALLED_APPS = [
|
|
159
|
+
"django.contrib.contenttypes",
|
|
160
|
+
"django.contrib.auth",
|
|
161
|
+
"rest_framework",
|
|
162
|
+
"restflow.caching",
|
|
163
|
+
"restflow.authentication",
|
|
164
|
+
]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The top-level `restflow` import is a regular Python package and does
|
|
168
|
+
not need to appear in `INSTALLED_APPS`.
|
|
169
|
+
|
|
170
|
+
### Requirements
|
|
171
|
+
|
|
172
|
+
- Python 3.10 or higher
|
|
173
|
+
- Django 3.2 or higher
|
|
174
|
+
- Django REST Framework 3.14 or higher
|
|
175
|
+
- PyJWT 2.8 or higher (installed automatically; powers the built-in
|
|
176
|
+
JWT authentication)
|
|
177
|
+
|
|
178
|
+
PostgreSQL is optional and is only required for the postgres-specific
|
|
179
|
+
filtering features (full-text search, array fields, trigram similarity,
|
|
180
|
+
range fields).
|
|
181
|
+
|
|
182
|
+
### Optional extras
|
|
183
|
+
|
|
184
|
+
| Extra | Use case | pip | uv |
|
|
185
|
+
| --- | --- | --- | --- |
|
|
186
|
+
| `redis` | Cache backend that supports prefix-based invalidation | `pip install drf-restflow[redis]` | `uv add 'drf-restflow[redis]'` |
|
|
187
|
+
| `celery` | Run cache invalidation as celery tasks | `pip install drf-restflow[celery]` | `uv add 'drf-restflow[celery]'` |
|
|
188
|
+
| `django-rq` | Run cache invalidation through django-rq | `pip install drf-restflow[django-rq]` | `uv add 'drf-restflow[django-rq]'` |
|
|
189
|
+
| `django-q` | Run cache invalidation through django-q2 | `pip install drf-restflow[django-q]` | `uv add 'drf-restflow[django-q]'` |
|
|
190
|
+
| `dramatiq` | Run cache invalidation through dramatiq | `pip install drf-restflow[dramatiq]` | `uv add 'drf-restflow[dramatiq]'` |
|
|
191
|
+
| `postgres` | psycopg2 driver for PostgreSQL filtering features | `pip install drf-restflow[postgres]` | `uv add 'drf-restflow[postgres]'` |
|
|
192
|
+
| `postgres-psycopg3` | psycopg3 driver for PostgreSQL filtering features | `pip install drf-restflow[postgres-psycopg3]` | `uv add 'drf-restflow[postgres-psycopg3]'` |
|
|
193
|
+
| `simplejwt` | Adapter for djangorestframework-simplejwt | `pip install drf-restflow[simplejwt]` | `uv add 'drf-restflow[simplejwt]'` |
|
|
194
|
+
| `spectacular` | OpenAPI schema generation through drf-spectacular | `pip install drf-restflow[spectacular]` | `uv add 'drf-restflow[spectacular]'` |
|
|
195
|
+
|
|
196
|
+
## Caching
|
|
197
|
+
|
|
198
|
+
The caching layer plugs into Django's cache framework and works with any
|
|
199
|
+
configured backend.
|
|
200
|
+
|
|
201
|
+
A small set of features only works on a redis-compatible backend:
|
|
202
|
+
`delete_by_prefix()`, `invalidate_all()`, and any `InvalidationRule`
|
|
203
|
+
that needs to wipe a partition rather than a single key. Anything that
|
|
204
|
+
relies on `delete_pattern` falls into this group. Without a
|
|
205
|
+
redis-compatible backend, those calls raise; the rest of the caching API
|
|
206
|
+
keeps working on Django's local-memory or database cache. Real-world
|
|
207
|
+
projects usually want partition wipes, so the recommended setup is
|
|
208
|
+
django-redis backed by redis (valkey, keydb, and dragonfly all work as
|
|
209
|
+
drop-in replacements).
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pip install drf-restflow[redis]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv add 'drf-restflow[redis]'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# settings.py
|
|
221
|
+
CACHES = {
|
|
222
|
+
"default": {
|
|
223
|
+
"BACKEND": "django_redis.cache.RedisCache",
|
|
224
|
+
"LOCATION": "redis://127.0.0.1:6379/1",
|
|
225
|
+
"OPTIONS": {
|
|
226
|
+
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
A `KeyConstructor` describes how to build a cache key from a function
|
|
233
|
+
call. Each attribute is a field that pulls a piece of data out of the
|
|
234
|
+
call and stringifies it deterministically. `@cache_result` wraps the
|
|
235
|
+
function in a `CachedWrapper` and registers `InvalidationRule` objects
|
|
236
|
+
against Django model signals.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from django.contrib.auth import get_user_model
|
|
240
|
+
from restflow.caching import (
|
|
241
|
+
KeyConstructor, ArgsKeyField, ConstantKeyField, QueryParamsKeyField,
|
|
242
|
+
cache_result, InvalidationRule,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
User = get_user_model()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class UserKey(KeyConstructor):
|
|
249
|
+
user = ArgsKeyField("user_id", partition=True)
|
|
250
|
+
version = ConstantKeyField("v", "1")
|
|
251
|
+
page = QueryParamsKeyField(["page", "size"])
|
|
252
|
+
|
|
253
|
+
class Meta:
|
|
254
|
+
namespace = "users"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@cache_result(
|
|
258
|
+
key_constructor=UserKey,
|
|
259
|
+
ttl=300,
|
|
260
|
+
invalidates_on=[
|
|
261
|
+
InvalidationRule(
|
|
262
|
+
model=User,
|
|
263
|
+
field_mapping={"user_id": "id"},
|
|
264
|
+
watch_fields=["email"],
|
|
265
|
+
rewarm=True,
|
|
266
|
+
),
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
def get_user_payload(user_id: int, request=None):
|
|
270
|
+
return expensive_lookup(user_id)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The wrapped function exposes `get_with_metadata`, `refresh`,
|
|
274
|
+
`bypass_cache`, `delete_cache`, `delete_by_prefix`, `invalidate_all`,
|
|
275
|
+
and the matching `a`-prefixed async methods.
|
|
276
|
+
|
|
277
|
+
See the [Caching guide](https://restflow.khanasfireza.dev/guide/caching/)
|
|
278
|
+
for the full API.
|
|
279
|
+
|
|
280
|
+
## Filtering
|
|
281
|
+
|
|
282
|
+
`FilterSet` validates query parameters and applies filters to a Django
|
|
283
|
+
queryset. Fields can be declared with type annotations, explicit field
|
|
284
|
+
classes, model-based generation, or any mix of those.
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from restflow.filters import (
|
|
288
|
+
FilterSet, StringField, IntegerField, BooleanField,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ProductFilterSet(FilterSet):
|
|
293
|
+
name = StringField(lookups=["icontains"])
|
|
294
|
+
price = IntegerField(lookups=["comparison"])
|
|
295
|
+
category: str
|
|
296
|
+
in_stock: bool
|
|
297
|
+
|
|
298
|
+
class Meta:
|
|
299
|
+
model = Product
|
|
300
|
+
order_fields = [
|
|
301
|
+
("price", "price"),
|
|
302
|
+
("name", "name"),
|
|
303
|
+
("created_at", "created_at"),
|
|
304
|
+
]
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
This declaration generates the following query parameters automatically:
|
|
308
|
+
|
|
309
|
+
- `name`, `name__icontains` and the negation variants `name!`,
|
|
310
|
+
`name__icontains!`.
|
|
311
|
+
- `price`, `price__gt`, `price__gte`, `price__lt`, `price__lte` and
|
|
312
|
+
their negation variants.
|
|
313
|
+
- `category`, `category!`, `in_stock`, `in_stock!`.
|
|
314
|
+
- `order_by` accepting `price`, `-price`, `name`, `-name`,
|
|
315
|
+
`created_at`, `-created_at`, or comma-separated combinations.
|
|
316
|
+
|
|
317
|
+
`RestflowFilterBackend` plugs the FilterSet into DRF's filter pipeline
|
|
318
|
+
and emits OpenAPI parameters for every declared field.
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from rest_framework import generics
|
|
322
|
+
from restflow.filters import RestflowFilterBackend
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class ProductView(generics.ListAPIView):
|
|
326
|
+
queryset = Product.objects.all()
|
|
327
|
+
serializer_class = ProductSerializer
|
|
328
|
+
filter_backends = [RestflowFilterBackend]
|
|
329
|
+
filterset_class = ProductFilterSet
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
See the [Filtering guide](https://restflow.khanasfireza.dev/guide/filtering/filterset/)
|
|
333
|
+
for custom methods, processors, ordering, PostgreSQL features, and the
|
|
334
|
+
DRF integration details.
|
|
335
|
+
|
|
336
|
+
## Serializers
|
|
337
|
+
|
|
338
|
+
`Serializer`, `ModelSerializer`, and `HyperlinkedModelSerializer`
|
|
339
|
+
subclasses driven by Python type annotations, plus an `InlineSerializer`
|
|
340
|
+
factory and an async surface (`ais_valid`, `asave`, `acreate`, `aupdate`,
|
|
341
|
+
`ato_internal_value`, `arun_validation`).
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from restflow.serializers import (
|
|
345
|
+
Serializer, ModelSerializer, Field, Email,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class UserSerializer(Serializer):
|
|
350
|
+
name: str
|
|
351
|
+
age: int
|
|
352
|
+
email: Email
|
|
353
|
+
bio: str | None
|
|
354
|
+
role: str = Field(read_only=True)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class UserModelSerializer(ModelSerializer):
|
|
358
|
+
full_name: str
|
|
359
|
+
|
|
360
|
+
class Meta:
|
|
361
|
+
model = User
|
|
362
|
+
fields = ["id", "username"]
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Annotations resolve to DRF fields through `SerializerFieldMap`. Optional
|
|
366
|
+
types (`str | None`, `Optional[T]`) become `allow_null=True`,
|
|
367
|
+
`Literal[...]` becomes `ChoiceField`, `list[T]` becomes `ListField`, and
|
|
368
|
+
nested `Serializer` subclasses nest as expected. See the
|
|
369
|
+
[Serializers guide](https://restflow.khanasfireza.dev/guide/serializers/)
|
|
370
|
+
for the resolution rules and the async hooks.
|
|
371
|
+
|
|
372
|
+
## Authentication
|
|
373
|
+
|
|
374
|
+
`JWTAuthentication` is a fully async JSON Web Token authenticator backed
|
|
375
|
+
by PyJWT. It validates signature, expiry, issuer, and audience, looks up
|
|
376
|
+
the user with async ORM, and consults a configurable blacklist on every
|
|
377
|
+
request. Built-in token obtain, refresh, and blacklist views ship as
|
|
378
|
+
async APIViews.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from datetime import timedelta
|
|
382
|
+
|
|
383
|
+
# settings.py
|
|
384
|
+
RESTFLOW_SETTINGS = {
|
|
385
|
+
"JWT": {
|
|
386
|
+
"SIGNING_KEY": "change-me-in-production",
|
|
387
|
+
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
|
|
388
|
+
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
# urls.py
|
|
395
|
+
from restflow.authentication import (
|
|
396
|
+
TokenObtainView, TokenRefreshView, TokenBlacklistView,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
urlpatterns = [
|
|
400
|
+
path("auth/token/", TokenObtainView.as_view()),
|
|
401
|
+
path("auth/refresh/", TokenRefreshView.as_view()),
|
|
402
|
+
path("auth/blacklist/", TokenBlacklistView.as_view()),
|
|
403
|
+
]
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from restflow.authentication import JWTAuthentication
|
|
408
|
+
from restflow.views import AsyncListAPIView
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class ProductView(AsyncListAPIView):
|
|
412
|
+
authentication_classes = [JWTAuthentication]
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Async-aware wrappers for `BasicAuthentication`, `TokenAuthentication`,
|
|
416
|
+
`SessionAuthentication`, and `RemoteUserAuthentication` are also
|
|
417
|
+
provided, plus a `SimpleJWTAuthentication` adapter for projects already
|
|
418
|
+
on `djangorestframework-simplejwt`. See the
|
|
419
|
+
[Authentication guide](https://restflow.khanasfireza.dev/guide/authentication/)
|
|
420
|
+
for the full configuration surface.
|
|
421
|
+
|
|
422
|
+
## Permissions
|
|
423
|
+
|
|
424
|
+
Async-aware permission classes that compose through DRF's existing
|
|
425
|
+
`&`, `|`, and `~` operators (with brackets for grouping; precedence
|
|
426
|
+
is `~` highest, then `&`, then `|`). Restflow contributes
|
|
427
|
+
async-native operator classes so combinator branches resolve through
|
|
428
|
+
the async hook , plus async overrides on the
|
|
429
|
+
standard permission set so `ahas_permission` is non-blocking. Custom
|
|
430
|
+
permissions can implement either the sync or async hook; the dispatch
|
|
431
|
+
path picks the async one when present and falls back to a thread for
|
|
432
|
+
legacy classes.
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from restflow.permissions import (
|
|
436
|
+
IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly,
|
|
437
|
+
)
|
|
438
|
+
from restflow.views import AsyncRetrieveUpdateDestroyAPIView
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class AdminOrReadOnly(AsyncRetrieveUpdateDestroyAPIView):
|
|
442
|
+
permission_classes = [IsAuthenticated & (IsAdminUser | IsAuthenticatedOrReadOnly)]
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`AllowAny`, `IsAuthenticated`, `IsAdminUser`, `IsAuthenticatedOrReadOnly`,
|
|
446
|
+
`DjangoModelPermissions`, `DjangoModelPermissionsOrAnonReadOnly`, and
|
|
447
|
+
`DjangoObjectPermissions` ship out of the box. See the
|
|
448
|
+
[Permissions guide](https://restflow.khanasfireza.dev/guide/permissions/)
|
|
449
|
+
for the async hook contract and combinator behaviour.
|
|
450
|
+
|
|
451
|
+
## Views
|
|
452
|
+
|
|
453
|
+
A complete async view stack: `AsyncAPIView`, eight generic views
|
|
454
|
+
(`AsyncListAPIView`, `AsyncCreateAPIView`, `AsyncRetrieveAPIView`,
|
|
455
|
+
`AsyncUpdateAPIView`, `AsyncDestroyAPIView`, plus the combined
|
|
456
|
+
`AsyncListCreate`, `AsyncRetrieveUpdate`, `AsyncRetrieveDestroy`, and
|
|
457
|
+
`AsyncRetrieveUpdateDestroy` variants), five model mixins
|
|
458
|
+
(`AsyncCreateModelMixin`, `AsyncListModelMixin`, `AsyncRetrieveModelMixin`,
|
|
459
|
+
`AsyncUpdateModelMixin`, `AsyncDestroyModelMixin`), and the viewset family
|
|
460
|
+
(`AsyncViewSet`, `AsyncGenericViewSet`, `AsyncReadOnlyModelViewSet`,
|
|
461
|
+
`AsyncModelViewSet`).
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
from restflow.views import AsyncModelViewSet, ActionConfig
|
|
465
|
+
from restflow.permissions import IsAuthenticated, IsAdminUser
|
|
466
|
+
from restflow.pagination import FastPageNumberPagination
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class ProductViewSet(AsyncModelViewSet):
|
|
470
|
+
queryset = Product.objects.all()
|
|
471
|
+
serializer_class = ProductSerializer
|
|
472
|
+
permission_classes = [IsAuthenticated]
|
|
473
|
+
action_configs = {
|
|
474
|
+
"list": ActionConfig(
|
|
475
|
+
response_serializer_class=ProductListSerializer,
|
|
476
|
+
pagination_class=FastPageNumberPagination,
|
|
477
|
+
),
|
|
478
|
+
"destroy": ActionConfig(permission_classes=[IsAdminUser]),
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`ActionConfig` overrides serializer, permission, throttle, parser,
|
|
483
|
+
renderer, pagination, and queryset on a per-action basis. `PostFetch`
|
|
484
|
+
attaches related rows to a list of base objects after pagination, useful
|
|
485
|
+
when `prefetch_related` cannot be used. See the
|
|
486
|
+
[Views guide](https://restflow.khanasfireza.dev/guide/views/) for the
|
|
487
|
+
full async pipeline and per-action override rules.
|
|
488
|
+
|
|
489
|
+
## Pagination
|
|
490
|
+
|
|
491
|
+
Async-aware paginators that drive the `apaginate_queryset()` hook on
|
|
492
|
+
async views and viewsets. `PageNumberPagination`, `LimitOffsetPagination`,
|
|
493
|
+
and `FastPageNumberPagination` use async ORM iteration directly.
|
|
494
|
+
`CursorPagination` falls back to DRF's sync logic via `sync_to_async`.
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
from restflow.pagination import FastPageNumberPagination
|
|
498
|
+
from restflow.views import AsyncListAPIView
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class ProductView(AsyncListAPIView):
|
|
502
|
+
pagination_class = FastPageNumberPagination
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`FastPageNumberPagination` skips the `COUNT(*)` query and decides whether
|
|
506
|
+
a next page exists based on whether the current page is full. That
|
|
507
|
+
matters on huge tables where a count scan dominates the request budget.
|
|
508
|
+
See the [Pagination guide](https://restflow.khanasfireza.dev/guide/pagination/)
|
|
509
|
+
for selection criteria and tuning.
|
|
510
|
+
|
|
511
|
+
## Throttling
|
|
512
|
+
|
|
513
|
+
Async-aware throttle classes that use Django's async cache to avoid
|
|
514
|
+
blocking the event loop on rate-limit checks. `AnonRateThrottle`,
|
|
515
|
+
`UserRateThrottle`, `ScopedRateThrottle`, and a `SimpleRateThrottle`
|
|
516
|
+
base class are provided.
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
from restflow.throttling import AnonRateThrottle, UserRateThrottle
|
|
520
|
+
from restflow.views import AsyncListAPIView
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class ProductView(AsyncListAPIView):
|
|
524
|
+
throttle_classes = [AnonRateThrottle, UserRateThrottle]
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
# settings.py
|
|
529
|
+
REST_FRAMEWORK = {
|
|
530
|
+
"DEFAULT_THROTTLE_RATES": {
|
|
531
|
+
"anon": "100/hour",
|
|
532
|
+
"user": "1000/hour",
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
See the [Throttling guide](https://restflow.khanasfireza.dev/guide/throttling/)
|
|
538
|
+
for cache-backend selection and per-action scoping.
|
|
539
|
+
|
|
540
|
+
## Responses
|
|
541
|
+
|
|
542
|
+
Three streaming responses for endpoints that produce large or open-ended
|
|
543
|
+
payloads.
|
|
544
|
+
|
|
545
|
+
```python
|
|
546
|
+
from restflow.responses import (
|
|
547
|
+
StreamingJSONListResponse, NDJSONResponse, SSEResponse,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
async def products(request):
|
|
552
|
+
async def items():
|
|
553
|
+
async for row in Product.objects.all():
|
|
554
|
+
yield {"id": row.id, "name": row.name}
|
|
555
|
+
return StreamingJSONListResponse(items())
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
`StreamingJSONListResponse` emits a single JSON array element-by-element.
|
|
559
|
+
`NDJSONResponse` emits one JSON object per line. `SSEResponse` formats
|
|
560
|
+
items as Server-Sent Events with `data`, `event`, `id`, and `retry`
|
|
561
|
+
fields. See the [Responses guide](https://restflow.khanasfireza.dev/guide/responses/)
|
|
562
|
+
for buffering, encoder customisation, and SSE reconnection notes.
|
|
563
|
+
|
|
564
|
+
## Exception handler
|
|
565
|
+
|
|
566
|
+
A drop-in DRF exception handler that renders every error as a uniform
|
|
567
|
+
envelope with a stable error code, message, and details payload.
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
# settings.py
|
|
571
|
+
REST_FRAMEWORK = {
|
|
572
|
+
"EXCEPTION_HANDLER": "restflow.exceptions.exception_handler",
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
from restflow.exceptions import APIException, ErrorCode
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class ProductLockedException(APIException):
|
|
581
|
+
code = ErrorCode.CONFLICT.value
|
|
582
|
+
status_code = 409
|
|
583
|
+
default_detail = "The product is locked for editing."
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Every error -- DRF, Django, or `restflow.exceptions.APIException` --
|
|
587
|
+
is mapped to `{"error": {"code": "...", "message": "...", "details": {...}}}`
|
|
588
|
+
with stable codes for clients to branch on. See the
|
|
589
|
+
[Exception handler guide](https://restflow.khanasfireza.dev/guide/exception-handler/)
|
|
590
|
+
for the full code list and customisation hooks.
|
|
591
|
+
|
|
592
|
+
## Spectacular
|
|
593
|
+
|
|
594
|
+
`RestflowAutoSchema` is a drop-in replacement for `drf-spectacular`'s
|
|
595
|
+
default schema generator. It resolves serializers from `action_configs`,
|
|
596
|
+
non-generic `serializer_class` plus the request and response variants,
|
|
597
|
+
and pagination classes attached either at the view level or per action.
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
pip install drf-restflow[spectacular]
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
uv add 'drf-restflow[spectacular]'
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
# settings.py
|
|
609
|
+
REST_FRAMEWORK = {
|
|
610
|
+
"DEFAULT_SCHEMA_CLASS": "restflow.spectacular.RestflowAutoSchema",
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
OpenAPI parameters from `RestflowFilterBackend` flow through the same
|
|
615
|
+
schema. See the [Spectacular guide](https://restflow.khanasfireza.dev/guide/spectacular/)
|
|
616
|
+
for action-config resolution rules and pagination handling.
|
|
617
|
+
|
|
618
|
+
## Testing
|
|
619
|
+
|
|
620
|
+
`AsyncAPIClient` and `AsyncAPIRequestFactory` send ASGI requests to
|
|
621
|
+
restflow async views. Four test case bases (`AsyncAPISimpleTestCase`,
|
|
622
|
+
`AsyncAPITestCase`, `AsyncAPITransactionTestCase`,
|
|
623
|
+
`AsyncAPILiveServerTestCase`) wire those into Django's test runner.
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
from restflow.test import AsyncAPIClient, AsyncAPITestCase
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class TestProducts(AsyncAPITestCase):
|
|
630
|
+
async def test_list(self):
|
|
631
|
+
client = AsyncAPIClient()
|
|
632
|
+
response = await client.get("/api/products/")
|
|
633
|
+
assert response.status_code == 200
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`force_authenticate(request, user=...)` bypasses the authenticator chain
|
|
637
|
+
in unit tests. See the [Testing guide](https://restflow.khanasfireza.dev/guide/testing/)
|
|
638
|
+
for picking the right base class and writing signal-driven cache
|
|
639
|
+
invalidation tests.
|
|
640
|
+
|
|
641
|
+
## Contributing
|
|
642
|
+
|
|
643
|
+
Contributions are welcome. See the
|
|
644
|
+
[contributing guide](https://restflow.khanasfireza.dev/contributing/)
|
|
645
|
+
for the development workflow, code conventions, and test setup.
|
|
646
|
+
|
|
647
|
+
## License
|
|
648
|
+
|
|
649
|
+
BSD 3-Clause License. See [LICENSE](LICENSE).
|