GeneralManager 0.5.1__py3-none-any.whl → 0.6.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.
- general_manager/api/graphql.py +2 -1
- general_manager/bucket/baseBucket.py +240 -0
- general_manager/bucket/calculationBucket.py +477 -0
- general_manager/bucket/databaseBucket.py +235 -0
- general_manager/bucket/groupBucket.py +296 -0
- general_manager/cache/modelDependencyCollector.py +5 -8
- general_manager/interface/__init__.py +0 -3
- general_manager/interface/baseInterface.py +28 -105
- general_manager/interface/calculationInterface.py +12 -299
- general_manager/interface/databaseBasedInterface.py +538 -0
- general_manager/interface/databaseInterface.py +16 -655
- general_manager/interface/readOnlyInterface.py +107 -0
- general_manager/manager/generalManager.py +11 -10
- general_manager/manager/groupManager.py +12 -187
- general_manager/manager/meta.py +19 -5
- general_manager/permission/basePermission.py +4 -6
- {generalmanager-0.5.1.dist-info → generalmanager-0.6.0.dist-info}/METADATA +43 -43
- {generalmanager-0.5.1.dist-info → generalmanager-0.6.0.dist-info}/RECORD +21 -15
- {generalmanager-0.5.1.dist-info → generalmanager-0.6.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.5.1.dist-info → generalmanager-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.5.1.dist-info → generalmanager-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import (
|
3
|
+
Any,
|
4
|
+
Type,
|
5
|
+
TYPE_CHECKING,
|
6
|
+
Iterable,
|
7
|
+
Union,
|
8
|
+
Optional,
|
9
|
+
Generator,
|
10
|
+
List,
|
11
|
+
)
|
12
|
+
from general_manager.interface.baseInterface import (
|
13
|
+
generalManagerClassName,
|
14
|
+
GeneralManagerType,
|
15
|
+
)
|
16
|
+
from general_manager.bucket.baseBucket import Bucket
|
17
|
+
from general_manager.manager.input import Input
|
18
|
+
from general_manager.auxiliary.filterParser import parse_filters
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from general_manager.manager.generalManager import GeneralManager
|
22
|
+
|
23
|
+
|
24
|
+
class CalculationBucket(Bucket[GeneralManagerType]):
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
manager_class: Type[GeneralManagerType],
|
28
|
+
filter_definitions: Optional[dict[str, dict]] = None,
|
29
|
+
exclude_definitions: Optional[dict[str, dict]] = None,
|
30
|
+
sort_key: Optional[Union[str, tuple[str]]] = None,
|
31
|
+
reverse: bool = False,
|
32
|
+
):
|
33
|
+
"""
|
34
|
+
Initializes a CalculationBucket for managing calculation input combinations.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
manager_class: The manager class whose interface must inherit from CalculationInterface.
|
38
|
+
filter_definitions: Optional filters to apply to input combinations.
|
39
|
+
exclude_definitions: Optional exclusions to remove certain input combinations.
|
40
|
+
sort_key: Optional key or tuple of keys to sort the generated combinations.
|
41
|
+
reverse: If True, reverses the sorting order.
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
TypeError: If the manager class interface does not inherit from CalculationInterface.
|
45
|
+
"""
|
46
|
+
from general_manager.interface.calculationInterface import (
|
47
|
+
CalculationInterface,
|
48
|
+
)
|
49
|
+
|
50
|
+
super().__init__(manager_class)
|
51
|
+
|
52
|
+
interface_class = manager_class.Interface
|
53
|
+
if not issubclass(interface_class, CalculationInterface):
|
54
|
+
raise TypeError(
|
55
|
+
"CalculationBucket can only be used with CalculationInterface subclasses"
|
56
|
+
)
|
57
|
+
self.input_fields = interface_class.input_fields
|
58
|
+
self.filters = {} if filter_definitions is None else filter_definitions
|
59
|
+
self.excludes = {} if exclude_definitions is None else exclude_definitions
|
60
|
+
self._current_combinations = None
|
61
|
+
self.sort_key = sort_key
|
62
|
+
self.reverse = reverse
|
63
|
+
|
64
|
+
def __reduce__(self) -> generalManagerClassName | tuple[Any, ...]:
|
65
|
+
"""
|
66
|
+
Prepares the CalculationBucket instance for pickling by returning its reconstruction data.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
A tuple containing the class and a tuple of initialization arguments needed to recreate the instance.
|
70
|
+
"""
|
71
|
+
return (
|
72
|
+
self.__class__,
|
73
|
+
(
|
74
|
+
self._manager_class,
|
75
|
+
self.filters,
|
76
|
+
self.excludes,
|
77
|
+
self.sort_key,
|
78
|
+
self.reverse,
|
79
|
+
),
|
80
|
+
{"current_combinations": self._current_combinations},
|
81
|
+
)
|
82
|
+
|
83
|
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
84
|
+
"""
|
85
|
+
Restores the CalculationBucket instance from its pickled state.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
state: A dictionary containing the state of the instance, including current combinations.
|
89
|
+
"""
|
90
|
+
self._current_combinations = state.get("current_combinations")
|
91
|
+
|
92
|
+
def __or__(
|
93
|
+
self, other: Bucket[GeneralManagerType] | GeneralManager[GeneralManagerType]
|
94
|
+
) -> CalculationBucket[GeneralManagerType]:
|
95
|
+
"""
|
96
|
+
Combines this CalculationBucket with another bucket or manager of the same type.
|
97
|
+
|
98
|
+
If combined with a manager instance, returns a bucket filtered to that manager's identification.
|
99
|
+
If combined with another CalculationBucket of the same manager class, returns a new bucket with filters and excludes that are present and identical in both.
|
100
|
+
|
101
|
+
Raises:
|
102
|
+
ValueError: If the other object is not a CalculationBucket or manager of the same class.
|
103
|
+
"""
|
104
|
+
from general_manager.manager.generalManager import GeneralManager
|
105
|
+
|
106
|
+
if isinstance(other, GeneralManager) and other.__class__ == self._manager_class:
|
107
|
+
return self.__or__(self.filter(id__in=[other.identification]))
|
108
|
+
if not isinstance(other, self.__class__):
|
109
|
+
raise ValueError("Cannot combine different bucket types")
|
110
|
+
if self._manager_class != other._manager_class:
|
111
|
+
raise ValueError("Cannot combine different manager classes")
|
112
|
+
|
113
|
+
combined_filters = {
|
114
|
+
key: value
|
115
|
+
for key, value in self.filters.items()
|
116
|
+
if key in other.filters and value == other.filters[key]
|
117
|
+
}
|
118
|
+
|
119
|
+
combined_excludes = {
|
120
|
+
key: value
|
121
|
+
for key, value in self.excludes.items()
|
122
|
+
if key in other.excludes and value == other.excludes[key]
|
123
|
+
}
|
124
|
+
|
125
|
+
return CalculationBucket(
|
126
|
+
self._manager_class,
|
127
|
+
combined_filters,
|
128
|
+
combined_excludes,
|
129
|
+
)
|
130
|
+
|
131
|
+
def __str__(self) -> str:
|
132
|
+
"""
|
133
|
+
Returns a string representation of the bucket, listing up to five calculation manager instances with their input combinations.
|
134
|
+
|
135
|
+
If more than five combinations exist, an ellipsis is appended to indicate additional entries.
|
136
|
+
"""
|
137
|
+
PRINT_MAX = 5
|
138
|
+
combinations = self.generate_combinations()
|
139
|
+
prefix = f"CalculationBucket ({len(combinations)})["
|
140
|
+
main = ",".join(
|
141
|
+
[
|
142
|
+
f"{self._manager_class.__name__}(**{comb})"
|
143
|
+
for comb in combinations[:PRINT_MAX]
|
144
|
+
]
|
145
|
+
)
|
146
|
+
sufix = "]"
|
147
|
+
if len(combinations) > PRINT_MAX:
|
148
|
+
sufix = ", ...]"
|
149
|
+
|
150
|
+
return f"{prefix}{main}{sufix}"
|
151
|
+
|
152
|
+
def __repr__(self) -> str:
|
153
|
+
"""
|
154
|
+
Returns a string representation of the CalculationBucket, showing a preview of its contents.
|
155
|
+
"""
|
156
|
+
return self.__str__()
|
157
|
+
|
158
|
+
def filter(self, **kwargs: Any) -> CalculationBucket:
|
159
|
+
"""
|
160
|
+
Returns a new CalculationBucket with additional filters applied to input combinations.
|
161
|
+
|
162
|
+
Additional filters are merged with existing filters, narrowing the set of valid input configurations.
|
163
|
+
"""
|
164
|
+
filters = self.filters.copy()
|
165
|
+
excludes = self.excludes.copy()
|
166
|
+
filters.update(parse_filters(kwargs, self.input_fields))
|
167
|
+
return CalculationBucket(self._manager_class, filters, excludes)
|
168
|
+
|
169
|
+
def exclude(self, **kwargs: Any) -> CalculationBucket:
|
170
|
+
"""
|
171
|
+
Returns a new CalculationBucket with additional exclusion criteria applied.
|
172
|
+
|
173
|
+
Keyword arguments specify input values to exclude from the generated combinations.
|
174
|
+
"""
|
175
|
+
filters = self.filters.copy()
|
176
|
+
excludes = self.excludes.copy()
|
177
|
+
excludes.update(parse_filters(kwargs, self.input_fields))
|
178
|
+
return CalculationBucket(self._manager_class, filters, excludes)
|
179
|
+
|
180
|
+
def all(self) -> CalculationBucket:
|
181
|
+
"""
|
182
|
+
Returns the current CalculationBucket instance.
|
183
|
+
|
184
|
+
This method allows for compatibility with interfaces expecting an `all()` method that returns the full set of items.
|
185
|
+
"""
|
186
|
+
return self
|
187
|
+
|
188
|
+
def __iter__(self) -> Generator[GeneralManagerType]:
|
189
|
+
"""
|
190
|
+
Yields manager instances for each valid combination of input parameters.
|
191
|
+
|
192
|
+
Iterates over all generated input combinations, instantiating the manager class with each set of parameters.
|
193
|
+
"""
|
194
|
+
combinations = self.generate_combinations()
|
195
|
+
for combo in combinations:
|
196
|
+
yield self._manager_class(**combo)
|
197
|
+
|
198
|
+
def generate_combinations(self) -> List[dict[str, Any]]:
|
199
|
+
"""
|
200
|
+
Generates and caches all valid input combinations based on filters, exclusions, and sorting.
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
A list of dictionaries, each representing a unique combination of input values that satisfy the current filters, exclusions, and sorting order.
|
204
|
+
"""
|
205
|
+
|
206
|
+
def key_func(item: dict[str, Any]) -> tuple:
|
207
|
+
return tuple(item[key] for key in sort_key)
|
208
|
+
|
209
|
+
if self._current_combinations is None:
|
210
|
+
# Implementierung ähnlich wie im InputManager
|
211
|
+
sorted_inputs = self.topological_sort_inputs()
|
212
|
+
current_combinations = self._generate_combinations(
|
213
|
+
sorted_inputs, self.filters, self.excludes
|
214
|
+
)
|
215
|
+
if self.sort_key is not None:
|
216
|
+
sort_key = self.sort_key
|
217
|
+
if isinstance(sort_key, str):
|
218
|
+
sort_key = (sort_key,)
|
219
|
+
current_combinations = sorted(
|
220
|
+
current_combinations,
|
221
|
+
key=key_func,
|
222
|
+
)
|
223
|
+
if self.reverse:
|
224
|
+
current_combinations.reverse()
|
225
|
+
self._current_combinations = current_combinations
|
226
|
+
|
227
|
+
return self._current_combinations
|
228
|
+
|
229
|
+
def topological_sort_inputs(self) -> List[str]:
|
230
|
+
"""
|
231
|
+
Performs a topological sort of input fields based on their dependencies.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
A list of input field names ordered so that each field appears after its dependencies.
|
235
|
+
|
236
|
+
Raises:
|
237
|
+
ValueError: If a cyclic dependency is detected among the input fields.
|
238
|
+
"""
|
239
|
+
from collections import defaultdict
|
240
|
+
|
241
|
+
dependencies = {
|
242
|
+
name: field.depends_on for name, field in self.input_fields.items()
|
243
|
+
}
|
244
|
+
graph = defaultdict(set)
|
245
|
+
for key, deps in dependencies.items():
|
246
|
+
for dep in deps:
|
247
|
+
graph[dep].add(key)
|
248
|
+
|
249
|
+
visited = set()
|
250
|
+
sorted_inputs = []
|
251
|
+
|
252
|
+
def visit(node, temp_mark):
|
253
|
+
"""
|
254
|
+
Performs a depth-first traversal to topologically sort nodes, detecting cycles.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
node: The current node to visit.
|
258
|
+
temp_mark: A set tracking nodes in the current traversal path to detect cycles.
|
259
|
+
|
260
|
+
Raises:
|
261
|
+
ValueError: If a cyclic dependency is detected involving the current node.
|
262
|
+
"""
|
263
|
+
if node in visited:
|
264
|
+
return
|
265
|
+
if node in temp_mark:
|
266
|
+
raise ValueError(f"Cyclic dependency detected: {node}")
|
267
|
+
temp_mark.add(node)
|
268
|
+
for m in graph.get(node, []):
|
269
|
+
visit(m, temp_mark)
|
270
|
+
temp_mark.remove(node)
|
271
|
+
visited.add(node)
|
272
|
+
sorted_inputs.append(node)
|
273
|
+
|
274
|
+
for node in self.input_fields:
|
275
|
+
if node not in visited:
|
276
|
+
visit(node, set())
|
277
|
+
|
278
|
+
sorted_inputs.reverse()
|
279
|
+
return sorted_inputs
|
280
|
+
|
281
|
+
def get_possible_values(
|
282
|
+
self, key_name: str, input_field: Input, current_combo: dict
|
283
|
+
) -> Union[Iterable[Any], Bucket[Any]]:
|
284
|
+
# Hole mögliche Werte
|
285
|
+
"""
|
286
|
+
Retrieves the possible values for a given input field based on its definition and current dependencies.
|
287
|
+
|
288
|
+
If the input field's `possible_values` is a callable, it is invoked with the current values of its dependencies. If it is an iterable or a `Bucket`, it is returned directly. Raises a `TypeError` if `possible_values` is not a valid type.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
key_name: The name of the input field.
|
292
|
+
input_field: The input field object whose possible values are to be determined.
|
293
|
+
current_combo: The current combination of input values, used to resolve dependencies.
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
An iterable or `Bucket` containing the possible values for the input field.
|
297
|
+
|
298
|
+
Raises:
|
299
|
+
TypeError: If `possible_values` is neither callable, iterable, nor a `Bucket`.
|
300
|
+
"""
|
301
|
+
if callable(input_field.possible_values):
|
302
|
+
depends_on = input_field.depends_on
|
303
|
+
dep_values = {dep_name: current_combo[dep_name] for dep_name in depends_on}
|
304
|
+
possible_values = input_field.possible_values(**dep_values)
|
305
|
+
elif isinstance(input_field.possible_values, (Iterable, Bucket)):
|
306
|
+
possible_values = input_field.possible_values
|
307
|
+
else:
|
308
|
+
raise TypeError(f"Invalid possible_values for input '{key_name}'")
|
309
|
+
return possible_values
|
310
|
+
|
311
|
+
def _generate_combinations(
|
312
|
+
self,
|
313
|
+
sorted_inputs: List[str],
|
314
|
+
filters: dict[str, dict],
|
315
|
+
excludes: dict[str, dict],
|
316
|
+
) -> List[dict[str, Any]]:
|
317
|
+
"""
|
318
|
+
Recursively generates all valid input combinations based on sorted input fields, applying filters and exclusions.
|
319
|
+
|
320
|
+
Args:
|
321
|
+
sorted_inputs: List of input field names ordered by dependency.
|
322
|
+
filters: Dictionary mapping input names to filter definitions.
|
323
|
+
excludes: Dictionary mapping input names to exclusion definitions.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
A list of dictionaries, each representing a valid combination of input values.
|
327
|
+
"""
|
328
|
+
|
329
|
+
def helper(index, current_combo):
|
330
|
+
if index == len(sorted_inputs):
|
331
|
+
yield current_combo.copy()
|
332
|
+
return
|
333
|
+
input_name: str = sorted_inputs[index]
|
334
|
+
input_field = self.input_fields[input_name]
|
335
|
+
|
336
|
+
# Hole mögliche Werte
|
337
|
+
possible_values = self.get_possible_values(
|
338
|
+
input_name, input_field, current_combo
|
339
|
+
)
|
340
|
+
|
341
|
+
# Wende die Filter an
|
342
|
+
field_filters = filters.get(input_name, {})
|
343
|
+
field_excludes = excludes.get(input_name, {})
|
344
|
+
|
345
|
+
if isinstance(possible_values, Bucket):
|
346
|
+
# Wende die Filter- und Exklusionsargumente direkt an
|
347
|
+
filter_kwargs = field_filters.get("filter_kwargs", {})
|
348
|
+
exclude_kwargs = field_excludes.get("filter_kwargs", {})
|
349
|
+
possible_values = possible_values.filter(**filter_kwargs).exclude(
|
350
|
+
**exclude_kwargs
|
351
|
+
)
|
352
|
+
else:
|
353
|
+
# Wende die Filterfunktionen an
|
354
|
+
filter_funcs = field_filters.get("filter_funcs", [])
|
355
|
+
for filter_func in filter_funcs:
|
356
|
+
possible_values = filter(filter_func, possible_values)
|
357
|
+
|
358
|
+
exclude_funcs = field_excludes.get("filter_funcs", [])
|
359
|
+
for exclude_func in exclude_funcs:
|
360
|
+
possible_values = filter(
|
361
|
+
lambda x: not exclude_func(x), possible_values
|
362
|
+
)
|
363
|
+
|
364
|
+
possible_values = list(possible_values)
|
365
|
+
|
366
|
+
for value in possible_values:
|
367
|
+
if not isinstance(value, input_field.type):
|
368
|
+
continue
|
369
|
+
current_combo[input_name] = value
|
370
|
+
yield from helper(index + 1, current_combo)
|
371
|
+
del current_combo[input_name]
|
372
|
+
|
373
|
+
return list(helper(0, {}))
|
374
|
+
|
375
|
+
def first(self) -> GeneralManagerType | None:
|
376
|
+
"""
|
377
|
+
Returns the first generated manager instance, or None if no combinations exist.
|
378
|
+
"""
|
379
|
+
try:
|
380
|
+
return next(iter(self))
|
381
|
+
except StopIteration:
|
382
|
+
return None
|
383
|
+
|
384
|
+
def last(self) -> GeneralManagerType | None:
|
385
|
+
"""
|
386
|
+
Returns the last generated manager instance, or None if no combinations exist.
|
387
|
+
"""
|
388
|
+
items = list(self)
|
389
|
+
if items:
|
390
|
+
return items[-1]
|
391
|
+
return None
|
392
|
+
|
393
|
+
def count(self) -> int:
|
394
|
+
"""
|
395
|
+
Returns the number of calculation combinations in the bucket.
|
396
|
+
"""
|
397
|
+
return self.__len__()
|
398
|
+
|
399
|
+
def __len__(self) -> int:
|
400
|
+
"""
|
401
|
+
Returns the number of generated calculation combinations in the bucket.
|
402
|
+
"""
|
403
|
+
return len(self.generate_combinations())
|
404
|
+
|
405
|
+
def __getitem__(
|
406
|
+
self, item: int | slice
|
407
|
+
) -> GeneralManagerType | CalculationBucket[GeneralManagerType]:
|
408
|
+
"""
|
409
|
+
Returns a manager instance or a new bucket for the specified index or slice.
|
410
|
+
|
411
|
+
If an integer index is provided, returns the corresponding manager instance.
|
412
|
+
If a slice is provided, returns a new CalculationBucket representing the sliced subset.
|
413
|
+
"""
|
414
|
+
items = self.generate_combinations()
|
415
|
+
result = items[item]
|
416
|
+
if isinstance(result, list):
|
417
|
+
new_bucket = CalculationBucket(
|
418
|
+
self._manager_class,
|
419
|
+
self.filters.copy(),
|
420
|
+
self.excludes.copy(),
|
421
|
+
self.sort_key,
|
422
|
+
self.reverse,
|
423
|
+
)
|
424
|
+
new_bucket._current_combinations = result
|
425
|
+
return new_bucket
|
426
|
+
return self._manager_class(**result)
|
427
|
+
|
428
|
+
def __contains__(self, item: GeneralManagerType) -> bool:
|
429
|
+
"""
|
430
|
+
Checks if the specified manager instance is present in the generated combinations.
|
431
|
+
|
432
|
+
Args:
|
433
|
+
item: The manager instance to check for membership.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
True if the instance is among the generated combinations, False otherwise.
|
437
|
+
"""
|
438
|
+
return any(item == mgr for mgr in self)
|
439
|
+
|
440
|
+
def get(self, **kwargs: Any) -> GeneralManagerType:
|
441
|
+
"""
|
442
|
+
Retrieves a single calculation manager instance matching the specified filters.
|
443
|
+
|
444
|
+
Args:
|
445
|
+
**kwargs: Filter criteria to apply.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
The unique manager instance matching the filters.
|
449
|
+
|
450
|
+
Raises:
|
451
|
+
ValueError: If no matching calculation is found or if multiple matches exist.
|
452
|
+
"""
|
453
|
+
filtered_bucket = self.filter(**kwargs)
|
454
|
+
items = list(filtered_bucket)
|
455
|
+
if len(items) == 1:
|
456
|
+
return items[0]
|
457
|
+
elif len(items) == 0:
|
458
|
+
raise ValueError("No matching calculation found.")
|
459
|
+
else:
|
460
|
+
raise ValueError("Multiple matching calculations found.")
|
461
|
+
|
462
|
+
def sort(
|
463
|
+
self, key: str | tuple[str], reverse: bool = False
|
464
|
+
) -> CalculationBucket[GeneralManagerType]:
|
465
|
+
"""
|
466
|
+
Returns a new CalculationBucket with updated sorting parameters.
|
467
|
+
|
468
|
+
Args:
|
469
|
+
key: The field name or tuple of field names to sort combinations by.
|
470
|
+
reverse: If True, sorts in descending order.
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
A new CalculationBucket instance with the specified sorting applied.
|
474
|
+
"""
|
475
|
+
return CalculationBucket(
|
476
|
+
self._manager_class, self.filters, self.excludes, key, reverse
|
477
|
+
)
|