statezero 0.1.0b3__py3-none-any.whl → 0.1.0b5__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.

Potentially problematic release.


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

@@ -13,11 +13,17 @@ from rest_framework import serializers
13
13
  from statezero.adaptors.django.config import config, registry
14
14
  from statezero.core.classes import FieldNode, ModelNode
15
15
  from statezero.core.event_bus import EventBus
16
- from statezero.core.exceptions import (MultipleObjectsReturned,
17
- NotFound, PermissionDenied,
18
- ValidationError)
19
- from statezero.core.interfaces import (AbstractCustomQueryset,
20
- AbstractORMProvider, AbstractPermission)
16
+ from statezero.core.exceptions import (
17
+ MultipleObjectsReturned,
18
+ NotFound,
19
+ PermissionDenied,
20
+ ValidationError,
21
+ )
22
+ from statezero.core.interfaces import (
23
+ AbstractCustomQueryset,
24
+ AbstractORMProvider,
25
+ AbstractPermission,
26
+ )
21
27
  from statezero.core.types import ActionType, RequestType
22
28
 
23
29
  logger = logging.getLogger(__name__)
@@ -79,14 +85,14 @@ class QueryASTVisitor:
79
85
  def _process_field_lookup(self, field: str, value: Any) -> Tuple[str, Any]:
80
86
  """
81
87
  This used to contain logic, right now it just passes through the field and value.
82
-
88
+
83
89
  Args:
84
90
  field: The field lookup string (e.g., 'datetime_field__hour__gt')
85
91
  value: The value to filter by
86
-
92
+
87
93
  Returns:
88
94
  A tuple of (lookup, value)
89
- """
95
+ """
90
96
  return field, value
91
97
 
92
98
  def visit_filter(self, node: Dict[str, Any]) -> Q:
@@ -133,7 +139,7 @@ class QueryASTVisitor:
133
139
  def visit_or(self, node: Dict[str, Any]) -> Q:
134
140
  """Process an OR node by combining all children with OR."""
135
141
  return self._combine(node.get("children", []), lambda a, b: a | b)
136
-
142
+
137
143
  def visit_search(self, node: Dict[str, Any]) -> Q:
138
144
  """
139
145
  Process a search node.
@@ -142,6 +148,7 @@ class QueryASTVisitor:
142
148
  """
143
149
  return Q()
144
150
 
151
+
145
152
  # -------------------------------------------------------------------
146
153
  # Django ORM Adapter (implements our generic engine/provider)
147
154
  # -------------------------------------------------------------------
@@ -197,35 +204,30 @@ def check_bulk_permissions(
197
204
 
198
205
  class DjangoORMAdapter(AbstractORMProvider):
199
206
  def __init__(self) -> None:
200
- self.queryset: Optional[Any] = None # Django QuerySet
201
- self.model: Optional[Type[models.Model]] = None
202
-
203
- def set_queryset(self, queryset: Any) -> None:
204
- self.queryset = queryset
205
- self.model = queryset.model
207
+ # No instance state - completely stateless
208
+ pass
206
209
 
207
210
  # --- QueryEngine Methods ---
208
- def filter_node(self, node: Dict[str, Any]) -> None:
209
- """Apply a filter node to the queryset."""
210
- assert self.model is not None, "Model must be set before filtering."
211
- visitor = QueryASTVisitor(self.model)
211
+ def filter_node(self, queryset: QuerySet, node: Dict[str, Any]) -> QuerySet:
212
+ """Apply a filter node to the queryset and return new queryset."""
213
+ model = queryset.model
214
+ visitor = QueryASTVisitor(model)
212
215
  q_object = visitor.visit(node)
213
- self.queryset = self.queryset.filter(q_object)
216
+ return queryset.filter(q_object)
214
217
 
215
- def search_node(self, search_query: str, search_fields: Set[str]) -> None:
218
+ def search_node(
219
+ self, queryset: QuerySet, search_query: str, search_fields: Set[str]
220
+ ) -> QuerySet:
216
221
  """
217
- Update the current queryset by applying a full-text search.
218
- Assumes that the queryset and model are already set.
222
+ Apply a full-text search to the queryset and return new queryset.
219
223
  Uses the search_provider from the global configuration.
220
224
  """
221
- # Ensure that a model is set (queryset should already be there as well).
222
- assert self.model is not None, "Model must be set before applying search."
223
- self.queryset = config.search_provider.search(self.queryset, search_query, search_fields)
225
+ return config.search_provider.search(queryset, search_query, search_fields)
224
226
 
225
- def exclude_node(self, node: Dict[str, Any]) -> None:
226
- """Apply an exclude node to the queryset."""
227
- assert self.model is not None, "Model must be set before filtering."
228
- visitor = QueryASTVisitor(self.model)
227
+ def exclude_node(self, queryset: QuerySet, node: Dict[str, Any]) -> QuerySet:
228
+ """Apply an exclude node to the queryset and return new queryset."""
229
+ model = queryset.model
230
+ visitor = QueryASTVisitor(model)
229
231
 
230
232
  # Handle both direct exclude nodes and exclude nodes with a child filter
231
233
  if "child" in node:
@@ -235,79 +237,91 @@ class DjangoORMAdapter(AbstractORMProvider):
235
237
  # Otherwise, treat it as a standard filter node to be negated
236
238
  q_object = visitor.visit(node)
237
239
 
238
- self.queryset = self.queryset.exclude(q_object)
240
+ return queryset.exclude(q_object)
239
241
 
240
- def create(self, data: Dict[str, Any], serializer, req, fields_map) -> models.Model:
241
- assert self.model is not None, "Model must be set before creating."
242
+ def create(
243
+ self,
244
+ model: Type[models.Model],
245
+ data: Dict[str, Any],
246
+ serializer,
247
+ req,
248
+ fields_map,
249
+ ) -> models.Model:
250
+ """Create a new model instance."""
242
251
  # Use the provided serializer's save method
243
252
  return serializer.save(
244
- model=self.model,
253
+ model=model,
245
254
  data=data,
246
255
  instance=None,
247
256
  partial=False,
248
257
  request=req,
249
- fields_map=fields_map
258
+ fields_map=fields_map,
250
259
  )
251
260
 
252
261
  def update_instance(
253
262
  self,
263
+ model: Type[models.Model],
254
264
  ast: Dict[str, Any],
255
265
  req: RequestType,
256
266
  permissions: List[Type[AbstractPermission]],
257
267
  serializer,
258
- fields_map
268
+ fields_map,
259
269
  ) -> models.Model:
270
+ """Update a single model instance."""
260
271
  data = ast.get("data", {})
261
272
  filter_ast = ast.get("filter")
262
273
  if not filter_ast:
263
274
  raise ValueError("Filter is required for update_instance operation")
264
275
 
265
- visitor = QueryASTVisitor(self.model)
276
+ visitor = QueryASTVisitor(model)
266
277
  q_obj = visitor.visit(filter_ast)
267
- instance = self.model.objects.get(q_obj)
278
+ instance = model.objects.get(q_obj)
268
279
 
269
280
  # Check object-level permissions for update.
270
281
  for perm_cls in permissions:
271
282
  perm = perm_cls()
272
- allowed = perm.allowed_object_actions(req, instance, self.model)
283
+ allowed = perm.allowed_object_actions(req, instance, model)
273
284
  if ActionType.UPDATE not in allowed:
274
285
  raise PermissionDenied(f"Update not permitted on {instance}")
275
286
 
276
287
  # Use the provided serializer's save method for the update
277
288
  return serializer.save(
278
- model=self.model,
289
+ model=model,
279
290
  data=data,
280
291
  instance=instance,
281
292
  partial=True,
282
293
  request=req,
283
- fields_map=fields_map
294
+ fields_map=fields_map,
284
295
  )
285
296
 
286
297
  def delete_instance(
287
298
  self,
299
+ model: Type[models.Model],
288
300
  ast: Dict[str, Any],
289
301
  req: RequestType,
290
302
  permissions: List[Type[AbstractPermission]],
291
303
  ) -> int:
304
+ """Delete a single model instance."""
292
305
  filter_ast = ast.get("filter")
293
306
  if not filter_ast:
294
307
  raise ValueError("Filter is required for delete_instance operation")
295
308
 
296
- visitor = QueryASTVisitor(self.model)
309
+ visitor = QueryASTVisitor(model)
297
310
  q_obj = visitor.visit(filter_ast)
298
- instance = self.model.objects.get(q_obj)
311
+ instance = model.objects.get(q_obj)
299
312
 
300
313
  # Check object-level permissions.
301
314
  for perm_cls in permissions:
302
315
  perm = perm_cls()
303
- allowed = perm.allowed_object_actions(req, instance, self.model)
316
+ allowed = perm.allowed_object_actions(req, instance, model)
304
317
  if ActionType.DELETE not in allowed:
305
318
  raise PermissionDenied(f"Delete not permitted on {instance}")
306
319
 
307
320
  instance.delete()
308
321
  return 1
309
-
310
- def get_pk_list(queryset: QuerySet):
322
+
323
+ @staticmethod
324
+ def get_pk_list(queryset: QuerySet) -> List[Any]:
311
325
  """
312
326
  Gets a list of primary key values from a QuerySet, handling different PK field names.
313
327
 
@@ -324,31 +338,30 @@ class DjangoORMAdapter(AbstractORMProvider):
324
338
 
325
339
  def update(
326
340
  self,
341
+ queryset: QuerySet,
327
342
  node: Dict[str, Any],
328
343
  req: RequestType,
329
344
  permissions: List[Type[AbstractPermission]],
330
- readable_fields: Set[str] = None
345
+ readable_fields: Set[str] = None,
331
346
  ) -> Tuple[int, List[Dict[str, Union[int, str]]]]:
332
347
  """
333
348
  Update operations with support for F expressions.
334
349
  Includes permission checks for fields referenced in F expressions.
335
350
  """
336
- assert self.model is not None, "Model must be set before updating."
351
+ model = queryset.model
337
352
  data: Dict[str, Any] = node.get("data", {})
338
353
  filter_ast: Optional[Dict[str, Any]] = node.get("filter")
339
-
340
- # Start with self.queryset which already has permission filtering
341
- qs: QuerySet = self.queryset
354
+
355
+ # Start with the provided queryset which already has permission filtering
356
+ qs: QuerySet = queryset
342
357
  if filter_ast:
343
- visitor = QueryASTVisitor(self.model)
358
+ visitor = QueryASTVisitor(model)
344
359
  q_obj = visitor.visit(filter_ast)
345
360
  qs = qs.filter(q_obj)
346
361
 
347
362
  # Check bulk update permissions
348
- check_bulk_permissions(req, qs, ActionType.UPDATE, permissions, self.model)
363
+ check_bulk_permissions(req, qs, ActionType.UPDATE, permissions, model)
349
364
 
350
- model = qs.model
351
-
352
365
  # Get the fields to update (keys from data plus primary key)
353
366
  update_fields = list(data.keys())
354
367
  update_fields.append(model._meta.pk.name)
@@ -356,36 +369,40 @@ class DjangoORMAdapter(AbstractORMProvider):
356
369
  # Process any F expressions in the update data
357
370
  processed_data = {}
358
371
  from statezero.adaptors.django.f_handler import FExpressionHandler
359
-
372
+
360
373
  for key, value in data.items():
361
- if isinstance(value, dict) and value.get('__f_expr'):
374
+ if isinstance(value, dict) and value.get("__f_expr"):
362
375
  # It's an F expression - check permissions and process it
363
376
  try:
364
377
  # Extract field names referenced in the F expression
365
- referenced_fields = FExpressionHandler.extract_referenced_fields(value)
366
-
378
+ referenced_fields = FExpressionHandler.extract_referenced_fields(
379
+ value
380
+ )
381
+
367
382
  # Check that user has READ permissions for all referenced fields
368
383
  for field in referenced_fields:
369
384
  if field not in readable_fields:
370
385
  raise PermissionDenied(
371
386
  f"No permission to read field '{field}' referenced in F expression"
372
387
  )
373
-
388
+
374
389
  # Process the F expression now that permissions are verified
375
390
  processed_data[key] = FExpressionHandler.process_expression(value)
376
391
  except ValueError as e:
377
392
  logger.error(f"Error processing F expression for field {key}: {e}")
378
- raise ValidationError(f"Invalid F expression for field {key}: {str(e)}")
393
+ raise ValidationError(
394
+ f"Invalid F expression for field {key}: {str(e)}"
395
+ )
379
396
  else:
380
397
  # Regular value, use as-is
381
398
  processed_data[key] = value
382
-
399
+
383
400
  # Execute the update with processed expressions
384
401
  rows_updated = qs.update(**processed_data)
385
402
 
386
403
  # After update, fetch the updated instances
387
404
  updated_instances = list(qs.only(*update_fields))
388
-
405
+
389
406
  # Triggers cache invalidation and broadcast to the frontend
390
407
  config.event_bus.emit_bulk_event(ActionType.BULK_UPDATE, updated_instances)
391
408
 
@@ -393,50 +410,56 @@ class DjangoORMAdapter(AbstractORMProvider):
393
410
 
394
411
  def delete(
395
412
  self,
413
+ queryset: QuerySet,
396
414
  node: Dict[str, Any],
397
415
  req: RequestType,
398
416
  permissions: List[Type[AbstractPermission]],
399
417
  ) -> Tuple[int, Tuple[int]]:
400
- assert self.model is not None, "Model must be set before deleting."
418
+ """Delete multiple model instances."""
419
+ model = queryset.model
401
420
  filter_ast: Optional[Dict[str, Any]] = node.get("filter")
402
- # Start with self.queryset which already has permission filtering
403
- qs: QuerySet = self.queryset
421
+ # Start with the provided queryset which already has permission filtering
422
+ qs: QuerySet = queryset
404
423
  if filter_ast:
405
- visitor = QueryASTVisitor(self.model)
424
+ visitor = QueryASTVisitor(model)
406
425
  q_obj = visitor.visit(filter_ast)
407
426
  qs = qs.filter(q_obj)
408
427
 
409
- check_bulk_permissions(req, qs, ActionType.DELETE, permissions, self.model)
410
-
428
+ check_bulk_permissions(req, qs, ActionType.DELETE, permissions, model)
429
+
411
430
  # TODO: this should be a values list, but we need to check the bulk event emitter code
412
- model = qs.model
413
431
  pk_field_name = model._meta.pk.name
414
432
  instances = list(qs.only(pk_field_name))
415
-
433
+
416
434
  deleted, _ = qs.delete()
417
435
 
418
436
  # Triggers cache invalidation and broadcast to the frontend
419
437
  config.event_bus.emit_bulk_event(ActionType.BULK_DELETE, instances)
420
438
 
421
439
  # Dynamically create a Meta inner class
422
- Meta = type("Meta", (), {
423
- "model": model,
424
- "fields": [pk_field_name], # Only include the PK field
425
- })
426
-
440
+ Meta = type(
441
+ "Meta",
442
+ (),
443
+ {
444
+ "model": model,
445
+ "fields": [pk_field_name], # Only include the PK field
446
+ },
447
+ )
448
+
427
449
  # Create the serializer class
428
450
  serializer_class = type(
429
- f"Dynamic{model.__name__}PkSerializer",
430
- (serializers.ModelSerializer,),
431
- {"Meta": Meta}
451
+ f"Dynamic{model.__name__}PkSerializer",
452
+ (serializers.ModelSerializer,),
453
+ {"Meta": Meta},
432
454
  )
433
455
 
434
456
  serializer = serializer_class(instances, many=True)
435
-
457
+
436
458
  return deleted, serializer.data
437
459
 
438
460
  def get(
439
461
  self,
462
+ queryset: QuerySet,
440
463
  node: Dict[str, Any],
441
464
  req: RequestType,
442
465
  permissions: List[Type[AbstractPermission]],
@@ -445,6 +468,7 @@ class DjangoORMAdapter(AbstractORMProvider):
445
468
  Retrieve a single model instance with permission checks.
446
469
 
447
470
  Args:
471
+ queryset: The base queryset to search in
448
472
  node: The query AST node
449
473
  req: The request object
450
474
  permissions: List of permission classes to check
@@ -457,34 +481,32 @@ class DjangoORMAdapter(AbstractORMProvider):
457
481
  PermissionDenied: If the user doesn't have permission to read the object
458
482
  MultipleObjectsReturned: If multiple objects match the query
459
483
  """
460
- assert self.model is not None, "Model must be set before retrieving."
484
+ model = queryset.model
461
485
  filter_ast: Optional[Dict[str, Any]] = node.get("filter")
462
486
 
463
487
  if filter_ast:
464
- visitor = QueryASTVisitor(self.model)
488
+ visitor = QueryASTVisitor(model)
465
489
  q_obj = visitor.visit(filter_ast)
466
490
  try:
467
- instance = self.queryset.filter(q_obj).get()
468
- except self.model.DoesNotExist:
469
- raise NotFound(f"No {self.model.__name__} matches the given query.")
470
- except self.model.MultipleObjectsReturned:
491
+ instance = queryset.filter(q_obj).get()
492
+ except model.DoesNotExist:
493
+ raise NotFound(f"No {model.__name__} matches the given query.")
494
+ except model.MultipleObjectsReturned:
471
495
  raise MultipleObjectsReturned(
472
- f"Multiple {self.model.__name__} instances match the given query."
496
+ f"Multiple {model.__name__} instances match the given query."
473
497
  )
474
498
  else:
475
499
  try:
476
- instance = self.queryset.get()
477
- except self.model.DoesNotExist:
478
- raise NotFound(f"No {self.model.__name__} matches the given query.")
479
- except self.model.MultipleObjectsReturned:
500
+ instance = queryset.get()
501
+ except model.DoesNotExist:
502
+ raise NotFound(f"No {model.__name__} matches the given query.")
503
+ except model.MultipleObjectsReturned:
480
504
  raise MultipleObjectsReturned(
481
- f"Multiple {self.model.__name__} instances match the given query."
505
+ f"Multiple {model.__name__} instances match the given query."
482
506
  )
483
507
 
484
508
  # Check object-level permissions for reading
485
- check_object_permissions(
486
- req, instance, ActionType.READ, permissions, self.model
487
- )
509
+ check_object_permissions(req, instance, ActionType.READ, permissions, model)
488
510
 
489
511
  return instance
490
512
 
@@ -503,15 +525,17 @@ class DjangoORMAdapter(AbstractORMProvider):
503
525
 
504
526
  def get_or_create(
505
527
  self,
528
+ queryset: QuerySet,
506
529
  node: Dict[str, Any],
507
530
  serializer,
508
531
  req: RequestType,
509
532
  permissions: List[Type[AbstractPermission]],
510
- create_fields_map
533
+ create_fields_map,
511
534
  ) -> Tuple[models.Model, bool]:
512
535
  """
513
536
  Get an existing object, or create it if it doesn't exist, with object-level permission checks.
514
537
  """
538
+ model = queryset.model
515
539
  lookup = node.get("lookup", {})
516
540
  defaults = node.get("defaults", {})
517
541
 
@@ -520,20 +544,18 @@ class DjangoORMAdapter(AbstractORMProvider):
520
544
 
521
545
  # Check if an instance exists
522
546
  try:
523
- instance = self.queryset.get(**lookup)
547
+ instance = queryset.get(**lookup)
524
548
  created = False
525
549
 
526
550
  # Check object-level permission to read the existing object
527
- check_object_permissions(
528
- req, instance, ActionType.READ, permissions, self.model
529
- )
530
- except self.model.DoesNotExist:
551
+ check_object_permissions(req, instance, ActionType.READ, permissions, model)
552
+ except model.DoesNotExist:
531
553
  # Object doesn't exist, we'll create it
532
554
  instance = None
533
555
  created = True
534
- except self.model.MultipleObjectsReturned as e:
556
+ except model.MultipleObjectsReturned as e:
535
557
  raise MultipleObjectsReturned(
536
- f"Multiple {self.model.__name__} instances match the given lookup parameters"
558
+ f"Multiple {model.__name__} instances match the given lookup parameters"
537
559
  )
538
560
 
539
561
  # If the instance exists, we don't need to update it, just return it
@@ -542,28 +564,30 @@ class DjangoORMAdapter(AbstractORMProvider):
542
564
 
543
565
  # Only create a new instance if it doesn't exist
544
566
  instance = serializer.save(
545
- model=self.model,
567
+ model=model,
546
568
  data=merged_data,
547
569
  instance=None, # No instance for creation
548
- partial=False, # Not a partial update for creation
570
+ partial=False, # Not a partial update for creation
549
571
  request=req,
550
- fields_map=create_fields_map
572
+ fields_map=create_fields_map,
551
573
  )
552
574
 
553
575
  return instance, created
554
576
 
555
577
  def update_or_create(
556
578
  self,
579
+ queryset: QuerySet,
557
580
  node: Dict[str, Any],
558
581
  req: RequestType,
559
582
  serializer,
560
583
  permissions: List[Type[AbstractPermission]],
561
584
  update_fields_map,
562
- create_fields_map
585
+ create_fields_map,
563
586
  ) -> Tuple[models.Model, bool]:
564
587
  """
565
588
  Update an existing object, or create it if it doesn't exist, with object-level permission checks.
566
589
  """
590
+ model = queryset.model
567
591
  lookup = node.get("lookup", {})
568
592
  defaults = node.get("defaults", {})
569
593
 
@@ -572,45 +596,51 @@ class DjangoORMAdapter(AbstractORMProvider):
572
596
 
573
597
  # Determine if the instance exists
574
598
  try:
575
- instance = self.queryset.get(**lookup)
599
+ instance = queryset.get(**lookup)
576
600
  created = False
577
601
 
578
602
  # Perform object-level permission check before update
579
603
  check_object_permissions(
580
- req, instance, ActionType.UPDATE, permissions, self.model
604
+ req, instance, ActionType.UPDATE, permissions, model
581
605
  )
582
- except self.model.DoesNotExist:
606
+ except model.DoesNotExist:
583
607
  # Object doesn't exist, we'll create it
584
608
  instance = None
585
609
  created = True
586
- except self.model.MultipleObjectsReturned as e:
610
+ except model.MultipleObjectsReturned as e:
587
611
  raise MultipleObjectsReturned(
588
- f"Multiple {self.model.__name__} instances match the given lookup parameters"
612
+ f"Multiple {model.__name__} instances match the given lookup parameters"
589
613
  )
590
-
614
+
591
615
  fields_map_to_use = create_fields_map if created else update_fields_map
592
616
 
593
617
  # Use the serializer's save method, which handles validation and saving
594
618
  instance = serializer.save(
595
- model=self.model,
619
+ model=model,
596
620
  data=merged_data,
597
621
  instance=instance,
598
622
  request=req,
599
- fields_map=fields_map_to_use
623
+ fields_map=fields_map_to_use,
600
624
  )
601
625
 
602
626
  return instance, created
603
627
 
604
- def first(self) -> Optional[models.Model]:
605
- return self.queryset.first() if self.queryset is not None else None
628
+ def first(self, queryset: QuerySet) -> Optional[models.Model]:
629
+ """Return the first record from the queryset."""
630
+ return queryset.first()
606
631
 
607
- def last(self) -> Optional[models.Model]:
608
- return self.queryset.last() if self.queryset is not None else None
632
+ def last(self, queryset: QuerySet) -> Optional[models.Model]:
633
+ """Return the last record from the queryset."""
634
+ return queryset.last()
609
635
 
610
- def exists(self) -> bool:
611
- return self.queryset.exists() if self.queryset is not None else False
636
+ def exists(self, queryset: QuerySet) -> bool:
637
+ """Return True if the queryset has any results; otherwise False."""
638
+ return queryset.exists()
612
639
 
613
- def aggregate(self, agg_list: List[Dict[str, Any]]) -> Dict[str, Any]:
640
+ def aggregate(
641
+ self, queryset: QuerySet, agg_list: List[Dict[str, Any]]
642
+ ) -> Dict[str, Any]:
643
+ """Perform aggregation operations on the queryset."""
614
644
  agg_expressions = {}
615
645
  for agg in agg_list:
616
646
  func = agg.get("function")
@@ -628,40 +658,52 @@ class DjangoORMAdapter(AbstractORMProvider):
628
658
  agg_expressions[alias] = Max(field)
629
659
  else:
630
660
  raise ValidationError(f"Unknown aggregate function: {func}")
631
- result = self.queryset.aggregate(**agg_expressions)
661
+ result = queryset.aggregate(**agg_expressions)
632
662
  return {"data": result, "metadata": {"aggregated": True}}
633
663
 
634
- def count(self, field: str) -> int:
635
- result = self.queryset.aggregate(result=Count(field))["result"]
664
+ def count(self, queryset: QuerySet, field: str) -> int:
665
+ """Count the number of records for the given field."""
666
+ result = queryset.aggregate(result=Count(field))["result"]
636
667
  return int(result) if result is not None else 0
637
668
 
638
- def sum(self, field: str) -> Optional[Union[int, float]]:
639
- return self.queryset.aggregate(result=Sum(field))["result"]
669
+ def sum(self, queryset: QuerySet, field: str) -> Optional[Union[int, float]]:
670
+ """Sum the values of the given field."""
671
+ return queryset.aggregate(result=Sum(field))["result"]
640
672
 
641
- def avg(self, field: str) -> Optional[float]:
642
- result = self.queryset.aggregate(result=Avg(field))["result"]
673
+ def avg(self, queryset: QuerySet, field: str) -> Optional[float]:
674
+ """Calculate the average of the given field."""
675
+ result = queryset.aggregate(result=Avg(field))["result"]
643
676
  return float(result) if result is not None else None
644
677
 
645
- def min(self, field: str) -> Optional[Union[int, float, str]]:
646
- return self.queryset.aggregate(result=Min(field))["result"]
678
+ def min(self, queryset: QuerySet, field: str) -> Optional[Union[int, float, str]]:
679
+ """Find the minimum value for the given field."""
680
+ return queryset.aggregate(result=Min(field))["result"]
647
681
 
648
- def max(self, field: str) -> Optional[Union[int, float, str]]:
649
- return self.queryset.aggregate(result=Max(field))["result"]
682
+ def max(self, queryset: QuerySet, field: str) -> Optional[Union[int, float, str]]:
683
+ """Find the maximum value for the given field."""
684
+ return queryset.aggregate(result=Max(field))["result"]
650
685
 
651
- def order_by(self, order_list: List[str]) -> None:
652
- self.queryset = self.queryset.order_by(*order_list)
686
+ def order_by(self, queryset: QuerySet, order_list: List[str]) -> QuerySet:
687
+ """Order the queryset based on a list of fields."""
688
+ return queryset.order_by(*order_list)
653
689
 
654
- def select_related(self, related_fields: List[str]) -> None:
655
- self.queryset = self.queryset.select_related(*related_fields)
690
+ def select_related(self, queryset: QuerySet, related_fields: List[str]) -> QuerySet:
691
+ """Optimize the queryset by eager loading the given related fields."""
692
+ return queryset.select_related(*related_fields)
656
693
 
657
- def prefetch_related(self, related_fields: List[str]) -> None:
658
- self.queryset = self.queryset.prefetch_related(*related_fields)
694
+ def prefetch_related(
695
+ self, queryset: QuerySet, related_fields: List[str]
696
+ ) -> QuerySet:
697
+ """Optimize the queryset by prefetching the given related fields."""
698
+ return queryset.prefetch_related(*related_fields)
659
699
 
660
- def select_fields(self, fields: List[str]) -> None:
661
- self.queryset = self.queryset.values(*fields)
700
+ def select_fields(self, queryset: QuerySet, fields: List[str]) -> QuerySet:
701
+ """Select only specific fields from the queryset."""
702
+ return queryset.values(*fields)
662
703
 
663
704
  def fetch_list(
664
705
  self,
706
+ queryset: QuerySet,
665
707
  offset: Optional[int] = None,
666
708
  limit: Optional[int] = None,
667
709
  req: RequestType = None,
@@ -671,31 +713,34 @@ class DjangoORMAdapter(AbstractORMProvider):
671
713
  Fetch a list of model instances with bulk permission checks.
672
714
 
673
715
  Args:
716
+ queryset: The queryset to paginate
674
717
  offset: The offset for pagination
675
718
  limit: The limit for pagination
676
719
  req: The request object
677
720
  permissions: List of permission classes to check
678
721
 
679
722
  Returns:
680
- A list of model instances after permission checks
723
+ A sliced queryset after permission checks
681
724
  """
725
+ model = queryset.model
682
726
  offset = offset or 0
683
727
 
684
728
  # FIXED: Perform bulk permission checks BEFORE slicing
685
729
  if req is not None and permissions:
686
730
  # Use the existing bulk permission check function on the unsliced queryset
687
- check_bulk_permissions(req, self.queryset, ActionType.READ, permissions, self.model)
731
+ check_bulk_permissions(req, queryset, ActionType.READ, permissions, model)
688
732
 
689
733
  # THEN apply pagination/slicing
690
734
  if limit is None:
691
- qs = self.queryset[offset:]
735
+ qs = queryset[offset:]
692
736
  else:
693
- qs = self.queryset[offset : offset + limit]
737
+ qs = queryset[offset : offset + limit]
694
738
 
695
739
  return qs
696
740
 
697
- def _build_conditions(self, conditions: dict) -> Q:
698
- visitor = QueryASTVisitor(self.model)
741
+ def _build_conditions(self, model: Type[models.Model], conditions: dict) -> Q:
742
+ """Build Q conditions from a dictionary."""
743
+ visitor = QueryASTVisitor(model)
699
744
  fake_ast = {"type": "filter", "conditions": conditions}
700
745
  return visitor.visit(fake_ast)
701
746
 
@@ -708,12 +753,13 @@ class DjangoORMAdapter(AbstractORMProvider):
708
753
  custom_querysets: Dict[str, Type[AbstractCustomQueryset]],
709
754
  registered_permissions: List[Type[AbstractPermission]],
710
755
  ) -> Any:
756
+ """Assemble and return the base QuerySet for the given model."""
711
757
  custom_name = initial_ast.get("custom_queryset")
712
758
  if custom_name and custom_name in custom_querysets:
713
759
  custom_queryset_class = custom_querysets[custom_name]
714
760
  return custom_queryset_class().get_queryset(req)
715
761
  return model.objects.all()
716
-
762
+
717
763
  def get_fields(self, model: models.Model) -> Set[str]:
718
764
  """
719
765
  Return a set of the model fields.
@@ -723,7 +769,9 @@ class DjangoORMAdapter(AbstractORMProvider):
723
769
  resolved_fields = model_config.fields
724
770
  else:
725
771
  resolved_fields = set((field.name for field in model._meta.get_fields()))
726
- additional_fields = set((field.name for field in model_config.additional_fields))
772
+ additional_fields = set(
773
+ (field.name for field in model_config.additional_fields)
774
+ )
727
775
  resolved_fields = resolved_fields.union(additional_fields)
728
776
  return resolved_fields
729
777
 
@@ -732,37 +780,37 @@ class DjangoORMAdapter(AbstractORMProvider):
732
780
  ) -> nx.DiGraph:
733
781
  """
734
782
  Build a directed graph of models and their fields, focusing on direct relationships.
735
-
783
+
736
784
  Args:
737
785
  model: The Django model to build the graph for
738
786
  model_graph: An existing graph to add to (optional)
739
-
787
+
740
788
  Returns:
741
789
  nx.DiGraph: The model graph
742
790
  """
743
791
  from django.db.models.fields.related import RelatedField, ForeignObjectRel
744
-
792
+
745
793
  if model_graph is None:
746
794
  model_graph = nx.DiGraph()
747
-
795
+
748
796
  # Use the adapter's get_model_name method.
749
797
  model_name = self.get_model_name(model)
750
-
798
+
751
799
  # Add the model node if it doesn't exist.
752
800
  if not model_graph.has_node(model_name):
753
801
  model_graph.add_node(
754
802
  model_name, data=ModelNode(model_name=model_name, model=model)
755
803
  )
756
-
804
+
757
805
  # Iterate over all fields in the model.
758
806
  for field in model._meta.get_fields():
759
807
  field_name = field.name
760
-
808
+
761
809
  # Skip reverse relations for validation purposes
762
810
  # These are relationships defined on other models pointing to this model
763
811
  if isinstance(field, ForeignObjectRel):
764
812
  continue
765
-
813
+
766
814
  field_node = f"{model_name}::{field_name}"
767
815
  field_node_data = FieldNode(
768
816
  model_name=model_name,
@@ -776,18 +824,18 @@ class DjangoORMAdapter(AbstractORMProvider):
776
824
  )
777
825
  model_graph.add_node(field_node, data=field_node_data)
778
826
  model_graph.add_edge(model_name, field_node)
779
-
827
+
780
828
  if field.is_relation and field.related_model:
781
829
  related_model = field.related_model
782
830
  related_model_name = self.get_model_name(related_model)
783
831
  if not model_graph.has_node(related_model_name):
784
832
  self.build_model_graph(related_model, model_graph)
785
833
  model_graph.add_edge(field_node, related_model_name)
786
-
834
+
787
835
  # Add additional (computed) fields from the model's configuration.
788
836
  try:
789
837
  from statezero.adaptors.django.config import registry
790
-
838
+
791
839
  config = registry.get_config(model)
792
840
  for additional_field in config.additional_fields:
793
841
  add_field_name = additional_field.name
@@ -812,14 +860,16 @@ class DjangoORMAdapter(AbstractORMProvider):
812
860
  model_graph.add_edge(model_name, add_field_node)
813
861
  except ValueError:
814
862
  pass
815
-
863
+
816
864
  return model_graph
817
865
 
818
866
  def register_event_signals(self, event_bus: EventBus) -> None:
867
+ """Register Django signals for model events."""
868
+
819
869
  def pre_save_receiver(sender, instance, **kwargs):
820
870
  if not instance.pk:
821
- return # It can't be used for cache invalidation, cause there's no pk
822
-
871
+ return # It can't be used for cache invalidation, cause there's no pk
872
+
823
873
  action = ActionType.PRE_UPDATE
824
874
  try:
825
875
  event_bus.emit_event(action, instance)
@@ -836,7 +886,7 @@ class DjangoORMAdapter(AbstractORMProvider):
836
886
  logger.exception(
837
887
  "Error emitting event %s for instance %s: %s", action, instance, e
838
888
  )
839
-
889
+
840
890
  def pre_delete_receiver(sender, instance, **kwargs):
841
891
  try:
842
892
  # Use PRE_DELETE action type for cache invalidation before DB operation
@@ -858,28 +908,28 @@ class DjangoORMAdapter(AbstractORMProvider):
858
908
 
859
909
  for model in registry._models_config.keys():
860
910
  model_name = config.orm_provider.get_model_name(model)
861
-
911
+
862
912
  # Register pre_save signals (new)
863
913
  uid_pre_save = f"statezero:{model_name}:pre_save"
864
914
  pre_save.disconnect(sender=model, dispatch_uid=uid_pre_save)
865
915
  receiver(pre_save, sender=model, weak=False, dispatch_uid=uid_pre_save)(
866
916
  pre_save_receiver
867
917
  )
868
-
918
+
869
919
  # Register post_save signals
870
920
  uid_save = f"statezero:{model_name}:post_save"
871
921
  post_save.disconnect(sender=model, dispatch_uid=uid_save)
872
922
  receiver(post_save, sender=model, weak=False, dispatch_uid=uid_save)(
873
923
  post_save_receiver
874
924
  )
875
-
925
+
876
926
  # Register pre_delete signals
877
927
  uid_pre_delete = f"statezero:{model_name}:pre_delete"
878
928
  pre_delete.disconnect(sender=model, dispatch_uid=uid_pre_delete)
879
929
  receiver(pre_delete, sender=model, weak=False, dispatch_uid=uid_pre_delete)(
880
930
  pre_delete_receiver
881
931
  )
882
-
932
+
883
933
  # Register post_delete signals
884
934
  uid_delete = f"statezero:{model_name}:post_delete"
885
935
  post_delete.disconnect(sender=model, dispatch_uid=uid_delete)
@@ -888,6 +938,7 @@ class DjangoORMAdapter(AbstractORMProvider):
888
938
  )
889
939
 
890
940
  def get_model_by_name(self, model_name: str) -> Type[models.Model]:
941
+ """Retrieve the model class based on a given model name."""
891
942
  try:
892
943
  app_label, model_cls = model_name.split(".")
893
944
  model = apps.get_model(app_label, model_cls)
@@ -899,9 +950,8 @@ class DjangoORMAdapter(AbstractORMProvider):
899
950
  f"Model name '{model_name}' must be in the format 'app_label.ModelName'"
900
951
  )
901
952
 
902
- def get_model_name(
903
- self, model: Union[models.Model, Type[models.Model]]
904
- ) -> str: # type:ignore
953
+ def get_model_name(self, model: Union[models.Model, Type[models.Model]]) -> str:
954
+ """Retrieve the model name for the given model class or instance."""
905
955
  if not isinstance(model, type):
906
956
  model = model.__class__
907
957
  if hasattr(model, "_meta"):
@@ -909,7 +959,7 @@ class DjangoORMAdapter(AbstractORMProvider):
909
959
  raise ValueError(
910
960
  f"Cannot determine model name from {model} of type {type(model)}: _meta attribute is missing from the model."
911
961
  )
912
-
962
+
913
963
  def get_user(self, request):
914
- """ Return the user """
915
- return request.user
964
+ """Return the user from the request."""
965
+ return request.user