GeneralManager 0.19.2__py3-none-any.whl → 0.20.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.
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 +52 -26
- 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.1.dist-info}/METADATA +1 -1
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.1.dist-info}/RECORD +20 -19
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.1.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.1.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.2.dist-info → generalmanager-0.20.1.dist-info}/top_level.txt +0 -0
|
@@ -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, IntegrityError
|
|
5
10
|
|
|
6
|
-
from typing import Type, Any, Callable, TYPE_CHECKING, cast, ClassVar
|
|
7
|
-
from django.db import models, transaction
|
|
8
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
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):
|
|
@@ -131,7 +132,11 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
131
132
|
"""
|
|
132
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
|
|
|
@@ -154,6 +159,7 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
154
159
|
data_list = cast(list[dict[str, Any]], parsed_data)
|
|
155
160
|
|
|
156
161
|
unique_fields = cls.get_unique_fields(model)
|
|
162
|
+
unique_field_order = tuple(sorted(unique_fields))
|
|
157
163
|
if not unique_fields:
|
|
158
164
|
raise MissingUniqueFieldError(parent_class.__name__)
|
|
159
165
|
|
|
@@ -163,29 +169,43 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
163
169
|
"deactivated": [],
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
editable_fields = {
|
|
173
|
+
f.name
|
|
174
|
+
for f in model._meta.local_fields
|
|
175
|
+
if getattr(f, "editable", True) and not getattr(f, "primary_key", False)
|
|
176
|
+
} - {"is_active"}
|
|
177
|
+
|
|
166
178
|
with transaction.atomic():
|
|
167
|
-
json_unique_values: set[Any] = set()
|
|
179
|
+
json_unique_values: set[tuple[Any, ...]] = set()
|
|
168
180
|
|
|
169
181
|
# data synchronization
|
|
170
182
|
for idx, data in enumerate(data_list):
|
|
171
183
|
try:
|
|
172
|
-
lookup = {field: data[field] for field in
|
|
184
|
+
lookup = {field: data[field] for field in unique_field_order}
|
|
173
185
|
except KeyError as e:
|
|
174
186
|
missing = e.args[0]
|
|
175
187
|
raise InvalidReadOnlyDataFormatError() from KeyError(
|
|
176
188
|
f"Item {idx} missing unique field '{missing}'."
|
|
177
189
|
)
|
|
178
|
-
unique_identifier = tuple(lookup[field] for field in
|
|
190
|
+
unique_identifier = tuple(lookup[field] for field in unique_field_order)
|
|
179
191
|
json_unique_values.add(unique_identifier)
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
instance = model.objects.filter(**lookup).first()
|
|
193
|
+
is_created = False
|
|
194
|
+
if instance is None:
|
|
195
|
+
# sanitize input and create with race-safety
|
|
196
|
+
allowed_fields = {f.name for f in model._meta.local_fields}
|
|
197
|
+
create_kwargs = {
|
|
198
|
+
k: v for k, v in data.items() if k in allowed_fields
|
|
199
|
+
}
|
|
200
|
+
try:
|
|
201
|
+
instance = model.objects.create(**create_kwargs)
|
|
202
|
+
is_created = True
|
|
203
|
+
except IntegrityError:
|
|
204
|
+
# created concurrently — fetch it
|
|
205
|
+
instance = model.objects.filter(**lookup).first()
|
|
206
|
+
if instance is None:
|
|
207
|
+
raise
|
|
182
208
|
updated = False
|
|
183
|
-
editable_fields = {
|
|
184
|
-
f.name
|
|
185
|
-
for f in model._meta.local_fields
|
|
186
|
-
if getattr(f, "editable", True)
|
|
187
|
-
and not getattr(f, "primary_key", False)
|
|
188
|
-
} - {"is_active"}
|
|
189
209
|
for field_name in editable_fields.intersection(data.keys()):
|
|
190
210
|
value = data[field_name]
|
|
191
211
|
if getattr(instance, field_name, None) != value:
|
|
@@ -199,8 +219,10 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
199
219
|
# deactivate instances not in JSON data
|
|
200
220
|
existing_instances = model.objects.filter(is_active=True)
|
|
201
221
|
for instance in existing_instances:
|
|
202
|
-
lookup = {
|
|
203
|
-
|
|
222
|
+
lookup = {
|
|
223
|
+
field: getattr(instance, field) for field in unique_field_order
|
|
224
|
+
}
|
|
225
|
+
unique_identifier = tuple(lookup[field] for field in unique_field_order)
|
|
204
226
|
if unique_identifier not in json_unique_values:
|
|
205
227
|
instance.is_active = False
|
|
206
228
|
instance.save()
|
|
@@ -208,10 +230,14 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
|
|
|
208
230
|
|
|
209
231
|
if changes["created"] or changes["updated"] or changes["deactivated"]:
|
|
210
232
|
logger.info(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
"readonly data synchronized",
|
|
234
|
+
context={
|
|
235
|
+
"manager": parent_class.__name__,
|
|
236
|
+
"model": model.__name__,
|
|
237
|
+
"created": len(changes["created"]),
|
|
238
|
+
"updated": len(changes["updated"]),
|
|
239
|
+
"deactivated": len(changes["deactivated"]),
|
|
240
|
+
},
|
|
215
241
|
)
|
|
216
242
|
|
|
217
243
|
@staticmethod
|
|
@@ -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",
|