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,312 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Set
3
+ from django.db.models import F, Value, ExpressionWrapper, IntegerField, FloatField
4
+ from django.db.models.expressions import Combinable
5
+ from django.db.models.functions import Abs, Round, Floor, Ceil, Greatest, Least
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class FExpressionHandler:
10
+ """
11
+ Handles F expressions from the frontend, converting math.js AST to Django ORM expressions.
12
+ This handler processes the JSON representation of math.js expressions.
13
+ Includes field extraction for permission validation.
14
+ """
15
+
16
+ # Define allowed functions and their mappings to Django functions
17
+ ALLOWED_FUNCTIONS = {
18
+ 'abs': Abs,
19
+ 'round': Round,
20
+ 'floor': Floor,
21
+ 'ceil': Ceil,
22
+ 'min': Least,
23
+ 'max': Greatest
24
+ }
25
+
26
+ @staticmethod
27
+ def process_expression(expr_obj: Dict[str, Any]) -> Any:
28
+ """
29
+ Process a structured F expression object into a Django ORM expression.
30
+
31
+ Args:
32
+ expr_obj: The F expression object from the frontend
33
+
34
+ Returns:
35
+ A Django ORM expression
36
+
37
+ Raises:
38
+ ValueError: If the expression is invalid
39
+ """
40
+ if not expr_obj or not isinstance(expr_obj, dict):
41
+ return expr_obj
42
+
43
+ # Check if it's an F expression
44
+ if expr_obj.get('__f_expr') and 'ast' in expr_obj:
45
+ try:
46
+ # Process the AST from math.js JSON representation
47
+ ast = expr_obj['ast']
48
+ return FExpressionHandler._process_mathjs_node(ast)
49
+ except Exception as e:
50
+ # Log the error for debugging
51
+ logger.error(f"Error processing F expression: {e}")
52
+ raise ValueError(f"Error in F expression: {e}")
53
+
54
+ return expr_obj
55
+
56
+ @staticmethod
57
+ def extract_referenced_fields(expr_obj: Dict[str, Any]) -> Set[str]:
58
+ """
59
+ Extract all field names referenced in an F expression.
60
+
61
+ Args:
62
+ expr_obj: The F expression object from the frontend
63
+
64
+ Returns:
65
+ Set[str]: Set of field names referenced in the expression
66
+ """
67
+ if not expr_obj or not isinstance(expr_obj, dict):
68
+ return set()
69
+
70
+ # Check if it's an F expression
71
+ if expr_obj.get('__f_expr') and 'ast' in expr_obj:
72
+ try:
73
+ # Extract field names from the AST
74
+ ast = expr_obj['ast']
75
+ return FExpressionHandler._extract_fields_from_node(ast)
76
+ except Exception as e:
77
+ logger.error(f"Error extracting fields from F expression: {e}")
78
+ return set()
79
+
80
+ return set()
81
+
82
+ @staticmethod
83
+ def _extract_fields_from_node(node: Dict[str, Any]) -> Set[str]:
84
+ """
85
+ Recursively extract field names from a math.js AST node.
86
+
87
+ Args:
88
+ node: Math.js AST node in JSON format
89
+
90
+ Returns:
91
+ Set[str]: Set of field names referenced in the node
92
+ """
93
+ if not node or not isinstance(node, dict):
94
+ return set()
95
+
96
+ field_names = set()
97
+ node_type = node.get('mathjs')
98
+
99
+ if not node_type:
100
+ return field_names
101
+
102
+ # SymbolNode - field reference
103
+ if node_type == 'SymbolNode':
104
+ field_name = node.get('name')
105
+ if field_name:
106
+ field_names.add(field_name)
107
+
108
+ # OperatorNode - binary operation
109
+ elif node_type == 'OperatorNode':
110
+ args = node.get('args', [])
111
+ for arg in args:
112
+ field_names.update(FExpressionHandler._extract_fields_from_node(arg))
113
+
114
+ # FunctionNode - function call
115
+ elif node_type == 'FunctionNode':
116
+ args = node.get('args', [])
117
+ for arg in args:
118
+ field_names.update(FExpressionHandler._extract_fields_from_node(arg))
119
+
120
+ # ParenthesisNode - parentheses (just process the content)
121
+ elif node_type == 'ParenthesisNode':
122
+ if 'content' in node:
123
+ field_names.update(FExpressionHandler._extract_fields_from_node(node.get('content')))
124
+
125
+ return field_names
126
+
127
+ @staticmethod
128
+ def _process_mathjs_node(node: Dict[str, Any]) -> Any:
129
+ """
130
+ Process a node from the math.js JSON representation
131
+
132
+ Args:
133
+ node: Math.js AST node in JSON format
134
+
135
+ Returns:
136
+ Django ORM expression
137
+
138
+ Raises:
139
+ ValueError: If the node type is unsupported
140
+ """
141
+ if not node or not isinstance(node, dict):
142
+ raise ValueError(f"Invalid node format: {node}")
143
+
144
+ node_type = node.get('mathjs') # Math.js consistently uses 'mathjs' as the type field in JSON
145
+
146
+ if not node_type:
147
+ raise ValueError(f"Missing node type in: {node}")
148
+
149
+ # SymbolNode - field reference
150
+ if node_type == 'SymbolNode':
151
+ field_name = node.get('name')
152
+ if not field_name:
153
+ raise ValueError("Field name is required")
154
+
155
+ # Basic validation for field name format
156
+ if not all(c.isalnum() or c == '_' for c in field_name):
157
+ raise ValueError(f"Invalid field name: {field_name}")
158
+
159
+ return F(field_name)
160
+
161
+ # ConstantNode - literal value
162
+ elif node_type == 'ConstantNode':
163
+ if 'value' not in node:
164
+ raise ValueError("Value node is missing the 'value' property")
165
+
166
+ value = node.get('value')
167
+ return Value(value)
168
+
169
+ # OperatorNode - binary operation
170
+ elif node_type == 'OperatorNode':
171
+ operator = node.get('op')
172
+
173
+ args = node.get('args', [])
174
+ if len(args) != 2: # Ensure binary operation
175
+ raise ValueError(f"Expected 2 arguments for operator {operator}, got {len(args)}")
176
+
177
+ left = FExpressionHandler._process_mathjs_node(args[0])
178
+ right = FExpressionHandler._process_mathjs_node(args[1])
179
+
180
+ return FExpressionHandler._apply_operation(operator, left, right)
181
+
182
+ # FunctionNode - function call
183
+ elif node_type == 'FunctionNode':
184
+ # Extract function name from the nested fn object, which is a SymbolNode
185
+ if 'fn' in node and isinstance(node['fn'], dict):
186
+ fn_node = node['fn']
187
+ if fn_node.get('mathjs') == 'SymbolNode':
188
+ func_name = fn_node.get('name')
189
+ else:
190
+ raise ValueError(f"Unsupported function node structure: {fn_node}")
191
+ else:
192
+ raise ValueError("Function node missing required 'fn' property")
193
+
194
+ if not func_name:
195
+ raise ValueError("Function name not found in function node")
196
+
197
+ # Check if function is allowed
198
+ if func_name not in FExpressionHandler.ALLOWED_FUNCTIONS:
199
+ raise ValueError(f"Unsupported function: {func_name}")
200
+
201
+ args = node.get('args', [])
202
+ if not args:
203
+ raise ValueError(f"Function {func_name} requires at least one argument")
204
+
205
+ processed_args = [FExpressionHandler._process_mathjs_node(arg) for arg in args]
206
+
207
+ return FExpressionHandler._apply_function(func_name, processed_args)
208
+
209
+ # ParenthesisNode - parentheses (just process the content)
210
+ elif node_type == 'ParenthesisNode':
211
+ if 'content' not in node:
212
+ raise ValueError("ParenthesisNode missing 'content'")
213
+
214
+ return FExpressionHandler._process_mathjs_node(node.get('content'))
215
+
216
+ # Other node types - not supported
217
+ else:
218
+ raise ValueError(f"Unsupported node type: {node_type}")
219
+
220
+ @staticmethod
221
+ def _apply_operation(operator: str, left: Any, right: Any) -> Combinable:
222
+ """
223
+ Apply a binary operation with proper output field handling
224
+
225
+ Args:
226
+ operator: Operation type
227
+ left: Left operand
228
+ right: Right operand
229
+
230
+ Returns:
231
+ Django expression result
232
+
233
+ Raises:
234
+ ValueError: If the operation is unsupported
235
+ """
236
+ # Apply the operation based on the operator
237
+ if operator == '+':
238
+ expression = left + right
239
+ # Addition between two fields might need output type specification
240
+ if isinstance(left, F) and isinstance(right, F):
241
+ return ExpressionWrapper(expression, output_field=FloatField())
242
+ return expression
243
+
244
+ elif operator == '-':
245
+ expression = left - right
246
+ # Subtraction between two fields might need output type specification
247
+ if isinstance(left, F) and isinstance(right, F):
248
+ return ExpressionWrapper(expression, output_field=FloatField())
249
+ return expression
250
+
251
+ elif operator == '*':
252
+ expression = left * right
253
+ # Multiplication might need float output field
254
+ return ExpressionWrapper(expression, output_field=FloatField())
255
+
256
+ elif operator == '/':
257
+ # Division always needs float output field
258
+ expression = left / right
259
+ return ExpressionWrapper(expression, output_field=FloatField())
260
+
261
+ elif operator == '%':
262
+ expression = left % right
263
+ return ExpressionWrapper(expression, output_field=IntegerField())
264
+
265
+ elif operator == '^':
266
+ # Power operation requires special handling
267
+ if isinstance(right, Value):
268
+ output_field = IntegerField() if isinstance(right.value, int) and right.value >= 0 else FloatField()
269
+ return ExpressionWrapper(left ** right.value, output_field=output_field)
270
+ else:
271
+ raise ValueError("Power operations require a constant right operand")
272
+
273
+ else:
274
+ # This shouldn't happen due to earlier validation
275
+ raise ValueError(f"Unsupported operator: {operator}")
276
+
277
+ @staticmethod
278
+ def _apply_function(func_name: str, args: List[Any]) -> Combinable:
279
+ """
280
+ Apply a function with proper output field handling
281
+
282
+ Args:
283
+ func_name: Function name
284
+ args: Function arguments
285
+
286
+ Returns:
287
+ Django function expression
288
+
289
+ Raises:
290
+ ValueError: If the function is unsupported
291
+ """
292
+ # Get the Django function class
293
+ func_class = FExpressionHandler.ALLOWED_FUNCTIONS.get(func_name)
294
+ if not func_class:
295
+ raise ValueError(f"Unsupported function: {func_name}")
296
+
297
+ # Apply function with appropriate output field
298
+ if func_name in ('abs', 'round', 'floor', 'ceil'):
299
+ # These functions preserve the input type
300
+ if len(args) != 1:
301
+ raise ValueError(f"Function {func_name} takes exactly 1 argument")
302
+ return func_class(args[0])
303
+
304
+ elif func_name in ('min', 'max'):
305
+ # These functions need at least 2 arguments
306
+ if len(args) < 2:
307
+ raise ValueError(f"Function {func_name} requires at least 2 arguments")
308
+ return func_class(*args)
309
+
310
+ else:
311
+ # This shouldn't happen due to earlier validation
312
+ raise ValueError(f"Function {func_name} implementation missing")
@@ -0,0 +1,153 @@
1
+ from typing import Dict, Set, List, Optional, Callable, Type, Union, Any
2
+ from django.db import models
3
+ from django.db.models.query import QuerySet
4
+ from django.apps import apps
5
+ from django.core.exceptions import FieldDoesNotExist
6
+
7
+ def collect_models_by_type(
8
+ obj,
9
+ fields_map: Dict[str, Set[str]],
10
+ collected: Optional[Dict[str, List[models.Model]]] = None,
11
+ get_model_name: Optional[Callable[[Union[models.Model, Type[models.Model]]], str]] = None,
12
+ visited: Optional[Set[str]] = None
13
+ ) -> Dict[str, List[models.Model]]:
14
+ """
15
+ Collects model instances by their type based on a fields_map.
16
+ Uses prefetched/preselected data that's already loaded.
17
+
18
+ Args:
19
+ obj: Django model instance or queryset
20
+ fields_map: Dict mapping model names to sets of field names
21
+ e.g. {
22
+ "django_app.deepmodellevel1": {"level2"},
23
+ "django_app.deepmodellevel2": {"level3"},
24
+ "django_app.deepmodellevel3": {"name"}
25
+ }
26
+ collected: Dict to store collected models by type
27
+ get_model_name: Optional function to get model name in the format used in fields_map
28
+ visited: Set of already visited instance IDs to prevent cycles
29
+
30
+ Returns:
31
+ Dict mapping model types to lists of model instances
32
+ """
33
+ # Initialize collection dictionary
34
+ if collected is None:
35
+ collected = {}
36
+
37
+ # Initialize visited set to prevent cycles
38
+ if visited is None:
39
+ visited = set()
40
+
41
+ # Handle querysets
42
+ if isinstance(obj, (QuerySet, list, tuple)):
43
+ for item in obj:
44
+ collect_models_by_type(item, fields_map, collected, get_model_name, visited)
45
+ return collected
46
+
47
+ # Skip None objects
48
+ if obj is None:
49
+ return collected
50
+
51
+ # Get model info using the provided function or default
52
+ model = obj.__class__
53
+ model_type = get_model_name(obj)
54
+
55
+ # Create a unique ID for this instance to detect cycles
56
+ instance_id = f"{model_type}:{obj.pk}"
57
+ if instance_id in visited:
58
+ # Already processed this instance
59
+ return collected
60
+
61
+ # Mark as visited
62
+ visited.add(instance_id)
63
+
64
+ # Add this model instance to the collection
65
+ if model_type.lower() in [k.lower() for k in fields_map.keys()]:
66
+ # Find the correct case in the fields_map
67
+ for key in fields_map.keys():
68
+ if key.lower() == model_type.lower():
69
+ model_type = key
70
+ break
71
+
72
+ if model_type not in collected:
73
+ collected[model_type] = []
74
+
75
+ # Check if this instance is already in the collection
76
+ if obj not in collected[model_type]:
77
+ collected[model_type].append(obj)
78
+
79
+ # Process related fields based on fields_map
80
+ # Find the case-insensitive match
81
+ model_key = None
82
+ for key in fields_map.keys():
83
+ if key.lower() == model_type.lower():
84
+ model_key = key
85
+ break
86
+
87
+ allowed_fields = fields_map.get(model_key, set()) if model_key else set()
88
+
89
+ # If no fields specified for this model, don't traverse further
90
+ if not allowed_fields:
91
+ return collected
92
+
93
+ # Process each allowed field
94
+ for field_name in allowed_fields:
95
+ try:
96
+ # Try to get model field definition
97
+ field_def = model._meta.get_field(field_name)
98
+
99
+ # Only process relation fields
100
+ if field_def.is_relation:
101
+ # Get the related value - this will use prefetched/selected data when available
102
+ related_obj = getattr(obj, field_name)
103
+
104
+ if related_obj is None:
105
+ continue
106
+
107
+ # For many-to-many or reverse FK relations
108
+ if field_def.many_to_many or field_def.one_to_many:
109
+ # This will use prefetch_related data if available
110
+ related_qs = related_obj.all()
111
+ collect_models_by_type(related_qs, fields_map, collected, get_model_name, visited)
112
+ else:
113
+ # This will use select_related data if available
114
+ collect_models_by_type(related_obj, fields_map, collected, get_model_name, visited)
115
+
116
+ except FieldDoesNotExist:
117
+ # Skip computed properties and non-existent fields
118
+ continue
119
+
120
+ return collected
121
+
122
+ def collect_from_queryset(
123
+ data: Any,
124
+ fields_map: Dict[str, Set[str]],
125
+ get_model_name: Optional[Callable[[Union[models.Model, Type[models.Model]]], str]] = None,
126
+ get_model: Optional[Callable[[str], Type[models.Model]]] = None
127
+ ) -> Dict[str, List[models.Model]]:
128
+ """
129
+ Collects model instances by type from a queryset or model instance.
130
+
131
+ Args:
132
+ data: Django model instance or queryset
133
+ fields_map: Dict mapping model names to sets of field names
134
+ get_model_name: Optional function to get model name in the format used in fields_map
135
+ get_model: Optional function to get model class from a model name
136
+
137
+ Returns:
138
+ Dict with each model type pointing to a list of model instances
139
+ """
140
+ if data is None:
141
+ return {}
142
+
143
+ # Process data with the provided get_model_name function
144
+ collected_models = collect_models_by_type(data, fields_map, get_model_name=get_model_name)
145
+
146
+ # If get_model is provided and we want to ensure all keys in fields_map have entries
147
+ if get_model:
148
+ for model_name in fields_map.keys():
149
+ if model_name not in collected_models:
150
+ # Create an empty list for model types that weren't collected
151
+ collected_models[model_name] = []
152
+
153
+ return collected_models
@@ -0,0 +1,10 @@
1
+ from django.utils.deprecation import MiddlewareMixin
2
+
3
+ from statezero.core.context_storage import current_operation_id
4
+
5
+
6
+ class OperationIDMiddleware(MiddlewareMixin):
7
+ def process_request(self, request):
8
+ # The header in Django is available via request.META (HTTP headers are prefixed with HTTP_)
9
+ op_id = request.META.get("HTTP_X_OPERATION_ID")
10
+ current_operation_id.set(op_id)
@@ -0,0 +1,33 @@
1
+ # Generated by Django 5.1.6 on 2025-05-25 17:24
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name='ModelViewSubscription',
19
+ fields=[
20
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+ ('model_name', models.CharField(max_length=255)),
22
+ ('ast_query', models.JSONField()),
23
+ ('response_hash', models.CharField(blank=True, max_length=64, null=True)),
24
+ ('channel_name', models.CharField(max_length=64)),
25
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='live_requests', to=settings.AUTH_USER_MODEL)),
26
+ ],
27
+ options={
28
+ 'db_table': 'model_view_subscriptions',
29
+ 'indexes': [models.Index(fields=['model_name'], name='model_view__model_n_16abfb_idx')],
30
+ 'unique_together': {('user', 'channel_name')},
31
+ },
32
+ ),
33
+ ]
@@ -0,0 +1,16 @@
1
+ # Generated by Django 5.1.6 on 2025-05-31 23:22
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('statezero', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.DeleteModel(
14
+ name='ModelViewSubscription',
15
+ ),
16
+ ]
File without changes