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,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
|