django-fastdrf 0.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.
- django_fastdrf-0.1.0.dist-info/METADATA +214 -0
- django_fastdrf-0.1.0.dist-info/RECORD +44 -0
- django_fastdrf-0.1.0.dist-info/WHEEL +4 -0
- django_fastdrf-0.1.0.dist-info/licenses/LICENSE +28 -0
- django_fastdrf-0.1.0.dist-info/licenses/NOTICE +8 -0
- fastdrf/__init__.py +3 -0
- fastdrf/_classify.py +210 -0
- fastdrf/_compiled.py +131 -0
- fastdrf/_field_cache.py +158 -0
- fastdrf/_field_copy.py +244 -0
- fastdrf/_field_options.py +68 -0
- fastdrf/_inspection.py +25 -0
- fastdrf/_relations.py +206 -0
- fastdrf/apps.py +22 -0
- fastdrf/checks.py +108 -0
- fastdrf/codecs.py +97 -0
- fastdrf/compat.py +7 -0
- fastdrf/compiler.py +1681 -0
- fastdrf/convert.py +1327 -0
- fastdrf/inputs.py +891 -0
- fastdrf/list_serializers.py +112 -0
- fastdrf/management/__init__.py +0 -0
- fastdrf/management/commands/__init__.py +0 -0
- fastdrf/management/commands/fastdrf_convert.py +105 -0
- fastdrf/management/commands/fastdrf_inspect_serializers.py +257 -0
- fastdrf/mixins.py +124 -0
- fastdrf/msgspec/__init__.py +36 -0
- fastdrf/msgspec/compiler.py +220 -0
- fastdrf/msgspec/parsers.py +29 -0
- fastdrf/msgspec/renderers.py +87 -0
- fastdrf/msgspec/serializers.py +364 -0
- fastdrf/output.py +189 -0
- fastdrf/prefetch.py +315 -0
- fastdrf/py.typed +0 -0
- fastdrf/pydantic/__init__.py +21 -0
- fastdrf/pydantic/compiler.py +80 -0
- fastdrf/pydantic/serializers.py +341 -0
- fastdrf/renderers.py +84 -0
- fastdrf/response.py +259 -0
- fastdrf/serializers.py +228 -0
- fastdrf/settings.py +135 -0
- fastdrf/typed.py +705 -0
- fastdrf/utils.py +193 -0
- fastdrf/views.py +445 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-fastdrf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opt-in serializer and query optimizations for synchronous Django REST framework.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ctolon/django-fastdrf
|
|
6
|
+
Project-URL: Documentation, https://github.com/ctolon/django-fastdrf/tree/main/docs
|
|
7
|
+
Project-URL: Changelog, https://github.com/ctolon/django-fastdrf/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/ctolon/django-fastdrf/issues
|
|
9
|
+
Project-URL: Source, https://github.com/ctolon/django-fastdrf
|
|
10
|
+
Author: Cevat Batuhan Tolon
|
|
11
|
+
License-Expression: BSD-3-Clause
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
License-File: NOTICE
|
|
14
|
+
Keywords: django,django-rest-framework,msgspec,performance,serializers
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Environment :: Web Environment
|
|
17
|
+
Classifier: Framework :: Django
|
|
18
|
+
Classifier: Framework :: Django :: 5.2
|
|
19
|
+
Classifier: Framework :: Django :: 6.0
|
|
20
|
+
Classifier: Framework :: Django :: 6.1
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
29
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.12
|
|
33
|
+
Requires-Dist: django>=5.2
|
|
34
|
+
Requires-Dist: djangorestframework>=3.16
|
|
35
|
+
Provides-Extra: msgspec
|
|
36
|
+
Requires-Dist: msgspec>=0.19; extra == 'msgspec'
|
|
37
|
+
Provides-Extra: pydantic
|
|
38
|
+
Requires-Dist: pydantic>=2.9; extra == 'pydantic'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# django-fastdrf
|
|
42
|
+
|
|
43
|
+
Opt-in serializer, query and response optimizations for synchronous
|
|
44
|
+
[Django REST framework](https://www.django-rest-framework.org/) projects.
|
|
45
|
+
|
|
46
|
+
django-fastdrf is for DRF projects that stay synchronous (WSGI, or ASGI with
|
|
47
|
+
synchronous views) and want less time per request without leaving DRF. It
|
|
48
|
+
adds subclasses, mixins and settings next to DRF's own classes. Each
|
|
49
|
+
optimization is enabled explicitly, per project, per view or per serializer,
|
|
50
|
+
and falls back to DRF's code wherever it cannot produce DRF's result.
|
|
51
|
+
|
|
52
|
+
It does not replace or patch any Django or DRF class, add async support, or
|
|
53
|
+
change DRF's validation errors.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
pip install django-fastdrf
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The distribution is `django-fastdrf`; the import name is `fastdrf`. Two
|
|
62
|
+
extras install the optional backends:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
pip install "django-fastdrf[msgspec]" # msgspec backend, renderer, parser, codec
|
|
66
|
+
pip install "django-fastdrf[pydantic]" # pydantic backend, schema serializers, codec
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Adding `"fastdrf"` to `INSTALLED_APPS` is optional. It registers system checks
|
|
70
|
+
and two management commands; everything else works without it.
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
Use fastdrf's serializer bases and choose a backend for their output:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# settings.py
|
|
78
|
+
FASTDRF = {
|
|
79
|
+
"SERIALIZER_BACKEND": "msgspec", # or "pydantic", or "python" (no dependency)
|
|
80
|
+
"CACHE_SERIALIZER_FIELDS": True,
|
|
81
|
+
"FIELD_COPY_MODE": "compiled",
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# serializers.py
|
|
87
|
+
from fastdrf import serializers
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ArticleSerializer(serializers.ModelSerializer):
|
|
91
|
+
class Meta:
|
|
92
|
+
model = Article
|
|
93
|
+
fields = ["id", "title", "author", "published_at"]
|
|
94
|
+
auto_prefetch = True
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# views.py
|
|
99
|
+
from rest_framework import viewsets
|
|
100
|
+
|
|
101
|
+
from fastdrf.views import DispatchOptimizationMixin, QueryOptimizationMixin
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ArticleViewSet(
|
|
105
|
+
DispatchOptimizationMixin, QueryOptimizationMixin, viewsets.ModelViewSet
|
|
106
|
+
):
|
|
107
|
+
queryset = Article.objects.all()
|
|
108
|
+
serializer_class = ArticleSerializer
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`.data`, `.is_valid()`, `.save()`, `many=True`, routers, pagination,
|
|
112
|
+
permissions and error responses are DRF's. With `"fastdrf"` in
|
|
113
|
+
`INSTALLED_APPS`, this command lists which serializers the backend compiles,
|
|
114
|
+
and why the others stay on DRF:
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
python manage.py fastdrf_inspect_serializers
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Features
|
|
121
|
+
|
|
122
|
+
Serialization:
|
|
123
|
+
|
|
124
|
+
- Compiled serializer output with the msgspec, pydantic or dependency-free
|
|
125
|
+
`python` backend, equal to DRF's `.data` in the default strict parity mode
|
|
126
|
+
([serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#output-backends)).
|
|
127
|
+
- Input recognition: JSON input that DRF would accept unchanged is validated
|
|
128
|
+
by a compiled class; everything else, including every error, is DRF's
|
|
129
|
+
([input recognition](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#input-recognition)).
|
|
130
|
+
- Cached field templates, copied per request with `deepcopy`, a scalar clone
|
|
131
|
+
or a compiled copy plan
|
|
132
|
+
([field caching](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#field-caching-and-copying)).
|
|
133
|
+
- List serializers whose child refers to the list weakly
|
|
134
|
+
([list serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#list-serializers)).
|
|
135
|
+
- Schema serializers: a msgspec `Struct` or a pydantic model as a DRF
|
|
136
|
+
serializer, and `SchemaViewMixin` for views
|
|
137
|
+
([schema serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/schema-serializers.md)).
|
|
138
|
+
|
|
139
|
+
Queries:
|
|
140
|
+
|
|
141
|
+
- `select_related` and `prefetch_related` derived from the serializer by
|
|
142
|
+
`QueryOptimizationMixin`, explicit `Meta.prefetch` hints, Django 6.1's
|
|
143
|
+
`FETCH_MODE`, one query for many-valued primary-key input, and
|
|
144
|
+
`PrefetchListSerializer` for per-list enrichment
|
|
145
|
+
([queries](https://github.com/ctolon/django-fastdrf/blob/main/docs/queries.md)).
|
|
146
|
+
|
|
147
|
+
Views and responses:
|
|
148
|
+
|
|
149
|
+
- Dispatch mixins that keep content negotiation and request construction
|
|
150
|
+
between requests, compiled create and update responses, a `Response` that
|
|
151
|
+
releases its request objects when closed, and `DataResponse`, rendered
|
|
152
|
+
without DRF's template response
|
|
153
|
+
([views and responses](https://github.com/ctolon/django-fastdrf/blob/main/docs/views.md)).
|
|
154
|
+
- A `JSONRenderer` that keeps its encoder, msgspec's JSON renderer and
|
|
155
|
+
parser, and msgspec and pydantic codecs for Django's Redis cache
|
|
156
|
+
([rendering and codecs](https://github.com/ctolon/django-fastdrf/blob/main/docs/rendering.md)).
|
|
157
|
+
|
|
158
|
+
Tooling:
|
|
159
|
+
|
|
160
|
+
- System checks for the `FASTDRF` setting and two management commands:
|
|
161
|
+
`fastdrf_inspect_serializers` and `fastdrf_convert`, which writes a schema
|
|
162
|
+
for a serializer or a serializer for a schema
|
|
163
|
+
([commands](https://github.com/ctolon/django-fastdrf/blob/main/docs/commands.md)).
|
|
164
|
+
|
|
165
|
+
## Guarantees and limits
|
|
166
|
+
|
|
167
|
+
- No Django or DRF class is replaced, patched or monkeypatched. fastdrf's
|
|
168
|
+
classes are subclasses and mixins that a project selects.
|
|
169
|
+
- Everything is opt-in. With the default settings, fastdrf's serializer bases
|
|
170
|
+
behave as DRF's.
|
|
171
|
+
- In strict parity, compiled output equals DRF's output; a field, value or
|
|
172
|
+
hook the compiler cannot prove equal keeps the serializer, or that one
|
|
173
|
+
source, on DRF. Input recognition never produces an error of its own.
|
|
174
|
+
- Synchronous only: no ORM call becomes asynchronous, and transactions,
|
|
175
|
+
authentication, permissions, throttling and pagination are DRF's and
|
|
176
|
+
Django's.
|
|
177
|
+
- Schema serializers, `fast` parity and the msgspec renderer have their own
|
|
178
|
+
documented output and validation rules; they are not DRF-identical by
|
|
179
|
+
design.
|
|
180
|
+
- A serializer instance belongs to one request; do not share it between
|
|
181
|
+
threads.
|
|
182
|
+
|
|
183
|
+
See [architecture](https://github.com/ctolon/django-fastdrf/blob/main/docs/architecture.md)
|
|
184
|
+
for how the caches are bounded and invalidated, and for the deliberate
|
|
185
|
+
differences.
|
|
186
|
+
|
|
187
|
+
## Compatibility
|
|
188
|
+
|
|
189
|
+
| Django | DRF | Python |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| 5.2 | 3.16, 3.17, 3.18 | 3.12, 3.13, 3.14 |
|
|
192
|
+
| 6.0 | 3.17, 3.18 | 3.12, 3.13, 3.14 |
|
|
193
|
+
| 6.1 | 3.18 | 3.12, 3.13, 3.14 |
|
|
194
|
+
|
|
195
|
+
The test suite also runs on free-threaded Python 3.14t (Django 6.1,
|
|
196
|
+
DRF 3.18) and at the declared minimum versions (Django 5.2, DRF 3.16,
|
|
197
|
+
msgspec 0.19, pydantic 2.9). `FETCH_MODE` needs Django 6.1.
|
|
198
|
+
|
|
199
|
+
## Documentation
|
|
200
|
+
|
|
201
|
+
- [Documentation index](https://github.com/ctolon/django-fastdrf/blob/main/docs/README.md)
|
|
202
|
+
- [Configuration reference](https://github.com/ctolon/django-fastdrf/blob/main/docs/configuration.md)
|
|
203
|
+
- [Example project](https://github.com/ctolon/django-fastdrf/blob/main/examples/blog/README.md):
|
|
204
|
+
the same API with plain DRF and with fastdrf, with parity tests and a
|
|
205
|
+
measurement script
|
|
206
|
+
- [Changelog](https://github.com/ctolon/django-fastdrf/blob/main/CHANGELOG.md)
|
|
207
|
+
- [Contributing](https://github.com/ctolon/django-fastdrf/blob/main/CONTRIBUTING.md)
|
|
208
|
+
- [Security policy](https://github.com/ctolon/django-fastdrf/blob/main/SECURITY.md)
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
BSD 3-Clause. See
|
|
213
|
+
[LICENSE](https://github.com/ctolon/django-fastdrf/blob/main/LICENSE) and
|
|
214
|
+
[NOTICE](https://github.com/ctolon/django-fastdrf/blob/main/NOTICE): the optimizations derive from the aiodrf project, and the list-construction protocol from Django REST framework.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
fastdrf/__init__.py,sha256=5I3edBDLCmaTShByretrBMm6FtyNFqPlAywvfmXj3fU,99
|
|
2
|
+
fastdrf/_classify.py,sha256=K4mvUSJ8RlwvAwZpNxCdaJcxwchYI-k2YlmMY9cYWcQ,8067
|
|
3
|
+
fastdrf/_compiled.py,sha256=IGePq5r-TXOWacY90MN_KYWH84s2KSONDNRd6gOs0gU,4631
|
|
4
|
+
fastdrf/_field_cache.py,sha256=zOWIef98mMlrcGT_4i47K5abpcBCjj-nfcan8X9Moik,5525
|
|
5
|
+
fastdrf/_field_copy.py,sha256=HTUiH2maQMae_sOexSsMnxCEt6zJAWHnvB63byBLEY4,8938
|
|
6
|
+
fastdrf/_field_options.py,sha256=zBg0PXBlY5SILY4sL8DxK8IiFOXRimsfE-uxAdxzqeY,2772
|
|
7
|
+
fastdrf/_inspection.py,sha256=ll0m5IJUl0aE18TfoCi9RS0rShGG1erAzPvyJ3WvqVA,659
|
|
8
|
+
fastdrf/_relations.py,sha256=qMkgKQVOKl3bFgLLB6NAPZsfAFs2l0x81NZEFEvvZOM,7851
|
|
9
|
+
fastdrf/apps.py,sha256=MrWUij4rMWQeRi7HcqV9x_3TDvUP9mq3C0nk7Z0mWlg,718
|
|
10
|
+
fastdrf/checks.py,sha256=P311eRZecAP0AP8m70GxwTKohWvkzHHB0A6ueXNkkQ8,4184
|
|
11
|
+
fastdrf/codecs.py,sha256=6cprANo93rmc7vx3uZcqeNTEIDrCZtbwWmMlC19oBmg,3613
|
|
12
|
+
fastdrf/compat.py,sha256=WjVmr7qawlkIDYHqAU1rTIJRmFvdrUuYRXWzYIvFdFk,309
|
|
13
|
+
fastdrf/compiler.py,sha256=CDnljlEY3ylKO7tRZgbPaRwUXurAnjW2tf0XVuq1Ch4,66465
|
|
14
|
+
fastdrf/convert.py,sha256=3Fny7qZu8rVX1NDxfDm26BfyuOh0ZaySEe8f9-mk25g,48396
|
|
15
|
+
fastdrf/inputs.py,sha256=NPA3CinEez8n53GXJIAudcNeytrJrnFUl9G7OKcIASI,32656
|
|
16
|
+
fastdrf/list_serializers.py,sha256=Xx8lXG4xaBUaM7iboE4yx6nwEpp60NadRAdK_t8eX6Q,4315
|
|
17
|
+
fastdrf/mixins.py,sha256=zMxSm1HgkMmVWo4gNlvvO-KJwJ7KszR4dfNdewHDsPI,4477
|
|
18
|
+
fastdrf/output.py,sha256=182k315wkYn-of8T8QEvGnVV6ZTz97stBNAw2WS2lXw,5700
|
|
19
|
+
fastdrf/prefetch.py,sha256=FQwnjdDISsfKNcaMqO6M4H4ggsezz--x-P6rv236HZM,11645
|
|
20
|
+
fastdrf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
fastdrf/renderers.py,sha256=SF_PwbZ25Ppp9-DfnxqoWI0_ffMuYQBGCqQXADxUI3M,3348
|
|
22
|
+
fastdrf/response.py,sha256=rdQxxT_5H8ctbDatZCzSF3-7tPITB7ZaGnVOFW3zrpk,9584
|
|
23
|
+
fastdrf/serializers.py,sha256=uQhtnYGRNndgjPkHY8nNGja6cjbWP7Rf3JkevlUiTQ8,6842
|
|
24
|
+
fastdrf/settings.py,sha256=S8jfn5dx8y5U4aCvXnFadhW3vD1olDhr4wx2T3TdOuk,4375
|
|
25
|
+
fastdrf/typed.py,sha256=LLljN-olgYWPeYW5IfueKbnKMZGjt6TE2JBBflLKXgA,28120
|
|
26
|
+
fastdrf/utils.py,sha256=yJ0p93NzyFj5KuW8GUTeKdoODBiWwDYEkTMYK_54gco,6200
|
|
27
|
+
fastdrf/views.py,sha256=e3RxOSler5VRxsA6eL3MAg-iHeJ-J3cc50Wencyw6Zk,16156
|
|
28
|
+
fastdrf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
fastdrf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
fastdrf/management/commands/fastdrf_convert.py,sha256=gF8YJ4NPL_DX3ToUte27ggNU5oh6yvaLD31ffHXdchs,3967
|
|
31
|
+
fastdrf/management/commands/fastdrf_inspect_serializers.py,sha256=SuGsOMxRUoBQh00fB_wChJzim9ccE2i2ETXQCj-KSEc,11069
|
|
32
|
+
fastdrf/msgspec/__init__.py,sha256=P1_WRWIhV3RQBbtzdMeWK2W05qjcfdibFeduxKygX4w,985
|
|
33
|
+
fastdrf/msgspec/compiler.py,sha256=9YbUvhy3ZOjDAcc1-2jduJTu6NdfDr586BgSv7A0vEg,8225
|
|
34
|
+
fastdrf/msgspec/parsers.py,sha256=Bw29XHdkv1onYuMTehfY6GajjiYdWgnbCoB_0g5wsxM,1067
|
|
35
|
+
fastdrf/msgspec/renderers.py,sha256=euFS4ybf4NMlAxkurvL9pxeeYlMDe29TIbtriED27gc,3795
|
|
36
|
+
fastdrf/msgspec/serializers.py,sha256=DocG9PCtpR49Nlos2VGVk9PZbvRh5p9_csV1hTS3HWQ,12897
|
|
37
|
+
fastdrf/pydantic/__init__.py,sha256=Wkl4T1sUef_tvTEetoTkqV1dDd1iKwm7shlJwtRCqe4,639
|
|
38
|
+
fastdrf/pydantic/compiler.py,sha256=3r64qZHcE6HBshX1bRAZWwYn-V7GyGAwhkTaH5IopCc,2836
|
|
39
|
+
fastdrf/pydantic/serializers.py,sha256=jwy2On3dFYEgVyUzQDQqX3NTMkJ1obKU0sJEmASkAko,12268
|
|
40
|
+
django_fastdrf-0.1.0.dist-info/METADATA,sha256=yF-gNJ7S-NesjRLtd7adUDTwaVliDqy53MBX_1a843I,8860
|
|
41
|
+
django_fastdrf-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
42
|
+
django_fastdrf-0.1.0.dist-info/licenses/LICENSE,sha256=rLcfHn1I1iqXETdM-Wi-o3r5l3WbYvqSE_bX-QV74IY,1493
|
|
43
|
+
django_fastdrf-0.1.0.dist-info/licenses/NOTICE,sha256=bpZ9XXWwEQOxItXGwBQXzW_OEPzav_1yHKZSHMc8oWY,494
|
|
44
|
+
django_fastdrf-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, ctolon
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
django-fastdrf includes code derived from aiodrf, Copyright (c) ctolon,
|
|
2
|
+
distributed under the BSD 3-Clause license included in LICENSE.
|
|
3
|
+
|
|
4
|
+
The many_init list-construction protocol derives from Django REST framework:
|
|
5
|
+
Copyright (c) 2011-present, Encode OSS Ltd. All rights reserved.
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
+
are permitted under the BSD 3-Clause license in LICENSE. The original DRF
|
|
8
|
+
project is https://github.com/encode/django-rest-framework.
|
fastdrf/__init__.py
ADDED
fastdrf/_classify.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Which serializers are a function of their class; coroutine hooks refused."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import weakref
|
|
5
|
+
from inspect import iscoroutinefunction
|
|
6
|
+
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.utils.choices import CallableChoiceIterator
|
|
9
|
+
from rest_framework import fields, relations, serializers
|
|
10
|
+
|
|
11
|
+
from fastdrf._inspection import _FIELD_HOOKS, _PLAIN_KWARGS
|
|
12
|
+
from fastdrf.utils import (
|
|
13
|
+
class_cache,
|
|
14
|
+
depends_on_classification,
|
|
15
|
+
is_framework_class,
|
|
16
|
+
user_defines,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Exact DRF field classes, defined in DRF's modules: a class of the
|
|
20
|
+
# project's, or one a module only imported, may bind or represent otherwise.
|
|
21
|
+
_BUILTIN_FIELDS = frozenset(
|
|
22
|
+
value
|
|
23
|
+
for module in (fields, relations)
|
|
24
|
+
for value in vars(module).values()
|
|
25
|
+
if inspect.isclass(value) and value.__module__ == module.__name__
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@depends_on_classification
|
|
30
|
+
@class_cache
|
|
31
|
+
def _model_fields_call_code(model):
|
|
32
|
+
"""
|
|
33
|
+
Return True if building a serializer field from a field of ``model`` may
|
|
34
|
+
call the project's code: callable ``choices`` or ``limit_choices_to``
|
|
35
|
+
(commonly a query), a ``limit_choices_to`` applied through the related
|
|
36
|
+
model's default manager of the project's (its ``get_queryset()``), a
|
|
37
|
+
model field class of the project's, whose attributes DRF reads, or a
|
|
38
|
+
FilePathField, which lists its directory.
|
|
39
|
+
"""
|
|
40
|
+
for field in model._meta.get_fields():
|
|
41
|
+
if isinstance(field, models.FilePathField) or not is_framework_class(
|
|
42
|
+
type(field)
|
|
43
|
+
):
|
|
44
|
+
return True
|
|
45
|
+
if isinstance(getattr(field, "choices", None), CallableChoiceIterator):
|
|
46
|
+
return True
|
|
47
|
+
limit = getattr(getattr(field, "remote_field", None), "limit_choices_to", None)
|
|
48
|
+
if callable(limit) or (
|
|
49
|
+
limit and user_defines(field.related_model._default_manager, "get_queryset")
|
|
50
|
+
):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# What DRF sets on a serializer instance over a class attribute of the same
|
|
56
|
+
# name. Any other instance attribute that shadows one of the class may change
|
|
57
|
+
# what DRF does: a method assigned to the instance, fields materialized (and
|
|
58
|
+
# perhaps edited), a ``Meta`` or ``url_field_name`` of its own.
|
|
59
|
+
_DRF_SHADOWS = frozenset({"_creation_counter", "initial", "default_empty_html"})
|
|
60
|
+
|
|
61
|
+
# Declared serializers are deep-copied into every instance, which builds them
|
|
62
|
+
# again from their arguments; these hooks could make the copy differ.
|
|
63
|
+
_BUILD_HOOKS = (*_FIELD_HOOKS, "bind", "__deepcopy__", "__new__")
|
|
64
|
+
|
|
65
|
+
# class -> the names an instance must not shadow, or None when it is not static
|
|
66
|
+
_static_classes: weakref.WeakKeyDictionary[type, frozenset[str] | None] = (
|
|
67
|
+
weakref.WeakKeyDictionary()
|
|
68
|
+
)
|
|
69
|
+
depends_on_classification(_static_classes)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_static(serializer):
|
|
73
|
+
"""
|
|
74
|
+
Return True if the fields of ``serializer`` (the child of a list
|
|
75
|
+
serializer), and those of the serializers nested in it, are a function
|
|
76
|
+
of its class.
|
|
77
|
+
|
|
78
|
+
That holds for an instance built with the usual arguments only that
|
|
79
|
+
shadows nothing of its class (no fields materialized, no method assigned),
|
|
80
|
+
of a class that builds its fields with DRF's code alone from declarations
|
|
81
|
+
and a model, whose declared fields are DRF's, children included, and
|
|
82
|
+
whose declared serializers are static too. The compiler, the input
|
|
83
|
+
recognizer and the classification below keep per-class answers for these
|
|
84
|
+
alone.
|
|
85
|
+
"""
|
|
86
|
+
if isinstance(serializer, serializers.ListSerializer):
|
|
87
|
+
serializer = serializer.child
|
|
88
|
+
names = _instance_shadow_names(type(serializer))
|
|
89
|
+
return (
|
|
90
|
+
names is not None
|
|
91
|
+
and _PLAIN_KWARGS.issuperset(serializer._kwargs)
|
|
92
|
+
and names.isdisjoint(vars(serializer))
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _instance_shadow_names(cls):
|
|
97
|
+
"""
|
|
98
|
+
The names an instance of ``cls`` must not set for its fields to be those
|
|
99
|
+
of its class (:func:`is_static`), or None when they never are.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
return _static_classes[cls]
|
|
103
|
+
except KeyError:
|
|
104
|
+
pass
|
|
105
|
+
meta = getattr(cls, "Meta", None)
|
|
106
|
+
model = getattr(meta, "model", None)
|
|
107
|
+
static = (
|
|
108
|
+
issubclass(cls, serializers.Serializer)
|
|
109
|
+
# A child may build fields from its parent's context, or change them
|
|
110
|
+
# when bound.
|
|
111
|
+
and not getattr(meta, "depth", 0)
|
|
112
|
+
and not user_defines(cls, *_BUILD_HOOKS)
|
|
113
|
+
# The project's code, run whenever a ModelSerializer builds its fields.
|
|
114
|
+
and not (
|
|
115
|
+
issubclass(cls, serializers.ModelSerializer)
|
|
116
|
+
and model is not None
|
|
117
|
+
and _model_fields_call_code(model)
|
|
118
|
+
)
|
|
119
|
+
and all(_static_declaration(field) for field in cls._declared_fields.values())
|
|
120
|
+
)
|
|
121
|
+
names = frozenset(dir(cls)) - _DRF_SHADOWS if static else None
|
|
122
|
+
# Racing threads store the same value.
|
|
123
|
+
return _static_classes.setdefault(cls, names)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _static_declaration(field):
|
|
127
|
+
if isinstance(field, serializers.ListSerializer):
|
|
128
|
+
return not user_defines(field, *_BUILD_HOOKS) and _static_declaration(
|
|
129
|
+
field.child
|
|
130
|
+
)
|
|
131
|
+
if isinstance(field, serializers.BaseSerializer):
|
|
132
|
+
return _instance_shadow_names(type(field)) is not None
|
|
133
|
+
# DRF's own fields; a custom one may bind differently per parent. DRF's
|
|
134
|
+
# collections and to-many relations bind the child they were declared with.
|
|
135
|
+
return type(field) in _BUILTIN_FIELDS and all(
|
|
136
|
+
_static_declaration(child)
|
|
137
|
+
for child in (
|
|
138
|
+
getattr(field, "child", None),
|
|
139
|
+
getattr(field, "child_relation", None),
|
|
140
|
+
)
|
|
141
|
+
if child is not None
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# What DRF materializes on an instance the first time it is read. Once any of
|
|
146
|
+
# these exists, the instance's fields may have been changed; the class says
|
|
147
|
+
# nothing about such an instance.
|
|
148
|
+
_MATERIALIZED = frozenset(
|
|
149
|
+
{"fields", "_validators", "_readable_fields", "_writable_fields"}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Static serializer class -> whether its representation has a coroutine hook.
|
|
153
|
+
_class_representation: weakref.WeakKeyDictionary[type, bool] = (
|
|
154
|
+
weakref.WeakKeyDictionary()
|
|
155
|
+
)
|
|
156
|
+
depends_on_classification(_class_representation)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _classified_by_class(serializer):
|
|
160
|
+
"""
|
|
161
|
+
Whether :func:`has_async_representation` of ``serializer`` is a property
|
|
162
|
+
of its class: its fields are (:func:`is_static`), none exists yet, and
|
|
163
|
+
nothing callable was set on the instance.
|
|
164
|
+
"""
|
|
165
|
+
state = vars(serializer)
|
|
166
|
+
return (
|
|
167
|
+
isinstance(serializer, serializers.Serializer)
|
|
168
|
+
and is_static(serializer)
|
|
169
|
+
and _MATERIALIZED.isdisjoint(state)
|
|
170
|
+
# ``default`` is ``fields.empty``, a class, on every instance.
|
|
171
|
+
and not any(
|
|
172
|
+
callable(value) and not isinstance(value, type) for value in state.values()
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def has_async_representation(serializer):
|
|
178
|
+
"""
|
|
179
|
+
Whether representing ``serializer`` reaches a coroutine function: an
|
|
180
|
+
``async def to_representation`` of a serializer or field, or an
|
|
181
|
+
``ato_representation`` / ``adata`` of the project's. Such a serializer is
|
|
182
|
+
not represented by a synchronous plan. A static serializer is answered
|
|
183
|
+
once per class, without building its fields again.
|
|
184
|
+
"""
|
|
185
|
+
if not _classified_by_class(serializer):
|
|
186
|
+
return _async_representation(serializer)
|
|
187
|
+
cls = type(serializer)
|
|
188
|
+
try:
|
|
189
|
+
return _class_representation[cls]
|
|
190
|
+
except KeyError:
|
|
191
|
+
pass
|
|
192
|
+
# Racing threads store the same value.
|
|
193
|
+
return _class_representation.setdefault(cls, _async_representation(serializer))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _async_representation(serializer):
|
|
197
|
+
if isinstance(serializer, serializers.ListSerializer):
|
|
198
|
+
return has_async_representation(serializer.child)
|
|
199
|
+
if not isinstance(serializer, serializers.Serializer):
|
|
200
|
+
return iscoroutinefunction(type(serializer).to_representation)
|
|
201
|
+
if iscoroutinefunction(
|
|
202
|
+
getattr(type(serializer), "to_representation", None)
|
|
203
|
+
) or user_defines(serializer, "ato_representation", "adata"):
|
|
204
|
+
return True
|
|
205
|
+
return any(
|
|
206
|
+
has_async_representation(field)
|
|
207
|
+
if isinstance(field, serializers.BaseSerializer)
|
|
208
|
+
else iscoroutinefunction(type(field).to_representation)
|
|
209
|
+
for field in serializer.fields.values()
|
|
210
|
+
)
|
fastdrf/_compiled.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""``serializer.data`` from a compiled class, for fastdrf's serializer bases."""
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from rest_framework import serializers
|
|
5
|
+
|
|
6
|
+
from fastdrf._classify import (
|
|
7
|
+
_instance_shadow_names,
|
|
8
|
+
has_async_representation,
|
|
9
|
+
is_static,
|
|
10
|
+
)
|
|
11
|
+
from fastdrf._inspection import _PLAIN_KWARGS
|
|
12
|
+
from fastdrf.settings import fastdrf_settings
|
|
13
|
+
from fastdrf.utils import user_defines
|
|
14
|
+
|
|
15
|
+
#: Set by :mod:`fastdrf.mixins` on a serializer that a generic view built,
|
|
16
|
+
#: validated and saved with framework code alone (:func:`fields_from_class`).
|
|
17
|
+
FIELDS_FROM_CLASS = "_fastdrf_fields_from_class"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fields_from_class(serializer):
|
|
21
|
+
"""
|
|
22
|
+
:func:`~fastdrf._classify.is_static` for a serializer whose fields exist
|
|
23
|
+
on the instance because framework code built and used them: a generic
|
|
24
|
+
view's serializer after validation and the save, when no
|
|
25
|
+
``get_serializer*`` or ``perform_*`` of the project's and no method of
|
|
26
|
+
the serializer's class could have edited them. Only the write mixins of
|
|
27
|
+
:mod:`fastdrf.mixins` mark such an instance (:data:`FIELDS_FROM_CLASS`).
|
|
28
|
+
"""
|
|
29
|
+
if not vars(serializer).get(FIELDS_FROM_CLASS, False):
|
|
30
|
+
return False
|
|
31
|
+
if isinstance(serializer, serializers.ListSerializer):
|
|
32
|
+
serializer = serializer.child
|
|
33
|
+
return _instance_shadow_names(type(serializer)) is not None and (
|
|
34
|
+
_PLAIN_KWARGS.issuperset(serializer._kwargs)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compiled_data(serializer):
|
|
39
|
+
"""
|
|
40
|
+
``serializer.data`` produced by a compiled class, when
|
|
41
|
+
``SERIALIZER_BACKEND`` (or ``Meta.serializer_backend``) asks for one and
|
|
42
|
+
the serializer compiles (see :mod:`fastdrf.compiler`), None for
|
|
43
|
+
DRF's code.
|
|
44
|
+
"""
|
|
45
|
+
source = serializer.instance
|
|
46
|
+
if (
|
|
47
|
+
(
|
|
48
|
+
fastdrf_settings.SERIALIZER_BACKEND == "drf"
|
|
49
|
+
and not _declares_backend(serializer)
|
|
50
|
+
)
|
|
51
|
+
or user_defines(serializer, "data")
|
|
52
|
+
or source is None
|
|
53
|
+
or getattr(serializer, "_errors", None)
|
|
54
|
+
or hasattr(serializer, "_data")
|
|
55
|
+
):
|
|
56
|
+
return None
|
|
57
|
+
from fastdrf.compiler import compiled_for, declines_source
|
|
58
|
+
|
|
59
|
+
# A static serializer is looked up by its class before it is classified:
|
|
60
|
+
# classifying one with nested serializers builds all their fields, which
|
|
61
|
+
# is what the compiled class saves.
|
|
62
|
+
if not is_static(serializer) and has_async_representation(serializer):
|
|
63
|
+
return None
|
|
64
|
+
encoder = compiled_for(serializer)
|
|
65
|
+
if encoder is None:
|
|
66
|
+
return None
|
|
67
|
+
if not isinstance(serializer, serializers.ListSerializer) and declines_source(
|
|
68
|
+
serializer, source
|
|
69
|
+
):
|
|
70
|
+
return None
|
|
71
|
+
return _producer(serializer, encoder, source)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _producer(serializer, encoder, source):
|
|
75
|
+
if isinstance(serializer, serializers.ListSerializer):
|
|
76
|
+
|
|
77
|
+
def produce(serializer):
|
|
78
|
+
from fastdrf.compiler import declines_source
|
|
79
|
+
|
|
80
|
+
items = (
|
|
81
|
+
source.all()
|
|
82
|
+
if isinstance(source, models.manager.BaseManager)
|
|
83
|
+
else source
|
|
84
|
+
)
|
|
85
|
+
items = list(items)
|
|
86
|
+
if declines_source(serializer, items):
|
|
87
|
+
# DRF's ListSerializer.to_representation, on the items read.
|
|
88
|
+
serializer._data = serializer.to_representation(items)
|
|
89
|
+
return serializer.data
|
|
90
|
+
try:
|
|
91
|
+
serializer._data = encoder.dump_many(items, serializer.context)
|
|
92
|
+
except encoder.error as exc:
|
|
93
|
+
serializer._data = _unreadable(serializer, exc, items)
|
|
94
|
+
return serializer.data
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
|
|
98
|
+
def produce(serializer):
|
|
99
|
+
try:
|
|
100
|
+
serializer._data = encoder.dump(source, serializer.context)
|
|
101
|
+
except encoder.error as exc:
|
|
102
|
+
serializer._data = _unreadable(serializer, exc, source)
|
|
103
|
+
return serializer.data
|
|
104
|
+
|
|
105
|
+
return produce
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _unreadable(serializer, error, source):
|
|
109
|
+
"""
|
|
110
|
+
DRF's representation of a source the compiled class failed to read, or
|
|
111
|
+
the backend's ``error`` when that is the result
|
|
112
|
+
(:func:`fastdrf.compiler.unreadable_source`). The backends take any
|
|
113
|
+
exception raised while reading an attribute for a missing attribute;
|
|
114
|
+
DRF's own read raises it as itself.
|
|
115
|
+
"""
|
|
116
|
+
from fastdrf.compiler import unreadable_source
|
|
117
|
+
|
|
118
|
+
if not unreadable_source(serializer, error):
|
|
119
|
+
raise error
|
|
120
|
+
return serializer.to_representation(source)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _declares_backend(serializer):
|
|
124
|
+
target = (
|
|
125
|
+
serializer.child
|
|
126
|
+
if isinstance(serializer, serializers.ListSerializer)
|
|
127
|
+
else serializer
|
|
128
|
+
)
|
|
129
|
+
return (
|
|
130
|
+
getattr(getattr(target, "Meta", None), "serializer_backend", None) is not None
|
|
131
|
+
)
|