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
|
@@ -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
|
general_manager/api/graphql.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
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
|
-
|
|
9
|
-
import
|
|
10
|
-
import
|
|
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.
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
451
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
+
|
|
5
5
|
import ast
|
|
6
6
|
import re
|
|
7
|
-
import
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
"readonly data synchronized",
|
|
217
|
+
context={
|
|
218
|
+
"manager": parent_class.__name__,
|
|
219
|
+
"model": model.__name__,
|
|
220
|
+
"created": len(changes["created"]),
|
|
221
|
+
"updated": len(changes["updated"]),
|
|
222
|
+
"deactivated": len(changes["deactivated"]),
|
|
223
|
+
},
|
|
215
224
|
)
|
|
216
225
|
|
|
217
226
|
@staticmethod
|