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.
Files changed (90) hide show
  1. {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/PKG-INFO +1 -1
  2. {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/SOURCES.txt +4 -1
  3. {generalmanager-0.6.0 → generalmanager-0.6.2}/PKG-INFO +1 -1
  4. {generalmanager-0.6.0 → generalmanager-0.6.2}/pyproject.toml +1 -1
  5. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/calculationBucket.py +19 -16
  6. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/calculationInterface.py +12 -11
  7. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/databaseInterface.py +58 -34
  8. generalmanager-0.6.2/tests/test_calculationBucket.py +373 -0
  9. generalmanager-0.6.2/tests/test_calculationInterface.py +124 -0
  10. generalmanager-0.6.2/tests/test_databaseInterface.py +181 -0
  11. {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/dependency_links.txt +0 -0
  12. {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/requires.txt +0 -0
  13. {generalmanager-0.6.0 → generalmanager-0.6.2}/GeneralManager.egg-info/top_level.txt +0 -0
  14. {generalmanager-0.6.0 → generalmanager-0.6.2}/LICENSE +0 -0
  15. {generalmanager-0.6.0 → generalmanager-0.6.2}/README.md +0 -0
  16. {generalmanager-0.6.0 → generalmanager-0.6.2}/setup.cfg +0 -0
  17. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/__init__.py +0 -0
  18. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/graphql.py +0 -0
  19. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/mutation.py +0 -0
  20. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/api/property.py +0 -0
  21. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/apps.py +0 -0
  22. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/__init__.py +0 -0
  23. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/argsToKwargs.py +0 -0
  24. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/filterParser.py +0 -0
  25. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/jsonEncoder.py +0 -0
  26. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/makeCacheKey.py +0 -0
  27. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/noneToZero.py +0 -0
  28. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/auxiliary/pathMapping.py +0 -0
  29. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/baseBucket.py +0 -0
  30. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/databaseBucket.py +0 -0
  31. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/bucket/groupBucket.py +0 -0
  32. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/cacheDecorator.py +0 -0
  33. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/cacheTracker.py +0 -0
  34. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/dependencyIndex.py +0 -0
  35. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/modelDependencyCollector.py +0 -0
  36. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/cache/signals.py +0 -0
  37. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/__init__.py +0 -0
  38. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/autoFactory.py +0 -0
  39. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/factories.py +0 -0
  40. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/factory/factoryMethods.py +0 -0
  41. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/__init__.py +0 -0
  42. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/baseInterface.py +0 -0
  43. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/databaseBasedInterface.py +0 -0
  44. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/interface/readOnlyInterface.py +0 -0
  45. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/__init__.py +0 -0
  46. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/generalManager.py +0 -0
  47. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/groupManager.py +0 -0
  48. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/input.py +0 -0
  49. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/manager/meta.py +0 -0
  50. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/__init__.py +0 -0
  51. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/measurement.py +0 -0
  52. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/measurement/measurementField.py +0 -0
  53. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/__init__.py +0 -0
  54. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/basePermission.py +0 -0
  55. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/fileBasedPermission.py +0 -0
  56. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/managerBasedPermission.py +0 -0
  57. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/permissionChecks.py +0 -0
  58. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/permission/permissionDataManager.py +0 -0
  59. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/__init__.py +0 -0
  60. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/handler.py +0 -0
  61. {generalmanager-0.6.0 → generalmanager-0.6.2}/src/general_manager/rule/rule.py +0 -0
  62. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_argsToKwargs.py +0 -0
  63. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_autoFactory.py +0 -0
  64. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_baseBucket.py +0 -0
  65. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_baseInterface.py +0 -0
  66. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_basePermission.py +0 -0
  67. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_cacheDecorator.py +0 -0
  68. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_cacheTracker.py +0 -0
  69. /generalmanager-0.6.0/tests/test_dabaseBasedInterface.py → /generalmanager-0.6.2/tests/test_databaseBasedInterface.py +0 -0
  70. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_databaseBucket.py +0 -0
  71. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_dependencyIndex.py +0 -0
  72. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_factories.py +0 -0
  73. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_factoryMethods.py +0 -0
  74. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_filterParser.py +0 -0
  75. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_generalManager.py +0 -0
  76. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_generalManagerMeta.py +0 -0
  77. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_graph_ql.py +0 -0
  78. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_groupManager.py +0 -0
  79. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_input.py +0 -0
  80. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_jsonEncoder.py +0 -0
  81. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_makeCacheKey.py +0 -0
  82. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_managerBasedPermission.py +0 -0
  83. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_measurement.py +0 -0
  84. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_measurement_field.py +0 -0
  85. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_modelDependencyCollector.py +0 -0
  86. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_noneToZero.py +0 -0
  87. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_rule_handler.py +0 -0
  88. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_rules.py +0 -0
  89. {generalmanager-0.6.0 → generalmanager-0.6.2}/tests/test_settings.py +0 -0
  90. {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.0
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/test_dabaseBasedInterface.py
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.0
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.0"
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" }]
@@ -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, showing a preview of its contents.
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.__str__()
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 to input combinations.
161
-
162
- Additional filters are merged with existing filters, narrowing the set of valid input configurations.
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 based on sorted input fields, applying filters and exclusions.
319
-
318
+ Recursively generates all valid input combinations for the specified input fields, applying filters and exclusions.
319
+
320
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
-
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)
@@ -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
- # Felder aus der Interface-Klasse sammeln
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 the specified input field.
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
- ValueError: If the specified field name does not exist in input_fields.
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 ValueError(f"Field '{field_name}' not found in input fields.")
119
+ raise KeyError(f"Field '{field_name}' not found in input fields.")
119
120
  return input.type
@@ -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.__checkForInvalidKwargs(cls._model, kwargs=kwargs)
24
- kwargs, many_to_many_kwargs = cls.__sortKwargs(cls._model, kwargs)
25
- instance = cls._model()
26
- for key, value in kwargs.items():
27
- if isinstance(value, GeneralManager):
28
- value = value.identification["id"]
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.__checkForInvalidKwargs(self._model, kwargs=kwargs)
41
- kwargs, many_to_many_kwargs = self.__sortKwargs(self._model, kwargs)
42
- instance = self._model.objects.get(pk=self.pk)
43
- for key, value in kwargs.items():
44
- if isinstance(value, GeneralManager):
45
- value = value.identification["id"]
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.__save_with_history(instance, creator_id, history_comment)
47
+ return self._save_with_history(instance, creator_id, history_comment)
60
48
 
61
49
  @staticmethod
62
- def __checkForInvalidKwargs(model: Type[models.Model], kwargs: dict[Any, Any]):
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
- fields = model._meta.get_fields()
88
+ field_names = {f.name for f in model._meta.get_fields()}
65
89
  for key in kwargs:
66
- if key not in attributes and key not in fields:
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 __sortKwargs(
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] = value
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 __save_with_history(
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()