GeneralManager 0.5.2__py3-none-any.whl → 0.6.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.
@@ -0,0 +1,480 @@
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 concise string representation of the CalculationBucket, including the manager class name, filters, excludes, sort key, and sort order.
155
+ """
156
+ return f"{self.__class__.__name__}({self._manager_class.__name__}, {self.filters}, {self.excludes}, {self.sort_key}, {self.reverse})"
157
+
158
+ def filter(self, **kwargs: Any) -> CalculationBucket:
159
+ """
160
+ Returns a new CalculationBucket with additional filters applied.
161
+
162
+ Merges the provided filter criteria with existing filters to further restrict valid input combinations.
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 for the specified input fields, applying filters and exclusions.
319
+
320
+ Args:
321
+ sorted_inputs: Input field names ordered to respect dependency constraints.
322
+ filters: Mapping of input field names to filter definitions.
323
+ excludes: Mapping of input field names to exclusion definitions.
324
+
325
+ Returns:
326
+ A list of dictionaries, each representing a valid combination of input values that satisfy all filters and exclusions.
327
+ """
328
+
329
+ def helper(index, current_combo):
330
+ """
331
+ Recursively generates all valid input combinations for calculation inputs.
332
+
333
+ Yields:
334
+ Dict[str, Any]: A dictionary representing a valid combination of input values, filtered and excluded according to the provided criteria.
335
+ """
336
+ if index == len(sorted_inputs):
337
+ yield current_combo.copy()
338
+ return
339
+ input_name: str = sorted_inputs[index]
340
+ input_field = self.input_fields[input_name]
341
+
342
+ possible_values = self.get_possible_values(
343
+ input_name, input_field, current_combo
344
+ )
345
+
346
+ field_filters = filters.get(input_name, {})
347
+ field_excludes = excludes.get(input_name, {})
348
+
349
+ # use filter_funcs and exclude_funcs to filter possible values
350
+ if isinstance(possible_values, Bucket):
351
+ filter_kwargs = field_filters.get("filter_kwargs", {})
352
+ exclude_kwargs = field_excludes.get("filter_kwargs", {})
353
+ possible_values = possible_values.filter(**filter_kwargs).exclude(
354
+ **exclude_kwargs
355
+ )
356
+ else:
357
+ filter_funcs = field_filters.get("filter_funcs", [])
358
+ for filter_func in filter_funcs:
359
+ possible_values = filter(filter_func, possible_values)
360
+
361
+ exclude_funcs = field_excludes.get("filter_funcs", [])
362
+ for exclude_func in exclude_funcs:
363
+ possible_values = filter(
364
+ lambda x: not exclude_func(x), possible_values
365
+ )
366
+
367
+ possible_values = list(possible_values)
368
+
369
+ for value in possible_values:
370
+ if not isinstance(value, input_field.type):
371
+ continue
372
+ current_combo[input_name] = value
373
+ yield from helper(index + 1, current_combo)
374
+ del current_combo[input_name]
375
+
376
+ return list(helper(0, {}))
377
+
378
+ def first(self) -> GeneralManagerType | None:
379
+ """
380
+ Returns the first generated manager instance, or None if no combinations exist.
381
+ """
382
+ try:
383
+ return next(iter(self))
384
+ except StopIteration:
385
+ return None
386
+
387
+ def last(self) -> GeneralManagerType | None:
388
+ """
389
+ Returns the last generated manager instance, or None if no combinations exist.
390
+ """
391
+ items = list(self)
392
+ if items:
393
+ return items[-1]
394
+ return None
395
+
396
+ def count(self) -> int:
397
+ """
398
+ Returns the number of calculation combinations in the bucket.
399
+ """
400
+ return self.__len__()
401
+
402
+ def __len__(self) -> int:
403
+ """
404
+ Returns the number of generated calculation combinations in the bucket.
405
+ """
406
+ return len(self.generate_combinations())
407
+
408
+ def __getitem__(
409
+ self, item: int | slice
410
+ ) -> GeneralManagerType | CalculationBucket[GeneralManagerType]:
411
+ """
412
+ Returns a manager instance or a new bucket for the specified index or slice.
413
+
414
+ If an integer index is provided, returns the corresponding manager instance.
415
+ If a slice is provided, returns a new CalculationBucket representing the sliced subset.
416
+ """
417
+ items = self.generate_combinations()
418
+ result = items[item]
419
+ if isinstance(result, list):
420
+ new_bucket = CalculationBucket(
421
+ self._manager_class,
422
+ self.filters.copy(),
423
+ self.excludes.copy(),
424
+ self.sort_key,
425
+ self.reverse,
426
+ )
427
+ new_bucket._current_combinations = result
428
+ return new_bucket
429
+ return self._manager_class(**result)
430
+
431
+ def __contains__(self, item: GeneralManagerType) -> bool:
432
+ """
433
+ Checks if the specified manager instance is present in the generated combinations.
434
+
435
+ Args:
436
+ item: The manager instance to check for membership.
437
+
438
+ Returns:
439
+ True if the instance is among the generated combinations, False otherwise.
440
+ """
441
+ return any(item == mgr for mgr in self)
442
+
443
+ def get(self, **kwargs: Any) -> GeneralManagerType:
444
+ """
445
+ Retrieves a single calculation manager instance matching the specified filters.
446
+
447
+ Args:
448
+ **kwargs: Filter criteria to apply.
449
+
450
+ Returns:
451
+ The unique manager instance matching the filters.
452
+
453
+ Raises:
454
+ ValueError: If no matching calculation is found or if multiple matches exist.
455
+ """
456
+ filtered_bucket = self.filter(**kwargs)
457
+ items = list(filtered_bucket)
458
+ if len(items) == 1:
459
+ return items[0]
460
+ elif len(items) == 0:
461
+ raise ValueError("No matching calculation found.")
462
+ else:
463
+ raise ValueError("Multiple matching calculations found.")
464
+
465
+ def sort(
466
+ self, key: str | tuple[str], reverse: bool = False
467
+ ) -> CalculationBucket[GeneralManagerType]:
468
+ """
469
+ Returns a new CalculationBucket with updated sorting parameters.
470
+
471
+ Args:
472
+ key: The field name or tuple of field names to sort combinations by.
473
+ reverse: If True, sorts in descending order.
474
+
475
+ Returns:
476
+ A new CalculationBucket instance with the specified sorting applied.
477
+ """
478
+ return CalculationBucket(
479
+ self._manager_class, self.filters, self.excludes, key, reverse
480
+ )