statezero 0.1.0b11__tar.gz → 0.1.0b13__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.

Potentially problematic release.


This version of statezero might be problematic. Click here for more details.

Files changed (54) hide show
  1. {statezero-0.1.0b11 → statezero-0.1.0b13}/PKG-INFO +1 -1
  2. {statezero-0.1.0b11 → statezero-0.1.0b13}/pyproject.toml +1 -1
  3. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/actions.py +25 -0
  4. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +8 -0
  5. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/query_optimizer.py +40 -2
  6. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/schemas.py +23 -0
  7. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/actions.py +6 -2
  8. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/classes.py +57 -1
  9. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/config.py +4 -0
  10. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero.egg-info/PKG-INFO +1 -1
  11. {statezero-0.1.0b11 → statezero-0.1.0b13}/README.md +0 -0
  12. {statezero-0.1.0b11 → statezero-0.1.0b13}/license.md +0 -0
  13. {statezero-0.1.0b11 → statezero-0.1.0b13}/requirements.txt +0 -0
  14. {statezero-0.1.0b11 → statezero-0.1.0b13}/setup.cfg +0 -0
  15. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/__init__.py +0 -0
  16. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/__init__.py +0 -0
  17. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/__init__.py +0 -0
  18. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/apps.py +0 -0
  19. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/config.py +0 -0
  20. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/context_manager.py +0 -0
  21. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/event_emitters.py +0 -0
  22. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/exception_handler.py +0 -0
  23. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/extensions/__init__.py +0 -0
  24. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  25. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +0 -0
  26. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/f_handler.py +0 -0
  27. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/helpers.py +0 -0
  28. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/middleware.py +0 -0
  29. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/migrations/0001_initial.py +0 -0
  30. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +0 -0
  31. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/migrations/__init__.py +0 -0
  32. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/orm.py +0 -0
  33. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/permissions.py +0 -0
  34. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/search_providers/__init__.py +0 -0
  35. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/search_providers/basic_search.py +0 -0
  36. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/search_providers/postgres_search.py +0 -0
  37. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/serializers.py +0 -0
  38. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/urls.py +0 -0
  39. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/adaptors/django/views.py +0 -0
  40. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/__init__.py +0 -0
  41. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/ast_parser.py +0 -0
  42. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/ast_validator.py +0 -0
  43. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/context_storage.py +0 -0
  44. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/event_bus.py +0 -0
  45. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/event_emitters.py +0 -0
  46. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/exceptions.py +0 -0
  47. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/hook_checks.py +0 -0
  48. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/interfaces.py +0 -0
  49. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/process_request.py +0 -0
  50. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero/core/types.py +0 -0
  51. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero.egg-info/SOURCES.txt +0 -0
  52. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero.egg-info/dependency_links.txt +0 -0
  53. {statezero-0.1.0b11 → statezero-0.1.0b13}/statezero.egg-info/requires.txt +0 -0
  54. {statezero-0.1.0b11 → statezero-0.1.0b13}/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.0b11
3
+ Version: 0.1.0b13
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.0b11"
7
+ version = "0.1.0b13"
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" }
@@ -48,6 +48,13 @@ class DjangoActionSchemaGenerator:
48
48
  action_config["response_serializer"]
49
49
  )
50
50
 
51
+ # Serialize display metadata if present
52
+ display_data = None
53
+ if action_config.get("display"):
54
+ display_data = DjangoActionSchemaGenerator._serialize_display_metadata(
55
+ action_config["display"]
56
+ )
57
+
51
58
  schema_info = {
52
59
  "action_name": action_name,
53
60
  "app": app_name,
@@ -62,6 +69,7 @@ class DjangoActionSchemaGenerator:
62
69
  "permissions": [
63
70
  perm.__name__ for perm in action_config.get("permissions", [])
64
71
  ],
72
+ "display": display_data,
65
73
  }
66
74
  actions_schema[action_name] = schema_info
67
75
 
@@ -212,4 +220,21 @@ class DjangoActionSchemaGenerator:
212
220
  "class_name": model.__name__,
213
221
  "primary_key_field": model._meta.pk.name,
214
222
  }
223
+ return None
224
+
225
+ @staticmethod
226
+ def _serialize_display_metadata(display):
227
+ """Convert DisplayMetadata dataclass to dict for JSON serialization"""
228
+ from dataclasses import asdict, is_dataclass
229
+
230
+ if display is None:
231
+ return None
232
+
233
+ if is_dataclass(display):
234
+ return asdict(display)
235
+
236
+ # If it's already a dict, return as-is
237
+ if isinstance(display, dict):
238
+ return display
239
+
215
240
  return None
@@ -15,6 +15,14 @@ class MoneyFieldSerializer(serializers.Field):
15
15
  self.decimal_places = kwargs.pop("decimal_places", 2)
16
16
  super().__init__(**kwargs)
17
17
 
18
+ @classmethod
19
+ def get_prefetch_db_fields(cls, field_name: str):
20
+ """
21
+ Return all database fields required for this field to serialize.
22
+ MoneyField creates two database columns: field_name and field_name_currency.
23
+ """
24
+ return [field_name, f"{field_name}_currency"]
25
+
18
26
  def to_representation(self, value):
19
27
  djmoney_field = MoneyField(
20
28
  max_digits=self.max_digits, decimal_places=self.decimal_places
@@ -477,7 +477,34 @@ def optimize_query(queryset, fields=None, fields_map=None, depth=0, use_only=Tru
477
477
  related_fields_to_fetch = set()
478
478
 
479
479
  if fields_map and related_model_name in fields_map:
480
- related_fields_to_fetch.update(fields_map[related_model_name])
480
+ # Process each field, checking for custom serializers
481
+ from statezero.adaptors.django.serializers import get_custom_serializer
482
+ related_meta = _get_model_meta(related_model)
483
+ for field_name in fields_map[related_model_name]:
484
+ try:
485
+ field_obj = related_meta.get_field(field_name)
486
+ if not field_obj.is_relation:
487
+ # Check if this field has a custom serializer with explicit DB field requirements
488
+ custom_serializer = get_custom_serializer(field_obj.__class__)
489
+ if custom_serializer and hasattr(custom_serializer, 'get_prefetch_db_fields'):
490
+ # Use the explicit list from the custom serializer
491
+ db_fields = custom_serializer.get_prefetch_db_fields(field_name)
492
+ for db_field in db_fields:
493
+ related_fields_to_fetch.add(db_field)
494
+ logger.debug(f"Using custom DB fields {db_fields} for field '{field_name}' in {related_model_name}")
495
+ else:
496
+ # No custom serializer, just add the field itself
497
+ related_fields_to_fetch.add(field_name)
498
+ else:
499
+ # Relation field, add as-is
500
+ related_fields_to_fetch.add(field_name)
501
+ except FieldDoesNotExist:
502
+ # Field doesn't exist, add it anyway (might be computed)
503
+ related_fields_to_fetch.add(field_name)
504
+ except Exception as e:
505
+ logger.error(f"Error checking custom serializer for field '{field_name}' in {related_model_name}: {e}")
506
+ # On error, add the field anyway to be safe
507
+ related_fields_to_fetch.add(field_name)
481
508
  else:
482
509
  # If no field restrictions are provided, get all fields
483
510
  all_fields = [f.name for f in related_model._meta.get_fields() if f.concrete]
@@ -531,7 +558,18 @@ def optimize_query(queryset, fields=None, fields_map=None, depth=0, use_only=Tru
531
558
  try:
532
559
  field_obj = root_meta.get_field(field_name)
533
560
  if not field_obj.is_relation:
534
- root_fields_to_fetch.add(field_name)
561
+ # Check if this field has a custom serializer with explicit DB field requirements
562
+ from statezero.adaptors.django.serializers import get_custom_serializer
563
+ custom_serializer = get_custom_serializer(field_obj.__class__)
564
+ if custom_serializer and hasattr(custom_serializer, 'get_prefetch_db_fields'):
565
+ # Use the explicit list from the custom serializer
566
+ db_fields = custom_serializer.get_prefetch_db_fields(field_name)
567
+ for db_field in db_fields:
568
+ root_fields_to_fetch.add(db_field)
569
+ logger.debug(f"Using custom DB fields {db_fields} for field '{field_name}'")
570
+ else:
571
+ # No custom serializer, just add the field itself
572
+ root_fields_to_fetch.add(field_name)
535
573
  elif isinstance(field_obj, (ForeignKey, OneToOneField)):
536
574
  # If FK/O2O itself is requested directly, include its id field
537
575
  root_fields_to_fetch.add(field_obj.attname)
@@ -127,6 +127,11 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
127
127
  if hasattr(model._meta, "ordering") and model._meta.ordering:
128
128
  default_ordering = list(model._meta.ordering)
129
129
 
130
+ # Serialize display metadata if present
131
+ display_data = None
132
+ if model_config.display:
133
+ display_data = self._serialize_display_metadata(model_config.display)
134
+
130
135
  schema_meta = ModelSchemaMetadata(
131
136
  model_name=config.orm_provider.get_model_name(model),
132
137
  title=model._meta.verbose_name.title(),
@@ -153,6 +158,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
153
158
  time_format=getattr(settings, "REST_FRAMEWORK", {}).get(
154
159
  "TIME_FORMAT", "iso-8601"
155
160
  ),
161
+ display=display_data,
156
162
  )
157
163
  return schema_meta
158
164
 
@@ -322,3 +328,20 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
322
328
  if isinstance(target_field, (models.UUIDField, models.CharField)):
323
329
  return FieldType.STRING
324
330
  return FieldType.INTEGER
331
+
332
+ @staticmethod
333
+ def _serialize_display_metadata(display) -> Dict[str, Any]:
334
+ """Convert DisplayMetadata dataclass to dict for JSON serialization"""
335
+ from dataclasses import asdict, is_dataclass
336
+
337
+ if display is None:
338
+ return None
339
+
340
+ if is_dataclass(display):
341
+ return asdict(display)
342
+
343
+ # If it's already a dict, return as-is
344
+ if isinstance(display, dict):
345
+ return display
346
+
347
+ return None
@@ -20,8 +20,9 @@ class ActionRegistry:
20
20
  List[AbstractActionPermission], AbstractActionPermission, None
21
21
  ] = None,
22
22
  name: Optional[str] = None,
23
+ display: Optional[Any] = None,
23
24
  ):
24
- """Register an action function with an optional, explicit docstring."""
25
+ """Register an action function with an optional, explicit docstring and display metadata."""
25
26
 
26
27
  def decorator(func: Callable):
27
28
  action_name = name or func.__name__
@@ -47,6 +48,7 @@ class ActionRegistry:
47
48
  "name": action_name,
48
49
  "module": func.__module__,
49
50
  "docstring": final_docstring, # Store the determined docstring
51
+ "display": display, # Store display metadata
50
52
  }
51
53
  return func
52
54
 
@@ -76,8 +78,9 @@ def action(
76
78
  List[AbstractActionPermission], AbstractActionPermission, None
77
79
  ] = None,
78
80
  name: Optional[str] = None,
81
+ display: Optional[Any] = None,
79
82
  ):
80
- """Framework-agnostic decorator to register an action."""
83
+ """Framework-agnostic decorator to register an action with optional display metadata."""
81
84
  return action_registry.register(
82
85
  func,
83
86
  docstring=docstring,
@@ -85,4 +88,5 @@ def action(
85
88
  response_serializer=response_serializer,
86
89
  permissions=permissions,
87
90
  name=name,
91
+ display=display,
88
92
  )
@@ -131,12 +131,15 @@ class ModelSchemaMetadata(BaseModel):
131
131
  default_ordering: Optional[List[str]] = None
132
132
  # Extra definitions (for schemas referenced via $ref) are merged in if provided.
133
133
  definitions: Dict[str, Any] = field(default_factory=dict)
134
-
134
+
135
135
  # Date / time formatting templates
136
136
  datetime_format: Optional[str] = None
137
137
  date_format: Optional[str] = None
138
138
  time_format: Optional[str] = None
139
139
 
140
+ # Display customization
141
+ display: Optional[Dict[str, Any]] = None
142
+
140
143
  @dataclass
141
144
  class ModelSummaryRepresentation:
142
145
  pk: Any
@@ -165,3 +168,56 @@ class FieldNode:
165
168
  is_relation: bool
166
169
  related_model: Optional[str] = None # The object name of the related model, if any
167
170
  type: str = "field"
171
+
172
+
173
+ @dataclass
174
+ class FieldDisplayConfig:
175
+ """
176
+ Configuration for customizing how a field is displayed in the frontend.
177
+
178
+ Attributes:
179
+ field_name: The name of the field this config applies to
180
+ display_component: Custom UI component name (e.g., "AddressAutocomplete", "DatePicker")
181
+ filter_queryset: Filter options for select/multi-select fields (dict passed to backend)
182
+ display_help_text: Additional help text for the field
183
+ extra: Additional custom metadata for framework-specific or UI-specific extensions
184
+ """
185
+ field_name: str
186
+ display_component: Optional[str] = None
187
+ filter_queryset: Optional[Dict[str, Any]] = None
188
+ display_help_text: Optional[str] = None
189
+ extra: Optional[Dict[str, Any]] = None
190
+
191
+
192
+ @dataclass
193
+ class FieldGroup:
194
+ """
195
+ Group related fields together for better UX.
196
+
197
+ Attributes:
198
+ display_title: Group heading
199
+ display_description: Group description
200
+ field_names: List of field names in this group
201
+ """
202
+ display_title: str
203
+ display_description: Optional[str] = None
204
+ field_names: Optional[List[str]] = None
205
+
206
+
207
+ @dataclass
208
+ class DisplayMetadata:
209
+ """
210
+ Rich display information for models and actions to customize frontend rendering.
211
+
212
+ Attributes:
213
+ display_title: Main heading/title override
214
+ display_description: Explanatory text about the model/action
215
+ field_groups: Logical grouping of fields (e.g., "Contact Info", "Address Details")
216
+ field_display_configs: Per-field customization (custom components, filters, help text)
217
+ extra: Additional custom metadata for framework-specific or UI-specific extensions
218
+ """
219
+ display_title: Optional[str] = None
220
+ display_description: Optional[str] = None
221
+ field_groups: Optional[List[FieldGroup]] = None
222
+ field_display_configs: Optional[List[FieldDisplayConfig]] = None
223
+ extra: Optional[Dict[str, Any]] = None
@@ -162,6 +162,8 @@ class ModelConfig:
162
162
  Fields that can be used for ordering
163
163
  fields: Optional[Optional[Union[Set[str], Literal["__all__"]]]]
164
164
  Expose just a subset of the model fields
165
+ display: Optional[Any], optional
166
+ Display metadata for frontend customization (DisplayMetadata instance)
165
167
  DEBUG: bool, default=False
166
168
  Enable debug mode for this model
167
169
  """
@@ -179,6 +181,7 @@ class ModelConfig:
179
181
  searchable_fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
180
182
  ordering_fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
181
183
  fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
184
+ display: Optional[Any] = None,
182
185
  DEBUG: bool = False,
183
186
  ):
184
187
  self.model = model
@@ -192,6 +195,7 @@ class ModelConfig:
192
195
  self.searchable_fields = searchable_fields or set()
193
196
  self.ordering_fields = ordering_fields or set()
194
197
  self.fields = fields or "__all__"
198
+ self.display = display
195
199
  self.DEBUG = DEBUG or False
196
200
 
197
201
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statezero
3
- Version: 0.1.0b11
3
+ Version: 0.1.0b13
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
File without changes
File without changes
File without changes