django-structured-json-field 1.6.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.
Files changed (48) hide show
  1. django_structured_json_field-1.6.0.dist-info/METADATA +252 -0
  2. django_structured_json_field-1.6.0.dist-info/RECORD +48 -0
  3. django_structured_json_field-1.6.0.dist-info/WHEEL +5 -0
  4. django_structured_json_field-1.6.0.dist-info/licenses/LICENSE +21 -0
  5. django_structured_json_field-1.6.0.dist-info/top_level.txt +1 -0
  6. structured/__init__.py +13 -0
  7. structured/apps.py +7 -0
  8. structured/cache/__init__.py +10 -0
  9. structured/cache/cache.py +257 -0
  10. structured/cache/engine.py +314 -0
  11. structured/cache/rel_info.py +20 -0
  12. structured/contrib/restframework.py +71 -0
  13. structured/fields.py +264 -0
  14. structured/management/__init__.py +0 -0
  15. structured/management/commands/__init__.py +0 -0
  16. structured/management/commands/generate_schema_migration.py +165 -0
  17. structured/orm.py +658 -0
  18. structured/pydantic/__init__.py +0 -0
  19. structured/pydantic/conditionals.py +260 -0
  20. structured/pydantic/fields/__init__.py +8 -0
  21. structured/pydantic/fields/foreignkey.py +128 -0
  22. structured/pydantic/fields/queryset.py +153 -0
  23. structured/pydantic/fields/serializer.py +38 -0
  24. structured/pydantic/models.py +92 -0
  25. structured/schema_migrations/__init__.py +0 -0
  26. structured/schema_migrations/structured_json_migration.py +315 -0
  27. structured/settings.py +32 -0
  28. structured/static/js/structured-field-init.js +36 -0
  29. structured/templates/json-forms/widget.html +21 -0
  30. structured/urls.py +6 -0
  31. structured/utils/__init__.py +0 -0
  32. structured/utils/cast.py +32 -0
  33. structured/utils/context.py +16 -0
  34. structured/utils/dict.py +7 -0
  35. structured/utils/django.py +33 -0
  36. structured/utils/errors.py +31 -0
  37. structured/utils/getter.py +17 -0
  38. structured/utils/namespace.py +22 -0
  39. structured/utils/options.py +31 -0
  40. structured/utils/pydantic.py +80 -0
  41. structured/utils/replace.py +12 -0
  42. structured/utils/serializer.py +99 -0
  43. structured/utils/setter.py +40 -0
  44. structured/utils/typing.py +85 -0
  45. structured/views.py +76 -0
  46. structured/widget/__init__.py +0 -0
  47. structured/widget/fields.py +68 -0
  48. structured/widget/widgets.py +110 -0
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-structured-json-field
3
+ Version: 1.6.0
4
+ Summary: Django json field empowered by pydantic
5
+ Author-email: Lotrèk <gabriele@lotrek.it>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/lotrekagency/django-structured-json-field
8
+ Keywords: django,pydantic,django pydantic,django json,json schema,django form
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 4.2
12
+ Classifier: Framework :: Django :: 5.0
13
+ Classifier: Framework :: Django :: 5.1
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: djangorestframework<4.0.0,>=3.14.0
27
+ Requires-Dist: Django>=4.2
28
+ Requires-Dist: pydantic>=2.12
29
+ Dynamic: license-file
30
+
31
+ # Django Structured JSON Field [![PyPI](https://img.shields.io/pypi/v/django-structured-json-field?style=flat-square)](https://pypi.org/project/django-structured-json-field) ![Codecov](https://img.shields.io/codecov/c/github/bnznamco/django-structured-field?style=flat-square&logo=codecov&logoSize=auto&cacheSeconds=0) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/lotrekagency/django-structured-field/ci.yml?style=flat-square) [![GitHub](https://img.shields.io/github/license/lotrekagency/django-structured-field?style=flat-square)](./LICENSE) [![Docs](https://img.shields.io/badge/docs-online-brightgreen?style=flat-square)](https://bnznamco.github.io/django-structured-field/)
32
+
33
+
34
+ <br>
35
+ <br>
36
+ <p align="center">
37
+ <picture>
38
+ <source media="(prefers-color-scheme: dark)" srcset="docs/.vuepress/public/images/logo-dark.svg" width="200">
39
+ <source media="(prefers-color-scheme: light)" srcset="docs/.vuepress/public/images/logo.svg" width="200">
40
+ <img alt="Django Structured JSON Field logo" src="docs/.vuepress/public/images/logo.svg" width="200">
41
+ </picture>
42
+ </p>
43
+ <br>
44
+ <br>
45
+ <br>
46
+ <br>
47
+
48
+
49
+
50
+ This is a Django field that allows you to declare the structure of a JSON field and validate it.
51
+
52
+ ## Features
53
+
54
+ - Define the structure of a JSON field using Pydantic models
55
+ - Validate the JSON field against the defined structure
56
+ - Use relationships between models inside the JSON field 🤯
57
+ - Django-native `prefetch_related` across JSON paths — `MyModel.objects.prefetch_related("structured_data__author__country")` Just Works 🚀
58
+ - Easily integrate with Django Rest Framework serializers
59
+ - Admin editor for the JSON field with autocomplete search for related models 👀
60
+
61
+
62
+ ## Documentation
63
+
64
+ Check out our [documentation](https://bnznamco.github.io/django-structured-field/) for detailed guides and examples on:
65
+
66
+ - Installation and basic usage
67
+ - Working with relationships
68
+ - Prefetching relations across JSON paths
69
+ - Admin integration
70
+ - REST Framework integration
71
+ - Caching
72
+ - Settings configuration
73
+
74
+
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ pip install django-structured-json-field
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ ```python
85
+ from django.db import models
86
+ from structured.fields import StructuredJSONField
87
+ from structured.pydantic.models import BaseModel
88
+
89
+ # Define this schema as you would do with a Pydantic model
90
+ class MySchema(BaseModel):
91
+ name: str
92
+ age: int = None
93
+
94
+ def init_data():
95
+ return MySchema(name='')
96
+
97
+ # Create a model with a StructuredJSONField with the schema you defined
98
+ class MyModel(models.Model):
99
+ structured_data = StructuredJSONField(schema=MySchema, default=init_data)
100
+
101
+ ```
102
+
103
+ ## Relationships
104
+
105
+ This field supports relationships between models, you can define them in your schema and they will be treated as normal django relationships. It also supports recursive schemas.
106
+
107
+ ### Recursion
108
+
109
+ You can define recursive schemas by declaring the attribute type as a string:
110
+
111
+ ```python
112
+ from typing import Optional, List
113
+
114
+ class MySchema(BaseModel):
115
+ name: str
116
+ age: int = None
117
+ parent: Optional['MySchema'] = None
118
+ relateds: List['MySchema'] = []
119
+ ```
120
+
121
+ ### Foreign Keys
122
+
123
+ You can also define model relationships in your schema:
124
+
125
+ ```python
126
+ from structured.pydantic.fields import ForeignKey
127
+
128
+ class MySchema(BaseModel):
129
+ name: str
130
+ age: int = None
131
+ fk_field: ForeignKey['MyModel'] = None
132
+ ```
133
+
134
+ This will treat the parent field as a normal django ForeignKey.
135
+
136
+ #### Tip:
137
+
138
+ You can omit the `ForeignKey` field and just use the model class as the type annotation:
139
+
140
+ ```python
141
+ class MySchema(BaseModel):
142
+ name: str
143
+ age: int = None
144
+ fk_field: MyModel = None
145
+ ```
146
+
147
+ the field will still be treated as a ForeignKey if the type annotation is a subclass of django `models.Model`.
148
+
149
+ ### ManyToMany
150
+
151
+ If you need a ManyToMany relationship, you can use the `QuerySet` field:
152
+
153
+ ```python
154
+ from structured.pydantic.fields import QuerySet
155
+
156
+ class MySchema(BaseModel):
157
+ name: str
158
+ age: int = None
159
+ parents: QuerySet['MyModel']
160
+ ```
161
+
162
+ `QuerySet` fields will generate a django object manager that will allow you to query the related objects as you would do with a normal django `QuerySet`.
163
+
164
+ ```python
165
+ instance = MySchema(name='test', age=10, parents=MyModel.objects.all())
166
+ # You can filter the queryset
167
+ instance.parents.filter(name='test')
168
+ # You can count the queryset
169
+ instance.parents.count()
170
+ # You can get the first element of the queryset, etc...
171
+ instance.parents.first()
172
+ ```
173
+
174
+ ### Admin integration
175
+
176
+ The field is integrated with the Django admin, you can use the autocomplete search to select the related models. To allow the autocomplete search you need to include structured.urls in your urls.py file:
177
+
178
+ ```python
179
+ from django.urls import path, include
180
+
181
+ urlpatterns = [
182
+ path('admin/', admin.site.urls),
183
+ path('', include('structured.urls')),
184
+ ]
185
+ ```
186
+
187
+ ### Rest Framework integration
188
+
189
+ You can easily integrate structured fields with Django Rest Framework serializers, just use the `StructuredModelSerializer` as the base class for your serializer:
190
+
191
+ ```python
192
+ from rest_framework import serializers
193
+ from structured.contrib.rest_framework import StructuredModelSerializer
194
+
195
+ class MyModelSerializer(StructuredModelSerializer):
196
+ class Meta:
197
+ model = MyModel
198
+ fields = '__all__'
199
+ ```
200
+
201
+ Errors generated by pydantic validation will be automatically translated to DRF errors.
202
+
203
+
204
+ ### Cache
205
+
206
+ To prevent the field from making multiple identical queries a caching technique is used. The cache is still a work in progress, please open an issue if you find any problem.
207
+ Actually the cache covers all the relations inside a StructuredJSONField, optimizing the queries during the serialization process.
208
+
209
+ #### Cache engine progress:
210
+
211
+ - [x] Shared cache between `ForeignKey` fields and `QuerySet` fields
212
+ - [x] Shared cache through nested schemas
213
+ - [x] Shared cache through nested lists of schemas
214
+ - [ ] Shared cache between all `StructuredJSONFields` in the same instance
215
+ - [ ] Shared cache between multiple instances of the same model
216
+ - [ ] Cache invalidation mechanism
217
+
218
+ ## Settings
219
+
220
+ You can manage structured field behaviour modifying the `STRUCTURED_FIELD` setting in your `settings.py` file. Here a list of the available settings and their default values:
221
+
222
+ ```python
223
+ STRUCTURED_FIELD = {
224
+ 'CACHE':{
225
+ 'ENABLED': True,
226
+ 'SHARED': False # ⚠️ EXPERIMENTAL: this enables a thread-shared cache, it's not recommended to use it in production.
227
+ },
228
+ }
229
+ ```
230
+
231
+ ## Contributing
232
+
233
+ The project is open to contributions, just open an issue or a PR.
234
+
235
+ ### Running tests
236
+
237
+ ```bash
238
+ pip install -r requirements-dev.txt
239
+ make test
240
+ ```
241
+
242
+ ### Running test app
243
+
244
+ ```bash
245
+ pip install -r requirements-dev.txt
246
+ python manage.py migrate
247
+ python manage.py runserver
248
+ ```
249
+
250
+ ## License
251
+
252
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
@@ -0,0 +1,48 @@
1
+ django_structured_json_field-1.6.0.dist-info/licenses/LICENSE,sha256=2_XPsmGfwHVMSsUsHX7f2lxyLluSjm7RcRlnd7iaP3c,1074
2
+ structured/__init__.py,sha256=DXEWnP5eZfUwa3rzzQ_vPP3po5wQtMqyeA2vat6xIkw,227
3
+ structured/apps.py,sha256=pUQi2lciJdIYwUXXoP3FTrUU2DdyCjL6QinNI8__c78,136
4
+ structured/fields.py,sha256=Va93mv5YhfwsEJ7Bhok0Jw__ovp1k2onyWlb6H2oMpk,10334
5
+ structured/orm.py,sha256=8GVCRdGfjQ_EGQJqQuby9heoOSpjPXt7kw1dNDGjpV0,24513
6
+ structured/settings.py,sha256=YlH3fwjs8eYawv0hILH3IHUgRXdA6a41qu-chKnxCTw,1067
7
+ structured/urls.py,sha256=TbwHE2n6v5XaHBmo1d7OWEXtOdIQFibGFVAALZhFJVw,173
8
+ structured/views.py,sha256=urNvjwurbcnRhGUJ8lGJGiNWaTjbN4pPU_StsQ6AfnY,3082
9
+ structured/cache/__init__.py,sha256=8xQBYjkgFVH9Bytgj5OAyBCRdCweHuwk6KiJeunWX4g,262
10
+ structured/cache/cache.py,sha256=Y4MhFYT57e-wONZi9QymD0vsuGMn8f6jUtNbHZx4N5M,8730
11
+ structured/cache/engine.py,sha256=SLv4vMOFp6ScpKUg1wn0NMzC6lKvziyZtjrLpUaqers,14411
12
+ structured/cache/rel_info.py,sha256=K8EEhUCufKcW4O0GQKQReG3cltNmuYRHZ_bMOwCo8Ac,606
13
+ structured/contrib/restframework.py,sha256=A7oX2uTHg8sXeoWOicLWlE41HOvjYurLRqz3dI2KYYk,2946
14
+ structured/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ structured/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ structured/management/commands/generate_schema_migration.py,sha256=ko9S1mS0VyovjprqjUPaddB69lF2sfDTs_6F3Jd52Es,6389
17
+ structured/pydantic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ structured/pydantic/conditionals.py,sha256=GW9ZpuSaSVMEloYg9p22i-TJ49jkeT_E51YgqO1UO8U,10423
19
+ structured/pydantic/models.py,sha256=9fMFmmJfRw0wB2cEC5dCPAAq2LBa7mC2EeCVjh5it5o,4079
20
+ structured/pydantic/fields/__init__.py,sha256=rkDcCYYMZWro8qQWcRM_c89mstFyHlmPU2tGa11rVMg,116
21
+ structured/pydantic/fields/foreignkey.py,sha256=_1CCBp3srU7aRPDokXiG4PPBzCVifqPDMg4zn6aPDRo,5224
22
+ structured/pydantic/fields/queryset.py,sha256=AfT7SUkLVWWpvkRC8uB2qEpJnMkONhxA8lGRWHK7ioM,5909
23
+ structured/pydantic/fields/serializer.py,sha256=vzEBPiY8D_oXBRVVyN0LwicTdCJaNgBLqadMokgk7_4,1655
24
+ structured/schema_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ structured/schema_migrations/structured_json_migration.py,sha256=5sywecUyGdWG4C7XCUWc2h_K-evr5D6NpqhptSUuW1Y,11863
26
+ structured/static/js/structured-field-init.js,sha256=NmTFO8Rgog7rQJgC9CvWdzXJS3qrHSE2Xpo4cIzAZI8,1210
27
+ structured/templates/json-forms/widget.html,sha256=BCIGYmtHEtmOK_u_-ojR5WOKIRihuZww6rY8kr_dEEs,535
28
+ structured/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ structured/utils/cast.py,sha256=q7E6WflY_DP2kcs-Znk7Sp2xQBwYZZWrtryT2gkPKuE,1129
30
+ structured/utils/context.py,sha256=xwDa118xsr70eaT-zQTPBP63hmNCTeccZ8dON_fYl3s,560
31
+ structured/utils/dict.py,sha256=RDCZtjwpEEwjvfUjQl2bEWFKw7NxTzkXco72VeO2M9w,255
32
+ structured/utils/django.py,sha256=cZjXnsQ-kQa18xzQ7BwZnsq_oM39ZgJF4HgZCw_cwUA,1345
33
+ structured/utils/errors.py,sha256=Mh-I9n1FpWr94Ry6LcqJCqOKgSUJXB_PmYASmuL869g,1366
34
+ structured/utils/getter.py,sha256=NrlByyd4SrFp3aVpdA21MMs3bcJFyequsxoIkrh7ATI,639
35
+ structured/utils/namespace.py,sha256=Pe3DmUryy8YWkI-fAuxV9cPH6AWEaYI1u9h3p_3CZkU,892
36
+ structured/utils/options.py,sha256=6GJpHc2R31gKNue8TC9hDcfgJnJQvcdriyx93TWxDo0,1037
37
+ structured/utils/pydantic.py,sha256=M73wr0dyyAmtE-3ZG8NywfBA0xXjoip_T1xmJw9d1b4,3526
38
+ structured/utils/replace.py,sha256=0pHmKCZhUBLUeUIv2OD3qces2yYhNOSXoNUNPk3iCzA,436
39
+ structured/utils/serializer.py,sha256=NEVsQLkNP7S0kW8ze26bzjhUBWEVspCznuHG8u1F4Fk,3836
40
+ structured/utils/setter.py,sha256=6iF3gG8RfatxBYeubU1KweP48UVbS1LcnungnmqMqm8,1358
41
+ structured/utils/typing.py,sha256=1PbRc0sy_BhT6KkYAaXkvYbzs1cCu1iSykdhvBaplek,3430
42
+ structured/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ structured/widget/fields.py,sha256=Woi5R0ob4-t416gHtxCurz2vtCaN9thoRUbKICJoPts,2607
44
+ structured/widget/widgets.py,sha256=9PYUGETpn-XbSb9bM3OYdJlUXIW9yRWBQHmSs7jOrm0,3844
45
+ django_structured_json_field-1.6.0.dist-info/METADATA,sha256=xC13yMMhEdLiFWgmT7sCiLaz8IyIAZoCmpOPqdLTqAI,7966
46
+ django_structured_json_field-1.6.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
47
+ django_structured_json_field-1.6.0.dist-info/top_level.txt,sha256=exTAxPKh1S71ZrY3PJ7PTnmRbnQEBRE09OPnyIpDJ28,11
48
+ django_structured_json_field-1.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Lotrèk 2024
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ structured
structured/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from structured.orm import (
2
+ StructuredManager,
3
+ StructuredQuerySet,
4
+ StructuredQuerySetMixin,
5
+ )
6
+
7
+ __version__ = "1.6.0"
8
+
9
+ __all__ = [
10
+ "StructuredManager",
11
+ "StructuredQuerySet",
12
+ "StructuredQuerySetMixin",
13
+ ]
structured/apps.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import unicode_literals
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class StructuredConfig(AppConfig):
7
+ name = "structured"
@@ -0,0 +1,10 @@
1
+ from structured.cache.engine import CacheEngine, CacheEnabledModel
2
+ from structured.cache.cache import get_global_cache, Cache, ThreadSafeCache
3
+
4
+ __all__ = [
5
+ "CacheEngine",
6
+ "CacheEnabledModel",
7
+ "Cache",
8
+ "ThreadSafeCache",
9
+ "get_global_cache",
10
+ ]
@@ -0,0 +1,257 @@
1
+ from collections import defaultdict
2
+ from typing import Type, Union, Iterable, Dict, Any, TYPE_CHECKING
3
+ from django.db.models import Model as DjangoModel
4
+ from django.db.models.query import QuerySet as DjangoQuerySet
5
+ from django.db.models.signals import post_save, pre_delete
6
+ from pydantic import ValidationInfo, model_validator
7
+ import threading
8
+ from structured.settings import settings
9
+
10
+ if TYPE_CHECKING: # pragma: no cover
11
+ from structured.pydantic.models import BaseModel
12
+ from structured.cache.engine import CacheEngine
13
+
14
+
15
+ CACHE_CONTEXT_KEY = "structured_parent_cache"
16
+
17
+
18
+ class CacheEnabledModel:
19
+ """
20
+ A model class that enables caching.
21
+
22
+ When validated via ``schema.validate_python(value, context={...})``,
23
+ a ``Cache`` instance under the ``CACHE_CONTEXT_KEY`` key is reused
24
+ instead of creating a fresh per-call cache — letting multiple
25
+ ``StructuredJSONField``s on the same Django instance share one
26
+ fetch pool.
27
+ """
28
+
29
+ _cache_engine: 'CacheEngine'
30
+
31
+ @model_validator(mode="wrap")
32
+ @classmethod
33
+ def build_cache(
34
+ cls,
35
+ data: Dict[str, Any],
36
+ handler: Any,
37
+ info: ValidationInfo,
38
+ ) -> Any:
39
+ """
40
+ Build and fetch cache for the given data.
41
+
42
+ ``info`` is declared as a required positional argument with a
43
+ typed annotation so pydantic's signature inspection always
44
+ passes it. With ``info=None`` defaults, pydantic v2 sometimes
45
+ elects the shorter ``(cls, data, handler)`` overload and the
46
+ validator never sees the parent cache.
47
+ """
48
+ parent_cache = None
49
+ if info is not None and info.context:
50
+ parent_cache = info.context.get(CACHE_CONTEXT_KEY)
51
+ data = cls._cache_engine.build_cache(data, parent_cache=parent_cache)
52
+ instance: "BaseModel" = handler(data)
53
+ return cls._cache_engine.fetch_cache(instance)
54
+
55
+
56
+ def get_global_cache():
57
+ if settings.STRUCTURED_FIELD_SHARED_CACHE:
58
+ return ThreadSafeCache()
59
+ return None
60
+
61
+
62
+ class Cache(defaultdict):
63
+ """
64
+ The cache class that stores the cache data.
65
+ Per-request caches do NOT connect to Django signals — they are short-lived
66
+ and discarded after validation. Only ThreadSafeCache (shared mode) connects
67
+ signals for cross-request cache invalidation.
68
+ The cache data is stored in the following format:
69
+ {
70
+ Model1: {
71
+ pk1: instance1,
72
+ pk2: instance2,
73
+ ...
74
+ },
75
+ Model2: {
76
+ pk1: instance1,
77
+ pk2: instance2,
78
+ ...
79
+ },
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ super().__init__(dict)
84
+
85
+ def flush(
86
+ self, data: Union[DjangoModel, Iterable[DjangoModel], None] = None, **kwargs
87
+ ) -> None:
88
+ """
89
+ Flush the cache for the given data and/or model; with no arguments,
90
+ clear the whole cache. An empty iterable flushes nothing.
91
+ """
92
+ model = kwargs.get("model", None)
93
+ if model is not None:
94
+ self._flush_model(model)
95
+ if data is not None:
96
+ self._flush_data(data)
97
+ if model is None and data is None:
98
+ self.clear()
99
+
100
+ def _flush_model(self, model: Union[str, Type[DjangoModel]]) -> None:
101
+ """
102
+ Flush the cache for the given model.
103
+ """
104
+ if isinstance(model, str):
105
+ model = next((m for m in self.keys() if m.__name__ == model), None)
106
+ if model and model in self:
107
+ del self[model]
108
+
109
+ def _flush_data(self, data: Union[DjangoModel, Iterable[DjangoModel]]) -> None:
110
+ """
111
+ Flush the cache for the given data.
112
+ """
113
+ if isinstance(data, DjangoModel):
114
+ model = data.__class__
115
+ if model in self and data.pk in self[model]:
116
+ del self[model][data.pk]
117
+ else:
118
+ for instance in data:
119
+ model = instance.__class__
120
+ if model in self and instance.pk in self[model]:
121
+ del self[model][instance.pk]
122
+
123
+ def set(self, data: Union[DjangoModel, Iterable[DjangoModel]]) -> None:
124
+ """
125
+ Set the cache for the given data.
126
+ """
127
+ if isinstance(data, DjangoModel):
128
+ self[data.__class__].update({data.pk: data})
129
+ else:
130
+ for instance in data:
131
+ self[instance.__class__].update({instance.pk: instance})
132
+
133
+
134
+ class ThreadSafeCache(Cache):
135
+ """
136
+ A singleton cache object that allows sharing cache data between fields and instances.
137
+ This encapsulates the cache data inside a thread-safe singleton object.
138
+ Every model instance invoking a cache operation will use the same cache object,
139
+ allowing the cache data to be shared between them.
140
+ All mutation methods are protected by a threading lock for thread safety.
141
+ Connects to Django signals for cross-request cache invalidation.
142
+ """
143
+
144
+ _instance = None
145
+ _lock = threading.RLock()
146
+
147
+ def __new__(cls):
148
+ if cls._instance is None:
149
+ with cls._lock:
150
+ if not cls._instance:
151
+ cls._instance = super().__new__(cls)
152
+
153
+ return cls._instance
154
+
155
+ def __init__(self) -> None:
156
+ if not hasattr(self, '_initialized'):
157
+ super().__init__()
158
+ self._initialized = True
159
+ post_save.connect(self._on_instance_save)
160
+ pre_delete.connect(self._on_instance_delete)
161
+
162
+ @staticmethod
163
+ def _on_instance_save(
164
+ sender: Type[DjangoModel], instance: DjangoModel, **kwargs
165
+ ) -> None:
166
+ """
167
+ Signal handler for saving a model instance.
168
+ """
169
+ cache = get_global_cache()
170
+ if cache and instance.__class__ in cache:
171
+ cache.set(instance)
172
+
173
+ @staticmethod
174
+ def _on_instance_delete(
175
+ sender: Type[DjangoModel], instance: DjangoModel, **kwargs
176
+ ) -> None:
177
+ """
178
+ Signal handler for deleting a model instance.
179
+ """
180
+ cache = get_global_cache()
181
+ if cache and instance.__class__ in cache:
182
+ cache.flush(instance)
183
+
184
+ def set(self, data: Union[DjangoModel, Iterable[DjangoModel]]) -> None:
185
+ with self._lock:
186
+ super().set(data)
187
+
188
+ def flush(
189
+ self, data: Union[DjangoModel, Iterable[DjangoModel], None] = None, **kwargs
190
+ ) -> None:
191
+ with self._lock:
192
+ super().flush(data, **kwargs)
193
+
194
+ def __setitem__(self, key, value):
195
+ with self._lock:
196
+ super().__setitem__(key, value)
197
+
198
+ def __delitem__(self, key):
199
+ with self._lock:
200
+ super().__delitem__(key)
201
+
202
+ def clear(self):
203
+ with self._lock:
204
+ super().clear()
205
+
206
+
207
+ class ValueWithCache:
208
+ """
209
+ A class that represents a cached value.
210
+ This is used to retrieve values from the cache during the serialization process.
211
+ The retrieve method fetches the needed values and returns accordingly to the field type (ForeignKey, QuerySet, etc.).
212
+ """
213
+
214
+ def __init__(
215
+ self,
216
+ cache: Cache,
217
+ model: Type[DjangoModel],
218
+ value: Union[Iterable[Union[str, int]], str, int],
219
+ ) -> None:
220
+ self.cache: Cache = cache
221
+ self.value: Union[Iterable[Union[str, int]], str, int] = value
222
+ self.model: Type[DjangoModel] = model
223
+
224
+ def retrieve(self) -> Union[DjangoModel, DjangoQuerySet]:
225
+ """
226
+ Retrieve the value from the cache or database.
227
+ """
228
+ cache = self.cache.get(self.model)
229
+ if hasattr(self.value, "__iter__") and not isinstance(self.value, str):
230
+ pks = list(self.value)
231
+ if cache:
232
+ cached = [cache[i] for i in pks if i in cache]
233
+ missing_pks = [i for i in pks if i not in cache]
234
+ if missing_pks:
235
+ fetched = list(self.model._default_manager.filter(pk__in=missing_pks))
236
+ cached.extend(fetched)
237
+ # Update cache with newly fetched instances
238
+ for obj in fetched:
239
+ cache[obj.pk] = obj
240
+ result = cached
241
+ else:
242
+ result = list(self.model._default_manager.filter(pk__in=pks))
243
+ # Preserve original PK ordering
244
+ pk_to_obj = {obj.pk: obj for obj in result}
245
+ ordered = [pk_to_obj[pk] for pk in pks if pk in pk_to_obj]
246
+ qs = self.model._default_manager.filter(pk__in=pks)
247
+ setattr(qs, "_result_cache", ordered)
248
+ return qs
249
+ else:
250
+ if cache:
251
+ val = cache.get(self.value, None)
252
+ else:
253
+ val = None
254
+ if val is None:
255
+ return self.model._default_manager.filter(pk=self.value).first()
256
+ else:
257
+ return val