GeneralManager 0.14.1__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general_manager/__init__.py +49 -0
- general_manager/api/__init__.py +36 -0
- general_manager/api/graphql.py +92 -43
- general_manager/api/mutation.py +35 -10
- general_manager/api/property.py +26 -3
- general_manager/apps.py +23 -16
- general_manager/bucket/__init__.py +32 -0
- general_manager/bucket/baseBucket.py +76 -64
- general_manager/bucket/calculationBucket.py +188 -108
- general_manager/bucket/databaseBucket.py +130 -49
- general_manager/bucket/groupBucket.py +113 -60
- general_manager/cache/__init__.py +38 -0
- general_manager/cache/cacheDecorator.py +29 -17
- general_manager/cache/cacheTracker.py +34 -15
- general_manager/cache/dependencyIndex.py +117 -33
- general_manager/cache/modelDependencyCollector.py +17 -8
- general_manager/cache/signals.py +17 -6
- general_manager/factory/__init__.py +34 -5
- general_manager/factory/autoFactory.py +57 -60
- general_manager/factory/factories.py +39 -14
- general_manager/factory/factoryMethods.py +38 -1
- general_manager/interface/__init__.py +36 -0
- general_manager/interface/baseInterface.py +71 -27
- general_manager/interface/calculationInterface.py +18 -10
- general_manager/interface/databaseBasedInterface.py +102 -71
- general_manager/interface/databaseInterface.py +66 -20
- general_manager/interface/models.py +10 -4
- general_manager/interface/readOnlyInterface.py +44 -30
- general_manager/manager/__init__.py +36 -3
- general_manager/manager/generalManager.py +73 -47
- general_manager/manager/groupManager.py +72 -17
- general_manager/manager/input.py +23 -15
- general_manager/manager/meta.py +53 -53
- general_manager/measurement/__init__.py +37 -2
- general_manager/measurement/measurement.py +135 -58
- general_manager/measurement/measurementField.py +161 -61
- general_manager/permission/__init__.py +32 -1
- general_manager/permission/basePermission.py +29 -12
- general_manager/permission/managerBasedPermission.py +32 -26
- general_manager/permission/mutationPermission.py +32 -3
- general_manager/permission/permissionChecks.py +9 -1
- general_manager/permission/permissionDataManager.py +49 -15
- general_manager/permission/utils.py +14 -3
- general_manager/rule/__init__.py +27 -1
- general_manager/rule/handler.py +90 -5
- general_manager/rule/rule.py +40 -27
- general_manager/utils/__init__.py +44 -2
- general_manager/utils/argsToKwargs.py +17 -9
- general_manager/utils/filterParser.py +29 -30
- general_manager/utils/formatString.py +2 -0
- general_manager/utils/jsonEncoder.py +14 -1
- general_manager/utils/makeCacheKey.py +18 -12
- general_manager/utils/noneToZero.py +8 -6
- general_manager/utils/pathMapping.py +92 -29
- general_manager/utils/public_api.py +49 -0
- general_manager/utils/testing.py +135 -69
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/METADATA +10 -2
- generalmanager-0.15.1.dist-info/RECORD +62 -0
- generalmanager-0.14.1.dist-info/RECORD +0 -58
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/WHEEL +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
|
|
1
|
+
"""Read-only interface that mirrors JSON datasets into Django models."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
import json
|
3
5
|
|
4
|
-
from typing import Type, Any, Callable, TYPE_CHECKING
|
6
|
+
from typing import Type, Any, Callable, TYPE_CHECKING, cast
|
5
7
|
from django.db import models, transaction
|
6
8
|
from general_manager.interface.databaseBasedInterface import (
|
7
9
|
DBBasedInterface,
|
@@ -25,15 +27,21 @@ logger = logging.getLogger(__name__)
|
|
25
27
|
|
26
28
|
|
27
29
|
class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
30
|
+
"""Interface that reads static JSON data into a managed read-only model."""
|
31
|
+
|
28
32
|
_interface_type = "readonly"
|
29
33
|
_parent_class: Type[GeneralManager]
|
30
34
|
|
31
35
|
@staticmethod
|
32
36
|
def getUniqueFields(model: Type[models.Model]) -> set[str]:
|
33
37
|
"""
|
34
|
-
Return
|
35
|
-
|
36
|
-
|
38
|
+
Return names of fields that uniquely identify instances of ``model``.
|
39
|
+
|
40
|
+
Parameters:
|
41
|
+
model (type[models.Model]): Django model inspected for uniqueness metadata.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
set[str]: Field names that participate in unique constraints.
|
37
45
|
"""
|
38
46
|
opts = model._meta
|
39
47
|
unique_fields: set[str] = set()
|
@@ -55,11 +63,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
55
63
|
|
56
64
|
@classmethod
|
57
65
|
def syncData(cls) -> None:
|
58
|
-
"""
|
59
|
-
Synchronizes the associated Django model with JSON data from the parent class, ensuring the database records match the provided data exactly.
|
60
|
-
|
61
|
-
Parses the JSON data, creates or updates model instances based on unique fields, and deactivates any database records not present in the JSON data. Raises a ValueError if required attributes are missing, if the JSON data is invalid, or if no unique fields are defined.
|
62
|
-
"""
|
66
|
+
"""Synchronise the backing model with the class-level JSON data."""
|
63
67
|
if cls.ensureSchemaIsUpToDate(cls._parent_class, cls._model):
|
64
68
|
logger.warning(
|
65
69
|
f"Schema for ReadOnlyInterface '{cls._parent_class.__name__}' is not up to date."
|
@@ -74,13 +78,17 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
74
78
|
f"For ReadOnlyInterface '{parent_class.__name__}' must set '_data'"
|
75
79
|
)
|
76
80
|
|
77
|
-
# JSON
|
81
|
+
# Parse JSON into Python structures
|
78
82
|
if isinstance(json_data, str):
|
79
|
-
|
83
|
+
parsed_data = json.loads(json_data)
|
84
|
+
if not isinstance(parsed_data, list):
|
85
|
+
raise TypeError("_data JSON must decode to a list of dictionaries")
|
80
86
|
elif isinstance(json_data, list):
|
81
|
-
|
87
|
+
parsed_data = json_data
|
82
88
|
else:
|
83
|
-
raise
|
89
|
+
raise TypeError("_data must be a JSON string or a list of dictionaries")
|
90
|
+
|
91
|
+
data_list = cast(list[dict[str, Any]], parsed_data)
|
84
92
|
|
85
93
|
unique_fields = cls.getUniqueFields(model)
|
86
94
|
if not unique_fields:
|
@@ -88,7 +96,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
88
96
|
f"For ReadOnlyInterface '{parent_class.__name__}' must have at least one unique field."
|
89
97
|
)
|
90
98
|
|
91
|
-
changes = {
|
99
|
+
changes: dict[str, list[models.Model]] = {
|
92
100
|
"created": [],
|
93
101
|
"updated": [],
|
94
102
|
"deactivated": [],
|
@@ -137,19 +145,23 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
137
145
|
new_manager_class: Type[GeneralManager], model: Type[models.Model]
|
138
146
|
) -> list[Warning]:
|
139
147
|
"""
|
140
|
-
Check
|
141
|
-
|
148
|
+
Check whether the database schema matches the model definition.
|
149
|
+
|
150
|
+
Parameters:
|
151
|
+
new_manager_class (type[GeneralManager]): Manager class owning the interface.
|
152
|
+
model (type[models.Model]): Django model whose table should be inspected.
|
153
|
+
|
142
154
|
Returns:
|
143
|
-
|
155
|
+
list[Warning]: Warnings describing schema mismatches; empty when up to date.
|
144
156
|
"""
|
145
157
|
|
146
158
|
def table_exists(table_name: str) -> bool:
|
147
159
|
"""
|
148
160
|
Determine whether a database table with the specified name exists.
|
149
|
-
|
161
|
+
|
150
162
|
Parameters:
|
151
163
|
table_name (str): Name of the database table to check.
|
152
|
-
|
164
|
+
|
153
165
|
Returns:
|
154
166
|
bool: True if the table exists, False otherwise.
|
155
167
|
"""
|
@@ -162,7 +174,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
162
174
|
) -> tuple[list[str], list[str]]:
|
163
175
|
"""
|
164
176
|
Compares the fields of a Django model to the columns of a specified database table.
|
165
|
-
|
177
|
+
|
166
178
|
Returns:
|
167
179
|
A tuple containing two lists:
|
168
180
|
- The first list contains column names defined in the model but missing from the database table.
|
@@ -203,7 +215,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
203
215
|
def readOnlyPostCreate(func: Callable[..., Any]) -> Callable[..., Any]:
|
204
216
|
"""
|
205
217
|
Decorator for post-creation hooks that registers a new manager class as read-only.
|
206
|
-
|
218
|
+
|
207
219
|
After the wrapped post-creation function is executed, the newly created manager class is added to the meta-class's list of read-only classes, marking it as a read-only interface.
|
208
220
|
"""
|
209
221
|
|
@@ -211,10 +223,10 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
211
223
|
new_class: Type[GeneralManager],
|
212
224
|
interface_cls: Type[ReadOnlyInterface],
|
213
225
|
model: Type[GeneralManagerBasisModel],
|
214
|
-
):
|
226
|
+
) -> None:
|
215
227
|
"""
|
216
228
|
Registers a newly created manager class as read-only after executing the wrapped post-creation function.
|
217
|
-
|
229
|
+
|
218
230
|
This function appends the new manager class to the list of read-only classes in the meta system, ensuring it is recognized as a read-only interface.
|
219
231
|
"""
|
220
232
|
from general_manager.manager.meta import GeneralManagerMeta
|
@@ -228,7 +240,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
228
240
|
def readOnlyPreCreate(func: Callable[..., Any]) -> Callable[..., Any]:
|
229
241
|
"""
|
230
242
|
Decorator for pre-creation hook functions that ensures the base model class is set to `GeneralManagerBasisModel`.
|
231
|
-
|
243
|
+
|
232
244
|
Wraps a pre-creation function, injecting `GeneralManagerBasisModel` as the `base_model_class` argument before the manager class is created.
|
233
245
|
"""
|
234
246
|
|
@@ -236,16 +248,18 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
236
248
|
name: generalManagerClassName,
|
237
249
|
attrs: attributes,
|
238
250
|
interface: interfaceBaseClass,
|
239
|
-
base_model_class=GeneralManagerBasisModel,
|
240
|
-
)
|
251
|
+
base_model_class: type[GeneralManagerBasisModel] = GeneralManagerBasisModel,
|
252
|
+
) -> tuple[
|
253
|
+
attributes, interfaceBaseClass, type[GeneralManagerBasisModel] | None
|
254
|
+
]:
|
241
255
|
"""
|
242
256
|
Wraps a function to ensure the `base_model_class` argument is set to `GeneralManagerBasisModel` before invocation.
|
243
|
-
|
257
|
+
|
244
258
|
Parameters:
|
245
259
|
name: The name of the manager class being created.
|
246
260
|
attrs: Attributes for the manager class.
|
247
261
|
interface: The interface base class to use.
|
248
|
-
|
262
|
+
|
249
263
|
Returns:
|
250
264
|
The result of calling the wrapped function with `base_model_class` set to `GeneralManagerBasisModel`.
|
251
265
|
"""
|
@@ -259,11 +273,11 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
259
273
|
def handleInterface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
|
260
274
|
"""
|
261
275
|
Return the pre- and post-creation hook methods for integrating the interface with a manager meta-class system.
|
262
|
-
|
276
|
+
|
263
277
|
The returned tuple includes:
|
264
278
|
- A pre-creation method that ensures the base model class is set for read-only operation.
|
265
279
|
- A post-creation method that registers the manager class as read-only.
|
266
|
-
|
280
|
+
|
267
281
|
Returns:
|
268
282
|
tuple: The pre-creation and post-creation hook methods for manager class lifecycle integration.
|
269
283
|
"""
|
@@ -1,3 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
from
|
1
|
+
"""Convenience re-exports for manager utilities."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from general_manager.utils.public_api import build_module_dir, resolve_export
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"GeneralManager",
|
11
|
+
"Input",
|
12
|
+
"graphQlProperty",
|
13
|
+
"GeneralManagerMeta",
|
14
|
+
"GroupManager",
|
15
|
+
]
|
16
|
+
|
17
|
+
_MODULE_MAP = {
|
18
|
+
"GeneralManager": ("general_manager.manager.generalManager", "GeneralManager"),
|
19
|
+
"GeneralManagerMeta": ("general_manager.manager.meta", "GeneralManagerMeta"),
|
20
|
+
"Input": ("general_manager.manager.input", "Input"),
|
21
|
+
"GroupManager": ("general_manager.manager.groupManager", "GroupManager"),
|
22
|
+
"graphQlProperty": ("general_manager.api.property", "graphQlProperty"),
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
def __getattr__(name: str) -> Any:
|
27
|
+
return resolve_export(
|
28
|
+
name,
|
29
|
+
module_all=__all__,
|
30
|
+
module_map=_MODULE_MAP,
|
31
|
+
module_globals=globals(),
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def __dir__() -> list[str]:
|
36
|
+
return build_module_dir(module_all=__all__, module_globals=globals())
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import
|
2
|
+
from typing import TYPE_CHECKING, Any, Iterator, Self, Type
|
3
3
|
from general_manager.manager.meta import GeneralManagerMeta
|
4
4
|
|
5
5
|
from general_manager.api.property import GraphQLProperty
|
@@ -9,17 +9,24 @@ from general_manager.bucket.baseBucket import Bucket
|
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
11
|
from general_manager.permission.basePermission import BasePermission
|
12
|
+
from general_manager.interface.baseInterface import InterfaceBase
|
12
13
|
|
13
14
|
|
14
15
|
class GeneralManager(metaclass=GeneralManagerMeta):
|
15
16
|
Permission: Type[BasePermission]
|
16
17
|
_attributes: dict[str, Any]
|
18
|
+
Interface: Type["InterfaceBase"]
|
17
19
|
|
18
|
-
def __init__(self, *args: Any, **kwargs: Any):
|
20
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
19
21
|
"""
|
20
|
-
|
22
|
+
Instantiate the manager by delegating to the interface and record its identification.
|
21
23
|
|
22
|
-
|
24
|
+
Parameters:
|
25
|
+
*args: Positional arguments forwarded to the interface constructor.
|
26
|
+
**kwargs: Keyword arguments forwarded to the interface constructor.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
None
|
23
30
|
"""
|
24
31
|
self._interface = self.Interface(*args, **kwargs)
|
25
32
|
self.__id: dict[str, Any] = self._interface.identification
|
@@ -27,15 +34,20 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
27
34
|
self.__class__.__name__, "identification", f"{self.__id}"
|
28
35
|
)
|
29
36
|
|
30
|
-
def __str__(self):
|
37
|
+
def __str__(self) -> str:
|
38
|
+
"""Return a user-friendly representation showing the identification."""
|
31
39
|
return f"{self.__class__.__name__}(**{self.__id})"
|
32
40
|
|
33
|
-
def __repr__(self):
|
41
|
+
def __repr__(self) -> str:
|
42
|
+
"""Return a detailed representation of the manager instance."""
|
34
43
|
return f"{self.__class__.__name__}(**{self.__id})"
|
35
44
|
|
36
45
|
def __reduce__(self) -> str | tuple[Any, ...]:
|
37
46
|
"""
|
38
|
-
|
47
|
+
Provide pickling support for the manager instance.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
tuple[Any, ...]: Reconstruction data consisting of the class and identification tuple.
|
39
51
|
"""
|
40
52
|
return (self.__class__, tuple(self.__id.values()))
|
41
53
|
|
@@ -44,12 +56,16 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
44
56
|
other: Self | Bucket[Self],
|
45
57
|
) -> Bucket[Self]:
|
46
58
|
"""
|
47
|
-
|
59
|
+
Merge this manager with another manager or bucket.
|
48
60
|
|
49
|
-
|
61
|
+
Parameters:
|
62
|
+
other (Self | Bucket[Self]): Manager instance or bucket to combine.
|
50
63
|
|
51
64
|
Returns:
|
52
|
-
Bucket[Self]:
|
65
|
+
Bucket[Self]: Bucket containing the union of both inputs.
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
TypeError: If `other` is neither a compatible bucket nor manager.
|
53
69
|
"""
|
54
70
|
if isinstance(other, Bucket):
|
55
71
|
return other | self
|
@@ -63,19 +79,25 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
63
79
|
other: object,
|
64
80
|
) -> bool:
|
65
81
|
"""
|
66
|
-
|
82
|
+
Compare managers based on their identification values.
|
83
|
+
|
84
|
+
Parameters:
|
85
|
+
other (object): Object to compare against this manager.
|
67
86
|
|
68
|
-
Returns
|
87
|
+
Returns:
|
88
|
+
bool: True when `other` is a manager with the same identification.
|
69
89
|
"""
|
70
90
|
if not isinstance(other, GeneralManager):
|
71
91
|
return False
|
72
92
|
return self.identification == other.identification
|
73
93
|
|
74
94
|
@property
|
75
|
-
def identification(self):
|
95
|
+
def identification(self) -> dict[str, Any]:
|
96
|
+
"""Return the identification dictionary used to fetch the managed object."""
|
76
97
|
return self.__id
|
77
98
|
|
78
|
-
def __iter__(self):
|
99
|
+
def __iter__(self) -> Iterator[tuple[str, Any]]:
|
100
|
+
"""Iterate over attribute names and resolved values for the managed object."""
|
79
101
|
for key, value in self._attributes.items():
|
80
102
|
if callable(value):
|
81
103
|
yield key, value(self._interface)
|
@@ -95,17 +117,19 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
95
117
|
**kwargs: Any,
|
96
118
|
) -> Self:
|
97
119
|
"""
|
98
|
-
Create a new managed object
|
99
|
-
|
100
|
-
Performs a permission check unless `ignore_permission` is True. All additional keyword arguments are passed to the interface's `create` method.
|
120
|
+
Create a new managed object through the interface.
|
101
121
|
|
102
122
|
Parameters:
|
103
|
-
creator_id (int | None): Optional
|
104
|
-
history_comment (str | None):
|
105
|
-
ignore_permission (bool):
|
123
|
+
creator_id (int | None): Optional identifier of the creating user.
|
124
|
+
history_comment (str | None): Audit comment stored with the change.
|
125
|
+
ignore_permission (bool): When True, skip permission validation.
|
126
|
+
**kwargs (Any): Additional fields forwarded to the interface `create` method.
|
106
127
|
|
107
128
|
Returns:
|
108
|
-
Self: Manager instance
|
129
|
+
Self: Manager instance representing the created object.
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
PermissionError: Propagated if the permission check fails.
|
109
133
|
"""
|
110
134
|
if not ignore_permission:
|
111
135
|
cls.Permission.checkCreatePermission(kwargs, cls, creator_id)
|
@@ -123,16 +147,19 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
123
147
|
**kwargs: Any,
|
124
148
|
) -> Self:
|
125
149
|
"""
|
126
|
-
Update the managed object
|
150
|
+
Update the managed object and return a fresh manager representing the new state.
|
127
151
|
|
128
152
|
Parameters:
|
129
|
-
creator_id (int | None):
|
130
|
-
history_comment (str | None):
|
131
|
-
ignore_permission (bool):
|
132
|
-
**kwargs:
|
153
|
+
creator_id (int | None): Optional identifier of the user performing the update.
|
154
|
+
history_comment (str | None): Audit comment recorded with the update.
|
155
|
+
ignore_permission (bool): When True, skip permission validation.
|
156
|
+
**kwargs (Any): Field updates forwarded to the interface.
|
133
157
|
|
134
158
|
Returns:
|
135
|
-
Self:
|
159
|
+
Self: Manager instance reflecting the updated object.
|
160
|
+
|
161
|
+
Raises:
|
162
|
+
PermissionError: Propagated if the permission check fails.
|
136
163
|
"""
|
137
164
|
if not ignore_permission:
|
138
165
|
self.Permission.checkUpdatePermission(kwargs, self, creator_id)
|
@@ -151,15 +178,18 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
151
178
|
ignore_permission: bool = False,
|
152
179
|
) -> Self:
|
153
180
|
"""
|
154
|
-
Deactivate the managed object and return a
|
181
|
+
Deactivate the managed object and return a manager for the resulting state.
|
155
182
|
|
156
183
|
Parameters:
|
157
|
-
creator_id (int | None): Optional
|
158
|
-
history_comment (str | None):
|
159
|
-
ignore_permission (bool):
|
184
|
+
creator_id (int | None): Optional identifier of the user performing the action.
|
185
|
+
history_comment (str | None): Audit comment recorded with the deactivation.
|
186
|
+
ignore_permission (bool): When True, skip permission validation.
|
160
187
|
|
161
188
|
Returns:
|
162
|
-
Self:
|
189
|
+
Self: Manager instance representing the deactivated object.
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
PermissionError: Propagated if the permission check fails.
|
163
193
|
"""
|
164
194
|
if not ignore_permission:
|
165
195
|
self.Permission.checkDeletePermission(self, creator_id)
|
@@ -171,13 +201,13 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
171
201
|
@classmethod
|
172
202
|
def filter(cls, **kwargs: Any) -> Bucket[Self]:
|
173
203
|
"""
|
174
|
-
Return a bucket
|
204
|
+
Return a bucket containing managers that satisfy the provided lookups.
|
175
205
|
|
176
206
|
Parameters:
|
177
|
-
kwargs:
|
207
|
+
**kwargs (Any): Django-style filter expressions forwarded to the interface.
|
178
208
|
|
179
209
|
Returns:
|
180
|
-
Bucket[Self]:
|
210
|
+
Bucket[Self]: Bucket of matching manager instances.
|
181
211
|
"""
|
182
212
|
DependencyTracker.track(
|
183
213
|
cls.__name__, "filter", f"{cls.__parse_identification(kwargs)}"
|
@@ -187,13 +217,13 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
187
217
|
@classmethod
|
188
218
|
def exclude(cls, **kwargs: Any) -> Bucket[Self]:
|
189
219
|
"""
|
190
|
-
Return a bucket
|
220
|
+
Return a bucket excluding managers that match the provided lookups.
|
191
221
|
|
192
222
|
Parameters:
|
193
|
-
kwargs:
|
223
|
+
**kwargs (Any): Django-style exclusion expressions forwarded to the interface.
|
194
224
|
|
195
225
|
Returns:
|
196
|
-
Bucket[Self]:
|
226
|
+
Bucket[Self]: Bucket of manager instances that do not satisfy the lookups.
|
197
227
|
"""
|
198
228
|
DependencyTracker.track(
|
199
229
|
cls.__name__, "exclude", f"{cls.__parse_identification(kwargs)}"
|
@@ -202,25 +232,21 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
202
232
|
|
203
233
|
@classmethod
|
204
234
|
def all(cls) -> Bucket[Self]:
|
205
|
-
"""
|
206
|
-
Return a bucket containing all managed objects of this class.
|
207
|
-
"""
|
235
|
+
"""Return a bucket containing every managed object of this class."""
|
208
236
|
return cls.Interface.filter()
|
209
237
|
|
210
238
|
@staticmethod
|
211
239
|
def __parse_identification(kwargs: dict[str, Any]) -> dict[str, Any] | None:
|
212
240
|
"""
|
213
|
-
|
214
|
-
|
215
|
-
For each key-value pair in the input, any GeneralManager instance is replaced by its identification. Lists and tuples are processed recursively, substituting contained GeneralManager instances with their identifications. Returns None if the resulting dictionary is empty.
|
241
|
+
Replace manager instances within a filter mapping by their identifications.
|
216
242
|
|
217
243
|
Parameters:
|
218
|
-
kwargs (dict[str, Any]):
|
244
|
+
kwargs (dict[str, Any]): Mapping containing potential manager instances.
|
219
245
|
|
220
246
|
Returns:
|
221
|
-
dict[str, Any] | None:
|
247
|
+
dict[str, Any] | None: Mapping with managers substituted by identification dictionaries, or None if no substitutions occurred.
|
222
248
|
"""
|
223
|
-
output = {}
|
249
|
+
output: dict[str, Any] = {}
|
224
250
|
for key, value in kwargs.items():
|
225
251
|
if isinstance(value, GeneralManager):
|
226
252
|
output[key] = value.identification
|
@@ -1,11 +1,7 @@
|
|
1
|
+
"""Utility manager that aggregates grouped GeneralManager data."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
|
-
from typing import
|
3
|
-
Type,
|
4
|
-
Any,
|
5
|
-
Generic,
|
6
|
-
get_args,
|
7
|
-
cast,
|
8
|
-
)
|
4
|
+
from typing import Any, Generic, Iterator, Type, cast, get_args
|
9
5
|
from datetime import datetime, date, time
|
10
6
|
from general_manager.api.graphql import GraphQLProperty
|
11
7
|
from general_manager.measurement import Measurement
|
@@ -17,23 +13,37 @@ from general_manager.bucket.baseBucket import (
|
|
17
13
|
|
18
14
|
|
19
15
|
class GroupManager(Generic[GeneralManagerType]):
|
20
|
-
"""
|
21
|
-
This class is used to group the data of a GeneralManager.
|
22
|
-
It is used to create a new GeneralManager with the grouped data.
|
23
|
-
"""
|
16
|
+
"""Represent aggregated results for grouped GeneralManager records."""
|
24
17
|
|
25
18
|
def __init__(
|
26
19
|
self,
|
27
20
|
manager_class: Type[GeneralManagerType],
|
28
21
|
group_by_value: dict[str, Any],
|
29
22
|
data: Bucket[GeneralManagerType],
|
30
|
-
):
|
23
|
+
) -> None:
|
24
|
+
"""
|
25
|
+
Initialise a grouped manager with the underlying bucket and grouping keys.
|
26
|
+
|
27
|
+
Parameters:
|
28
|
+
manager_class (type[GeneralManagerType]): Manager subclass whose records were grouped.
|
29
|
+
group_by_value (dict[str, Any]): Key values describing this group.
|
30
|
+
data (Bucket[GeneralManagerType]): Bucket of records belonging to the group.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
None
|
34
|
+
"""
|
31
35
|
self._manager_class = manager_class
|
32
36
|
self._group_by_value = group_by_value
|
33
37
|
self._data = data
|
34
38
|
self._grouped_data: dict[str, Any] = {}
|
35
39
|
|
36
40
|
def __hash__(self) -> int:
|
41
|
+
"""
|
42
|
+
Return a stable hash based on the manager class, keys, and grouped data.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
int: Hash value combining class, keys, and data.
|
46
|
+
"""
|
37
47
|
return hash(
|
38
48
|
(
|
39
49
|
self._manager_class,
|
@@ -43,6 +53,15 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
43
53
|
)
|
44
54
|
|
45
55
|
def __eq__(self, other: object) -> bool:
|
56
|
+
"""
|
57
|
+
Compare grouped managers by manager class, keys, and grouped data.
|
58
|
+
|
59
|
+
Parameters:
|
60
|
+
other (object): Object to compare against.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
bool: True when both grouped managers describe the same data.
|
64
|
+
"""
|
46
65
|
return (
|
47
66
|
isinstance(other, self.__class__)
|
48
67
|
and self._manager_class == other._manager_class
|
@@ -51,9 +70,21 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
51
70
|
)
|
52
71
|
|
53
72
|
def __repr__(self) -> str:
|
73
|
+
"""
|
74
|
+
Return a debug representation showing grouped keys and data.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
str: Debug string summarising the grouped manager.
|
78
|
+
"""
|
54
79
|
return f"{self.__class__.__name__}({self._manager_class}, {self._group_by_value}, {self._data})"
|
55
80
|
|
56
|
-
def __iter__(self):
|
81
|
+
def __iter__(self) -> Iterator[tuple[str, Any]]:
|
82
|
+
"""
|
83
|
+
Iterate over attribute names and their aggregated values.
|
84
|
+
|
85
|
+
Yields:
|
86
|
+
tuple[str, Any]: Attribute name and aggregated value pairs.
|
87
|
+
"""
|
57
88
|
for attribute in self._manager_class.Interface.getAttributes().keys():
|
58
89
|
yield attribute, getattr(self, attribute)
|
59
90
|
for attribute, attr_value in self._manager_class.__dict__.items():
|
@@ -61,6 +92,18 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
61
92
|
yield attribute, getattr(self, attribute)
|
62
93
|
|
63
94
|
def __getattr__(self, item: str) -> Any:
|
95
|
+
"""
|
96
|
+
Lazily compute aggregated attribute values when accessed.
|
97
|
+
|
98
|
+
Parameters:
|
99
|
+
item (str): Attribute name requested by the caller.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Any: Aggregated value stored for the given attribute.
|
103
|
+
|
104
|
+
Raises:
|
105
|
+
AttributeError: If the attribute cannot be resolved from group data.
|
106
|
+
"""
|
64
107
|
if item in self._group_by_value:
|
65
108
|
return self._group_by_value[item]
|
66
109
|
if item not in self._grouped_data.keys():
|
@@ -68,12 +111,24 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
68
111
|
return self._grouped_data[item]
|
69
112
|
|
70
113
|
def combineValue(self, item: str) -> Any:
|
114
|
+
"""
|
115
|
+
Aggregate attribute values across the grouped records.
|
116
|
+
|
117
|
+
Parameters:
|
118
|
+
item (str): Name of the attribute to aggregate.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Any: Aggregated value corresponding to `item`.
|
122
|
+
|
123
|
+
Raises:
|
124
|
+
AttributeError: If the attribute is not defined on the manager or property set.
|
125
|
+
"""
|
71
126
|
if item == "id":
|
72
127
|
return None
|
73
128
|
|
74
|
-
|
75
|
-
|
76
|
-
|
129
|
+
attribute_types = self._manager_class.Interface.getAttributeTypes()
|
130
|
+
attr_info = attribute_types.get(item)
|
131
|
+
data_type = attr_info["type"] if attr_info else None
|
77
132
|
if data_type is None and item in self._manager_class.__dict__:
|
78
133
|
attr_value = self._manager_class.__dict__[item]
|
79
134
|
if isinstance(attr_value, GraphQLProperty):
|
@@ -83,7 +138,7 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
83
138
|
if type_hints
|
84
139
|
else cast(type, attr_value.graphql_type_hint)
|
85
140
|
)
|
86
|
-
if data_type is None:
|
141
|
+
if data_type is None or not isinstance(data_type, type):
|
87
142
|
raise AttributeError(f"{self.__class__.__name__} has no attribute {item}")
|
88
143
|
|
89
144
|
total_data = []
|