statezero 0.1.0b1__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.
- statezero/__init__.py +0 -0
- statezero/adaptors/__init__.py +0 -0
- statezero/adaptors/django/__init__.py +0 -0
- statezero/adaptors/django/apps.py +97 -0
- statezero/adaptors/django/config.py +99 -0
- statezero/adaptors/django/context_manager.py +12 -0
- statezero/adaptors/django/event_emitters.py +78 -0
- statezero/adaptors/django/exception_handler.py +98 -0
- statezero/adaptors/django/extensions/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
- statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
- statezero/adaptors/django/f_handler.py +312 -0
- statezero/adaptors/django/helpers.py +153 -0
- statezero/adaptors/django/middleware.py +10 -0
- statezero/adaptors/django/migrations/0001_initial.py +33 -0
- statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
- statezero/adaptors/django/migrations/__init__.py +0 -0
- statezero/adaptors/django/orm.py +915 -0
- statezero/adaptors/django/permissions.py +252 -0
- statezero/adaptors/django/query_optimizer.py +772 -0
- statezero/adaptors/django/schemas.py +324 -0
- statezero/adaptors/django/search_providers/__init__.py +0 -0
- statezero/adaptors/django/search_providers/basic_search.py +24 -0
- statezero/adaptors/django/search_providers/postgres_search.py +51 -0
- statezero/adaptors/django/serializers.py +554 -0
- statezero/adaptors/django/urls.py +14 -0
- statezero/adaptors/django/views.py +336 -0
- statezero/core/__init__.py +34 -0
- statezero/core/ast_parser.py +821 -0
- statezero/core/ast_validator.py +266 -0
- statezero/core/classes.py +167 -0
- statezero/core/config.py +263 -0
- statezero/core/context_storage.py +4 -0
- statezero/core/event_bus.py +175 -0
- statezero/core/event_emitters.py +60 -0
- statezero/core/exceptions.py +106 -0
- statezero/core/interfaces.py +492 -0
- statezero/core/process_request.py +184 -0
- statezero/core/types.py +63 -0
- statezero-0.1.0b1.dist-info/METADATA +252 -0
- statezero-0.1.0b1.dist-info/RECORD +45 -0
- statezero-0.1.0b1.dist-info/WHEEL +5 -0
- statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
- statezero-0.1.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Set, Type, Union
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.utils.module_loading import import_string
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
import contextvars
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
import logging
|
|
9
|
+
from cytoolz import pluck
|
|
10
|
+
from zen_queries import queries_disabled
|
|
11
|
+
|
|
12
|
+
from statezero.adaptors.django.config import config, registry
|
|
13
|
+
from statezero.core.interfaces import AbstractDataSerializer, AbstractQueryOptimizer
|
|
14
|
+
from statezero.core.types import RequestType
|
|
15
|
+
from statezero.adaptors.django.helpers import collect_from_queryset
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Context variables remain the same
|
|
20
|
+
fields_map_var = contextvars.ContextVar('fields_map', default=None)
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def fields_map_context(fields_map):
|
|
24
|
+
"""
|
|
25
|
+
Context manager that sets the fields_map for the current context.
|
|
26
|
+
"""
|
|
27
|
+
token = fields_map_var.set(fields_map)
|
|
28
|
+
try:
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
fields_map_var.reset(token)
|
|
32
|
+
|
|
33
|
+
def get_current_fields_map():
|
|
34
|
+
"""
|
|
35
|
+
Get the fields_map from the current context.
|
|
36
|
+
Returns an empty dict if no fields_map is set.
|
|
37
|
+
"""
|
|
38
|
+
return fields_map_var.get() or {}
|
|
39
|
+
|
|
40
|
+
def extract_fields(model_name:str=None) -> Set[str]:
|
|
41
|
+
"""
|
|
42
|
+
Extract the set of fields that should be included based on the fields_map and current path.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
model_name (str): Optional model name for model-based filtering
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
set: Set of field names that should be included, or None if all fields should be included
|
|
49
|
+
"""
|
|
50
|
+
return get_current_fields_map().get(model_name)
|
|
51
|
+
|
|
52
|
+
def get_custom_serializer(field_class: Type) -> Optional[Type[serializers.Field]]:
|
|
53
|
+
"""
|
|
54
|
+
Look up a custom serializer override for a given model field.
|
|
55
|
+
First, it checks the config registry, and then falls back to Django settings.
|
|
56
|
+
"""
|
|
57
|
+
if field_class in config.custom_serializers:
|
|
58
|
+
return config.custom_serializers[field_class]
|
|
59
|
+
|
|
60
|
+
custom_serializers = getattr(settings, "CUSTOM_FIELD_SERIALIZERS", {})
|
|
61
|
+
key = f"{field_class.__module__}.{field_class.__name__}"
|
|
62
|
+
serializer_path = custom_serializers.get(key)
|
|
63
|
+
if serializer_path:
|
|
64
|
+
return import_string(serializer_path)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
class FlexiblePrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
|
68
|
+
"""
|
|
69
|
+
A custom PrimaryKeyRelatedField that can handle both primary keys and model instances.
|
|
70
|
+
"""
|
|
71
|
+
def to_internal_value(self, data):
|
|
72
|
+
# If data is already a model instance, extract its primary key
|
|
73
|
+
if hasattr(data, '_meta'):
|
|
74
|
+
pk_field = data._meta.pk.name
|
|
75
|
+
pk_value = getattr(data, pk_field)
|
|
76
|
+
return super().to_internal_value(pk_value)
|
|
77
|
+
|
|
78
|
+
# If data is a dictionary with a key matching the PK field name, extract the value
|
|
79
|
+
if isinstance(data, dict) and self.queryset.model._meta.pk.name in data:
|
|
80
|
+
pk_value = data[self.queryset.model._meta.pk.name]
|
|
81
|
+
return super().to_internal_value(pk_value)
|
|
82
|
+
|
|
83
|
+
# Otherwise, use the standard to_internal_value
|
|
84
|
+
return super().to_internal_value(data)
|
|
85
|
+
|
|
86
|
+
class FExpressionMixin:
|
|
87
|
+
"""
|
|
88
|
+
A mixin that can handle F expression objects in serializer write operations.
|
|
89
|
+
"""
|
|
90
|
+
def to_internal_value(self, data):
|
|
91
|
+
"""
|
|
92
|
+
Override to_internal_value to handle F expressions before standard validation.
|
|
93
|
+
"""
|
|
94
|
+
# Check if data is a dictionary, if not let the parent handle it
|
|
95
|
+
if not isinstance(data, dict):
|
|
96
|
+
return super().to_internal_value(data)
|
|
97
|
+
|
|
98
|
+
# First extract F expressions
|
|
99
|
+
f_expressions = {}
|
|
100
|
+
data_copy = {**data} # Create a copy to modify
|
|
101
|
+
|
|
102
|
+
for field_name, value in data.items():
|
|
103
|
+
if isinstance(value, dict) and value.get('__f_expr'):
|
|
104
|
+
# Store F expressions for later
|
|
105
|
+
f_expressions[field_name] = value
|
|
106
|
+
# Remove them from the data to avoid validation errors
|
|
107
|
+
data_copy.pop(field_name)
|
|
108
|
+
|
|
109
|
+
# Standard validation for remaining fields
|
|
110
|
+
validated_data = super().to_internal_value(data_copy)
|
|
111
|
+
|
|
112
|
+
# Add F expressions back to the validated data
|
|
113
|
+
for field_name, value in f_expressions.items():
|
|
114
|
+
validated_data[field_name] = value
|
|
115
|
+
|
|
116
|
+
return validated_data
|
|
117
|
+
|
|
118
|
+
class DynamicModelSerializer(FExpressionMixin, serializers.ModelSerializer):
|
|
119
|
+
"""
|
|
120
|
+
A dynamic serializer that adds a read-only 'repr' field
|
|
121
|
+
and applies custom serializers for model fields.
|
|
122
|
+
"""
|
|
123
|
+
repr = serializers.SerializerMethodField()
|
|
124
|
+
|
|
125
|
+
def __init__(self, *args, **kwargs):
|
|
126
|
+
self.get_model_name = kwargs.pop("get_model_name", config.orm_provider.get_model_name)
|
|
127
|
+
self.depth = kwargs.pop("depth", 0) # Always 0
|
|
128
|
+
self.request = kwargs.pop("request", None)
|
|
129
|
+
|
|
130
|
+
super().__init__(*args, **kwargs)
|
|
131
|
+
|
|
132
|
+
# Get the model name
|
|
133
|
+
model_name = config.orm_provider.get_model_name(self.Meta.model)
|
|
134
|
+
pk_field = self.Meta.model._meta.pk.name
|
|
135
|
+
|
|
136
|
+
# Use the extracted function to get the allowed fields
|
|
137
|
+
allowed_fields = extract_fields(model_name=model_name)
|
|
138
|
+
|
|
139
|
+
# Allowed fields must exist
|
|
140
|
+
allowed_fields = allowed_fields or set()
|
|
141
|
+
|
|
142
|
+
# Always include the primary key and the 'repr' field
|
|
143
|
+
allowed_fields.add(pk_field)
|
|
144
|
+
allowed_fields.add("repr")
|
|
145
|
+
|
|
146
|
+
# Filter the fields based on the result
|
|
147
|
+
if allowed_fields:
|
|
148
|
+
self.fields = {
|
|
149
|
+
name: field for name, field in self.fields.items()
|
|
150
|
+
if name in allowed_fields
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def get_repr(self, obj):
|
|
154
|
+
"""
|
|
155
|
+
Returns a standard Repr of the model displayed in the model summary
|
|
156
|
+
"""
|
|
157
|
+
img_repr = obj.__img__() if hasattr(obj, "__img__") else None
|
|
158
|
+
str_repr = str(obj)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"str": str_repr,
|
|
162
|
+
"img": img_repr
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def create(self, validated_data):
|
|
166
|
+
"""
|
|
167
|
+
Override create method to handle nested relationships.
|
|
168
|
+
Specifically extracts M2M relationships to set after instance creation.
|
|
169
|
+
"""
|
|
170
|
+
many_to_many = {}
|
|
171
|
+
for field_name, field in self.fields.items():
|
|
172
|
+
if field_name in validated_data and isinstance(field, serializers.ListSerializer):
|
|
173
|
+
many_to_many[field_name] = validated_data.pop(field_name)
|
|
174
|
+
|
|
175
|
+
# Create the instance with the remaining data
|
|
176
|
+
instance = super().create(validated_data)
|
|
177
|
+
|
|
178
|
+
# Set many-to-many relationships after instance creation
|
|
179
|
+
for field_name, value in many_to_many.items():
|
|
180
|
+
field = getattr(instance, field_name)
|
|
181
|
+
field.set(value)
|
|
182
|
+
|
|
183
|
+
return instance
|
|
184
|
+
|
|
185
|
+
def update(self, instance, validated_data):
|
|
186
|
+
"""
|
|
187
|
+
Override update method to handle nested relationships.
|
|
188
|
+
"""
|
|
189
|
+
many_to_many = {}
|
|
190
|
+
for field_name, field in self.fields.items():
|
|
191
|
+
if field_name in validated_data and isinstance(field, serializers.ListSerializer):
|
|
192
|
+
many_to_many[field_name] = validated_data.pop(field_name)
|
|
193
|
+
|
|
194
|
+
# Update the instance with the remaining data
|
|
195
|
+
instance = super().update(instance, validated_data)
|
|
196
|
+
|
|
197
|
+
# Update many-to-many relationships
|
|
198
|
+
for field_name, value in many_to_many.items():
|
|
199
|
+
field = getattr(instance, field_name)
|
|
200
|
+
field.set(value)
|
|
201
|
+
|
|
202
|
+
return instance
|
|
203
|
+
|
|
204
|
+
class Meta:
|
|
205
|
+
model = None # To be set dynamically.
|
|
206
|
+
fields = "__all__"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _setup_relation_fields(cls, serializer_class, model, allowed_fields):
|
|
211
|
+
"""Configure relation fields to use PrimaryKeyRelatedField."""
|
|
212
|
+
allowed_fields = allowed_fields or set()
|
|
213
|
+
|
|
214
|
+
for field in model._meta.get_fields():
|
|
215
|
+
# Skip fields that won't be presented
|
|
216
|
+
if field.name not in allowed_fields:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if getattr(field, "auto_created", False) and not field.concrete:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if field.is_relation:
|
|
223
|
+
queryset = field.related_model.objects.all()
|
|
224
|
+
serializer_class._declared_fields[field.name] = FlexiblePrimaryKeyRelatedField(
|
|
225
|
+
queryset=queryset,
|
|
226
|
+
required=not (field.null or field.blank),
|
|
227
|
+
allow_null=field.null,
|
|
228
|
+
many= field.many_to_many or field.one_to_many
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return serializer_class
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _setup_custom_serializers(cls, serializer_class, model, allowed_fields):
|
|
235
|
+
"""Configure custom serializers for non-relation fields."""
|
|
236
|
+
allowed_fields = allowed_fields or set()
|
|
237
|
+
|
|
238
|
+
for field in model._meta.get_fields():
|
|
239
|
+
# Skip fields that won't be presented
|
|
240
|
+
if field.name not in allowed_fields:
|
|
241
|
+
continue
|
|
242
|
+
if getattr(field, "auto_created", False) and not field.concrete:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if not field.is_relation:
|
|
246
|
+
custom_field_serializer = get_custom_serializer(field.__class__)
|
|
247
|
+
if custom_field_serializer:
|
|
248
|
+
serializer_class.serializer_field_mapping[field.__class__] = custom_field_serializer
|
|
249
|
+
return serializer_class
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def _setup_computed_fields(cls, serializer_class, model, allowed_fields):
|
|
253
|
+
"""Set up additional computed fields from the model registry."""
|
|
254
|
+
try:
|
|
255
|
+
model_config = registry.get_config(model)
|
|
256
|
+
except ValueError:
|
|
257
|
+
return serializer_class # No model config, return unchanged
|
|
258
|
+
|
|
259
|
+
mapping = serializers.ModelSerializer.serializer_field_mapping
|
|
260
|
+
|
|
261
|
+
for additional_field in model_config.additional_fields:
|
|
262
|
+
if additional_field.name not in allowed_fields:
|
|
263
|
+
continue
|
|
264
|
+
drf_field_class = mapping.get(type(additional_field.field))
|
|
265
|
+
if not drf_field_class:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
field_kwargs = {"read_only": True}
|
|
269
|
+
if additional_field.title:
|
|
270
|
+
field_kwargs["label"] = additional_field.title
|
|
271
|
+
|
|
272
|
+
# Pass along required attributes based on field type.
|
|
273
|
+
if isinstance(additional_field.field, models.DecimalField):
|
|
274
|
+
field_kwargs["max_digits"] = additional_field.field.max_digits
|
|
275
|
+
field_kwargs["decimal_places"] = additional_field.field.decimal_places
|
|
276
|
+
elif isinstance(additional_field.field, models.CharField):
|
|
277
|
+
field_kwargs["max_length"] = additional_field.field.max_length
|
|
278
|
+
|
|
279
|
+
# Instantiate the serializer field.
|
|
280
|
+
serializer_field = drf_field_class(**field_kwargs)
|
|
281
|
+
serializer_field.source = additional_field.name
|
|
282
|
+
serializer_class._declared_fields[additional_field.name] = serializer_field
|
|
283
|
+
|
|
284
|
+
return serializer_class
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def for_model(cls, model: Type[models.Model]):
|
|
288
|
+
"""
|
|
289
|
+
Create a DynamicModelSerializer class for the given model.
|
|
290
|
+
This configures all serialization behavior including:
|
|
291
|
+
- Setting up the Meta class
|
|
292
|
+
- Configuring list serialization
|
|
293
|
+
- Setting up relation fields
|
|
294
|
+
- Registering custom serializers
|
|
295
|
+
- Adding computed fields from the registry
|
|
296
|
+
"""
|
|
297
|
+
pk_field = model._meta.pk.name
|
|
298
|
+
|
|
299
|
+
# Dynamically create a Meta inner class
|
|
300
|
+
Meta = type("Meta", (), {
|
|
301
|
+
"model": model,
|
|
302
|
+
"fields": "__all__",
|
|
303
|
+
"read_only_fields": (pk_field,)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
# Create the serializer class
|
|
307
|
+
serializer_class = type(
|
|
308
|
+
f"Dynamic{model.__name__}Serializer",
|
|
309
|
+
(cls,),
|
|
310
|
+
{"Meta": Meta}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Get allowed fields for this model
|
|
314
|
+
model_name = config.orm_provider.get_model_name(model)
|
|
315
|
+
allowed_fields = extract_fields(model_name)
|
|
316
|
+
|
|
317
|
+
# Only proceed with field setup if we have allowed fields
|
|
318
|
+
if allowed_fields:
|
|
319
|
+
# Register custom serializers for model fields
|
|
320
|
+
serializer_class = cls._setup_custom_serializers(
|
|
321
|
+
serializer_class, model, allowed_fields
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Add computed fields from the registry
|
|
325
|
+
serializer_class = cls._setup_computed_fields(serializer_class, model, allowed_fields)
|
|
326
|
+
# Add relationship fields
|
|
327
|
+
serializer_class = cls._setup_relation_fields(serializer_class, model, allowed_fields)
|
|
328
|
+
|
|
329
|
+
return serializer_class
|
|
330
|
+
|
|
331
|
+
class DRFDynamicSerializer(AbstractDataSerializer):
|
|
332
|
+
"""
|
|
333
|
+
Uses collect_from_queryset to gather model instances
|
|
334
|
+
and applies DynamicModelSerializer for each group of models.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def _optimize_queryset(self, data, model, fields_map):
|
|
338
|
+
if config.query_optimizer is None:
|
|
339
|
+
return data
|
|
340
|
+
if isinstance(data, models.QuerySet) or isinstance(data, model):
|
|
341
|
+
try:
|
|
342
|
+
query_optimizer: Type[AbstractQueryOptimizer] = config.query_optimizer(
|
|
343
|
+
depth=0, # Always use depth 0 since we're collecting models explicitly
|
|
344
|
+
fields_per_model=fields_map,
|
|
345
|
+
get_model_name_func=config.orm_provider.get_model_name,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if "requested-fields::" in fields_map:
|
|
349
|
+
requested_fields = fields_map["requested-fields::"]
|
|
350
|
+
data = query_optimizer.optimize(
|
|
351
|
+
queryset=data,
|
|
352
|
+
fields=requested_fields
|
|
353
|
+
)
|
|
354
|
+
logger.debug(f"Query optimized for {model.__name__} with fields: {requested_fields}")
|
|
355
|
+
else:
|
|
356
|
+
data = query_optimizer.optimize(
|
|
357
|
+
queryset=data
|
|
358
|
+
)
|
|
359
|
+
logger.debug(f"Query optimized for {model.__name__} with no explicit field selection")
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Error optimizing query for {model.__name__}: {e}")
|
|
362
|
+
|
|
363
|
+
return data
|
|
364
|
+
|
|
365
|
+
def serialize(
|
|
366
|
+
self,
|
|
367
|
+
data: Any,
|
|
368
|
+
model: Type[models.Model],
|
|
369
|
+
depth: int, # Parameter kept for API compatibility, but no longer used
|
|
370
|
+
fields_map: Optional[Dict[str, Set[str]]],
|
|
371
|
+
many: bool = False,
|
|
372
|
+
request: Optional[RequestType] = None
|
|
373
|
+
) -> Any:
|
|
374
|
+
"""
|
|
375
|
+
Serializes data using collect_from_queryset and applies DynamicModelSerializer
|
|
376
|
+
for each group of models.
|
|
377
|
+
|
|
378
|
+
Returns a format of:
|
|
379
|
+
{
|
|
380
|
+
"data": [pks], # list of primary keys for top-level models
|
|
381
|
+
"included": {
|
|
382
|
+
"modelName": [objects], # full serialized objects per model type
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
"""
|
|
386
|
+
# Validate fields_map
|
|
387
|
+
assert fields_map is not None, "fields_map is required and cannot be None"
|
|
388
|
+
|
|
389
|
+
# Handle None data
|
|
390
|
+
if data is None:
|
|
391
|
+
return {
|
|
392
|
+
"data": [],
|
|
393
|
+
"included": {},
|
|
394
|
+
"model_name": None
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Apply query optimization
|
|
398
|
+
data = self._optimize_queryset(data, model, fields_map)
|
|
399
|
+
|
|
400
|
+
# Use the fields_map context for all operations
|
|
401
|
+
with fields_map_context(fields_map):
|
|
402
|
+
# Collect all model instances based on the fields_map
|
|
403
|
+
collected_models = collect_from_queryset(
|
|
404
|
+
data=data,
|
|
405
|
+
fields_map=fields_map,
|
|
406
|
+
get_model_name=config.orm_provider.get_model_name,
|
|
407
|
+
get_model=config.orm_provider.get_model_by_name
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Extract primary keys for the top-level model
|
|
411
|
+
model_name = config.orm_provider.get_model_name(model)
|
|
412
|
+
pk_field = model._meta.pk.name
|
|
413
|
+
top_level_instances = []
|
|
414
|
+
|
|
415
|
+
# Initialize the response structure
|
|
416
|
+
result = {
|
|
417
|
+
"data": [],
|
|
418
|
+
"included": {},
|
|
419
|
+
"model_name": model_name
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# For QuerySets, gather all instances
|
|
423
|
+
if isinstance(data, models.QuerySet):
|
|
424
|
+
top_level_instances = list(data)
|
|
425
|
+
# For single instance
|
|
426
|
+
elif isinstance(data, model):
|
|
427
|
+
top_level_instances = [data]
|
|
428
|
+
# For many=True with a list of instances
|
|
429
|
+
elif many and isinstance(data, list):
|
|
430
|
+
top_level_instances = [item for item in data if isinstance(item, model)]
|
|
431
|
+
|
|
432
|
+
# Extract primary keys for top-level instances
|
|
433
|
+
result["data"] = [getattr(instance, pk_field) for instance in top_level_instances]
|
|
434
|
+
|
|
435
|
+
# Apply zen-queries protection if configured
|
|
436
|
+
query_protection = getattr(settings, 'ZEN_STRICT_SERIALIZATION', False)
|
|
437
|
+
|
|
438
|
+
# Serialize each group of models
|
|
439
|
+
for model_type, instances in collected_models.items():
|
|
440
|
+
# Skip empty collections
|
|
441
|
+
if not instances:
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
# Get the model class for this type
|
|
446
|
+
model_class = config.orm_provider.get_model_by_name(model_type)
|
|
447
|
+
|
|
448
|
+
# Create a serializer for this model type
|
|
449
|
+
serializer_class = DynamicModelSerializer.for_model(model_class)
|
|
450
|
+
|
|
451
|
+
# Apply zen-queries protection if configured
|
|
452
|
+
if query_protection:
|
|
453
|
+
with queries_disabled():
|
|
454
|
+
# This will raise an exception if any query is executed
|
|
455
|
+
serialized_data = serializer_class(instances, many=True).data
|
|
456
|
+
else:
|
|
457
|
+
# Original code path without zen-queries
|
|
458
|
+
serialized_data = serializer_class(instances, many=True).data
|
|
459
|
+
|
|
460
|
+
pk_field_name = model_class._meta.pk.name
|
|
461
|
+
# [{pk: 1, ...}, {pk: 2, ...}] -> {1: {...}, 2: {...}}
|
|
462
|
+
# Create a dictionary indexed by primary key for easy lookup in the frontend
|
|
463
|
+
pk_indexed_data = dict(zip(pluck(pk_field_name, serialized_data), serialized_data))
|
|
464
|
+
|
|
465
|
+
# Add the serialized data to the result
|
|
466
|
+
result["included"][model_type] = pk_indexed_data
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error(f"Error serializing {model_type}: {e}")
|
|
470
|
+
# Include an empty list for this model type to maintain the expected structure
|
|
471
|
+
result["included"][model_type] = {}
|
|
472
|
+
|
|
473
|
+
return result
|
|
474
|
+
|
|
475
|
+
def deserialize(
|
|
476
|
+
self,
|
|
477
|
+
model: Type[models.Model],
|
|
478
|
+
data: Dict[str, Any],
|
|
479
|
+
fields_map: Optional[Dict[str, Set[str]]],
|
|
480
|
+
partial: bool = False,
|
|
481
|
+
request: Optional[RequestType] = None,
|
|
482
|
+
) -> Dict[str, Any]:
|
|
483
|
+
# Serious security issue if fields_map is None
|
|
484
|
+
assert fields_map is not None, "fields_map is required and cannot be None"
|
|
485
|
+
|
|
486
|
+
# Use the context manager for the duration of deserialization
|
|
487
|
+
with fields_map_context(fields_map):
|
|
488
|
+
# Create serializer class
|
|
489
|
+
serializer_class = DynamicModelSerializer.for_model(model)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
model_config = registry.get_config(model)
|
|
493
|
+
if model_config.pre_hooks:
|
|
494
|
+
for hook in model_config.pre_hooks:
|
|
495
|
+
data = hook(data, request=request)
|
|
496
|
+
except ValueError:
|
|
497
|
+
# No model config available
|
|
498
|
+
model_config = None
|
|
499
|
+
|
|
500
|
+
# Create serializer
|
|
501
|
+
serializer = serializer_class(
|
|
502
|
+
data=data,
|
|
503
|
+
partial=partial,
|
|
504
|
+
request=request
|
|
505
|
+
)
|
|
506
|
+
serializer.is_valid(raise_exception=True)
|
|
507
|
+
validated_data = serializer.validated_data
|
|
508
|
+
|
|
509
|
+
if model_config and model_config.post_hooks:
|
|
510
|
+
for hook in model_config.post_hooks:
|
|
511
|
+
validated_data = hook(validated_data, request=request)
|
|
512
|
+
|
|
513
|
+
return validated_data
|
|
514
|
+
|
|
515
|
+
def save(
|
|
516
|
+
self,
|
|
517
|
+
model: Type[models.Model],
|
|
518
|
+
data: Dict[str, Any],
|
|
519
|
+
fields_map: Optional[Dict[str, Set[str]]],
|
|
520
|
+
instance: Optional[Any] = None,
|
|
521
|
+
partial: bool = True,
|
|
522
|
+
request: Optional[RequestType] = None
|
|
523
|
+
) -> Any:
|
|
524
|
+
"""
|
|
525
|
+
Save data to create a new instance or update an existing one.
|
|
526
|
+
"""
|
|
527
|
+
# Serious security issue if fields_map is None
|
|
528
|
+
assert fields_map is not None, "fields_map is required and cannot be None"
|
|
529
|
+
|
|
530
|
+
# Get all fields using the ORM provider
|
|
531
|
+
all_fields = config.orm_provider.get_fields(model)
|
|
532
|
+
model_name = config.orm_provider.get_model_name(model)
|
|
533
|
+
|
|
534
|
+
# Create an unrestricted fields map
|
|
535
|
+
unrestricted_fields_map = {model_name: all_fields}
|
|
536
|
+
|
|
537
|
+
# Use the context manager with the unrestricted fields map
|
|
538
|
+
with fields_map_context(unrestricted_fields_map):
|
|
539
|
+
# Create serializer class
|
|
540
|
+
serializer_class = DynamicModelSerializer.for_model(model)
|
|
541
|
+
|
|
542
|
+
# Create serializer
|
|
543
|
+
serializer = serializer_class(
|
|
544
|
+
instance=instance, # Will be None for creation
|
|
545
|
+
data=data,
|
|
546
|
+
partial=partial if instance else False, # partial only makes sense for updates
|
|
547
|
+
request=request
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Validate the data
|
|
551
|
+
serializer.is_valid(raise_exception=True)
|
|
552
|
+
|
|
553
|
+
# Save and return the instance
|
|
554
|
+
return serializer.save()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from .views import EventsAuthView, ModelListView, ModelView, SchemaView, FileUploadView, FastUploadView
|
|
4
|
+
|
|
5
|
+
app_name = "statezero"
|
|
6
|
+
|
|
7
|
+
urlpatterns = [
|
|
8
|
+
path("events/auth/", EventsAuthView.as_view(), name="events_auth"),
|
|
9
|
+
path("models/", ModelListView.as_view(), name="model_list"),
|
|
10
|
+
path("files/upload/", FileUploadView.as_view(), name="file_upload"),
|
|
11
|
+
path("files/fast-upload/", FastUploadView.as_view(), name="fast_file_upload"),
|
|
12
|
+
path("<str:model_name>/", ModelView.as_view(), name="model_view"),
|
|
13
|
+
path("<str:model_name>/get-schema/", SchemaView.as_view(), name="schema_view")
|
|
14
|
+
]
|