statezero 0.1.0b3__tar.gz → 0.1.0b4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {statezero-0.1.0b3 → statezero-0.1.0b4}/PKG-INFO +1 -1
  2. {statezero-0.1.0b3 → statezero-0.1.0b4}/pyproject.toml +1 -1
  3. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/serializers.py +45 -26
  4. statezero-0.1.0b4/statezero/core/hook_checks.py +86 -0
  5. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero.egg-info/PKG-INFO +1 -1
  6. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero.egg-info/SOURCES.txt +1 -0
  7. {statezero-0.1.0b3 → statezero-0.1.0b4}/README.md +0 -0
  8. {statezero-0.1.0b3 → statezero-0.1.0b4}/license.md +0 -0
  9. {statezero-0.1.0b3 → statezero-0.1.0b4}/requirements.txt +0 -0
  10. {statezero-0.1.0b3 → statezero-0.1.0b4}/setup.cfg +0 -0
  11. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/__init__.py +0 -0
  12. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/__init__.py +0 -0
  13. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/__init__.py +0 -0
  14. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/apps.py +0 -0
  15. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/config.py +0 -0
  16. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/context_manager.py +0 -0
  17. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/event_emitters.py +0 -0
  18. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/exception_handler.py +0 -0
  19. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/extensions/__init__.py +0 -0
  20. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  21. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +0 -0
  22. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +0 -0
  23. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/f_handler.py +0 -0
  24. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/helpers.py +0 -0
  25. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/middleware.py +0 -0
  26. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/migrations/0001_initial.py +0 -0
  27. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +0 -0
  28. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/migrations/__init__.py +0 -0
  29. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/orm.py +0 -0
  30. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/permissions.py +0 -0
  31. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/query_optimizer.py +0 -0
  32. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/schemas.py +0 -0
  33. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/search_providers/__init__.py +0 -0
  34. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/search_providers/basic_search.py +0 -0
  35. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/search_providers/postgres_search.py +0 -0
  36. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/urls.py +0 -0
  37. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/adaptors/django/views.py +0 -0
  38. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/__init__.py +0 -0
  39. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/ast_parser.py +0 -0
  40. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/ast_validator.py +0 -0
  41. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/classes.py +0 -0
  42. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/config.py +0 -0
  43. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/context_storage.py +0 -0
  44. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/event_bus.py +0 -0
  45. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/event_emitters.py +0 -0
  46. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/exceptions.py +0 -0
  47. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/interfaces.py +0 -0
  48. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/process_request.py +0 -0
  49. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero/core/types.py +0 -0
  50. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero.egg-info/dependency_links.txt +0 -0
  51. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero.egg-info/requires.txt +0 -0
  52. {statezero-0.1.0b3 → statezero-0.1.0b4}/statezero.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statezero
3
- Version: 0.1.0b3
3
+ Version: 0.1.0b4
4
4
  Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
5
5
  Author-email: Robert <robert.herring@statezero.dev>
6
6
  Project-URL: homepage, https://www.statezero.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "statezero"
7
- version = "0.1.0b3"
7
+ version = "0.1.0b4"
8
8
  description = "Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -13,6 +13,7 @@ from statezero.adaptors.django.config import config, registry
13
13
  from statezero.core.interfaces import AbstractDataSerializer, AbstractQueryOptimizer
14
14
  from statezero.core.types import RequestType
15
15
  from statezero.adaptors.django.helpers import collect_from_queryset
16
+ from statezero.core.hook_checks import _check_pre_hook_result, _check_post_hook_result
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
@@ -82,7 +83,7 @@ class FlexiblePrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
82
83
 
83
84
  # Otherwise, use the standard to_internal_value
84
85
  return super().to_internal_value(data)
85
-
86
+
86
87
  class FExpressionMixin:
87
88
  """
88
89
  A mixin that can handle F expression objects in serializer write operations.
@@ -344,7 +345,7 @@ class DRFDynamicSerializer(AbstractDataSerializer):
344
345
  fields_per_model=fields_map,
345
346
  get_model_name_func=config.orm_provider.get_model_name,
346
347
  )
347
-
348
+
348
349
  if "requested-fields::" in fields_map:
349
350
  requested_fields = fields_map["requested-fields::"]
350
351
  data = query_optimizer.optimize(
@@ -359,7 +360,7 @@ class DRFDynamicSerializer(AbstractDataSerializer):
359
360
  logger.debug(f"Query optimized for {model.__name__} with no explicit field selection")
360
361
  except Exception as e:
361
362
  logger.error(f"Error optimizing query for {model.__name__}: {e}")
362
-
363
+
363
364
  return data
364
365
 
365
366
  def serialize(
@@ -385,7 +386,7 @@ class DRFDynamicSerializer(AbstractDataSerializer):
385
386
  """
386
387
  # Validate fields_map
387
388
  assert fields_map is not None, "fields_map is required and cannot be None"
388
-
389
+
389
390
  # Handle None data
390
391
  if data is None:
391
392
  return {
@@ -393,10 +394,10 @@ class DRFDynamicSerializer(AbstractDataSerializer):
393
394
  "included": {},
394
395
  "model_name": None
395
396
  }
396
-
397
+
397
398
  # Apply query optimization
398
399
  data = self._optimize_queryset(data, model, fields_map)
399
-
400
+
400
401
  # Use the fields_map context for all operations
401
402
  with fields_map_context(fields_map):
402
403
  # Collect all model instances based on the fields_map
@@ -406,7 +407,7 @@ class DRFDynamicSerializer(AbstractDataSerializer):
406
407
  get_model_name=config.orm_provider.get_model_name,
407
408
  get_model=config.orm_provider.get_model_by_name
408
409
  )
409
-
410
+
410
411
  # Extract primary keys for the top-level model
411
412
  model_name = config.orm_provider.get_model_name(model)
412
413
  pk_field = model._meta.pk.name
@@ -418,7 +419,7 @@ class DRFDynamicSerializer(AbstractDataSerializer):
418
419
  "included": {},
419
420
  "model_name": model_name
420
421
  }
421
-
422
+
422
423
  # For QuerySets, gather all instances
423
424
  if isinstance(data, models.QuerySet):
424
425
  top_level_instances = list(data)
@@ -428,26 +429,26 @@ class DRFDynamicSerializer(AbstractDataSerializer):
428
429
  # For many=True with a list of instances
429
430
  elif many and isinstance(data, list):
430
431
  top_level_instances = [item for item in data if isinstance(item, model)]
431
-
432
+
432
433
  # Extract primary keys for top-level instances
433
434
  result["data"] = [getattr(instance, pk_field) for instance in top_level_instances]
434
-
435
+
435
436
  # Apply zen-queries protection if configured
436
437
  query_protection = getattr(settings, 'ZEN_STRICT_SERIALIZATION', False)
437
-
438
+
438
439
  # Serialize each group of models
439
440
  for model_type, instances in collected_models.items():
440
441
  # Skip empty collections
441
442
  if not instances:
442
443
  continue
443
-
444
+
444
445
  try:
445
446
  # Get the model class for this type
446
447
  model_class = config.orm_provider.get_model_by_name(model_type)
447
-
448
+
448
449
  # Create a serializer for this model type
449
450
  serializer_class = DynamicModelSerializer.for_model(model_class)
450
-
451
+
451
452
  # Apply zen-queries protection if configured
452
453
  if query_protection:
453
454
  with queries_disabled():
@@ -461,15 +462,15 @@ class DRFDynamicSerializer(AbstractDataSerializer):
461
462
  # [{pk: 1, ...}, {pk: 2, ...}] -> {1: {...}, 2: {...}}
462
463
  # Create a dictionary indexed by primary key for easy lookup in the frontend
463
464
  pk_indexed_data = dict(zip(pluck(pk_field_name, serialized_data), serialized_data))
464
-
465
+
465
466
  # Add the serialized data to the result
466
467
  result["included"][model_type] = pk_indexed_data
467
-
468
+
468
469
  except Exception as e:
469
470
  logger.error(f"Error serializing {model_type}: {e}")
470
471
  # Include an empty list for this model type to maintain the expected structure
471
472
  result["included"][model_type] = {}
472
-
473
+
473
474
  return result
474
475
 
475
476
  def deserialize(
@@ -487,12 +488,22 @@ class DRFDynamicSerializer(AbstractDataSerializer):
487
488
  with fields_map_context(fields_map):
488
489
  # Create serializer class
489
490
  serializer_class = DynamicModelSerializer.for_model(model)
491
+ available_fields = set(serializer_class().fields.keys())
490
492
 
491
493
  try:
492
494
  model_config = registry.get_config(model)
493
495
  if model_config.pre_hooks:
494
496
  for hook in model_config.pre_hooks:
495
- data = hook(data, request=request)
497
+ hook_result = hook(data, request=request)
498
+ if settings.DEBUG:
499
+ data = _check_pre_hook_result(
500
+ original_data=data,
501
+ result_data=hook_result,
502
+ model=model,
503
+ serializer_fields=available_fields
504
+ )
505
+ else:
506
+ data = hook_result or data
496
507
  except ValueError:
497
508
  # No model config available
498
509
  model_config = None
@@ -508,10 +519,18 @@ class DRFDynamicSerializer(AbstractDataSerializer):
508
519
 
509
520
  if model_config and model_config.post_hooks:
510
521
  for hook in model_config.post_hooks:
511
- validated_data = hook(validated_data, request=request)
512
-
522
+ hook_result = hook(validated_data, request=request)
523
+ if settings.DEBUG:
524
+ validated_data = _check_post_hook_result(
525
+ original_data=validated_data,
526
+ result_data=hook_result,
527
+ model=model
528
+ )
529
+ else:
530
+ validated_data = hook_result or validated_data
531
+
513
532
  return validated_data
514
-
533
+
515
534
  def save(
516
535
  self,
517
536
  model: Type[models.Model],
@@ -533,12 +552,12 @@ class DRFDynamicSerializer(AbstractDataSerializer):
533
552
 
534
553
  # Create an unrestricted fields map
535
554
  unrestricted_fields_map = {model_name: all_fields}
536
-
555
+
537
556
  # Use the context manager with the unrestricted fields map
538
557
  with fields_map_context(unrestricted_fields_map):
539
558
  # Create serializer class
540
559
  serializer_class = DynamicModelSerializer.for_model(model)
541
-
560
+
542
561
  # Create serializer
543
562
  serializer = serializer_class(
544
563
  instance=instance, # Will be None for creation
@@ -546,9 +565,9 @@ class DRFDynamicSerializer(AbstractDataSerializer):
546
565
  partial=partial if instance else False, # partial only makes sense for updates
547
566
  request=request
548
567
  )
549
-
568
+
550
569
  # Validate the data
551
570
  serializer.is_valid(raise_exception=True)
552
-
571
+
553
572
  # Save and return the instance
554
- return serializer.save()
573
+ return serializer.save()
@@ -0,0 +1,86 @@
1
+ import warnings
2
+ from django.conf import settings
3
+ from typing import Any, Dict, Set, Type
4
+
5
+ def _check_pre_hook_result(
6
+ original_data: Dict, result_data: Any, model: Type, serializer_fields: Set[str]
7
+ ):
8
+ """Check pre-hook result and warn about common issues in DEBUG mode only."""
9
+ if not getattr(settings, "DEBUG", False):
10
+ return result_data or original_data
11
+
12
+ model_name = model.__name__
13
+
14
+ # Warning 1: Hook returned None
15
+ if result_data is None:
16
+ warnings.warn(
17
+ f"Pre-hook for {model_name} returned None (should return dict). HINT: If you want to skip changes, return the original data.",
18
+ stacklevel=5,
19
+ )
20
+ return original_data
21
+
22
+ if not isinstance(result_data, dict):
23
+ warnings.warn(
24
+ f"Pre-hook for {model_name} returned {type(result_data).__name__} (should return dict). HINT: If you want to skip changes, return the original data.",
25
+ stacklevel=5,
26
+ )
27
+ return original_data
28
+
29
+ # Warning 2: Added fields not in serializer
30
+ added_keys = set(result_data.keys()) - set(original_data.keys())
31
+ missing_fields = added_keys - serializer_fields
32
+ if missing_fields:
33
+ warnings.warn(
34
+ f"Pre-hook for {model_name} added unavailable fields {missing_fields}. HINT: Add the field to permission.editable_fields() or use post-hook.",
35
+ stacklevel=5,
36
+ )
37
+
38
+ # Warning 3: Removed fields that were in original data
39
+ removed_keys = set(original_data.keys()) - set(result_data.keys())
40
+ if removed_keys:
41
+ warnings.warn(
42
+ f"Pre-hook for {model_name} removed fields {removed_keys} that were in original data. This might be intentional, or it could be caused by a hook not returning the full input data.",
43
+ stacklevel=5,
44
+ )
45
+
46
+ return result_data
47
+
48
+ def _check_post_hook_result(original_data: Dict, result_data: Any, model: Type):
49
+ """Check post-hook result and warn about common issues in DEBUG mode only."""
50
+ if not getattr(settings, "DEBUG", False):
51
+ return result_data or original_data
52
+
53
+ model_name = model.__name__
54
+
55
+ # Warning 1: Hook returned None
56
+ if result_data is None:
57
+ warnings.warn(
58
+ f"Post-hook for {model_name} returned None (should return dict). HINT: Return the validated_data dict.",
59
+ stacklevel=5,
60
+ )
61
+ return original_data
62
+
63
+ if not isinstance(result_data, dict):
64
+ warnings.warn(
65
+ f"Post-hook for {model_name} returned {type(result_data).__name__} (should return dict). HINT: Return the validated_data dict.",
66
+ stacklevel=5,
67
+ )
68
+ return original_data
69
+
70
+ # Warning 2: Removed validated fields (more serious than pre-hook)
71
+ removed_keys = set(original_data.keys()) - set(result_data.keys())
72
+ if removed_keys:
73
+ warnings.warn(
74
+ f"Post-hook for {model_name} removed validated fields {removed_keys}. These fields won't be saved.",
75
+ stacklevel=5,
76
+ )
77
+
78
+ # Warning 3: Added fields that weren't validated
79
+ added_keys = set(result_data.keys()) - set(original_data.keys())
80
+ if added_keys:
81
+ warnings.warn(
82
+ f"Post-hook for {model_name} added unvalidated fields {added_keys}. These bypassed serializer validation.",
83
+ stacklevel=5,
84
+ )
85
+
86
+ return result_data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statezero
3
- Version: 0.1.0b3
3
+ Version: 0.1.0b4
4
4
  Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
5
5
  Author-email: Robert <robert.herring@statezero.dev>
6
6
  Project-URL: homepage, https://www.statezero.dev
@@ -44,6 +44,7 @@ statezero/core/context_storage.py
44
44
  statezero/core/event_bus.py
45
45
  statezero/core/event_emitters.py
46
46
  statezero/core/exceptions.py
47
+ statezero/core/hook_checks.py
47
48
  statezero/core/interfaces.py
48
49
  statezero/core/process_request.py
49
50
  statezero/core/types.py
File without changes
File without changes
File without changes