GeneralManager 0.19.1__py3-none-any.whl → 0.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GeneralManager might be problematic. Click here for more details.
- general_manager/_types/api.py +4 -4
- general_manager/_types/bucket.py +4 -4
- general_manager/_types/cache.py +6 -6
- general_manager/_types/factory.py +35 -35
- general_manager/_types/general_manager.py +11 -9
- general_manager/_types/interface.py +5 -5
- general_manager/_types/manager.py +4 -4
- general_manager/_types/measurement.py +1 -1
- general_manager/_types/permission.py +3 -3
- general_manager/_types/utils.py +12 -12
- general_manager/api/graphql.py +207 -98
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +120 -65
- general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
- general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
- general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
- general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
- general_manager/cache/{cacheDecorator.py → cache_decorator.py} +27 -6
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
- general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
- general_manager/cache/signals.py +1 -1
- general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
- general_manager/factory/factories.py +10 -13
- general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
- general_manager/interface/{baseInterface.py → base_interface.py} +30 -22
- general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
- general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
- general_manager/interface/{databaseInterface.py → database_interface.py} +21 -21
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
- general_manager/logging.py +133 -0
- general_manager/manager/{generalManager.py → general_manager.py} +75 -17
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +63 -17
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +55 -32
- general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
- general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
- general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
- general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
- general_manager/permission/utils.py +6 -6
- general_manager/public_api_registry.py +76 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +102 -11
- general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
- general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
- general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
- general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
- general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
- general_manager/utils/public_api.py +19 -0
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- generalmanager-0.20.0.dist-info/RECORD +78 -0
- generalmanager-0.19.1.dist-info/RECORD +0 -77
- /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
- /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
- /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
- /general_manager/utils/{formatString.py → format_string.py} +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ from typing import (
|
|
|
8
8
|
)
|
|
9
9
|
from django.db import models, transaction
|
|
10
10
|
from simple_history.utils import update_change_reason # type: ignore
|
|
11
|
-
from general_manager.interface.
|
|
11
|
+
from general_manager.interface.database_based_interface import (
|
|
12
12
|
DBBasedInterface,
|
|
13
13
|
GeneralManagerModel,
|
|
14
14
|
)
|
|
@@ -69,7 +69,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
69
69
|
@classmethod
|
|
70
70
|
def create(
|
|
71
71
|
cls, creator_id: int | None, history_comment: str | None = None, **kwargs: Any
|
|
72
|
-
) ->
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
73
|
"""
|
|
74
74
|
Create a new model instance using the provided field values.
|
|
75
75
|
|
|
@@ -86,16 +86,16 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
86
86
|
ValidationError: If model validation fails during save.
|
|
87
87
|
"""
|
|
88
88
|
model_cls = cast(type[GeneralManagerModel], cls._model)
|
|
89
|
-
cls.
|
|
90
|
-
kwargs, many_to_many_kwargs = cls.
|
|
91
|
-
instance = cls.
|
|
89
|
+
cls._check_for_invalid_kwargs(model_cls, kwargs=kwargs)
|
|
90
|
+
kwargs, many_to_many_kwargs = cls._sort_kwargs(model_cls, kwargs)
|
|
91
|
+
instance = cls.__set_attr_for_write(model_cls(), kwargs)
|
|
92
92
|
pk = cls._save_with_history(instance, creator_id, history_comment)
|
|
93
|
-
cls.
|
|
94
|
-
return pk
|
|
93
|
+
cls.__set_many_to_many_attributes(instance, many_to_many_kwargs)
|
|
94
|
+
return {"id": pk}
|
|
95
95
|
|
|
96
96
|
def update(
|
|
97
97
|
self, creator_id: int | None, history_comment: str | None = None, **kwargs: Any
|
|
98
|
-
) ->
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
99
|
"""
|
|
100
100
|
Update this instance with the provided field values.
|
|
101
101
|
|
|
@@ -112,16 +112,16 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
112
112
|
ValidationError: If model validation fails during save.
|
|
113
113
|
"""
|
|
114
114
|
model_cls = cast(type[GeneralManagerModel], self._model)
|
|
115
|
-
self.
|
|
116
|
-
kwargs, many_to_many_kwargs = self.
|
|
117
|
-
instance = self.
|
|
115
|
+
self._check_for_invalid_kwargs(model_cls, kwargs=kwargs)
|
|
116
|
+
kwargs, many_to_many_kwargs = self._sort_kwargs(model_cls, kwargs)
|
|
117
|
+
instance = self.__set_attr_for_write(model_cls.objects.get(pk=self.pk), kwargs)
|
|
118
118
|
pk = self._save_with_history(instance, creator_id, history_comment)
|
|
119
|
-
self.
|
|
120
|
-
return pk
|
|
119
|
+
self.__set_many_to_many_attributes(instance, many_to_many_kwargs)
|
|
120
|
+
return {"id": pk}
|
|
121
121
|
|
|
122
122
|
def deactivate(
|
|
123
123
|
self, creator_id: int | None, history_comment: str | None = None
|
|
124
|
-
) ->
|
|
124
|
+
) -> dict[str, Any]:
|
|
125
125
|
"""
|
|
126
126
|
Mark the current model instance as inactive and record the change.
|
|
127
127
|
|
|
@@ -139,10 +139,10 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
139
139
|
history_comment = f"{history_comment} (deactivated)"
|
|
140
140
|
else:
|
|
141
141
|
history_comment = "Deactivated"
|
|
142
|
-
return self._save_with_history(instance, creator_id, history_comment)
|
|
142
|
+
return {"id": self._save_with_history(instance, creator_id, history_comment)}
|
|
143
143
|
|
|
144
144
|
@staticmethod
|
|
145
|
-
def
|
|
145
|
+
def __set_many_to_many_attributes(
|
|
146
146
|
instance: GeneralManagerModel, many_to_many_kwargs: dict[str, list[Any]]
|
|
147
147
|
) -> GeneralManagerModel:
|
|
148
148
|
"""
|
|
@@ -155,7 +155,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
155
155
|
Returns:
|
|
156
156
|
GeneralManagerModel: Updated instance.
|
|
157
157
|
"""
|
|
158
|
-
from general_manager.manager.
|
|
158
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
159
159
|
|
|
160
160
|
for key, value in many_to_many_kwargs.items():
|
|
161
161
|
if value is None or value is NOT_PROVIDED:
|
|
@@ -173,7 +173,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
173
173
|
return instance
|
|
174
174
|
|
|
175
175
|
@staticmethod
|
|
176
|
-
def
|
|
176
|
+
def __set_attr_for_write(
|
|
177
177
|
instance: GeneralManagerModel,
|
|
178
178
|
kwargs: dict[str, Any],
|
|
179
179
|
) -> GeneralManagerModel:
|
|
@@ -193,7 +193,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
193
193
|
InvalidFieldValueError: If setting an attribute raises a `ValueError`.
|
|
194
194
|
InvalidFieldTypeError: If setting an attribute raises a `TypeError`.
|
|
195
195
|
"""
|
|
196
|
-
from general_manager.manager.
|
|
196
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
197
197
|
|
|
198
198
|
for key, value in kwargs.items():
|
|
199
199
|
if isinstance(value, GeneralManager):
|
|
@@ -210,7 +210,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
210
210
|
return instance
|
|
211
211
|
|
|
212
212
|
@staticmethod
|
|
213
|
-
def
|
|
213
|
+
def _check_for_invalid_kwargs(
|
|
214
214
|
model: Type[models.Model], kwargs: dict[str, Any]
|
|
215
215
|
) -> None:
|
|
216
216
|
"""
|
|
@@ -231,7 +231,7 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
|
|
|
231
231
|
raise UnknownFieldError(key, model.__name__)
|
|
232
232
|
|
|
233
233
|
@staticmethod
|
|
234
|
-
def
|
|
234
|
+
def _sort_kwargs(
|
|
235
235
|
model: Type[models.Model], kwargs: dict[Any, Any]
|
|
236
236
|
) -> tuple[dict[str, Any], dict[str, list[Any]]]:
|
|
237
237
|
"""
|
|
@@ -10,13 +10,13 @@ from django.core.exceptions import ValidationError
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from general_manager.manager.
|
|
13
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
14
14
|
from general_manager.rule.rule import Rule
|
|
15
15
|
|
|
16
16
|
modelsModel = TypeVar("modelsModel", bound=models.Model)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def get_full_clean_methode(model: Type[models.Model]) -> Callable[..., None]:
|
|
20
20
|
"""
|
|
21
21
|
Return a custom `full_clean` method for a Django model that performs both standard validation and additional rule-based checks.
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ def getFullCleanMethode(model: Type[models.Model]) -> Callable[..., None]:
|
|
|
44
44
|
rules: list[Rule] = getattr(self._meta, "rules", [])
|
|
45
45
|
for rule in rules:
|
|
46
46
|
if rule.evaluate(self) is False:
|
|
47
|
-
error_message = rule.
|
|
47
|
+
error_message = rule.get_error_message()
|
|
48
48
|
if error_message:
|
|
49
49
|
errors.update(error_message)
|
|
50
50
|
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
"""Read-only interface that mirrors JSON datasets into Django models."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Type, cast
|
|
7
|
+
|
|
8
|
+
from django.core.checks import Warning
|
|
9
|
+
from django.db import connection, models, transaction
|
|
5
10
|
|
|
6
|
-
from
|
|
7
|
-
from django.db import models, transaction
|
|
8
|
-
from general_manager.interface.databaseBasedInterface import (
|
|
11
|
+
from general_manager.interface.database_based_interface import (
|
|
9
12
|
DBBasedInterface,
|
|
10
13
|
GeneralManagerBasisModel,
|
|
11
|
-
|
|
14
|
+
attributes,
|
|
12
15
|
classPostCreationMethod,
|
|
16
|
+
classPreCreationMethod,
|
|
13
17
|
generalManagerClassName,
|
|
14
|
-
attributes,
|
|
15
18
|
interfaceBaseClass,
|
|
16
19
|
)
|
|
17
|
-
from
|
|
18
|
-
from django.core.checks import Warning
|
|
19
|
-
import logging
|
|
20
|
+
from general_manager.logging import get_logger
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
|
-
from general_manager.manager.
|
|
23
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
logger =
|
|
26
|
+
logger = get_logger("interface.read_only")
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class MissingReadOnlyDataError(ValueError):
|
|
@@ -86,7 +87,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
86
87
|
_parent_class: ClassVar[Type["GeneralManager"]]
|
|
87
88
|
|
|
88
89
|
@staticmethod
|
|
89
|
-
def
|
|
90
|
+
def get_unique_fields(model: Type[models.Model]) -> set[str]:
|
|
90
91
|
"""
|
|
91
92
|
Determine which fields on the given Django model uniquely identify its instances.
|
|
92
93
|
|
|
@@ -117,7 +118,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
117
118
|
return unique_fields
|
|
118
119
|
|
|
119
120
|
@classmethod
|
|
120
|
-
def
|
|
121
|
+
def sync_data(cls) -> None:
|
|
121
122
|
"""
|
|
122
123
|
Synchronize the Django model with the parent manager's class-level `_data` JSON.
|
|
123
124
|
|
|
@@ -129,9 +130,13 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
129
130
|
InvalidReadOnlyDataTypeError: If `_data` is neither a string nor a list.
|
|
130
131
|
MissingUniqueFieldError: If the model exposes no unique fields to identify records.
|
|
131
132
|
"""
|
|
132
|
-
if cls.
|
|
133
|
+
if cls.ensure_schema_is_up_to_date(cls._parent_class, cls._model):
|
|
133
134
|
logger.warning(
|
|
134
|
-
|
|
135
|
+
"readonly schema out of date",
|
|
136
|
+
context={
|
|
137
|
+
"manager": cls._parent_class.__name__,
|
|
138
|
+
"model": cls._model.__name__,
|
|
139
|
+
},
|
|
135
140
|
)
|
|
136
141
|
return
|
|
137
142
|
|
|
@@ -153,7 +158,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
153
158
|
|
|
154
159
|
data_list = cast(list[dict[str, Any]], parsed_data)
|
|
155
160
|
|
|
156
|
-
unique_fields = cls.
|
|
161
|
+
unique_fields = cls.get_unique_fields(model)
|
|
157
162
|
if not unique_fields:
|
|
158
163
|
raise MissingUniqueFieldError(parent_class.__name__)
|
|
159
164
|
|
|
@@ -208,14 +213,18 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
208
213
|
|
|
209
214
|
if changes["created"] or changes["updated"] or changes["deactivated"]:
|
|
210
215
|
logger.info(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
"readonly data synchronized",
|
|
217
|
+
context={
|
|
218
|
+
"manager": parent_class.__name__,
|
|
219
|
+
"model": model.__name__,
|
|
220
|
+
"created": len(changes["created"]),
|
|
221
|
+
"updated": len(changes["updated"]),
|
|
222
|
+
"deactivated": len(changes["deactivated"]),
|
|
223
|
+
},
|
|
215
224
|
)
|
|
216
225
|
|
|
217
226
|
@staticmethod
|
|
218
|
-
def
|
|
227
|
+
def ensure_schema_is_up_to_date(
|
|
219
228
|
new_manager_class: Type[GeneralManager], model: Type[models.Model]
|
|
220
229
|
) -> list[Warning]:
|
|
221
230
|
"""
|
|
@@ -286,7 +295,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
286
295
|
return []
|
|
287
296
|
|
|
288
297
|
@staticmethod
|
|
289
|
-
def
|
|
298
|
+
def read_only_post_create(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
290
299
|
"""
|
|
291
300
|
Decorator for post-creation hooks that registers a new manager class as read-only.
|
|
292
301
|
|
|
@@ -311,7 +320,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
311
320
|
return wrapper
|
|
312
321
|
|
|
313
322
|
@staticmethod
|
|
314
|
-
def
|
|
323
|
+
def read_only_pre_create(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
315
324
|
"""
|
|
316
325
|
Decorator for pre-creation hook functions that ensures the base model class is set to `GeneralManagerBasisModel`.
|
|
317
326
|
|
|
@@ -344,7 +353,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
344
353
|
return wrapper
|
|
345
354
|
|
|
346
355
|
@classmethod
|
|
347
|
-
def
|
|
356
|
+
def handle_interface(cls) -> tuple[classPreCreationMethod, classPostCreationMethod]:
|
|
348
357
|
"""
|
|
349
358
|
Return the pre- and post-creation hook methods for integrating the interface with a manager meta-class system.
|
|
350
359
|
|
|
@@ -355,6 +364,6 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
355
364
|
Returns:
|
|
356
365
|
tuple: The pre-creation and post-creation hook methods for manager class lifecycle integration.
|
|
357
366
|
"""
|
|
358
|
-
return cls.
|
|
359
|
-
cls.
|
|
367
|
+
return cls.read_only_pre_create(cls._pre_create), cls.read_only_post_create(
|
|
368
|
+
cls._post_create
|
|
360
369
|
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared logging utilities for the GeneralManager package.
|
|
3
|
+
|
|
4
|
+
The helpers defined here keep logger names consistent (``general_manager.*``),
|
|
5
|
+
expose lightweight context support, and stay fully compatible with Django's
|
|
6
|
+
``LOGGING`` settings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Mapping, MutableMapping
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
BASE_LOGGER_NAME = "general_manager"
|
|
16
|
+
COMPONENT_EXTRA_FIELD = "component"
|
|
17
|
+
CONTEXT_EXTRA_FIELD = "context"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidContextError(TypeError):
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__("context must be a mapping when provided.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvalidExtraError(TypeError):
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
super().__init__("extra must be a mutable mapping.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BlankComponentError(ValueError):
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
super().__init__("component cannot be blank or only dots.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"BASE_LOGGER_NAME",
|
|
37
|
+
"COMPONENT_EXTRA_FIELD",
|
|
38
|
+
"CONTEXT_EXTRA_FIELD",
|
|
39
|
+
"GeneralManagerLoggerAdapter",
|
|
40
|
+
"build_logger_name",
|
|
41
|
+
"get_logger",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GeneralManagerLoggerAdapter(logging.LoggerAdapter[Any]):
|
|
46
|
+
"""
|
|
47
|
+
Attach structured metadata (component + context) to log records.
|
|
48
|
+
|
|
49
|
+
The adapter keeps ``extra`` mutable, merges ``context`` mappings, and can be
|
|
50
|
+
used anywhere ``logging.Logger`` is expected.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
54
|
+
context_mapping = self._pop_context(kwargs)
|
|
55
|
+
if context_mapping is not None:
|
|
56
|
+
kwargs["context"] = context_mapping
|
|
57
|
+
super().log(level, msg, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _pop_context(
|
|
61
|
+
kwargs: MutableMapping[str, Any],
|
|
62
|
+
) -> Mapping[str, Any] | None:
|
|
63
|
+
context = kwargs.pop("context", None)
|
|
64
|
+
if context is None:
|
|
65
|
+
return None
|
|
66
|
+
if not isinstance(context, Mapping):
|
|
67
|
+
raise InvalidContextError()
|
|
68
|
+
return context
|
|
69
|
+
|
|
70
|
+
def process(
|
|
71
|
+
self, msg: Any, kwargs: MutableMapping[str, Any]
|
|
72
|
+
) -> tuple[Any, MutableMapping[str, Any]]:
|
|
73
|
+
context = self._pop_context(kwargs)
|
|
74
|
+
|
|
75
|
+
extra_obj = kwargs.setdefault("extra", {})
|
|
76
|
+
if not isinstance(extra_obj, MutableMapping):
|
|
77
|
+
raise InvalidExtraError()
|
|
78
|
+
extra = cast(MutableMapping[str, Any], extra_obj)
|
|
79
|
+
|
|
80
|
+
extra_metadata = cast(Mapping[str, Any], self.extra or {})
|
|
81
|
+
component = extra_metadata.get(COMPONENT_EXTRA_FIELD)
|
|
82
|
+
if component is not None:
|
|
83
|
+
extra.setdefault(COMPONENT_EXTRA_FIELD, component)
|
|
84
|
+
|
|
85
|
+
if context is not None:
|
|
86
|
+
current_context = cast(Mapping[str, Any], context)
|
|
87
|
+
existing_context = extra.get(CONTEXT_EXTRA_FIELD)
|
|
88
|
+
if existing_context is None:
|
|
89
|
+
merged_context: dict[str, Any] = dict(current_context)
|
|
90
|
+
elif isinstance(existing_context, Mapping):
|
|
91
|
+
merged_context = {**dict(existing_context), **current_context}
|
|
92
|
+
else:
|
|
93
|
+
raise InvalidContextError()
|
|
94
|
+
|
|
95
|
+
extra[CONTEXT_EXTRA_FIELD] = merged_context
|
|
96
|
+
|
|
97
|
+
return msg, kwargs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _normalize_component_name(component: str | None) -> str | None:
|
|
101
|
+
if component is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
normalized = component.strip().strip(".")
|
|
105
|
+
if not normalized:
|
|
106
|
+
raise BlankComponentError()
|
|
107
|
+
|
|
108
|
+
return normalized.replace(" ", "_")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_logger_name(component: str | None = None) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Build a fully-qualified logger name within the ``general_manager`` namespace.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
normalized_component = _normalize_component_name(component)
|
|
117
|
+
if not normalized_component:
|
|
118
|
+
return BASE_LOGGER_NAME
|
|
119
|
+
|
|
120
|
+
return ".".join([BASE_LOGGER_NAME, normalized_component])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_logger(component: str | None = None) -> GeneralManagerLoggerAdapter:
|
|
124
|
+
"""
|
|
125
|
+
Return a ``GeneralManagerLoggerAdapter`` scoped to the requested component.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
normalized_component = _normalize_component_name(component)
|
|
129
|
+
logger_name = build_logger_name(normalized_component)
|
|
130
|
+
adapter_extra: dict[str, Any] = {}
|
|
131
|
+
if normalized_component:
|
|
132
|
+
adapter_extra[COMPONENT_EXTRA_FIELD] = normalized_component
|
|
133
|
+
return GeneralManagerLoggerAdapter(logging.getLogger(logger_name), adapter_extra)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import TYPE_CHECKING, Any, Iterator, Self, Type
|
|
3
|
-
from general_manager.manager.meta import GeneralManagerMeta
|
|
4
3
|
|
|
5
4
|
from general_manager.api.property import GraphQLProperty
|
|
6
|
-
from general_manager.
|
|
7
|
-
from general_manager.cache.
|
|
8
|
-
from general_manager.
|
|
5
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
6
|
+
from general_manager.cache.cache_tracker import DependencyTracker
|
|
7
|
+
from general_manager.cache.signals import data_change
|
|
8
|
+
from general_manager.logging import get_logger
|
|
9
|
+
from general_manager.manager.meta import GeneralManagerMeta
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class UnsupportedUnionOperandError(TypeError):
|
|
@@ -22,8 +23,11 @@ class UnsupportedUnionOperandError(TypeError):
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
|
-
from general_manager.permission.
|
|
26
|
-
from general_manager.interface.
|
|
26
|
+
from general_manager.permission.base_permission import BasePermission
|
|
27
|
+
from general_manager.interface.base_interface import InterfaceBase
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_logger("manager.general")
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class GeneralManager(metaclass=GeneralManagerMeta):
|
|
@@ -45,6 +49,13 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
45
49
|
DependencyTracker.track(
|
|
46
50
|
self.__class__.__name__, "identification", f"{self.__id}"
|
|
47
51
|
)
|
|
52
|
+
logger.debug(
|
|
53
|
+
"instantiated manager",
|
|
54
|
+
context={
|
|
55
|
+
"manager": self.__class__.__name__,
|
|
56
|
+
"identification": self.__id,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
48
59
|
|
|
49
60
|
def __str__(self) -> str:
|
|
50
61
|
"""Return a user-friendly representation showing the identification."""
|
|
@@ -117,7 +128,7 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
117
128
|
yield name, getattr(self, name)
|
|
118
129
|
|
|
119
130
|
@classmethod
|
|
120
|
-
@
|
|
131
|
+
@data_change
|
|
121
132
|
def create(
|
|
122
133
|
cls,
|
|
123
134
|
creator_id: int | None = None,
|
|
@@ -141,13 +152,23 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
141
152
|
PermissionError: Propagated if the permission check fails.
|
|
142
153
|
"""
|
|
143
154
|
if not ignore_permission:
|
|
144
|
-
cls.Permission.
|
|
155
|
+
cls.Permission.check_create_permission(kwargs, cls, creator_id)
|
|
145
156
|
identification = cls.Interface.create(
|
|
146
157
|
creator_id=creator_id, history_comment=history_comment, **kwargs
|
|
147
158
|
)
|
|
148
|
-
|
|
159
|
+
logger.info(
|
|
160
|
+
"manager created",
|
|
161
|
+
context={
|
|
162
|
+
"manager": cls.__name__,
|
|
163
|
+
"creator_id": creator_id,
|
|
164
|
+
"ignore_permission": ignore_permission,
|
|
165
|
+
"fields": sorted(kwargs.keys()),
|
|
166
|
+
"identification": identification,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
return cls(**identification)
|
|
149
170
|
|
|
150
|
-
@
|
|
171
|
+
@data_change
|
|
151
172
|
def update(
|
|
152
173
|
self,
|
|
153
174
|
creator_id: int | None = None,
|
|
@@ -171,15 +192,25 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
171
192
|
PermissionError: Propagated if the permission check fails.
|
|
172
193
|
"""
|
|
173
194
|
if not ignore_permission:
|
|
174
|
-
self.Permission.
|
|
195
|
+
self.Permission.check_update_permission(kwargs, self, creator_id)
|
|
175
196
|
self._interface.update(
|
|
176
197
|
creator_id=creator_id,
|
|
177
198
|
history_comment=history_comment,
|
|
178
199
|
**kwargs,
|
|
179
200
|
)
|
|
201
|
+
logger.info(
|
|
202
|
+
"manager updated",
|
|
203
|
+
context={
|
|
204
|
+
"manager": self.__class__.__name__,
|
|
205
|
+
"creator_id": creator_id,
|
|
206
|
+
"ignore_permission": ignore_permission,
|
|
207
|
+
"fields": sorted(kwargs.keys()),
|
|
208
|
+
"identification": self.identification,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
180
211
|
return self.__class__(**self.identification)
|
|
181
212
|
|
|
182
|
-
@
|
|
213
|
+
@data_change
|
|
183
214
|
def deactivate(
|
|
184
215
|
self,
|
|
185
216
|
creator_id: int | None = None,
|
|
@@ -201,10 +232,19 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
201
232
|
PermissionError: Propagated if the permission check fails.
|
|
202
233
|
"""
|
|
203
234
|
if not ignore_permission:
|
|
204
|
-
self.Permission.
|
|
235
|
+
self.Permission.check_delete_permission(self, creator_id)
|
|
205
236
|
self._interface.deactivate(
|
|
206
237
|
creator_id=creator_id, history_comment=history_comment
|
|
207
238
|
)
|
|
239
|
+
logger.info(
|
|
240
|
+
"manager deactivated",
|
|
241
|
+
context={
|
|
242
|
+
"manager": self.__class__.__name__,
|
|
243
|
+
"creator_id": creator_id,
|
|
244
|
+
"ignore_permission": ignore_permission,
|
|
245
|
+
"identification": self.identification,
|
|
246
|
+
},
|
|
247
|
+
)
|
|
208
248
|
return self.__class__(**self.identification)
|
|
209
249
|
|
|
210
250
|
@classmethod
|
|
@@ -218,8 +258,14 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
218
258
|
Returns:
|
|
219
259
|
Bucket[Self]: Bucket of matching manager instances.
|
|
220
260
|
"""
|
|
221
|
-
|
|
222
|
-
|
|
261
|
+
identifier_map = cls.__parse_identification(kwargs) or kwargs
|
|
262
|
+
DependencyTracker.track(cls.__name__, "filter", repr(identifier_map))
|
|
263
|
+
logger.debug(
|
|
264
|
+
"manager filter",
|
|
265
|
+
context={
|
|
266
|
+
"manager": cls.__name__,
|
|
267
|
+
"filters": identifier_map,
|
|
268
|
+
},
|
|
223
269
|
)
|
|
224
270
|
return cls.Interface.filter(**kwargs)
|
|
225
271
|
|
|
@@ -234,14 +280,26 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
234
280
|
Returns:
|
|
235
281
|
Bucket[Self]: Bucket of manager instances that do not satisfy the lookups.
|
|
236
282
|
"""
|
|
237
|
-
|
|
238
|
-
|
|
283
|
+
identifier_map = cls.__parse_identification(kwargs) or kwargs
|
|
284
|
+
DependencyTracker.track(cls.__name__, "exclude", repr(identifier_map))
|
|
285
|
+
logger.debug(
|
|
286
|
+
"manager exclude",
|
|
287
|
+
context={
|
|
288
|
+
"manager": cls.__name__,
|
|
289
|
+
"filters": identifier_map,
|
|
290
|
+
},
|
|
239
291
|
)
|
|
240
292
|
return cls.Interface.exclude(**kwargs)
|
|
241
293
|
|
|
242
294
|
@classmethod
|
|
243
295
|
def all(cls) -> Bucket[Self]:
|
|
244
296
|
"""Return a bucket containing every managed object of this class."""
|
|
297
|
+
logger.debug(
|
|
298
|
+
"manager all",
|
|
299
|
+
context={
|
|
300
|
+
"manager": cls.__name__,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
245
303
|
return cls.Interface.filter()
|
|
246
304
|
|
|
247
305
|
@staticmethod
|
|
@@ -5,8 +5,8 @@ from typing import Any, Generic, Iterator, Type, cast, get_args
|
|
|
5
5
|
from datetime import datetime, date, time
|
|
6
6
|
from general_manager.api.property import GraphQLProperty
|
|
7
7
|
from general_manager.measurement import Measurement
|
|
8
|
-
from general_manager.manager.
|
|
9
|
-
from general_manager.bucket.
|
|
8
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
9
|
+
from general_manager.bucket.base_bucket import (
|
|
10
10
|
Bucket,
|
|
11
11
|
GeneralManagerType,
|
|
12
12
|
)
|
|
@@ -99,7 +99,7 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
|
99
99
|
Yields:
|
|
100
100
|
tuple[str, Any]: Attribute name and aggregated value pairs.
|
|
101
101
|
"""
|
|
102
|
-
for attribute in self._manager_class.Interface.
|
|
102
|
+
for attribute in self._manager_class.Interface.get_attributes().keys():
|
|
103
103
|
yield attribute, getattr(self, attribute)
|
|
104
104
|
for attribute, attr_value in self._manager_class.__dict__.items():
|
|
105
105
|
if isinstance(attr_value, GraphQLProperty):
|
|
@@ -121,10 +121,10 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
|
121
121
|
if item in self._group_by_value:
|
|
122
122
|
return self._group_by_value[item]
|
|
123
123
|
if item not in self._grouped_data.keys():
|
|
124
|
-
self._grouped_data[item] = self.
|
|
124
|
+
self._grouped_data[item] = self.combine_value(item)
|
|
125
125
|
return self._grouped_data[item]
|
|
126
126
|
|
|
127
|
-
def
|
|
127
|
+
def combine_value(self, item: str) -> Any:
|
|
128
128
|
"""
|
|
129
129
|
Aggregate the values of a named attribute across all records in the group.
|
|
130
130
|
|
|
@@ -140,7 +140,7 @@ class GroupManager(Generic[GeneralManagerType]):
|
|
|
140
140
|
if item == "id":
|
|
141
141
|
return None
|
|
142
142
|
|
|
143
|
-
attribute_types = self._manager_class.Interface.
|
|
143
|
+
attribute_types = self._manager_class.Interface.get_attribute_types()
|
|
144
144
|
attr_info = attribute_types.get(item)
|
|
145
145
|
data_type = attr_info["type"] if attr_info else None
|
|
146
146
|
if data_type is None and item in self._manager_class.__dict__:
|
general_manager/manager/input.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from typing import Iterable, Optional, Callable, List, TypeVar, Generic, Any, Type, cast
|
|
5
5
|
import inspect
|
|
6
6
|
|
|
7
|
-
from general_manager.manager.
|
|
7
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
8
8
|
from datetime import date, datetime
|
|
9
9
|
from general_manager.measurement import Measurement
|
|
10
10
|
|