GeneralManager 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/PKG-INFO +1 -1
- {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/SOURCES.txt +4 -1
- {generalmanager-0.6.0 → generalmanager-0.6.2}/PKG-INFO +1 -1
- {generalmanager-0.6.0 → generalmanager-0.6.2}/pyproject.toml +1 -1
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/calculationBucket.py +19 -16
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/calculationInterface.py +12 -11
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/databaseInterface.py +58 -34
- generalmanager-0.6.2/tests/test_calculationBucket.py +373 -0
- generalmanager-0.6.2/tests/test_calculationInterface.py +124 -0
- generalmanager-0.6.2/tests/test_databaseInterface.py +181 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/LICENSE +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/README.md +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/setup.cfg +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/apps.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/filterParser.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/noneToZero.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/pathMapping.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/baseBucket.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/databaseBucket.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/groupBucket.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/cacheDecorator.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/cacheTracker.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/dependencyIndex.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/modelDependencyCollector.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/autoFactory.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/factoryMethods.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/baseInterface.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/databaseBasedInterface.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/readOnlyInterface.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/generalManager.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/groupManager.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/measurementField.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/basePermission.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/fileBasedPermission.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/managerBasedPermission.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/permissionChecks.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/permissionDataManager.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_argsToKwargs.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_autoFactory.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_baseBucket.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_baseInterface.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_basePermission.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_cacheDecorator.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_cacheTracker.py +0 -0
- /generalmanager-0.6.0/tests/test_dabaseBasedInterface.py → /generalmanager-0.6.2/tests/test_databaseBasedInterface.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_databaseBucket.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_dependencyIndex.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_factories.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_factoryMethods.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_filterParser.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_generalManager.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_generalManagerMeta.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_graph_ql.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_groupManager.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_input.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_jsonEncoder.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_makeCacheKey.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_managerBasedPermission.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_measurement.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_measurement_field.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_modelDependencyCollector.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_noneToZero.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_rule_handler.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_rules.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_settings.py +0 -0
- {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_signals.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GeneralManager
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.2
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
6
6
|
License-Expression: MIT
|
@@ -61,8 +61,11 @@ tests/test_baseInterface.py
|
|
61
61
|
tests/test_basePermission.py
|
62
62
|
tests/test_cacheDecorator.py
|
63
63
|
tests/test_cacheTracker.py
|
64
|
-
tests/
|
64
|
+
tests/test_calculationBucket.py
|
65
|
+
tests/test_calculationInterface.py
|
66
|
+
tests/test_databaseBasedInterface.py
|
65
67
|
tests/test_databaseBucket.py
|
68
|
+
tests/test_databaseInterface.py
|
66
69
|
tests/test_dependencyIndex.py
|
67
70
|
tests/test_factories.py
|
68
71
|
tests/test_factoryMethods.py
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GeneralManager
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.2
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
6
6
|
License-Expression: MIT
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "GeneralManager"
|
7
|
-
version = "0.6.
|
7
|
+
version = "0.6.2"
|
8
8
|
description = "Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching."
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [{ name = "Tim Kleindick", email = "tkleindick@yahoo.de" }]
|
{generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/calculationBucket.py
RENAMED
@@ -151,15 +151,15 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
151
151
|
|
152
152
|
def __repr__(self) -> str:
|
153
153
|
"""
|
154
|
-
Returns a string representation of the CalculationBucket,
|
154
|
+
Returns a concise string representation of the CalculationBucket, including the manager class name, filters, excludes, sort key, and sort order.
|
155
155
|
"""
|
156
|
-
return self.
|
156
|
+
return f"{self.__class__.__name__}({self._manager_class.__name__}, {self.filters}, {self.excludes}, {self.sort_key}, {self.reverse})"
|
157
157
|
|
158
158
|
def filter(self, **kwargs: Any) -> CalculationBucket:
|
159
159
|
"""
|
160
|
-
Returns a new CalculationBucket with additional filters applied
|
161
|
-
|
162
|
-
|
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
163
|
"""
|
164
164
|
filters = self.filters.copy()
|
165
165
|
excludes = self.excludes.copy()
|
@@ -315,42 +315,45 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
315
315
|
excludes: dict[str, dict],
|
316
316
|
) -> List[dict[str, Any]]:
|
317
317
|
"""
|
318
|
-
Recursively generates all valid input combinations
|
319
|
-
|
318
|
+
Recursively generates all valid input combinations for the specified input fields, applying filters and exclusions.
|
319
|
+
|
320
320
|
Args:
|
321
|
-
sorted_inputs:
|
322
|
-
filters:
|
323
|
-
excludes:
|
324
|
-
|
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
325
|
Returns:
|
326
|
-
A list of dictionaries, each representing a valid combination of input values.
|
326
|
+
A list of dictionaries, each representing a valid combination of input values that satisfy all filters and exclusions.
|
327
327
|
"""
|
328
328
|
|
329
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
|
+
"""
|
330
336
|
if index == len(sorted_inputs):
|
331
337
|
yield current_combo.copy()
|
332
338
|
return
|
333
339
|
input_name: str = sorted_inputs[index]
|
334
340
|
input_field = self.input_fields[input_name]
|
335
341
|
|
336
|
-
# Hole mögliche Werte
|
337
342
|
possible_values = self.get_possible_values(
|
338
343
|
input_name, input_field, current_combo
|
339
344
|
)
|
340
345
|
|
341
|
-
# Wende die Filter an
|
342
346
|
field_filters = filters.get(input_name, {})
|
343
347
|
field_excludes = excludes.get(input_name, {})
|
344
348
|
|
349
|
+
# use filter_funcs and exclude_funcs to filter possible values
|
345
350
|
if isinstance(possible_values, Bucket):
|
346
|
-
# Wende die Filter- und Exklusionsargumente direkt an
|
347
351
|
filter_kwargs = field_filters.get("filter_kwargs", {})
|
348
352
|
exclude_kwargs = field_excludes.get("filter_kwargs", {})
|
349
353
|
possible_values = possible_values.filter(**filter_kwargs).exclude(
|
350
354
|
**exclude_kwargs
|
351
355
|
)
|
352
356
|
else:
|
353
|
-
# Wende die Filterfunktionen an
|
354
357
|
filter_funcs = field_filters.get("filter_funcs", [])
|
355
358
|
for filter_func in filter_funcs:
|
356
359
|
possible_values = filter(filter_func, possible_values)
|
{generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/calculationInterface.py
RENAMED
@@ -61,7 +61,15 @@ class CalculationInterface(InterfaceBase):
|
|
61
61
|
def _preCreate(
|
62
62
|
name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
|
63
63
|
) -> tuple[attributes, interfaceBaseClass, None]:
|
64
|
-
|
64
|
+
|
65
|
+
"""
|
66
|
+
Prepares attributes and a new interface class before creating a GeneralManager class.
|
67
|
+
|
68
|
+
Collects all `Input` instances from the provided interface class, sets the interface type in the attributes, dynamically creates a new interface class with these input fields, and adds it to the attributes.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
A tuple containing the updated attributes dictionary, the new interface class, and None.
|
72
|
+
"""
|
65
73
|
input_fields: dict[str, Input[Any]] = {}
|
66
74
|
for key, value in vars(interface).items():
|
67
75
|
if key.startswith("__"):
|
@@ -69,7 +77,6 @@ class CalculationInterface(InterfaceBase):
|
|
69
77
|
if isinstance(value, Input):
|
70
78
|
input_fields[key] = value
|
71
79
|
|
72
|
-
# Interface-Typ bestimmen
|
73
80
|
attrs["_interface_type"] = interface._interface_type
|
74
81
|
interface_cls = type(
|
75
82
|
interface.__name__, (interface,), {"input_fields": input_fields}
|
@@ -102,18 +109,12 @@ class CalculationInterface(InterfaceBase):
|
|
102
109
|
@classmethod
|
103
110
|
def getFieldType(cls, field_name: str) -> type:
|
104
111
|
"""
|
105
|
-
Returns the type of
|
106
|
-
|
107
|
-
Args:
|
108
|
-
field_name: The name of the input field.
|
109
|
-
|
110
|
-
Returns:
|
111
|
-
The Python type associated with the input field.
|
112
|
+
Returns the Python type of a specified input field.
|
112
113
|
|
113
114
|
Raises:
|
114
|
-
|
115
|
+
KeyError: If the field name does not exist in input_fields.
|
115
116
|
"""
|
116
117
|
input = cls.input_fields.get(field_name)
|
117
118
|
if input is None:
|
118
|
-
raise
|
119
|
+
raise KeyError(f"Field '{field_name}' not found in input fields.")
|
119
120
|
return input.type
|
{generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/databaseInterface.py
RENAMED
@@ -18,36 +18,24 @@ class DatabaseInterface(DBBasedInterface):
|
|
18
18
|
def create(
|
19
19
|
cls, creator_id: int, history_comment: str | None = None, **kwargs: Any
|
20
20
|
) -> int:
|
21
|
-
from general_manager.manager.generalManager import GeneralManager
|
22
21
|
|
23
|
-
cls.
|
24
|
-
kwargs, many_to_many_kwargs = cls.
|
25
|
-
instance = cls._model()
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
key = f"{key}_id"
|
30
|
-
setattr(instance, key, value)
|
31
|
-
for key, value in many_to_many_kwargs.items():
|
32
|
-
getattr(instance, key).set(value)
|
33
|
-
return cls.__save_with_history(instance, creator_id, history_comment)
|
22
|
+
cls._checkForInvalidKwargs(cls._model, kwargs=kwargs)
|
23
|
+
kwargs, many_to_many_kwargs = cls._sortKwargs(cls._model, kwargs)
|
24
|
+
instance = cls.__setAttrForWrite(cls._model(), kwargs)
|
25
|
+
pk = cls._save_with_history(instance, creator_id, history_comment)
|
26
|
+
cls.__setManyToManyAttributes(instance, many_to_many_kwargs)
|
27
|
+
return pk
|
34
28
|
|
35
29
|
def update(
|
36
30
|
self, creator_id: int, history_comment: str | None = None, **kwargs: Any
|
37
31
|
) -> int:
|
38
|
-
from general_manager.manager.generalManager import GeneralManager
|
39
32
|
|
40
|
-
self.
|
41
|
-
kwargs, many_to_many_kwargs = self.
|
42
|
-
instance = self._model.objects.get(pk=self.pk)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
key = f"{key}_id"
|
47
|
-
setattr(instance, key, value)
|
48
|
-
for key, value in many_to_many_kwargs.items():
|
49
|
-
getattr(instance, key).set(value)
|
50
|
-
return self.__save_with_history(instance, creator_id, history_comment)
|
33
|
+
self._checkForInvalidKwargs(self._model, kwargs=kwargs)
|
34
|
+
kwargs, many_to_many_kwargs = self._sortKwargs(self._model, kwargs)
|
35
|
+
instance = self.__setAttrForWrite(self._model.objects.get(pk=self.pk), kwargs)
|
36
|
+
pk = self._save_with_history(instance, creator_id, history_comment)
|
37
|
+
self.__setManyToManyAttributes(instance, many_to_many_kwargs)
|
38
|
+
return pk
|
51
39
|
|
52
40
|
def deactivate(self, creator_id: int, history_comment: str | None = None) -> int:
|
53
41
|
instance = self._model.objects.get(pk=self.pk)
|
@@ -56,32 +44,68 @@ class DatabaseInterface(DBBasedInterface):
|
|
56
44
|
history_comment = f"{history_comment} (deactivated)"
|
57
45
|
else:
|
58
46
|
history_comment = "Deactivated"
|
59
|
-
return self.
|
47
|
+
return self._save_with_history(instance, creator_id, history_comment)
|
60
48
|
|
61
49
|
@staticmethod
|
62
|
-
def
|
50
|
+
def __setManyToManyAttributes(
|
51
|
+
instance: GeneralManagerModel, many_to_many_kwargs: dict[str, list[Any]]
|
52
|
+
) -> GeneralManagerModel:
|
53
|
+
"""
|
54
|
+
Sets many-to-many attributes for the given instance based on the provided kwargs.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
instance: The model instance to update.
|
58
|
+
many_to_many_kwargs: A dictionary containing many-to-many field names and their corresponding values.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
The updated model instance.
|
62
|
+
"""
|
63
|
+
for key, value in many_to_many_kwargs.items():
|
64
|
+
if not value:
|
65
|
+
continue
|
66
|
+
field_name = key.split("_id_list")[0]
|
67
|
+
getattr(instance, field_name).set(value)
|
68
|
+
|
69
|
+
return instance
|
70
|
+
|
71
|
+
@staticmethod
|
72
|
+
def __setAttrForWrite(
|
73
|
+
instance: GeneralManagerModel,
|
74
|
+
kwargs: dict[str, Any],
|
75
|
+
) -> GeneralManagerModel:
|
76
|
+
from general_manager.manager.generalManager import GeneralManager
|
77
|
+
|
78
|
+
for key, value in kwargs.items():
|
79
|
+
if isinstance(value, GeneralManager):
|
80
|
+
value = value.identification["id"]
|
81
|
+
key = f"{key}_id"
|
82
|
+
setattr(instance, key, value)
|
83
|
+
return instance
|
84
|
+
|
85
|
+
@staticmethod
|
86
|
+
def _checkForInvalidKwargs(model: Type[models.Model], kwargs: dict[str, Any]):
|
63
87
|
attributes = vars(model)
|
64
|
-
|
88
|
+
field_names = {f.name for f in model._meta.get_fields()}
|
65
89
|
for key in kwargs:
|
66
|
-
|
90
|
+
temp_key = key.split("_id_list")[0] # Remove '_id_list' suffix
|
91
|
+
if temp_key not in attributes and temp_key not in field_names:
|
67
92
|
raise ValueError(f"{key} does not exsist in {model.__name__}")
|
68
93
|
|
69
94
|
@staticmethod
|
70
|
-
def
|
95
|
+
def _sortKwargs(
|
71
96
|
model: Type[models.Model], kwargs: dict[Any, Any]
|
72
97
|
) -> tuple[dict[str, Any], dict[str, list[Any]]]:
|
73
|
-
many_to_many_fields = model._meta.many_to_many
|
98
|
+
many_to_many_fields = [field.name for field in model._meta.many_to_many]
|
74
99
|
many_to_many_kwargs: dict[Any, Any] = {}
|
75
|
-
for key, value in kwargs.items():
|
100
|
+
for key, value in list(kwargs.items()):
|
76
101
|
many_to_many_key = key.split("_id_list")[0]
|
77
102
|
if many_to_many_key in many_to_many_fields:
|
78
|
-
many_to_many_kwargs[key] =
|
79
|
-
kwargs.pop(key)
|
103
|
+
many_to_many_kwargs[key] = kwargs.pop(key)
|
80
104
|
return kwargs, many_to_many_kwargs
|
81
105
|
|
82
106
|
@classmethod
|
83
107
|
@transaction.atomic
|
84
|
-
def
|
108
|
+
def _save_with_history(
|
85
109
|
cls, instance: GeneralManagerModel, creator_id: int, history_comment: str | None
|
86
110
|
) -> int:
|
87
111
|
"""
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# type: ignore
|
2
|
+
from django.test import TestCase
|
3
|
+
from unittest.mock import patch
|
4
|
+
from general_manager.bucket.calculationBucket import CalculationBucket
|
5
|
+
from general_manager.interface.calculationInterface import CalculationInterface
|
6
|
+
from general_manager.manager.input import Input
|
7
|
+
|
8
|
+
|
9
|
+
# Create a dummy CalculationInterface with no input fields for simplicity
|
10
|
+
class DummyCalculationInterface(CalculationInterface):
|
11
|
+
input_fields = {}
|
12
|
+
|
13
|
+
|
14
|
+
# Dummy manager class that uses the dummy interface
|
15
|
+
class DummyGeneralManager:
|
16
|
+
Interface = DummyCalculationInterface
|
17
|
+
|
18
|
+
def __init__(self, **kwargs):
|
19
|
+
# Initialize with any keyword arguments, simulating a manager
|
20
|
+
"""
|
21
|
+
Initializes the dummy manager with provided keyword arguments.
|
22
|
+
|
23
|
+
Stores all keyword arguments for later comparison and representation.
|
24
|
+
"""
|
25
|
+
self.kwargs = kwargs
|
26
|
+
|
27
|
+
def __eq__(self, value: object) -> bool:
|
28
|
+
"""
|
29
|
+
Checks equality with another DummyGeneralManager based on initialization arguments.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
True if the other object is a DummyGeneralManager with identical kwargs; otherwise, False.
|
33
|
+
"""
|
34
|
+
if not isinstance(value, DummyGeneralManager):
|
35
|
+
return False
|
36
|
+
return self.kwargs == value.kwargs
|
37
|
+
|
38
|
+
def __repr__(self):
|
39
|
+
"""
|
40
|
+
Returns a string representation of the DummyGeneralManager instance with its initialization arguments.
|
41
|
+
"""
|
42
|
+
return f"DummyGeneralManager({self.kwargs})"
|
43
|
+
|
44
|
+
|
45
|
+
# Link parent class for the interface
|
46
|
+
DummyCalculationInterface._parent_class = DummyGeneralManager
|
47
|
+
|
48
|
+
|
49
|
+
@patch(
|
50
|
+
"general_manager.bucket.calculationBucket.parse_filters",
|
51
|
+
return_value={"dummy": {"filter_kwargs": {}}},
|
52
|
+
)
|
53
|
+
class TestCalculationBucket(TestCase):
|
54
|
+
def test_initialization_defaults(self, mock_parse):
|
55
|
+
# Test basic initialization without optional parameters
|
56
|
+
"""
|
57
|
+
Tests that CalculationBucket initializes with default values when only the manager class is provided.
|
58
|
+
|
59
|
+
Verifies that filters, excludes, sort key, and reverse flag are set to their defaults, and that input fields are sourced from the associated interface.
|
60
|
+
"""
|
61
|
+
bucket = CalculationBucket(manager_class=DummyGeneralManager)
|
62
|
+
self.assertIsInstance(bucket, CalculationBucket)
|
63
|
+
self.assertEqual(bucket._manager_class, DummyGeneralManager)
|
64
|
+
self.assertEqual(bucket.filters, {})
|
65
|
+
self.assertEqual(bucket.excludes, {})
|
66
|
+
self.assertIsNone(bucket.sort_key)
|
67
|
+
self.assertFalse(bucket.reverse)
|
68
|
+
# input_fields should come from the interface
|
69
|
+
self.assertEqual(bucket.input_fields, DummyCalculationInterface.input_fields)
|
70
|
+
|
71
|
+
def test_initialization_with_filters_and_excludes(self, mock_parse):
|
72
|
+
# Filters and excludes passed directly to constructor
|
73
|
+
"""
|
74
|
+
Tests that CalculationBucket initializes with provided filter and exclude definitions, sort key, and reverse flag.
|
75
|
+
|
76
|
+
Verifies that the constructor correctly assigns the given filters, excludes, sort key, and reverse attributes.
|
77
|
+
"""
|
78
|
+
fdefs = {"f": {"filter_kwargs": {"f": 1}}}
|
79
|
+
edefs = {"e": {"filter_kwargs": {"e": 2}}}
|
80
|
+
bucket = CalculationBucket(
|
81
|
+
manager_class=DummyGeneralManager,
|
82
|
+
filter_definitions=fdefs,
|
83
|
+
exclude_definitions=edefs,
|
84
|
+
sort_key="key",
|
85
|
+
reverse=True,
|
86
|
+
)
|
87
|
+
self.assertEqual(bucket.filters, fdefs)
|
88
|
+
self.assertEqual(bucket.excludes, edefs)
|
89
|
+
self.assertEqual(bucket.sort_key, "key")
|
90
|
+
self.assertTrue(bucket.reverse)
|
91
|
+
|
92
|
+
def test_reduce_and_setstate(self, mock_parse):
|
93
|
+
# Test pickling support
|
94
|
+
"""
|
95
|
+
Tests that CalculationBucket supports pickling and unpickling via __reduce__ and __setstate__.
|
96
|
+
|
97
|
+
Verifies that the reduced state includes current combinations and that state restoration
|
98
|
+
correctly sets the internal combinations on a new instance.
|
99
|
+
"""
|
100
|
+
bucket = CalculationBucket(DummyGeneralManager, {"a": 1}, {"b": 2}, "k", True)
|
101
|
+
# Prepopulate state
|
102
|
+
bucket._current_combinations = [{"x": 10}]
|
103
|
+
cls, args, state = bucket.__reduce__()
|
104
|
+
# Check reduce data
|
105
|
+
self.assertEqual(cls, CalculationBucket)
|
106
|
+
self.assertEqual(args, (DummyGeneralManager, {"a": 1}, {"b": 2}, "k", True))
|
107
|
+
self.assertIn("current_combinations", state)
|
108
|
+
# Restore state on new instance
|
109
|
+
new_bucket = CalculationBucket(*args)
|
110
|
+
new_bucket.__setstate__(state)
|
111
|
+
self.assertEqual(new_bucket._current_combinations, [{"x": 10}])
|
112
|
+
|
113
|
+
def test_or_with_same_bucket(self, mock_parse):
|
114
|
+
# Combining two buckets of same class should intersect filters/excludes
|
115
|
+
"""
|
116
|
+
Tests that combining two CalculationBucket instances with the same manager class using the bitwise OR operator results in a new bucket containing only the filters and excludes that are identical in both buckets.
|
117
|
+
"""
|
118
|
+
b1 = CalculationBucket(DummyGeneralManager, {"f1": 1}, {"e1": 2})
|
119
|
+
b2 = CalculationBucket(
|
120
|
+
DummyGeneralManager, {"f1": 1, "f2": 3}, {"e1": 2, "e2": 4}
|
121
|
+
)
|
122
|
+
combined = b1 | b2
|
123
|
+
self.assertIsInstance(combined, CalculationBucket)
|
124
|
+
# Only common identical definitions should remain
|
125
|
+
self.assertEqual(combined.filters, {"f1": 1})
|
126
|
+
self.assertEqual(combined.excludes, {"e1": 2})
|
127
|
+
|
128
|
+
def test_or_with_invalid(self, mock_parse):
|
129
|
+
"""
|
130
|
+
Tests that combining a CalculationBucket with an incompatible type or a bucket of a different manager class raises a ValueError.
|
131
|
+
"""
|
132
|
+
b1 = CalculationBucket(DummyGeneralManager)
|
133
|
+
# Combining with different type should raise
|
134
|
+
with self.assertRaises(ValueError):
|
135
|
+
_ = b1 | 123
|
136
|
+
|
137
|
+
# Combining with bucket of different manager class should raise
|
138
|
+
class OtherManager:
|
139
|
+
Interface = DummyCalculationInterface
|
140
|
+
|
141
|
+
b2 = CalculationBucket(OtherManager)
|
142
|
+
with self.assertRaises(ValueError):
|
143
|
+
_ = b1 | b2
|
144
|
+
|
145
|
+
def test_str_and_repr_formatting(self, mock_parse):
|
146
|
+
"""
|
147
|
+
Tests the string and repr formatting of CalculationBucket instances.
|
148
|
+
|
149
|
+
Verifies that the string representation displays the total count and up to five combinations, using an ellipsis if more exist, and that the repr shows the constructor parameters.
|
150
|
+
"""
|
151
|
+
bucket = CalculationBucket(DummyGeneralManager)
|
152
|
+
# Manually set combinations for string formatting tests
|
153
|
+
combos = [{"x": i} for i in range(7)]
|
154
|
+
bucket._current_combinations = combos
|
155
|
+
s = str(bucket)
|
156
|
+
# Should show total count and at most 5 entries
|
157
|
+
self.assertTrue(s.startswith("CalculationBucket (7)["))
|
158
|
+
self.assertIn("...", s)
|
159
|
+
# Test below threshold (no ellipsis)
|
160
|
+
bucket._current_combinations = combos[:3]
|
161
|
+
s2 = str(bucket)
|
162
|
+
self.assertFalse("..." in s2)
|
163
|
+
|
164
|
+
s3 = repr(bucket)
|
165
|
+
self.assertEqual(
|
166
|
+
s3,
|
167
|
+
f"CalculationBucket({DummyGeneralManager.__name__}, {{}}, {{}}, None, False)",
|
168
|
+
)
|
169
|
+
|
170
|
+
def test_all_iter_len_count(self, mock_parse):
|
171
|
+
"""
|
172
|
+
Tests that CalculationBucket's all(), iteration, count(), and length methods behave as expected.
|
173
|
+
|
174
|
+
Verifies that all() returns the bucket itself, iteration yields one manager instance per combination, and both count() and len() return the correct number of combinations.
|
175
|
+
"""
|
176
|
+
bucket = CalculationBucket(DummyGeneralManager)
|
177
|
+
# Set a single empty combination so manager(**{}) works
|
178
|
+
bucket._current_combinations = [{}] * 4
|
179
|
+
# all() returns self
|
180
|
+
self.assertIs(bucket.all(), bucket)
|
181
|
+
# Iteration yields one manager per combo
|
182
|
+
items = list(bucket)
|
183
|
+
self.assertEqual(len(items), 4)
|
184
|
+
# count() and len() reflect number of combos
|
185
|
+
self.assertEqual(bucket.count(), 4)
|
186
|
+
self.assertEqual(len(bucket), 4)
|
187
|
+
|
188
|
+
def test_first_last_empty_and_nonempty(self, mock_parse):
|
189
|
+
"""
|
190
|
+
Tests the behavior of the `first()` and `last()` methods on a `CalculationBucket`.
|
191
|
+
|
192
|
+
Verifies that `first()` and `last()` return `None` when the bucket has no combinations, and return the same manager instance when only one combination exists.
|
193
|
+
"""
|
194
|
+
bucket = CalculationBucket(DummyGeneralManager)
|
195
|
+
# Empty combos
|
196
|
+
bucket._current_combinations = []
|
197
|
+
self.assertIsNone(bucket.first())
|
198
|
+
self.assertIsNone(bucket.last())
|
199
|
+
# Single combo
|
200
|
+
bucket._current_combinations = [{"test": 1}]
|
201
|
+
first = bucket.first()
|
202
|
+
last = bucket.last()
|
203
|
+
self.assertIsNotNone(first)
|
204
|
+
self.assertEqual(first, last)
|
205
|
+
|
206
|
+
def test_getitem_index_and_slice(self, mock_parse):
|
207
|
+
"""
|
208
|
+
Tests that indexing a CalculationBucket returns a manager instance and slicing returns a new CalculationBucket with the correct subset of combinations.
|
209
|
+
"""
|
210
|
+
bucket = CalculationBucket(DummyGeneralManager)
|
211
|
+
# Create distinct combos for index and slice
|
212
|
+
bucket._current_combinations = [{"i": 1}, {"i": 2}, {"i": 3}]
|
213
|
+
# Index __getitem__
|
214
|
+
mgr = bucket[1]
|
215
|
+
self.assertIsInstance(mgr, DummyGeneralManager)
|
216
|
+
# Slice __getitem__
|
217
|
+
sliced = bucket[0:2]
|
218
|
+
self.assertIsInstance(sliced, CalculationBucket)
|
219
|
+
# Sliced bucket should have its own combinations
|
220
|
+
self.assertEqual(sliced._current_combinations, [{"i": 1}, {"i": 2}])
|
221
|
+
|
222
|
+
def test_sort_returns_new_bucket(self, mock_parse):
|
223
|
+
"""
|
224
|
+
Tests that the sort() method returns a new CalculationBucket with updated sort key and reverse flag, leaving the original bucket unchanged.
|
225
|
+
"""
|
226
|
+
bucket = CalculationBucket(DummyGeneralManager, {"a": 1}, {"b": 2}, None, False)
|
227
|
+
sorted_bucket = bucket.sort(key="a", reverse=True)
|
228
|
+
self.assertIsInstance(sorted_bucket, CalculationBucket)
|
229
|
+
# Original bucket unchanged
|
230
|
+
self.assertIsNone(bucket.sort_key)
|
231
|
+
# New bucket has updated sort settings
|
232
|
+
self.assertEqual(sorted_bucket.sort_key, "a")
|
233
|
+
self.assertTrue(sorted_bucket.reverse)
|
234
|
+
|
235
|
+
|
236
|
+
@patch("general_manager.bucket.calculationBucket.parse_filters", return_value={})
|
237
|
+
class TestGenerateCombinations(TestCase):
|
238
|
+
|
239
|
+
def _make_bucket_with_fields(self, fields):
|
240
|
+
# Dynamically create an interface and manager class with given input_fields
|
241
|
+
"""
|
242
|
+
Creates a CalculationBucket with dynamically defined input fields.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
fields: A list of input field definitions to assign to the generated interface.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
A CalculationBucket instance using a dynamically created manager and interface with the specified input fields.
|
249
|
+
"""
|
250
|
+
class DynInterface(CalculationInterface):
|
251
|
+
input_fields = fields
|
252
|
+
|
253
|
+
class DynManager:
|
254
|
+
Interface = DynInterface
|
255
|
+
|
256
|
+
DynInterface._parent_class = DynManager
|
257
|
+
return CalculationBucket(DynManager)
|
258
|
+
|
259
|
+
def test_basic_cartesian_product(self, mock_parse):
|
260
|
+
# Two independent fields produce a Cartesian product
|
261
|
+
"""
|
262
|
+
Tests that generate_combinations produces the Cartesian product of independent input fields.
|
263
|
+
|
264
|
+
Verifies that two fields with independent possible values yield all possible combinations.
|
265
|
+
"""
|
266
|
+
fields = {
|
267
|
+
"num": Input(type=int, possible_values=[1, 2]),
|
268
|
+
"char": Input(type=str, possible_values=["a", "b"]),
|
269
|
+
}
|
270
|
+
bucket = self._make_bucket_with_fields(fields)
|
271
|
+
combos = bucket.generate_combinations()
|
272
|
+
# Expect 4 combinations
|
273
|
+
expected = [
|
274
|
+
{"num": 1, "char": "a"},
|
275
|
+
{"num": 1, "char": "b"},
|
276
|
+
{"num": 2, "char": "a"},
|
277
|
+
{"num": 2, "char": "b"},
|
278
|
+
]
|
279
|
+
# Compare as multisets since insertion order of fields may vary
|
280
|
+
self.assertCountEqual(combos, expected)
|
281
|
+
|
282
|
+
def test_empty_possible_values(self, mock_parse):
|
283
|
+
# A field with no possible_values yields no combinations
|
284
|
+
"""
|
285
|
+
Tests that a field with an empty list of possible values results in no generated combinations.
|
286
|
+
"""
|
287
|
+
fields = {
|
288
|
+
"x": Input(type=int, possible_values=[]),
|
289
|
+
}
|
290
|
+
bucket = self._make_bucket_with_fields(fields)
|
291
|
+
combos = bucket.generate_combinations()
|
292
|
+
self.assertEqual(
|
293
|
+
combos, [], "Expected no combinations when possible_values is empty"
|
294
|
+
)
|
295
|
+
|
296
|
+
def test_dependent_field(self, mock_parse):
|
297
|
+
# Field2 depends on field1 and its possible_values is a callable
|
298
|
+
"""
|
299
|
+
Tests that a dependent input field with callable possible values generates combinations reflecting the dependency.
|
300
|
+
|
301
|
+
Verifies that when one field's possible values depend on another field's value, the generated combinations correctly incorporate this relationship.
|
302
|
+
"""
|
303
|
+
def pv_func(a):
|
304
|
+
return [a * 10]
|
305
|
+
|
306
|
+
fields = {
|
307
|
+
"a": Input(type=int, possible_values=[1, 2]),
|
308
|
+
"b": Input(type=int, possible_values=pv_func, depends_on=["a"]),
|
309
|
+
}
|
310
|
+
bucket = self._make_bucket_with_fields(fields)
|
311
|
+
combos = bucket.generate_combinations()
|
312
|
+
expected = [
|
313
|
+
{"a": 1, "b": 10},
|
314
|
+
{"a": 2, "b": 20},
|
315
|
+
]
|
316
|
+
self.assertCountEqual(combos, expected)
|
317
|
+
|
318
|
+
def test_filters_and_excludes(self, mock_parse):
|
319
|
+
# Apply filter_funcs to include only even numbers, and exclude a specific value
|
320
|
+
"""
|
321
|
+
Tests that filter and exclude functions are correctly applied to input values.
|
322
|
+
|
323
|
+
Verifies that only even numbers are included and a specific value is excluded from the generated combinations.
|
324
|
+
"""
|
325
|
+
fields = {
|
326
|
+
"n": Input(type=int, possible_values=[1, 2, 3, 4]),
|
327
|
+
}
|
328
|
+
bucket = self._make_bucket_with_fields(fields)
|
329
|
+
# Manually set filter and exclude definitions
|
330
|
+
bucket.filters = {"n": {"filter_funcs": [lambda x: x % 2 == 0]}}
|
331
|
+
bucket.excludes = {"n": {"filter_funcs": [lambda x: x == 4]}}
|
332
|
+
combos = bucket.generate_combinations()
|
333
|
+
# Should include only 2, excluding 4
|
334
|
+
self.assertEqual(combos, [{"n": 2}])
|
335
|
+
|
336
|
+
def test_sort_and_reverse_and_caching(self, mock_parse):
|
337
|
+
# Three values, sorted and reversed
|
338
|
+
"""
|
339
|
+
Tests that sorting and reversing combinations works as expected and that results are cached.
|
340
|
+
|
341
|
+
Verifies that combinations are sorted in descending order by the specified key, and that repeated calls to `generate_combinations` return the cached result.
|
342
|
+
"""
|
343
|
+
fields = {
|
344
|
+
"v": Input(type=int, possible_values=[3, 1, 2]),
|
345
|
+
}
|
346
|
+
# Create unsorted bucket
|
347
|
+
bucket = self._make_bucket_with_fields(fields)
|
348
|
+
# New bucket with sort_key
|
349
|
+
sorted_bucket = CalculationBucket(
|
350
|
+
bucket._manager_class,
|
351
|
+
bucket.filters,
|
352
|
+
bucket.excludes,
|
353
|
+
sort_key="v",
|
354
|
+
reverse=True,
|
355
|
+
)
|
356
|
+
combos = sorted_bucket.generate_combinations()
|
357
|
+
# Should be [3,2,1]
|
358
|
+
self.assertEqual([d["v"] for d in combos], [3, 2, 1])
|
359
|
+
# Test caching: calling again yields same object
|
360
|
+
combos2 = sorted_bucket.generate_combinations()
|
361
|
+
self.assertIs(combos, combos2)
|
362
|
+
|
363
|
+
def test_invalid_possible_values_type(self, mock_parse):
|
364
|
+
# possible_values not iterable or callable should raise TypeError
|
365
|
+
"""
|
366
|
+
Tests that a TypeError is raised when a field's possible_values is neither iterable nor callable.
|
367
|
+
"""
|
368
|
+
fields = {
|
369
|
+
"z": Input(type=int, possible_values=123),
|
370
|
+
}
|
371
|
+
bucket = self._make_bucket_with_fields(fields)
|
372
|
+
with self.assertRaises(TypeError):
|
373
|
+
bucket.generate_combinations()
|