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.

@@ -11,6 +11,7 @@ __all__ = [
11
11
  "ManagerBasedPermission",
12
12
  "ReadOnlyInterface",
13
13
  "Rule",
14
+ "get_logger",
14
15
  "graph_ql_mutation",
15
16
  "graph_ql_property",
16
17
  ]
@@ -23,5 +24,6 @@ from general_manager.manager.input import Input
23
24
  from general_manager.permission.manager_based_permission import ManagerBasedPermission
24
25
  from general_manager.interface.read_only_interface import ReadOnlyInterface
25
26
  from general_manager.rule.rule import Rule
27
+ from general_manager.logging import get_logger
26
28
  from general_manager.api.mutation import graph_ql_mutation
27
29
  from general_manager.api.property import graph_ql_property
@@ -39,11 +39,12 @@ from graphql.language.ast import (
39
39
  from asgiref.sync import async_to_sync
40
40
  from channels.layers import BaseChannelLayer, get_channel_layer
41
41
 
42
+ from general_manager.bucket.base_bucket import Bucket
42
43
  from general_manager.cache.cache_tracker import DependencyTracker
43
44
  from general_manager.cache.dependency_index import Dependency
44
45
  from general_manager.cache.signals import post_data_change
45
- from general_manager.bucket.base_bucket import Bucket
46
46
  from general_manager.interface.base_interface import InterfaceBase
47
+ from general_manager.logging import get_logger
47
48
  from general_manager.manager.general_manager import GeneralManager
48
49
  from general_manager.measurement.measurement import Measurement
49
50
 
@@ -57,6 +58,9 @@ if TYPE_CHECKING:
57
58
  from graphene import ResolveInfo as GraphQLResolveInfo
58
59
 
59
60
 
61
+ logger = get_logger("api.graphql")
62
+
63
+
60
64
  @dataclass(slots=True)
61
65
  class SubscriptionEvent:
62
66
  """Payload delivered to GraphQL subscription resolvers."""
@@ -345,18 +349,39 @@ class GraphQL:
345
349
  cls._mutations[create_name] = cls.generate_create_mutation_class(
346
350
  generalManagerClass, default_return_values
347
351
  )
352
+ logger.debug(
353
+ "registered graphql mutation",
354
+ context={
355
+ "manager": generalManagerClass.__name__,
356
+ "mutation": create_name,
357
+ },
358
+ )
348
359
 
349
360
  if InterfaceBase.update.__code__ != interface_cls.update.__code__:
350
361
  update_name = f"update{generalManagerClass.__name__}"
351
362
  cls._mutations[update_name] = cls.generate_update_mutation_class(
352
363
  generalManagerClass, default_return_values
353
364
  )
365
+ logger.debug(
366
+ "registered graphql mutation",
367
+ context={
368
+ "manager": generalManagerClass.__name__,
369
+ "mutation": update_name,
370
+ },
371
+ )
354
372
 
355
373
  if InterfaceBase.deactivate.__code__ != interface_cls.deactivate.__code__:
356
374
  delete_name = f"delete{generalManagerClass.__name__}"
357
375
  cls._mutations[delete_name] = cls.generate_delete_mutation_class(
358
376
  generalManagerClass, default_return_values
359
377
  )
378
+ logger.debug(
379
+ "registered graphql mutation",
380
+ context={
381
+ "manager": generalManagerClass.__name__,
382
+ "mutation": delete_name,
383
+ },
384
+ )
360
385
 
361
386
  @classmethod
362
387
  def create_graphql_interface(
@@ -376,6 +401,11 @@ class GraphQL:
376
401
  if not interface_cls:
377
402
  return None
378
403
 
404
+ logger.info(
405
+ "building graphql interface",
406
+ context={"manager": generalManagerClass.__name__},
407
+ )
408
+
379
409
  graphene_type_name = f"{generalManagerClass.__name__}Type"
380
410
  fields: dict[str, Any] = {}
381
411
 
@@ -434,6 +464,16 @@ class GraphQL:
434
464
  cls.manager_registry[generalManagerClass.__name__] = generalManagerClass
435
465
  cls._add_queries_to_schema(graphene_type, generalManagerClass)
436
466
  cls._add_subscription_field(graphene_type, generalManagerClass)
467
+ exposed_fields = sorted(
468
+ name for name in fields.keys() if not name.startswith("resolve_")
469
+ )
470
+ logger.debug(
471
+ "registered graphql interface",
472
+ context={
473
+ "manager": generalManagerClass.__name__,
474
+ "fields": exposed_fields,
475
+ },
476
+ )
437
477
 
438
478
  @staticmethod
439
479
  def _sort_by_options(
@@ -1811,23 +1851,47 @@ class GraphQL:
1811
1851
  Returns:
1812
1852
  GraphQLError: GraphQL error containing the original message and an `extensions['code']` indicating the error category.
1813
1853
  """
1854
+ message = str(error)
1855
+ error_name = type(error).__name__
1814
1856
  if isinstance(error, PermissionError):
1857
+ logger.info(
1858
+ "graphql permission error",
1859
+ context={
1860
+ "error": error_name,
1861
+ "message": message,
1862
+ },
1863
+ )
1815
1864
  return GraphQLError(
1816
- str(error),
1865
+ message,
1817
1866
  extensions={
1818
1867
  "code": "PERMISSION_DENIED",
1819
1868
  },
1820
1869
  )
1821
1870
  elif isinstance(error, (ValueError, ValidationError, TypeError)):
1871
+ logger.warning(
1872
+ "graphql user error",
1873
+ context={
1874
+ "error": error_name,
1875
+ "message": message,
1876
+ },
1877
+ )
1822
1878
  return GraphQLError(
1823
- str(error),
1879
+ message,
1824
1880
  extensions={
1825
1881
  "code": "BAD_USER_INPUT",
1826
1882
  },
1827
1883
  )
1828
1884
  else:
1885
+ logger.error(
1886
+ "graphql internal error",
1887
+ context={
1888
+ "error": error_name,
1889
+ "message": message,
1890
+ },
1891
+ exc_info=error,
1892
+ )
1829
1893
  return GraphQLError(
1830
- str(error),
1894
+ message,
1831
1895
  extensions={
1832
1896
  "code": "INTERNAL_SERVER_ERROR",
1833
1897
  },
@@ -1860,10 +1924,24 @@ class GraphQL:
1860
1924
  manager_class = instance.__class__
1861
1925
 
1862
1926
  if manager_class.__name__ not in cls.manager_registry:
1927
+ logger.debug(
1928
+ "skipping subscription event for unregistered manager",
1929
+ context={
1930
+ "manager": manager_class.__name__,
1931
+ "action": action,
1932
+ },
1933
+ )
1863
1934
  return
1864
1935
 
1865
1936
  channel_layer = cls._get_channel_layer()
1866
1937
  if channel_layer is None:
1938
+ logger.warning(
1939
+ "channel layer unavailable for subscription event",
1940
+ context={
1941
+ "manager": manager_class.__name__,
1942
+ "action": action,
1943
+ },
1944
+ )
1867
1945
  return
1868
1946
 
1869
1947
  group_name = cls._group_name(manager_class, instance.identification)
@@ -1874,6 +1952,14 @@ class GraphQL:
1874
1952
  "action": action,
1875
1953
  },
1876
1954
  )
1955
+ logger.debug(
1956
+ "dispatched subscription event",
1957
+ context={
1958
+ "manager": manager_class.__name__,
1959
+ "action": action,
1960
+ "group": group_name,
1961
+ },
1962
+ )
1877
1963
 
1878
1964
 
1879
1965
  post_data_change.connect(GraphQL._handle_data_change, weak=False)
general_manager/apps.py CHANGED
@@ -1,22 +1,25 @@
1
1
  from __future__ import annotations
2
- from django.apps import AppConfig
3
- import graphene # type: ignore[import]
2
+
3
+ import importlib.abc
4
4
  import os
5
+ import sys
6
+ from importlib import import_module, util
7
+ from typing import TYPE_CHECKING, Any, Callable, Type, cast
8
+
9
+ import graphene # type: ignore[import]
10
+ from django.apps import AppConfig
5
11
  from django.conf import settings
12
+ from django.core.checks import register
13
+ from django.core.management.base import BaseCommand
6
14
  from django.urls import path, re_path
7
15
  from graphene_django.views import GraphQLView # type: ignore[import]
8
- from importlib import import_module, util
9
- import importlib.abc
10
- import sys
16
+
17
+ from general_manager.api.graphql import GraphQL
18
+ from general_manager.api.property import graph_ql_property
19
+ from general_manager.logging import get_logger
11
20
  from general_manager.manager.general_manager import GeneralManager
12
- from general_manager.manager.meta import GeneralManagerMeta
13
21
  from general_manager.manager.input import Input
14
- from general_manager.api.property import graph_ql_property
15
- from general_manager.api.graphql import GraphQL
16
- from typing import TYPE_CHECKING, Any, Callable, Type, cast
17
- from django.core.checks import register
18
- import logging
19
- from django.core.management.base import BaseCommand
22
+ from general_manager.manager.meta import GeneralManagerMeta
20
23
 
21
24
 
22
25
  class MissingRootUrlconfError(RuntimeError):
@@ -45,7 +48,7 @@ class InvalidPermissionClassError(TypeError):
45
48
  if TYPE_CHECKING:
46
49
  from general_manager.interface.read_only_interface import ReadOnlyInterface
47
50
 
48
- logger = logging.getLogger(__name__)
51
+ logger = get_logger("apps")
49
52
 
50
53
 
51
54
  class GeneralmanagerConfig(AppConfig):
@@ -79,7 +82,10 @@ class GeneralmanagerConfig(AppConfig):
79
82
  GeneralmanagerConfig.patch_read_only_interface_sync(read_only_classes)
80
83
  from general_manager.interface.read_only_interface import ReadOnlyInterface
81
84
 
82
- logger.debug("starting to register ReadOnlyInterface schema warnings...")
85
+ logger.debug(
86
+ "registering read-only schema checks",
87
+ context={"count": len(read_only_classes)},
88
+ )
83
89
 
84
90
  def _build_schema_check(
85
91
  manager_cls: Type[GeneralManager], model: Any
@@ -154,14 +160,27 @@ class GeneralmanagerConfig(AppConfig):
154
160
  run_main = os.environ.get("RUN_MAIN") == "true"
155
161
  command = argv[1] if len(argv) > 1 else None
156
162
  if command != "runserver" or run_main:
157
- logger.debug("start syncing ReadOnlyInterface data...")
163
+ logger.debug(
164
+ "syncing read-only interfaces",
165
+ context={
166
+ "command": command,
167
+ "autoreload": not run_main if command == "runserver" else False,
168
+ "count": len(general_manager_classes),
169
+ },
170
+ )
158
171
  for general_manager_class in general_manager_classes:
159
172
  read_only_interface = cast(
160
173
  Type[ReadOnlyInterface], general_manager_class.Interface
161
174
  )
162
175
  read_only_interface.sync_data()
163
176
 
164
- logger.debug("finished syncing ReadOnlyInterface data.")
177
+ logger.debug(
178
+ "finished syncing read-only interfaces",
179
+ context={
180
+ "command": command,
181
+ "count": len(general_manager_classes),
182
+ },
183
+ )
165
184
 
166
185
  result = original_run_from_argv(self, argv)
167
186
  return result
@@ -182,7 +201,13 @@ class GeneralmanagerConfig(AppConfig):
182
201
  pending_attribute_initialization (list[type[GeneralManager]]): GeneralManager classes whose Interface attributes need to be initialized and whose attribute properties should be created.
183
202
  all_classes (list[type[GeneralManager]]): All registered GeneralManager classes to inspect for input-field connections and to validate permissions.
184
203
  """
185
- logger.debug("Initializing GeneralManager classes...")
204
+ logger.debug(
205
+ "initializing general manager classes",
206
+ context={
207
+ "pending_attributes": len(pending_attribute_initialization),
208
+ "total": len(all_classes),
209
+ },
210
+ )
186
211
 
187
212
  def _build_connection_resolver(
188
213
  attribute_key: str, manager_cls: Type[GeneralManager]
@@ -204,7 +229,10 @@ class GeneralmanagerConfig(AppConfig):
204
229
  resolver.__annotations__ = {"return": manager_cls}
205
230
  return resolver
206
231
 
207
- logger.debug("starting to create attributes for GeneralManager classes...")
232
+ logger.debug(
233
+ "creating manager attributes",
234
+ context={"pending_attributes": len(pending_attribute_initialization)},
235
+ )
208
236
  for general_manager_class in pending_attribute_initialization:
209
237
  attributes = general_manager_class.Interface.get_attributes()
210
238
  general_manager_class._attributes = attributes
@@ -212,7 +240,10 @@ class GeneralmanagerConfig(AppConfig):
212
240
  attributes.keys(), general_manager_class
213
241
  )
214
242
 
215
- logger.debug("starting to connect inputs to other general manager classes...")
243
+ logger.debug(
244
+ "linking manager inputs",
245
+ context={"total_classes": len(all_classes)},
246
+ )
216
247
  for general_manager_class in all_classes:
217
248
  attributes = getattr(general_manager_class.Interface, "input_fields", {})
218
249
  for attribute_name, attribute in attributes.items():
@@ -241,7 +272,10 @@ class GeneralmanagerConfig(AppConfig):
241
272
  Parameters:
242
273
  pending_graphql_interfaces (list[Type[GeneralManager]]): GeneralManager classes for which GraphQL interfaces, mutations, and optional subscriptions should be created and included in the assembled schema.
243
274
  """
244
- logger.debug("Starting to create GraphQL interfaces and mutations...")
275
+ logger.debug(
276
+ "creating graphql interfaces and mutations",
277
+ context={"pending": len(GeneralManagerMeta.pending_graphql_interfaces)},
278
+ )
245
279
  for general_manager_class in pending_graphql_interfaces:
246
280
  GraphQL.create_graphql_interface(general_manager_class)
247
281
  GraphQL.create_graphql_mutation(general_manager_class)
@@ -292,7 +326,13 @@ class GeneralmanagerConfig(AppConfig):
292
326
  Raises:
293
327
  MissingRootUrlconfError: If ROOT_URLCONF is not defined in Django settings.
294
328
  """
295
- logging.debug("Adding GraphQL URL to Django settings...")
329
+ logger.debug(
330
+ "configuring graphql http endpoint",
331
+ context={
332
+ "root_urlconf": getattr(settings, "ROOT_URLCONF", None),
333
+ "graphql_url": getattr(settings, "GRAPHQL_URL", "graphql"),
334
+ },
335
+ )
296
336
  root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
297
337
  graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql")
298
338
  if not root_url_conf_path:
@@ -317,15 +357,18 @@ class GeneralmanagerConfig(AppConfig):
317
357
  """
318
358
  asgi_path = getattr(settings, "ASGI_APPLICATION", None)
319
359
  if not asgi_path:
320
- logger.debug("ASGI_APPLICATION not configured; skipping websocket setup.")
360
+ logger.debug(
361
+ "asgi application missing",
362
+ context={"graphql_url": graphql_url},
363
+ )
321
364
  return
322
365
 
323
366
  try:
324
367
  module_path, attr_name = asgi_path.rsplit(".", 1)
325
368
  except ValueError:
326
369
  logger.warning(
327
- "ASGI_APPLICATION '%s' is not a valid module path; skipping websocket setup.",
328
- asgi_path,
370
+ "invalid asgi application path",
371
+ context={"asgi_application": asgi_path},
329
372
  )
330
373
  return
331
374
 
@@ -334,9 +377,12 @@ class GeneralmanagerConfig(AppConfig):
334
377
  except RuntimeError as exc:
335
378
  if "populate() isn't reentrant" not in str(exc):
336
379
  logger.warning(
337
- "Unable to import ASGI module '%s': %s",
338
- module_path,
339
- exc,
380
+ "unable to import asgi module",
381
+ context={
382
+ "module": module_path,
383
+ "error": type(exc).__name__,
384
+ "message": str(exc),
385
+ },
340
386
  exc_info=True,
341
387
  )
342
388
  return
@@ -344,8 +390,8 @@ class GeneralmanagerConfig(AppConfig):
344
390
  spec = util.find_spec(module_path)
345
391
  if spec is None or spec.loader is None:
346
392
  logger.warning(
347
- "Could not locate loader for ASGI module '%s'; skipping websocket setup.",
348
- module_path,
393
+ "missing loader for asgi module",
394
+ context={"module": module_path},
349
395
  )
350
396
  return
351
397
 
@@ -416,7 +462,13 @@ class GeneralmanagerConfig(AppConfig):
416
462
  return
417
463
  except ImportError as exc: # pragma: no cover - defensive
418
464
  logger.warning(
419
- "Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
465
+ "unable to import asgi module",
466
+ context={
467
+ "module": module_path,
468
+ "error": type(exc).__name__,
469
+ "message": str(exc),
470
+ },
471
+ exc_info=True,
420
472
  )
421
473
  return
422
474
 
@@ -447,8 +499,11 @@ class GeneralmanagerConfig(AppConfig):
447
499
  RuntimeError,
448
500
  ) as exc: # pragma: no cover - optional dependency
449
501
  logger.debug(
450
- "Channels or GraphQL subscription consumer unavailable (%s); skipping websocket setup.",
451
- exc,
502
+ "channels dependencies unavailable",
503
+ context={
504
+ "error": type(exc).__name__,
505
+ "message": str(exc),
506
+ },
452
507
  )
453
508
  return
454
509
 
@@ -459,8 +514,8 @@ class GeneralmanagerConfig(AppConfig):
459
514
 
460
515
  if not hasattr(websocket_patterns, "append"):
461
516
  logger.warning(
462
- "websocket_urlpatterns in '%s' does not support appending; skipping websocket setup.",
463
- asgi_module.__name__,
517
+ "websocket_urlpatterns not appendable",
518
+ context={"module": asgi_module.__name__},
464
519
  )
465
520
  return
466
521
 
@@ -1,11 +1,14 @@
1
1
  """Helpers for caching GeneralManager computations with dependency tracking."""
2
2
 
3
- from typing import Any, Callable, Optional, Protocol, Set, TypeVar, cast
4
3
  from functools import wraps
4
+ from typing import Any, Callable, Optional, Protocol, Set, TypeVar, cast
5
+
5
6
  from django.core.cache import cache as django_cache
7
+
6
8
  from general_manager.cache.cache_tracker import DependencyTracker
7
- from general_manager.cache.dependency_index import record_dependencies, Dependency
9
+ from general_manager.cache.dependency_index import Dependency, record_dependencies
8
10
  from general_manager.cache.model_dependency_collector import ModelDependencyCollector
11
+ from general_manager.logging import get_logger
9
12
  from general_manager.utils.make_cache_key import make_cache_key
10
13
 
11
14
 
@@ -42,6 +45,7 @@ RecordFn = Callable[[str, Set[Dependency]], None]
42
45
  FuncT = TypeVar("FuncT", bound=Callable[..., object])
43
46
 
44
47
  _SENTINEL = object()
48
+ logger = get_logger("cache.decorator")
45
49
 
46
50
 
47
51
  def cached(
@@ -74,6 +78,14 @@ def cached(
74
78
  if cached_deps:
75
79
  for class_name, operation, identifier in cached_deps:
76
80
  DependencyTracker.track(class_name, operation, identifier)
81
+ logger.debug(
82
+ "cache hit",
83
+ context={
84
+ "function": func.__qualname__,
85
+ "key": key,
86
+ "dependency_count": len(cached_deps) if cached_deps else 0,
87
+ },
88
+ )
77
89
  return cached_result
78
90
 
79
91
  with DependencyTracker() as dependencies:
@@ -86,6 +98,15 @@ def cached(
86
98
  if dependencies and timeout is None:
87
99
  record_fn(key, dependencies)
88
100
 
101
+ logger.debug(
102
+ "cache miss recorded",
103
+ context={
104
+ "function": func.__qualname__,
105
+ "key": key,
106
+ "dependency_count": len(dependencies),
107
+ "timeout": timeout,
108
+ },
109
+ )
89
110
  return result
90
111
 
91
112
  # fix for python 3.14:
@@ -1,16 +1,18 @@
1
1
  """Dependency index management for cached GeneralManager query results."""
2
2
 
3
3
  from __future__ import annotations
4
- import time
4
+
5
5
  import ast
6
6
  import re
7
- import logging
7
+ import time
8
8
  from datetime import date, datetime
9
+ from typing import TYPE_CHECKING, Any, Iterable, Literal, Tuple, Type, cast
9
10
 
10
11
  from django.core.cache import cache
11
- from general_manager.cache.signals import post_data_change, pre_data_change
12
12
  from django.dispatch import receiver
13
- from typing import Literal, Any, Iterable, TYPE_CHECKING, Type, Tuple, cast
13
+
14
+ from general_manager.cache.signals import post_data_change, pre_data_change
15
+ from general_manager.logging import get_logger
14
16
 
15
17
  if TYPE_CHECKING:
16
18
  from general_manager.manager.general_manager import GeneralManager
@@ -31,7 +33,7 @@ type dependency_index = dict[
31
33
  type filter_type = Literal["filter", "exclude", "identification"]
32
34
  type Dependency = Tuple[general_manager_name, filter_type, str]
33
35
 
34
- logger = logging.getLogger(__name__)
36
+ logger = get_logger("cache.dependency_index")
35
37
 
36
38
 
37
39
  class DependencyLockTimeoutError(TimeoutError):
@@ -616,7 +618,14 @@ def generic_cache_invalidation(
616
618
  )
617
619
  if should_invalidate:
618
620
  logger.info(
619
- f"Invalidate cache key {ck} for filter {lookup} with value {val_key}"
621
+ "invalidating cache key",
622
+ context={
623
+ "manager": manager_name,
624
+ "key": ck,
625
+ "lookup": lookup,
626
+ "action": action,
627
+ "value": val_key,
628
+ },
620
629
  )
621
630
  invalidate_cache_key(ck)
622
631
  remove_cache_key_from_index(ck)
@@ -634,7 +643,14 @@ def generic_cache_invalidation(
634
643
  )
635
644
  if should_invalidate:
636
645
  logger.info(
637
- f"Invalidate cache key {ck} for exclude {lookup} with value {val_key}"
646
+ "invalidating cache key",
647
+ context={
648
+ "manager": manager_name,
649
+ "key": ck,
650
+ "lookup": lookup,
651
+ "action": action,
652
+ "value": val_key,
653
+ },
638
654
  )
639
655
  invalidate_cache_key(ck)
640
656
  remove_cache_key_from_index(ck)
@@ -302,7 +302,7 @@ class InterfaceBase(ABC):
302
302
  raise InvalidInputValueError(name, value, allowed_values)
303
303
 
304
304
  @classmethod
305
- def create(cls, *args: Any, **kwargs: Any) -> Any:
305
+ def create(cls, *args: Any, **kwargs: Any) -> dict[str, Any]:
306
306
  """
307
307
  Create a new managed record in the underlying data store using the interface's inputs.
308
308
 
@@ -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
- ) -> int:
72
+ ) -> dict[str, Any]:
73
73
  """
74
74
  Create a new model instance using the provided field values.
75
75
 
@@ -91,11 +91,11 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
91
91
  instance = cls.__set_attr_for_write(model_cls(), kwargs)
92
92
  pk = cls._save_with_history(instance, creator_id, history_comment)
93
93
  cls.__set_many_to_many_attributes(instance, many_to_many_kwargs)
94
- return pk
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
- ) -> int:
98
+ ) -> dict[str, Any]:
99
99
  """
100
100
  Update this instance with the provided field values.
101
101
 
@@ -117,11 +117,11 @@ class DatabaseInterface(DBBasedInterface[GeneralManagerModel]):
117
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
119
  self.__set_many_to_many_attributes(instance, many_to_many_kwargs)
120
- return pk
120
+ return {"id": pk}
121
121
 
122
122
  def deactivate(
123
123
  self, creator_id: int | None, history_comment: str | None = None
124
- ) -> int:
124
+ ) -> dict[str, Any]:
125
125
  """
126
126
  Mark the current model instance as inactive and record the change.
127
127
 
@@ -139,7 +139,7 @@ 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
145
  def __set_many_to_many_attributes(
@@ -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 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
- classPreCreationMethod,
14
+ attributes,
12
15
  classPostCreationMethod,
16
+ classPreCreationMethod,
13
17
  generalManagerClassName,
14
- attributes,
15
18
  interfaceBaseClass,
16
19
  )
17
- from django.db import connection
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 = logging.getLogger(__name__)
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
- f"Schema for ReadOnlyInterface '{cls._parent_class.__name__}' is not up to date."
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
 
@@ -208,10 +213,14 @@ class ReadOnlyInterface(DBBasedInterface[GeneralManagerBasisModel]):
208
213
 
209
214
  if changes["created"] or changes["updated"] or changes["deactivated"]:
210
215
  logger.info(
211
- f"Data changes for ReadOnlyInterface '{parent_class.__name__}': "
212
- f"Created: {len(changes['created'])}, "
213
- f"Updated: {len(changes['updated'])}, "
214
- f"Deactivated: {len(changes['deactivated'])}"
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