GeneralManager 0.13.1__py3-none-any.whl → 0.14.0__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.
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from types import UnionType
2
3
  from typing import (
3
4
  Any,
4
5
  Type,
@@ -8,7 +9,10 @@ from typing import (
8
9
  Optional,
9
10
  Generator,
10
11
  List,
12
+ TypedDict,
13
+ get_origin,
11
14
  )
15
+ from operator import attrgetter
12
16
  from copy import deepcopy
13
17
  from general_manager.interface.baseInterface import (
14
18
  generalManagerClassName,
@@ -19,8 +23,14 @@ from general_manager.manager.input import Input
19
23
  from general_manager.utils.filterParser import parse_filters
20
24
 
21
25
  if TYPE_CHECKING:
22
- from general_manager.manager.generalManager import GeneralManager
23
- from general_manager.interface.calculationInterface import CalculationInterface
26
+ from general_manager.api.property import GraphQLProperty
27
+
28
+
29
+ class SortedFilters(TypedDict):
30
+ prop_filters: dict[str, Any]
31
+ input_filters: dict[str, Any]
32
+ prop_excludes: dict[str, Any]
33
+ input_excludes: dict[str, Any]
24
34
 
25
35
 
26
36
  class CalculationBucket(Bucket[GeneralManagerType]):
@@ -57,12 +67,40 @@ class CalculationBucket(Bucket[GeneralManagerType]):
57
67
  "CalculationBucket can only be used with CalculationInterface subclasses"
58
68
  )
59
69
  self.input_fields = interface_class.input_fields
60
- self.filters = {} if filter_definitions is None else filter_definitions
61
- self.excludes = {} if exclude_definitions is None else exclude_definitions
62
- self._current_combinations = None
70
+ self.filter_definitions = (
71
+ {} if filter_definitions is None else filter_definitions
72
+ )
73
+ self.exclude_definitions = (
74
+ {} if exclude_definitions is None else exclude_definitions
75
+ )
76
+
77
+ properties = self._manager_class.Interface.getGraphQLProperties()
78
+ possible_values = self.transformPropertiesToInputFields(
79
+ properties, self.input_fields
80
+ )
81
+
82
+ self._filters = parse_filters(self.filter_definitions, possible_values)
83
+ self._excludes = parse_filters(self.exclude_definitions, possible_values)
84
+
85
+ self._data = None
63
86
  self.sort_key = sort_key
64
87
  self.reverse = reverse
65
88
 
89
+ def __eq__(self, other: object) -> bool:
90
+ """
91
+ Checks if this Bucket is equal to another by comparing class, data, and manager class.
92
+
93
+ Returns:
94
+ True if both objects are of the same class and have equal internal data and manager class; otherwise, False.
95
+ """
96
+ if not isinstance(other, self.__class__):
97
+ return False
98
+ return (
99
+ self.filter_definitions == other.filter_definitions
100
+ and self.exclude_definitions == other.exclude_definitions
101
+ and self._manager_class == other._manager_class
102
+ )
103
+
66
104
  def __reduce__(self) -> generalManagerClassName | tuple[Any, ...]:
67
105
  """
68
106
  Prepares the CalculationBucket instance for pickling by returning its reconstruction data.
@@ -74,12 +112,12 @@ class CalculationBucket(Bucket[GeneralManagerType]):
74
112
  self.__class__,
75
113
  (
76
114
  self._manager_class,
77
- self.filters,
78
- self.excludes,
115
+ self.filter_definitions,
116
+ self.exclude_definitions,
79
117
  self.sort_key,
80
118
  self.reverse,
81
119
  ),
82
- {"current_combinations": self._current_combinations},
120
+ {"data": self._data},
83
121
  )
84
122
 
85
123
  def __setstate__(self, state: dict[str, Any]) -> None:
@@ -89,20 +127,20 @@ class CalculationBucket(Bucket[GeneralManagerType]):
89
127
  Args:
90
128
  state: A dictionary containing the state of the instance, including current combinations.
91
129
  """
92
- self._current_combinations = state.get("current_combinations")
130
+ self._data = state.get("data")
93
131
 
94
132
  def __or__(
95
133
  self,
96
134
  other: Bucket[GeneralManagerType] | GeneralManagerType,
97
135
  ) -> CalculationBucket[GeneralManagerType]:
98
136
  """
99
- Combine this CalculationBucket with another bucket or a manager instance of the same manager class.
100
-
101
- If combined with a manager instance, returns a bucket filtered to that instance's identification. If combined with another CalculationBucket of the same manager class, returns a new bucket containing only the filters and excludes that are present and identical in both buckets.
102
-
137
+ Combine this CalculationBucket with another bucket or manager instance of the same manager class.
138
+
139
+ If combined with a manager instance, returns a bucket filtered to that manager's identification. If combined with another CalculationBucket of the same manager class, returns a new bucket containing only the filters and excludes that are present and identical in both buckets.
140
+
103
141
  Raises:
104
142
  ValueError: If the other object is not a CalculationBucket or manager of the same class.
105
-
143
+
106
144
  Returns:
107
145
  CalculationBucket[GeneralManagerType]: A new CalculationBucket representing the intersection of filters and excludes, or a filtered bucket for the given manager instance.
108
146
  """
@@ -117,14 +155,16 @@ class CalculationBucket(Bucket[GeneralManagerType]):
117
155
 
118
156
  combined_filters = {
119
157
  key: value
120
- for key, value in self.filters.items()
121
- if key in other.filters and value == other.filters[key]
158
+ for key, value in self.filter_definitions.items()
159
+ if key in other.filter_definitions
160
+ and value == other.filter_definitions[key]
122
161
  }
123
162
 
124
163
  combined_excludes = {
125
164
  key: value
126
- for key, value in self.excludes.items()
127
- if key in other.excludes and value == other.excludes[key]
165
+ for key, value in self.exclude_definitions.items()
166
+ if key in other.exclude_definitions
167
+ and value == other.exclude_definitions[key]
128
168
  }
129
169
 
130
170
  return CalculationBucket(
@@ -158,7 +198,41 @@ class CalculationBucket(Bucket[GeneralManagerType]):
158
198
  """
159
199
  Returns a concise string representation of the CalculationBucket, including the manager class name, filters, excludes, sort key, and sort order.
160
200
  """
161
- return f"{self.__class__.__name__}({self._manager_class.__name__}, {self.filters}, {self.excludes}, {self.sort_key}, {self.reverse})"
201
+ return f"{self.__class__.__name__}({self._manager_class.__name__}, {self.filter_definitions}, {self.exclude_definitions}, {self.sort_key}, {self.reverse})"
202
+
203
+ @staticmethod
204
+ def transformPropertiesToInputFields(
205
+ properties: dict[str, GraphQLProperty], input_fields: dict[str, Input]
206
+ ) -> dict[str, Input]:
207
+ """
208
+ Returns a dictionary of possible values for each input field based on the provided properties.
209
+
210
+ This method analyzes the properties and input fields to determine valid values for each input parameter.
211
+
212
+ Args:
213
+ properties (dict[str, Any]): The GraphQL properties of the manager class.
214
+ input_fields (dict[str, Any]): The input fields to analyze.
215
+
216
+ Returns:
217
+ dict[str, Any]: A dictionary mapping input field names to their possible values.
218
+ """
219
+ parsed_inputs = {**input_fields}
220
+ for prop_name, prop in properties.items():
221
+ type_hint = prop.graphql_type_hint
222
+ origin = get_origin(type_hint)
223
+ if origin in (Union, UnionType):
224
+ type_hint = type_hint.__args__[0] if type_hint.__args__ else str # type: ignore
225
+
226
+ elif isinstance(type_hint, type) and issubclass(
227
+ type_hint, (list, tuple, set, dict)
228
+ ):
229
+ type_hint: type = (
230
+ type_hint.__args__[0] if hasattr(type_hint, "__args__") else str # type: ignore
231
+ )
232
+ prop_input = Input(type=type_hint, possible_values=None, depends_on=None)
233
+ parsed_inputs[prop_name] = prop_input
234
+
235
+ return parsed_inputs
162
236
 
163
237
  def filter(self, **kwargs: Any) -> CalculationBucket:
164
238
  """
@@ -166,10 +240,14 @@ class CalculationBucket(Bucket[GeneralManagerType]):
166
240
 
167
241
  Merges the provided filter criteria with existing filters to further restrict valid input combinations.
168
242
  """
169
- filters = self.filters.copy()
170
- excludes = self.excludes.copy()
171
- filters.update(parse_filters(kwargs, self.input_fields))
172
- return CalculationBucket(self._manager_class, filters, excludes)
243
+ return CalculationBucket(
244
+ manager_class=self._manager_class,
245
+ filter_definitions={
246
+ **self.filter_definitions.copy(),
247
+ **kwargs,
248
+ },
249
+ exclude_definitions=self.exclude_definitions.copy(),
250
+ )
173
251
 
174
252
  def exclude(self, **kwargs: Any) -> CalculationBucket:
175
253
  """
@@ -177,10 +255,14 @@ class CalculationBucket(Bucket[GeneralManagerType]):
177
255
 
178
256
  Keyword arguments specify input values to exclude from the generated combinations.
179
257
  """
180
- filters = self.filters.copy()
181
- excludes = self.excludes.copy()
182
- excludes.update(parse_filters(kwargs, self.input_fields))
183
- return CalculationBucket(self._manager_class, filters, excludes)
258
+ return CalculationBucket(
259
+ manager_class=self._manager_class,
260
+ filter_definitions=self.filter_definitions.copy(),
261
+ exclude_definitions={
262
+ **self.exclude_definitions.copy(),
263
+ **kwargs,
264
+ },
265
+ )
184
266
 
185
267
  def all(self) -> CalculationBucket:
186
268
  """
@@ -201,6 +283,30 @@ class CalculationBucket(Bucket[GeneralManagerType]):
201
283
  for combo in combinations:
202
284
  yield self._manager_class(**combo)
203
285
 
286
+ def _sortFilters(self, sorted_inputs: List[str]) -> SortedFilters:
287
+ input_filters: dict[str, dict] = {}
288
+ prop_filters: dict[str, dict] = {}
289
+ input_excludes: dict[str, dict] = {}
290
+ prop_excludes: dict[str, dict] = {}
291
+
292
+ for filter_name, filter_def in self._filters.items():
293
+ if filter_name in sorted_inputs:
294
+ input_filters[filter_name] = filter_def
295
+ else:
296
+ prop_filters[filter_name] = filter_def
297
+ for exclude_name, exclude_def in self._excludes.items():
298
+ if exclude_name in sorted_inputs:
299
+ input_excludes[exclude_name] = exclude_def
300
+ else:
301
+ prop_excludes[exclude_name] = exclude_def
302
+
303
+ return {
304
+ "prop_filters": prop_filters,
305
+ "input_filters": input_filters,
306
+ "prop_excludes": prop_excludes,
307
+ "input_excludes": input_excludes,
308
+ }
309
+
204
310
  def generate_combinations(self) -> List[dict[str, Any]]:
205
311
  """
206
312
  Generates and caches all valid input combinations based on filters, exclusions, and sorting.
@@ -209,28 +315,37 @@ class CalculationBucket(Bucket[GeneralManagerType]):
209
315
  A list of dictionaries, each representing a unique combination of input values that satisfy the current filters, exclusions, and sorting order.
210
316
  """
211
317
 
212
- def key_func(item: dict[str, Any]) -> tuple:
213
- return tuple(item[key] for key in sort_key)
318
+ def key_func(manager_obj: GeneralManagerType) -> tuple:
319
+ getters = [attrgetter(key) for key in sort_key]
320
+ return tuple(getter(manager_obj) for getter in getters)
214
321
 
215
- if self._current_combinations is None:
216
- # Implementierung ähnlich wie im InputManager
322
+ if self._data is None:
217
323
  sorted_inputs = self.topological_sort_inputs()
218
- current_combinations = self._generate_combinations(
219
- sorted_inputs, self.filters, self.excludes
324
+ sorted_filters = self._sortFilters(sorted_inputs)
325
+ current_combinations = self._generate_input_combinations(
326
+ sorted_inputs,
327
+ sorted_filters["input_filters"],
328
+ sorted_filters["input_excludes"],
220
329
  )
330
+ manager_combinations = self._generate_prop_combinations(
331
+ current_combinations,
332
+ sorted_filters["prop_filters"],
333
+ sorted_filters["prop_excludes"],
334
+ )
335
+
221
336
  if self.sort_key is not None:
222
337
  sort_key = self.sort_key
223
338
  if isinstance(sort_key, str):
224
339
  sort_key = (sort_key,)
225
- current_combinations = sorted(
226
- current_combinations,
340
+ manager_combinations = sorted(
341
+ manager_combinations,
227
342
  key=key_func,
228
343
  )
229
344
  if self.reverse:
230
- current_combinations.reverse()
231
- self._current_combinations = current_combinations
345
+ manager_combinations.reverse()
346
+ self._data = [manager.identification for manager in manager_combinations]
232
347
 
233
- return self._current_combinations
348
+ return self._data
234
349
 
235
350
  def topological_sort_inputs(self) -> List[str]:
236
351
  """
@@ -306,15 +421,15 @@ class CalculationBucket(Bucket[GeneralManagerType]):
306
421
  """
307
422
  if callable(input_field.possible_values):
308
423
  depends_on = input_field.depends_on
309
- dep_values = {dep_name: current_combo[dep_name] for dep_name in depends_on}
310
- possible_values = input_field.possible_values(**dep_values)
424
+ dep_values = [current_combo[dep_name] for dep_name in depends_on]
425
+ possible_values = input_field.possible_values(*dep_values)
311
426
  elif isinstance(input_field.possible_values, (Iterable, Bucket)):
312
427
  possible_values = input_field.possible_values
313
428
  else:
314
429
  raise TypeError(f"Invalid possible_values for input '{key_name}'")
315
430
  return possible_values
316
431
 
317
- def _generate_combinations(
432
+ def _generate_input_combinations(
318
433
  self,
319
434
  sorted_inputs: List[str],
320
435
  filters: dict[str, dict],
@@ -381,6 +496,45 @@ class CalculationBucket(Bucket[GeneralManagerType]):
381
496
 
382
497
  return list(helper(0, {}))
383
498
 
499
+ def _generate_prop_combinations(
500
+ self,
501
+ current_combos: list[dict[str, Any]],
502
+ prop_filters: dict[str, Any],
503
+ prop_excludes: dict[str, Any],
504
+ ) -> list[GeneralManagerType]:
505
+
506
+ prop_filter_needed = set(prop_filters.keys()) | set(prop_excludes.keys())
507
+ manager_combinations = [
508
+ self._manager_class(**combo) for combo in current_combos
509
+ ]
510
+ if not prop_filter_needed:
511
+ return manager_combinations
512
+
513
+ # Apply property filters and exclusions
514
+ filtered_combos = []
515
+ for manager in manager_combinations:
516
+ keep = True
517
+ # include filters
518
+ for prop_name, defs in prop_filters.items():
519
+ for func in defs.get("filter_funcs", []):
520
+ if not func(getattr(manager, prop_name)):
521
+ keep = False
522
+ break
523
+ if not keep:
524
+ break
525
+ # excludes
526
+ if keep:
527
+ for prop_name, defs in prop_excludes.items():
528
+ for func in defs.get("filter_funcs", []):
529
+ if func(getattr(manager, prop_name)):
530
+ keep = False
531
+ break
532
+ if not keep:
533
+ break
534
+ if keep:
535
+ filtered_combos.append(manager)
536
+ return filtered_combos
537
+
384
538
  def first(self) -> GeneralManagerType | None:
385
539
  """
386
540
  Returns the first generated manager instance, or None if no combinations exist.
@@ -425,12 +579,12 @@ class CalculationBucket(Bucket[GeneralManagerType]):
425
579
  if isinstance(result, list):
426
580
  new_bucket = CalculationBucket(
427
581
  self._manager_class,
428
- self.filters.copy(),
429
- self.excludes.copy(),
582
+ self.filter_definitions.copy(),
583
+ self.exclude_definitions.copy(),
430
584
  self.sort_key,
431
585
  self.reverse,
432
586
  )
433
- new_bucket._current_combinations = result
587
+ new_bucket._data = result
434
588
  return new_bucket
435
589
  return self._manager_class(**result)
436
590
 
@@ -482,7 +636,11 @@ class CalculationBucket(Bucket[GeneralManagerType]):
482
636
  CalculationBucket: A new bucket instance sorted according to the specified key and order.
483
637
  """
484
638
  return CalculationBucket(
485
- self._manager_class, self.filters, self.excludes, key, reverse
639
+ self._manager_class,
640
+ self.filter_definitions,
641
+ self.exclude_definitions,
642
+ key,
643
+ reverse,
486
644
  )
487
645
 
488
646
  def none(self) -> CalculationBucket[GeneralManagerType]:
@@ -4,6 +4,7 @@ from django.db import models
4
4
  from general_manager.interface.baseInterface import (
5
5
  GeneralManagerType,
6
6
  )
7
+ from general_manager.utils.filterParser import create_filter_function
7
8
  from general_manager.bucket.baseBucket import Bucket
8
9
 
9
10
  from general_manager.manager.generalManager import GeneralManager
@@ -94,19 +95,78 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
94
95
  kwarg_filter[key].append(value)
95
96
  return kwarg_filter
96
97
 
98
+ def __parseFilterDeifintions(self, **kwargs: Any):
99
+ annotations: dict[str, Any] = {}
100
+ orm_kwargs: dict[str, list[Any]] = {}
101
+ python_filters: list[tuple[str, Any, str]] = []
102
+ properties = self._manager_class.Interface.getGraphQLProperties()
103
+
104
+ for k, v in kwargs.items():
105
+ root = k.split("__")[0]
106
+ if root in properties:
107
+ if not properties[root].filterable:
108
+ raise ValueError(
109
+ f"Property '{root}' is not filterable in {self._manager_class.__name__}"
110
+ )
111
+ prop = properties[root]
112
+ if prop.query_annotation is not None:
113
+ annotations[root] = prop.query_annotation
114
+ orm_kwargs[k] = v
115
+ else:
116
+ python_filters.append((k, v, root))
117
+ else:
118
+ orm_kwargs[k] = v
119
+
120
+ return annotations, orm_kwargs, python_filters
121
+
122
+ def __parsePythonFilters(
123
+ self, query_set: models.QuerySet, python_filters: list[tuple[str, Any, str]]
124
+ ) -> list[int]:
125
+ ids: list[int] = []
126
+ for obj in query_set:
127
+ inst = self._manager_class(obj.pk)
128
+ keep = True
129
+ for k, val, root in python_filters:
130
+ lookup = k.split("__", 1)[1] if "__" in k else ""
131
+ func = create_filter_function(lookup, val)
132
+ if not func(getattr(inst, root)):
133
+ keep = False
134
+ break
135
+ if keep:
136
+ ids.append(obj.pk)
137
+ return ids
138
+
97
139
  def filter(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
98
140
  """
99
141
  Returns a new bucket with manager instances matching the combined filter criteria.
100
142
 
101
143
  Additional filter arguments are merged with any existing filters to further restrict the queryset, producing a new DatabaseBucket instance.
102
144
  """
103
- merged_filter = self.__mergeFilterDefinitions(self.filters, **kwargs)
104
- return self.__class__(
105
- self._data.filter(**kwargs),
106
- self._manager_class,
107
- merged_filter,
108
- self.excludes,
145
+ annotations, orm_kwargs, python_filters = self.__parseFilterDeifintions(
146
+ **kwargs
109
147
  )
148
+ qs = self._data
149
+ if annotations:
150
+ other_annotations: dict[str, Any] = {}
151
+ for key, value in annotations.items():
152
+ if not callable(value):
153
+ other_annotations[key] = value
154
+ continue
155
+ qs = value(qs)
156
+ if not isinstance(qs, models.QuerySet):
157
+ raise TypeError("Query annotation must return a Django QuerySet")
158
+ qs = qs.annotate(**other_annotations)
159
+ try:
160
+ qs = qs.filter(**orm_kwargs)
161
+ except Exception as e:
162
+ raise ValueError(f"Error filtering queryset: {e}")
163
+
164
+ if python_filters:
165
+ ids = self.__parsePythonFilters(qs, python_filters)
166
+ qs = qs.filter(pk__in=ids)
167
+
168
+ merged_filter = self.__mergeFilterDefinitions(self.filters, **kwargs)
169
+ return self.__class__(qs, self._manager_class, merged_filter, self.excludes)
110
170
 
111
171
  def exclude(self, **kwargs: Any) -> DatabaseBucket[GeneralManagerType]:
112
172
  """
@@ -114,13 +174,28 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
114
174
 
115
175
  Keyword arguments specify field lookups to exclude from the queryset. The resulting bucket contains only items that do not satisfy these exclusion filters.
116
176
  """
117
- merged_exclude = self.__mergeFilterDefinitions(self.excludes, **kwargs)
118
- return self.__class__(
119
- self._data.exclude(**kwargs),
120
- self._manager_class,
121
- self.filters,
122
- merged_exclude,
177
+ annotations, orm_kwargs, python_filters = self.__parseFilterDeifintions(
178
+ **kwargs
123
179
  )
180
+ qs = self._data
181
+ if annotations:
182
+ other_annotations: dict[str, Any] = {}
183
+ for key, value in annotations.items():
184
+ if not callable(value):
185
+ other_annotations[key] = value
186
+ continue
187
+ qs = value(qs)
188
+ if not isinstance(qs, models.QuerySet):
189
+ raise TypeError("Query annotation must return a Django QuerySet")
190
+ qs = qs.annotate(**other_annotations)
191
+ qs = qs.exclude(**orm_kwargs)
192
+
193
+ if python_filters:
194
+ ids = self.__parsePythonFilters(qs, python_filters)
195
+ qs = qs.exclude(pk__in=ids)
196
+
197
+ merged_exclude = self.__mergeFilterDefinitions(self.excludes, **kwargs)
198
+ return self.__class__(qs, self._manager_class, self.filters, merged_exclude)
124
199
 
125
200
  def first(self) -> GeneralManagerType | None:
126
201
  """
@@ -184,11 +259,17 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
184
259
  """
185
260
  return self._data.count()
186
261
 
262
+ def __str__(self) -> str:
263
+ """
264
+ Returns a string representation of the bucket, showing the manager class name and the underlying queryset.
265
+ """
266
+ return f"{self._manager_class.__name__}Bucket {self._data} ({len(self._data)} items)"
267
+
187
268
  def __repr__(self) -> str:
188
269
  """
189
270
  Returns a string representation of the bucket, showing the manager class name and the underlying queryset.
190
271
  """
191
- return f"{self._manager_class.__name__}Bucket ({self._data})"
272
+ return f"DatabaseBucket ({self._data}, manager_class={self._manager_class.__name__}, filters={self.filters}, excludes={self.excludes})"
192
273
 
193
274
  def __contains__(self, item: GeneralManagerType | models.Model) -> bool:
194
275
  """
@@ -222,11 +303,61 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
222
303
  """
223
304
  if isinstance(key, str):
224
305
  key = (key,)
225
- if reverse:
226
- sorted_data = self._data.order_by(*[f"-{k}" for k in key])
306
+ properties = self._manager_class.Interface.getGraphQLProperties()
307
+ annotations: dict[str, Any] = {}
308
+ python_keys: list[str] = []
309
+ qs = self._data
310
+ for k in key:
311
+ if k in properties:
312
+ prop = properties[k]
313
+ if not prop.sortable:
314
+ raise ValueError(
315
+ f"Property '{k}' is not sortable in {self._manager_class.__name__}"
316
+ )
317
+ if prop.query_annotation is not None:
318
+ if callable(prop.query_annotation):
319
+ qs = prop.query_annotation(qs)
320
+ else:
321
+ annotations[k] = prop.query_annotation
322
+ else:
323
+ python_keys.append(k)
324
+ if not isinstance(qs, models.QuerySet):
325
+ raise TypeError("Query annotation must return a Django QuerySet")
326
+ if annotations:
327
+ qs = qs.annotate(**annotations)
328
+
329
+ if python_keys:
330
+ objs = list(qs)
331
+
332
+ def key_func(obj):
333
+ inst = self._manager_class(obj.pk)
334
+ values = []
335
+ for k in key:
336
+ if k in properties:
337
+ if k in python_keys:
338
+ values.append(getattr(inst, k))
339
+ else:
340
+ values.append(getattr(obj, k))
341
+ else:
342
+ values.append(getattr(obj, k))
343
+ return tuple(values)
344
+
345
+ objs.sort(key=key_func, reverse=reverse)
346
+ ordered_ids = [obj.pk for obj in objs]
347
+ case = models.Case(
348
+ *[models.When(pk=pk, then=pos) for pos, pk in enumerate(ordered_ids)],
349
+ output_field=models.IntegerField(),
350
+ )
351
+ qs = qs.filter(pk__in=ordered_ids).annotate(_order=case).order_by("_order")
227
352
  else:
228
- sorted_data = self._data.order_by(*key)
229
- return self.__class__(sorted_data, self._manager_class)
353
+ order_fields = [f"-{k}" if reverse else k for k in key]
354
+ try:
355
+ qs = qs.order_by(*order_fields)
356
+ except Exception as e:
357
+ raise ValueError(f"Error ordering queryset: {e}")
358
+
359
+ return self.__class__(qs, self._manager_class)
360
+
230
361
 
231
362
  def none(self) -> DatabaseBucket[GeneralManagerType]:
232
363
  """
@@ -237,3 +368,4 @@ class DatabaseBucket(Bucket[GeneralManagerType]):
237
368
  own = self.all()
238
369
  own._data = own._data.none()
239
370
  return own
371
+
@@ -13,7 +13,9 @@ from typing import (
13
13
  from datetime import datetime
14
14
  from django.conf import settings
15
15
  from django.db.models import Model
16
+
16
17
  from general_manager.utils import args_to_kwargs
18
+ from general_manager.api.property import GraphQLProperty
17
19
 
18
20
  if TYPE_CHECKING:
19
21
  from general_manager.manager.input import Input
@@ -60,8 +62,8 @@ class InterfaceBase(ABC):
60
62
  input_fields: dict[str, Input]
61
63
 
62
64
  def __init__(self, *args: Any, **kwargs: Any):
63
- self.identification = self.parseInputFieldsToIdentification(*args, **kwargs)
64
- self.formatIdentification()
65
+ identification = self.parseInputFieldsToIdentification(*args, **kwargs)
66
+ self.identification = self.formatIdentification(identification)
65
67
 
66
68
  def parseInputFieldsToIdentification(
67
69
  self,
@@ -113,18 +115,27 @@ class InterfaceBase(ABC):
113
115
  )
114
116
  return identification
115
117
 
116
- def formatIdentification(self) -> dict[str, Any]:
118
+ @staticmethod
119
+ def formatIdentification(identification: dict[str, Any]) -> dict[str, Any]:
117
120
  from general_manager.manager.generalManager import GeneralManager
118
121
 
119
- for key, value in self.identification.items():
122
+ for key, value in identification.items():
120
123
  if isinstance(value, GeneralManager):
121
- self.identification[key] = value.identification
124
+ identification[key] = value.identification
122
125
  elif isinstance(value, (list, tuple)):
123
- self.identification[key] = [
124
- v.identification if isinstance(v, GeneralManager) else v
125
- for v in value
126
- ]
127
- return self.identification
126
+ identification[key] = []
127
+ for v in value:
128
+ if isinstance(v, GeneralManager):
129
+ identification[key].append(v.identification)
130
+ elif isinstance(v, dict):
131
+ identification[key].append(
132
+ InterfaceBase.formatIdentification(v)
133
+ )
134
+ else:
135
+ identification[key].append(v)
136
+ elif isinstance(value, dict):
137
+ identification[key] = InterfaceBase.formatIdentification(value)
138
+ return identification
128
139
 
129
140
  def _process_input(
130
141
  self, name: str, value: Any, identification: dict[str, Any]
@@ -184,6 +195,17 @@ class InterfaceBase(ABC):
184
195
  def getAttributes(cls) -> dict[str, Any]:
185
196
  raise NotImplementedError
186
197
 
198
+ @classmethod
199
+ def getGraphQLProperties(cls) -> dict[str, GraphQLProperty]:
200
+ """Return GraphQL properties defined on the parent manager."""
201
+ if not hasattr(cls, "_parent_class"):
202
+ return {}
203
+ return {
204
+ name: prop
205
+ for name, prop in vars(cls._parent_class).items()
206
+ if isinstance(prop, GraphQLProperty)
207
+ }
208
+
187
209
  @classmethod
188
210
  @abstractmethod
189
211
  def filter(cls, **kwargs: Any) -> Bucket[Any]: