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.
Files changed (45) hide show
  1. statezero/__init__.py +0 -0
  2. statezero/adaptors/__init__.py +0 -0
  3. statezero/adaptors/django/__init__.py +0 -0
  4. statezero/adaptors/django/apps.py +97 -0
  5. statezero/adaptors/django/config.py +99 -0
  6. statezero/adaptors/django/context_manager.py +12 -0
  7. statezero/adaptors/django/event_emitters.py +78 -0
  8. statezero/adaptors/django/exception_handler.py +98 -0
  9. statezero/adaptors/django/extensions/__init__.py +0 -0
  10. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
  13. statezero/adaptors/django/f_handler.py +312 -0
  14. statezero/adaptors/django/helpers.py +153 -0
  15. statezero/adaptors/django/middleware.py +10 -0
  16. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  17. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  18. statezero/adaptors/django/migrations/__init__.py +0 -0
  19. statezero/adaptors/django/orm.py +915 -0
  20. statezero/adaptors/django/permissions.py +252 -0
  21. statezero/adaptors/django/query_optimizer.py +772 -0
  22. statezero/adaptors/django/schemas.py +324 -0
  23. statezero/adaptors/django/search_providers/__init__.py +0 -0
  24. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  25. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  26. statezero/adaptors/django/serializers.py +554 -0
  27. statezero/adaptors/django/urls.py +14 -0
  28. statezero/adaptors/django/views.py +336 -0
  29. statezero/core/__init__.py +34 -0
  30. statezero/core/ast_parser.py +821 -0
  31. statezero/core/ast_validator.py +266 -0
  32. statezero/core/classes.py +167 -0
  33. statezero/core/config.py +263 -0
  34. statezero/core/context_storage.py +4 -0
  35. statezero/core/event_bus.py +175 -0
  36. statezero/core/event_emitters.py +60 -0
  37. statezero/core/exceptions.py +106 -0
  38. statezero/core/interfaces.py +492 -0
  39. statezero/core/process_request.py +184 -0
  40. statezero/core/types.py +63 -0
  41. statezero-0.1.0b1.dist-info/METADATA +252 -0
  42. statezero-0.1.0b1.dist-info/RECORD +45 -0
  43. statezero-0.1.0b1.dist-info/WHEEL +5 -0
  44. statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
  45. 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
+ ]