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.

@@ -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.bucket.base_bucket import Bucket
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
- return cls(identification)
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
- DependencyTracker.track(
222
- cls.__name__, "filter", f"{cls.__parse_identification(kwargs)}"
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
- DependencyTracker.track(
238
- cls.__name__, "exclude", f"{cls.__parse_identification(kwargs)}"
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
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from django.conf import settings
6
- from typing import Any, Type, TYPE_CHECKING, ClassVar, TypeVar, Iterable, cast
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 AnonymousUser, AbstractBaseUser, AbstractUser
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 = logging.getLogger(__name__)
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.debug(
93
- f"Permission denied for {key} with value {data[key]} for user {request_user}"
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.debug(
128
- f"Permission denied for {key} with value {data[key]} for user {request_user}"
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.debug(
161
- f"Permission denied for {key} with value {getattr(manager_instance, key)} for user {request_user}"
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(id=user)
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",
@@ -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.19.2
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