GeneralManager 0.19.2__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/general_manager.py +2 -0
- general_manager/api/graphql.py +90 -4
- general_manager/apps.py +89 -34
- general_manager/cache/cache_decorator.py +23 -2
- general_manager/cache/dependency_index.py +23 -7
- general_manager/interface/base_interface.py +1 -1
- general_manager/interface/database_interface.py +6 -6
- general_manager/interface/read_only_interface.py +22 -13
- general_manager/logging.py +133 -0
- general_manager/manager/general_manager.py +65 -7
- general_manager/manager/meta.py +47 -1
- general_manager/permission/base_permission.py +36 -13
- general_manager/public_api_registry.py +1 -0
- general_manager/rule/rule.py +85 -0
- general_manager/utils/public_api.py +19 -0
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.0.dist-info}/RECORD +20 -19
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
5
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
6
6
|
from general_manager.cache.cache_tracker import DependencyTracker
|
|
7
7
|
from general_manager.cache.signals import data_change
|
|
8
|
-
from general_manager.
|
|
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):
|
|
@@ -26,6 +27,9 @@ if TYPE_CHECKING:
|
|
|
26
27
|
from general_manager.interface.base_interface import InterfaceBase
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
logger = get_logger("manager.general")
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
class GeneralManager(metaclass=GeneralManagerMeta):
|
|
30
34
|
Permission: Type[BasePermission]
|
|
31
35
|
_attributes: dict[str, Any]
|
|
@@ -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."""
|
|
@@ -145,7 +156,17 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
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(
|
|
@@ -177,6 +198,16 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
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
|
|
@@ -205,6 +236,15 @@ class GeneralManager(metaclass=GeneralManagerMeta):
|
|
|
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
|
general_manager/manager/meta.py
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Type, TypeVar, cast
|
|
7
|
+
|
|
7
8
|
from general_manager.interface.base_interface import InterfaceBase
|
|
9
|
+
from general_manager.logging import get_logger
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from general_manager.manager.general_manager import GeneralManager
|
|
@@ -12,6 +14,8 @@ if TYPE_CHECKING:
|
|
|
12
14
|
|
|
13
15
|
GeneralManagerType = TypeVar("GeneralManagerType", bound="GeneralManager")
|
|
14
16
|
|
|
17
|
+
logger = get_logger("manager.meta")
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class InvalidInterfaceTypeError(TypeError):
|
|
17
21
|
"""Raised when a GeneralManager is configured with an incompatible Interface class."""
|
|
@@ -89,6 +93,14 @@ class GeneralManagerMeta(type):
|
|
|
89
93
|
Returns:
|
|
90
94
|
type: The newly created subclass, possibly modified by Interface hooks.
|
|
91
95
|
"""
|
|
96
|
+
logger.debug(
|
|
97
|
+
"creating manager class",
|
|
98
|
+
context={
|
|
99
|
+
"class_name": name,
|
|
100
|
+
"module": attrs.get("__module__"),
|
|
101
|
+
"has_interface": "Interface" in attrs,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
92
104
|
|
|
93
105
|
def create_new_general_manager_class(
|
|
94
106
|
mcs: type["GeneralManagerMeta"],
|
|
@@ -109,12 +121,31 @@ class GeneralManagerMeta(type):
|
|
|
109
121
|
post_creation(new_class, interface_cls, model)
|
|
110
122
|
mcs.pending_attribute_initialization.append(new_class)
|
|
111
123
|
mcs.all_classes.append(new_class)
|
|
124
|
+
logger.debug(
|
|
125
|
+
"registered manager class with interface",
|
|
126
|
+
context={
|
|
127
|
+
"class_name": new_class.__name__,
|
|
128
|
+
"interface": interface_cls.__name__,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
112
131
|
|
|
113
132
|
else:
|
|
114
133
|
new_class = create_new_general_manager_class(mcs, name, bases, attrs)
|
|
134
|
+
logger.debug(
|
|
135
|
+
"registered manager class without interface",
|
|
136
|
+
context={
|
|
137
|
+
"class_name": new_class.__name__,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
115
140
|
|
|
116
141
|
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
|
117
142
|
mcs.pending_graphql_interfaces.append(new_class)
|
|
143
|
+
logger.debug(
|
|
144
|
+
"queued manager for graphql generation",
|
|
145
|
+
context={
|
|
146
|
+
"class_name": new_class.__name__,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
118
149
|
|
|
119
150
|
return new_class
|
|
120
151
|
|
|
@@ -179,6 +210,13 @@ class GeneralManagerMeta(type):
|
|
|
179
210
|
return self._class.Interface.get_field_type(self._attr_name)
|
|
180
211
|
attribute = instance._attributes.get(self._attr_name, _nonExistent)
|
|
181
212
|
if attribute is _nonExistent:
|
|
213
|
+
logger.warning(
|
|
214
|
+
"missing attribute on manager instance",
|
|
215
|
+
context={
|
|
216
|
+
"attribute": self._attr_name,
|
|
217
|
+
"manager": instance.__class__.__name__,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
182
220
|
raise MissingAttributeError(
|
|
183
221
|
self._attr_name, instance.__class__.__name__
|
|
184
222
|
)
|
|
@@ -186,6 +224,14 @@ class GeneralManagerMeta(type):
|
|
|
186
224
|
try:
|
|
187
225
|
attribute = attribute(instance._interface)
|
|
188
226
|
except Exception as e:
|
|
227
|
+
logger.exception(
|
|
228
|
+
"attribute evaluation failed",
|
|
229
|
+
context={
|
|
230
|
+
"attribute": self._attr_name,
|
|
231
|
+
"manager": instance.__class__.__name__,
|
|
232
|
+
"error": type(e).__name__,
|
|
233
|
+
},
|
|
234
|
+
)
|
|
189
235
|
raise AttributeEvaluationError(self._attr_name, e) from e
|
|
190
236
|
return attribute
|
|
191
237
|
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""Base permission contract used by GeneralManager instances."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
from abc import ABC, abstractmethod
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
|
|
6
|
-
from general_manager.permission.permission_checks import permission_functions
|
|
7
7
|
|
|
8
|
-
from django.contrib.auth.models import
|
|
8
|
+
from django.contrib.auth.models import AbstractBaseUser, AbstractUser, AnonymousUser
|
|
9
|
+
|
|
10
|
+
from general_manager.logging import get_logger
|
|
11
|
+
from general_manager.permission.permission_checks import permission_functions
|
|
9
12
|
from general_manager.permission.permission_data_manager import PermissionDataManager
|
|
10
13
|
from general_manager.permission.utils import (
|
|
11
|
-
validate_permission_string,
|
|
12
14
|
PermissionNotFoundError,
|
|
15
|
+
validate_permission_string,
|
|
13
16
|
)
|
|
14
|
-
import logging
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
17
19
|
from general_manager.manager.general_manager import GeneralManager
|
|
18
20
|
from general_manager.manager.meta import GeneralManagerMeta
|
|
19
21
|
|
|
20
|
-
logger =
|
|
22
|
+
logger = get_logger("permission.base")
|
|
21
23
|
|
|
22
24
|
UserLike: TypeAlias = AbstractBaseUser | AnonymousUser
|
|
23
25
|
|
|
@@ -86,11 +88,18 @@ class BasePermission(ABC):
|
|
|
86
88
|
errors = []
|
|
87
89
|
permission_data = PermissionDataManager(permission_data=data, manager=manager)
|
|
88
90
|
Permission = cls(permission_data, request_user)
|
|
91
|
+
user_identifier = getattr(request_user, "id", None)
|
|
89
92
|
for key in data.keys():
|
|
90
93
|
is_allowed = Permission.check_permission("create", key)
|
|
91
94
|
if not is_allowed:
|
|
92
|
-
logger.
|
|
93
|
-
|
|
95
|
+
logger.info(
|
|
96
|
+
"permission denied",
|
|
97
|
+
context={
|
|
98
|
+
"manager": manager.__name__,
|
|
99
|
+
"action": "create",
|
|
100
|
+
"attribute": key,
|
|
101
|
+
"user_id": user_identifier,
|
|
102
|
+
},
|
|
94
103
|
)
|
|
95
104
|
errors.append(f"Create permission denied for attribute '{key}'")
|
|
96
105
|
if errors:
|
|
@@ -121,11 +130,18 @@ class BasePermission(ABC):
|
|
|
121
130
|
base_data=old_manager_instance, update_data=data
|
|
122
131
|
)
|
|
123
132
|
Permission = cls(permission_data, request_user)
|
|
133
|
+
user_identifier = getattr(request_user, "id", None)
|
|
124
134
|
for key in data.keys():
|
|
125
135
|
is_allowed = Permission.check_permission("update", key)
|
|
126
136
|
if not is_allowed:
|
|
127
|
-
logger.
|
|
128
|
-
|
|
137
|
+
logger.info(
|
|
138
|
+
"permission denied",
|
|
139
|
+
context={
|
|
140
|
+
"manager": old_manager_instance.__class__.__name__,
|
|
141
|
+
"action": "update",
|
|
142
|
+
"attribute": key,
|
|
143
|
+
"user_id": user_identifier,
|
|
144
|
+
},
|
|
129
145
|
)
|
|
130
146
|
errors.append(f"Update permission denied for attribute '{key}'")
|
|
131
147
|
if errors:
|
|
@@ -154,11 +170,18 @@ class BasePermission(ABC):
|
|
|
154
170
|
errors = []
|
|
155
171
|
permission_data = PermissionDataManager(manager_instance)
|
|
156
172
|
Permission = cls(permission_data, request_user)
|
|
173
|
+
user_identifier = getattr(request_user, "id", None)
|
|
157
174
|
for key in manager_instance.__dict__.keys():
|
|
158
175
|
is_allowed = Permission.check_permission("delete", key)
|
|
159
176
|
if not is_allowed:
|
|
160
|
-
logger.
|
|
161
|
-
|
|
177
|
+
logger.info(
|
|
178
|
+
"permission denied",
|
|
179
|
+
context={
|
|
180
|
+
"manager": manager_instance.__class__.__name__,
|
|
181
|
+
"action": "delete",
|
|
182
|
+
"attribute": key,
|
|
183
|
+
"user_id": user_identifier,
|
|
184
|
+
},
|
|
162
185
|
)
|
|
163
186
|
errors.append(f"Delete permission denied for attribute '{key}'")
|
|
164
187
|
if errors:
|
|
@@ -185,8 +208,8 @@ class BasePermission(ABC):
|
|
|
185
208
|
if isinstance(user, (AbstractBaseUser, AnonymousUser)):
|
|
186
209
|
return user
|
|
187
210
|
try:
|
|
188
|
-
return User.objects.get(
|
|
189
|
-
except User.DoesNotExist:
|
|
211
|
+
return User.objects.get(pk=user)
|
|
212
|
+
except (User.DoesNotExist, ValueError, TypeError):
|
|
190
213
|
return AnonymousUser()
|
|
191
214
|
|
|
192
215
|
@abstractmethod
|
|
@@ -18,6 +18,7 @@ GENERAL_MANAGER_EXPORTS: LazyExportMap = {
|
|
|
18
18
|
"graph_ql_mutation": ("general_manager.api.mutation", "graph_ql_mutation"),
|
|
19
19
|
"GeneralManager": ("general_manager.manager.general_manager", "GeneralManager"),
|
|
20
20
|
"Input": ("general_manager.manager.input", "Input"),
|
|
21
|
+
"get_logger": ("general_manager.logging", "get_logger"),
|
|
21
22
|
"CalculationInterface": (
|
|
22
23
|
"general_manager.interface.calculation_interface",
|
|
23
24
|
"CalculationInterface",
|
general_manager/rule/rule.py
CHANGED
|
@@ -19,10 +19,12 @@ from general_manager.rule.handler import (
|
|
|
19
19
|
SumHandler,
|
|
20
20
|
)
|
|
21
21
|
from general_manager.manager.general_manager import GeneralManager
|
|
22
|
+
from general_manager.logging import get_logger
|
|
22
23
|
|
|
23
24
|
GeneralManagerType = TypeVar("GeneralManagerType", bound=GeneralManager)
|
|
24
25
|
|
|
25
26
|
NOTEXISTENT = object()
|
|
27
|
+
logger = get_logger("rule.engine")
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class NonexistentAttributeError(AttributeError):
|
|
@@ -131,6 +133,13 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
131
133
|
handler_cls: type[BaseRuleHandler] = import_string(path)
|
|
132
134
|
inst = handler_cls()
|
|
133
135
|
self._handlers[inst.function_name] = inst
|
|
136
|
+
logger.debug(
|
|
137
|
+
"initialised rule",
|
|
138
|
+
context={
|
|
139
|
+
"rule": self._func.__qualname__,
|
|
140
|
+
"variables": self._variables,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
134
143
|
|
|
135
144
|
@property
|
|
136
145
|
def func(self) -> Callable[[GeneralManagerType], bool]:
|
|
@@ -173,12 +182,47 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
173
182
|
if self._primary_param is not None:
|
|
174
183
|
self._last_args[self._primary_param] = x
|
|
175
184
|
|
|
185
|
+
logger.debug(
|
|
186
|
+
"evaluating rule",
|
|
187
|
+
context={
|
|
188
|
+
"rule": self._func.__qualname__,
|
|
189
|
+
"manager": type(x).__name__,
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
176
193
|
vals = self._extract_variable_values(x)
|
|
177
194
|
if self._ignore_if_none and any(v is None for v in vals.values()):
|
|
178
195
|
self._last_result = None
|
|
196
|
+
logger.debug(
|
|
197
|
+
"skipped rule evaluation due to missing values",
|
|
198
|
+
context={
|
|
199
|
+
"rule": self._func.__qualname__,
|
|
200
|
+
"manager": type(x).__name__,
|
|
201
|
+
"null_variables": [
|
|
202
|
+
name for name, value in vals.items() if value is None
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
)
|
|
179
206
|
return None
|
|
180
207
|
|
|
181
208
|
self._last_result = self._func(x)
|
|
209
|
+
if self._last_result:
|
|
210
|
+
logger.debug(
|
|
211
|
+
"rule evaluation passed",
|
|
212
|
+
context={
|
|
213
|
+
"rule": self._func.__qualname__,
|
|
214
|
+
"manager": type(x).__name__,
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
logger.info(
|
|
219
|
+
"rule evaluation failed",
|
|
220
|
+
context={
|
|
221
|
+
"rule": self._func.__qualname__,
|
|
222
|
+
"manager": type(x).__name__,
|
|
223
|
+
"variables": self._variables,
|
|
224
|
+
},
|
|
225
|
+
)
|
|
182
226
|
return self._last_result
|
|
183
227
|
|
|
184
228
|
def validate_custom_error_message(self) -> None:
|
|
@@ -214,6 +258,14 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
214
258
|
# Validate and substitute template placeholders
|
|
215
259
|
self.validate_custom_error_message()
|
|
216
260
|
vals = self._extract_variable_values(self._last_input)
|
|
261
|
+
manager_class = type(self._last_input).__name__
|
|
262
|
+
logger.debug(
|
|
263
|
+
"generating rule error messages",
|
|
264
|
+
context={
|
|
265
|
+
"rule": self._func.__qualname__,
|
|
266
|
+
"manager": manager_class,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
217
269
|
|
|
218
270
|
if self._custom_error_message:
|
|
219
271
|
formatted = re.sub(
|
|
@@ -221,9 +273,34 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
221
273
|
lambda m: str(vals.get(m.group(1), m.group(0))),
|
|
222
274
|
self._custom_error_message,
|
|
223
275
|
)
|
|
276
|
+
logger.info(
|
|
277
|
+
"rule produced custom error message",
|
|
278
|
+
context={
|
|
279
|
+
"rule": self._func.__qualname__,
|
|
280
|
+
"manager": manager_class,
|
|
281
|
+
"variables": self._variables,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
224
284
|
return {v: formatted for v in self._variables}
|
|
225
285
|
|
|
226
286
|
errors = self._generate_error_messages(vals)
|
|
287
|
+
if errors:
|
|
288
|
+
logger.info(
|
|
289
|
+
"rule produced error messages",
|
|
290
|
+
context={
|
|
291
|
+
"rule": self._func.__qualname__,
|
|
292
|
+
"manager": manager_class,
|
|
293
|
+
"variables": list(errors.keys()),
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
logger.debug(
|
|
298
|
+
"rule generated no error messages",
|
|
299
|
+
context={
|
|
300
|
+
"rule": self._func.__qualname__,
|
|
301
|
+
"manager": manager_class,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
227
304
|
return errors or None
|
|
228
305
|
|
|
229
306
|
def _extract_variables(self) -> List[str]:
|
|
@@ -355,6 +432,14 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
355
432
|
fn = self._get_node_name(left.func)
|
|
356
433
|
handler = self._handlers.get(fn)
|
|
357
434
|
if handler:
|
|
435
|
+
logger.debug(
|
|
436
|
+
"rule handler invoked",
|
|
437
|
+
context={
|
|
438
|
+
"rule": self._func.__qualname__,
|
|
439
|
+
"handler": handler.__class__.__name__,
|
|
440
|
+
"function": fn,
|
|
441
|
+
},
|
|
442
|
+
)
|
|
358
443
|
errors.update(
|
|
359
444
|
handler.handle(cmp, left, right, op, var_values, self)
|
|
360
445
|
)
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
from importlib import import_module
|
|
6
6
|
from typing import Any, Iterable, Mapping, MutableMapping, overload
|
|
7
7
|
|
|
8
|
+
from general_manager.logging import get_logger
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class MissingExportError(AttributeError):
|
|
10
12
|
"""Raised when a requested export is not defined in the public API."""
|
|
@@ -24,6 +26,7 @@ class MissingExportError(AttributeError):
|
|
|
24
26
|
|
|
25
27
|
ModuleTarget = tuple[str, str]
|
|
26
28
|
ModuleMap = Mapping[str, str | ModuleTarget]
|
|
29
|
+
logger = get_logger("utils.public_api")
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
@overload
|
|
@@ -63,11 +66,27 @@ def resolve_export(
|
|
|
63
66
|
MissingExportError: If `name` is not present in `module_all`.
|
|
64
67
|
"""
|
|
65
68
|
if name not in module_all:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"missing public api export",
|
|
71
|
+
context={
|
|
72
|
+
"module": module_globals["__name__"],
|
|
73
|
+
"export": name,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
66
76
|
raise MissingExportError(module_globals["__name__"], name)
|
|
67
77
|
module_path, attr_name = _normalize_target(name, module_map[name])
|
|
68
78
|
module = import_module(module_path)
|
|
69
79
|
value = getattr(module, attr_name)
|
|
70
80
|
module_globals[name] = value
|
|
81
|
+
logger.debug(
|
|
82
|
+
"resolved public api export",
|
|
83
|
+
context={
|
|
84
|
+
"module": module_globals["__name__"],
|
|
85
|
+
"export": name,
|
|
86
|
+
"target_module": module_path,
|
|
87
|
+
"target_attribute": attr_name,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
71
90
|
return value
|
|
72
91
|
|
|
73
92
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GeneralManager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
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: MIT License
|