GeneralManager 0.13.1__py3-none-any.whl → 0.14.1__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.
- general_manager/api/graphql.py +162 -67
- general_manager/api/mutation.py +15 -7
- general_manager/api/property.py +95 -10
- general_manager/apps.py +14 -11
- general_manager/bucket/calculationBucket.py +203 -45
- general_manager/bucket/databaseBucket.py +149 -17
- general_manager/interface/baseInterface.py +32 -10
- general_manager/permission/mutationPermission.py +3 -1
- general_manager/utils/argsToKwargs.py +7 -7
- {generalmanager-0.13.1.dist-info → generalmanager-0.14.1.dist-info}/METADATA +29 -3
- {generalmanager-0.13.1.dist-info → generalmanager-0.14.1.dist-info}/RECORD +14 -14
- generalmanager-0.14.1.dist-info/licenses/LICENSE +21 -0
- generalmanager-0.13.1.dist-info/licenses/LICENSE +0 -29
- {generalmanager-0.13.1.dist-info → generalmanager-0.14.1.dist-info}/WHEEL +0 -0
- {generalmanager-0.13.1.dist-info → generalmanager-0.14.1.dist-info}/top_level.txt +0 -0
@@ -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.
|
23
|
-
|
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.
|
61
|
-
|
62
|
-
|
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.
|
78
|
-
self.
|
115
|
+
self.filter_definitions,
|
116
|
+
self.exclude_definitions,
|
79
117
|
self.sort_key,
|
80
118
|
self.reverse,
|
81
119
|
),
|
82
|
-
{"
|
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.
|
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
|
100
|
-
|
101
|
-
If combined with a manager instance, returns a bucket filtered to that
|
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.
|
121
|
-
if key in other.
|
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.
|
127
|
-
if key in other.
|
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.
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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(
|
213
|
-
|
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.
|
216
|
-
# Implementierung ähnlich wie im InputManager
|
322
|
+
if self._data is None:
|
217
323
|
sorted_inputs = self.topological_sort_inputs()
|
218
|
-
|
219
|
-
|
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
|
-
|
226
|
-
|
340
|
+
manager_combinations = sorted(
|
341
|
+
manager_combinations,
|
227
342
|
key=key_func,
|
228
343
|
)
|
229
344
|
if self.reverse:
|
230
|
-
|
231
|
-
self.
|
345
|
+
manager_combinations.reverse()
|
346
|
+
self._data = [manager.identification for manager in manager_combinations]
|
232
347
|
|
233
|
-
return self.
|
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 =
|
310
|
-
possible_values = input_field.possible_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
|
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.
|
429
|
-
self.
|
582
|
+
self.filter_definitions.copy(),
|
583
|
+
self.exclude_definitions.copy(),
|
430
584
|
self.sort_key,
|
431
585
|
self.reverse,
|
432
586
|
)
|
433
|
-
new_bucket.
|
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,
|
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
|
-
|
104
|
-
|
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
|
-
|
118
|
-
|
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__}
|
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
|
-
|
226
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
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
|
-
|
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
|
122
|
+
for key, value in identification.items():
|
120
123
|
if isinstance(value, GeneralManager):
|
121
|
-
|
124
|
+
identification[key] = value.identification
|
122
125
|
elif isinstance(value, (list, tuple)):
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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]:
|