GeneralManager 0.19.1__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/api.py +4 -4
- general_manager/_types/bucket.py +4 -4
- general_manager/_types/cache.py +6 -6
- general_manager/_types/factory.py +35 -35
- general_manager/_types/general_manager.py +11 -9
- general_manager/_types/interface.py +5 -5
- general_manager/_types/manager.py +4 -4
- general_manager/_types/measurement.py +1 -1
- general_manager/_types/permission.py +3 -3
- general_manager/_types/utils.py +12 -12
- general_manager/api/graphql.py +207 -98
- general_manager/api/mutation.py +9 -9
- general_manager/api/property.py +4 -4
- general_manager/apps.py +120 -65
- general_manager/bucket/{baseBucket.py → base_bucket.py} +5 -5
- general_manager/bucket/{calculationBucket.py → calculation_bucket.py} +10 -10
- general_manager/bucket/{databaseBucket.py → database_bucket.py} +16 -19
- general_manager/bucket/{groupBucket.py → group_bucket.py} +8 -8
- general_manager/cache/{cacheDecorator.py → cache_decorator.py} +27 -6
- general_manager/cache/{cacheTracker.py → cache_tracker.py} +1 -1
- general_manager/cache/{dependencyIndex.py → dependency_index.py} +24 -8
- general_manager/cache/{modelDependencyCollector.py → model_dependency_collector.py} +4 -4
- general_manager/cache/signals.py +1 -1
- general_manager/factory/{autoFactory.py → auto_factory.py} +24 -19
- general_manager/factory/factories.py +10 -13
- general_manager/factory/{factoryMethods.py → factory_methods.py} +19 -17
- general_manager/interface/{baseInterface.py → base_interface.py} +30 -22
- general_manager/interface/{calculationInterface.py → calculation_interface.py} +10 -10
- general_manager/interface/{databaseBasedInterface.py → database_based_interface.py} +42 -42
- general_manager/interface/{databaseInterface.py → database_interface.py} +21 -21
- general_manager/interface/models.py +3 -3
- general_manager/interface/{readOnlyInterface.py → read_only_interface.py} +34 -25
- general_manager/logging.py +133 -0
- general_manager/manager/{generalManager.py → general_manager.py} +75 -17
- general_manager/manager/{groupManager.py → group_manager.py} +6 -6
- general_manager/manager/input.py +1 -1
- general_manager/manager/meta.py +63 -17
- general_manager/measurement/measurement.py +3 -3
- general_manager/permission/{basePermission.py → base_permission.py} +55 -32
- general_manager/permission/{managerBasedPermission.py → manager_based_permission.py} +21 -21
- general_manager/permission/{mutationPermission.py → mutation_permission.py} +12 -12
- general_manager/permission/{permissionChecks.py → permission_checks.py} +2 -2
- general_manager/permission/{permissionDataManager.py → permission_data_manager.py} +6 -6
- general_manager/permission/utils.py +6 -6
- general_manager/public_api_registry.py +76 -66
- general_manager/rule/handler.py +2 -2
- general_manager/rule/rule.py +102 -11
- general_manager/utils/{filterParser.py → filter_parser.py} +3 -3
- general_manager/utils/{jsonEncoder.py → json_encoder.py} +1 -1
- general_manager/utils/{makeCacheKey.py → make_cache_key.py} +1 -1
- general_manager/utils/{noneToZero.py → none_to_zero.py} +1 -1
- general_manager/utils/{pathMapping.py → path_mapping.py} +14 -14
- general_manager/utils/public_api.py +19 -0
- general_manager/utils/testing.py +14 -14
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/METADATA +1 -1
- generalmanager-0.20.0.dist-info/RECORD +78 -0
- generalmanager-0.19.1.dist-info/RECORD +0 -77
- /general_manager/measurement/{measurementField.py → measurement_field.py} +0 -0
- /general_manager/permission/{fileBasedPermission.py → file_based_permission.py} +0 -0
- /general_manager/utils/{argsToKwargs.py → args_to_kwargs.py} +0 -0
- /general_manager/utils/{formatString.py → format_string.py} +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {generalmanager-0.19.1.dist-info → generalmanager-0.20.0.dist-info}/top_level.txt +0 -0
general_manager/api/mutation.py
CHANGED
|
@@ -20,10 +20,10 @@ import graphene # type: ignore[import]
|
|
|
20
20
|
from graphql import GraphQLResolveInfo
|
|
21
21
|
|
|
22
22
|
from general_manager.api.graphql import GraphQL, HANDLED_MANAGER_ERRORS
|
|
23
|
-
from general_manager.manager.
|
|
23
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
24
24
|
|
|
25
|
-
from general_manager.utils.
|
|
26
|
-
from general_manager.permission.
|
|
25
|
+
from general_manager.utils.format_string import snake_to_camel
|
|
26
|
+
from general_manager.permission.mutation_permission import MutationPermission
|
|
27
27
|
from types import UnionType
|
|
28
28
|
|
|
29
29
|
|
|
@@ -91,7 +91,7 @@ class DuplicateMutationOutputNameError(ValueError):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def
|
|
94
|
+
def graph_ql_mutation(
|
|
95
95
|
_func: FuncT | type[MutationPermission] | None = None,
|
|
96
96
|
permission: Optional[Type[MutationPermission]] = None,
|
|
97
97
|
) -> FuncT | Callable[[FuncT], FuncT]:
|
|
@@ -162,14 +162,14 @@ def graphQlMutation(
|
|
|
162
162
|
if get_origin(ann) is list or get_origin(ann) is List:
|
|
163
163
|
inner = get_args(ann)[0]
|
|
164
164
|
field = graphene.List(
|
|
165
|
-
GraphQL.
|
|
165
|
+
GraphQL._map_field_to_graphene_base_type(inner),
|
|
166
166
|
**kwargs,
|
|
167
167
|
)
|
|
168
168
|
else:
|
|
169
169
|
if inspect.isclass(ann) and issubclass(ann, GeneralManager):
|
|
170
170
|
field = graphene.ID(**kwargs)
|
|
171
171
|
else:
|
|
172
|
-
field = GraphQL.
|
|
172
|
+
field = GraphQL._map_field_to_graphene_base_type(ann)(**kwargs)
|
|
173
173
|
|
|
174
174
|
arg_fields[name] = field
|
|
175
175
|
|
|
@@ -201,7 +201,7 @@ def graphQlMutation(
|
|
|
201
201
|
|
|
202
202
|
basis_type = out.__value__ if is_named_type else out
|
|
203
203
|
|
|
204
|
-
outputs[field_name] = GraphQL.
|
|
204
|
+
outputs[field_name] = GraphQL._map_field_to_graphene_read(
|
|
205
205
|
basis_type, field_name
|
|
206
206
|
)
|
|
207
207
|
|
|
@@ -220,7 +220,7 @@ def graphQlMutation(
|
|
|
220
220
|
**kwargs: Mutation arguments provided by the client.
|
|
221
221
|
|
|
222
222
|
Returns:
|
|
223
|
-
mutation_class: Instance of the mutation with output fields populated; `success` is `True` on successful execution and `False` if a handled manager error occurred (after being forwarded to GraphQL.
|
|
223
|
+
mutation_class: Instance of the mutation with output fields populated; `success` is `True` on successful execution and `False` if a handled manager error occurred (after being forwarded to GraphQL._handle_graph_ql_error).
|
|
224
224
|
"""
|
|
225
225
|
if permission:
|
|
226
226
|
permission.check(kwargs, info.context.user)
|
|
@@ -244,7 +244,7 @@ def graphQlMutation(
|
|
|
244
244
|
data["success"] = True
|
|
245
245
|
return mutation_class(**data)
|
|
246
246
|
except HANDLED_MANAGER_ERRORS as error:
|
|
247
|
-
raise GraphQL.
|
|
247
|
+
raise GraphQL._handle_graph_ql_error(error) from error
|
|
248
248
|
|
|
249
249
|
# Assemble class dict
|
|
250
250
|
class_dict: dict[str, Any] = {
|
general_manager/api/property.py
CHANGED
|
@@ -109,9 +109,9 @@ class GraphQLProperty(property):
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
@overload
|
|
112
|
-
def
|
|
112
|
+
def graph_ql_property(func: T) -> GraphQLProperty: ...
|
|
113
113
|
@overload
|
|
114
|
-
def
|
|
114
|
+
def graph_ql_property(
|
|
115
115
|
*,
|
|
116
116
|
sortable: bool = False,
|
|
117
117
|
filterable: bool = False,
|
|
@@ -119,14 +119,14 @@ def graphQlProperty(
|
|
|
119
119
|
) -> Callable[[T], GraphQLProperty]: ...
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
def
|
|
122
|
+
def graph_ql_property(
|
|
123
123
|
func: Callable[..., Any] | None = None,
|
|
124
124
|
*,
|
|
125
125
|
sortable: bool = False,
|
|
126
126
|
filterable: bool = False,
|
|
127
127
|
query_annotation: Any | None = None,
|
|
128
128
|
) -> GraphQLProperty | Callable[[T], GraphQLProperty]:
|
|
129
|
-
from general_manager.cache.
|
|
129
|
+
from general_manager.cache.cache_decorator import cached
|
|
130
130
|
|
|
131
131
|
"""
|
|
132
132
|
Decorate a resolver to return a cached ``GraphQLProperty`` descriptor.
|
general_manager/apps.py
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import graphene # type: ignore[import]
|
|
4
|
-
import os
|
|
5
|
-
from django.conf import settings
|
|
6
|
-
from django.urls import path, re_path
|
|
7
|
-
from graphene_django.views import GraphQLView # type: ignore[import]
|
|
8
|
-
from importlib import import_module, util
|
|
2
|
+
|
|
9
3
|
import importlib.abc
|
|
4
|
+
import os
|
|
10
5
|
import sys
|
|
11
|
-
from
|
|
12
|
-
from general_manager.manager.meta import GeneralManagerMeta
|
|
13
|
-
from general_manager.manager.input import Input
|
|
14
|
-
from general_manager.api.property import graphQlProperty
|
|
15
|
-
from general_manager.api.graphql import GraphQL
|
|
6
|
+
from importlib import import_module, util
|
|
16
7
|
from typing import TYPE_CHECKING, Any, Callable, Type, cast
|
|
8
|
+
|
|
9
|
+
import graphene # type: ignore[import]
|
|
10
|
+
from django.apps import AppConfig
|
|
11
|
+
from django.conf import settings
|
|
17
12
|
from django.core.checks import register
|
|
18
|
-
import logging
|
|
19
13
|
from django.core.management.base import BaseCommand
|
|
14
|
+
from django.urls import path, re_path
|
|
15
|
+
from graphene_django.views import GraphQLView # type: ignore[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
|
|
20
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
21
|
+
from general_manager.manager.input import Input
|
|
22
|
+
from general_manager.manager.meta import GeneralManagerMeta
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class MissingRootUrlconfError(RuntimeError):
|
|
@@ -43,9 +46,9 @@ class InvalidPermissionClassError(TypeError):
|
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
if TYPE_CHECKING:
|
|
46
|
-
from general_manager.interface.
|
|
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):
|
|
@@ -58,16 +61,16 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
58
61
|
|
|
59
62
|
Sets up synchronization and schema validation for read-only interfaces, initializes attributes and property accessors for general manager classes, and configures the GraphQL schema and endpoint if enabled in settings.
|
|
60
63
|
"""
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
64
|
+
self.handle_read_only_interface(GeneralManagerMeta.read_only_classes)
|
|
65
|
+
self.initialize_general_manager_classes(
|
|
63
66
|
GeneralManagerMeta.pending_attribute_initialization,
|
|
64
67
|
GeneralManagerMeta.all_classes,
|
|
65
68
|
)
|
|
66
69
|
if getattr(settings, "AUTOCREATE_GRAPHQL", False):
|
|
67
|
-
self.
|
|
70
|
+
self.handle_graph_ql(GeneralManagerMeta.pending_graphql_interfaces)
|
|
68
71
|
|
|
69
72
|
@staticmethod
|
|
70
|
-
def
|
|
73
|
+
def handle_read_only_interface(
|
|
71
74
|
read_only_classes: list[Type[GeneralManager]],
|
|
72
75
|
) -> None:
|
|
73
76
|
"""
|
|
@@ -76,10 +79,13 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
76
79
|
Parameters:
|
|
77
80
|
read_only_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose Interface implements a ReadOnlyInterface; each class will have its read-only data synchronized before management commands and a Django system check registered to verify the Interface schema is up to date.
|
|
78
81
|
"""
|
|
79
|
-
GeneralmanagerConfig.
|
|
80
|
-
from general_manager.interface.
|
|
82
|
+
GeneralmanagerConfig.patch_read_only_interface_sync(read_only_classes)
|
|
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
|
|
@@ -96,7 +102,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
96
102
|
"""
|
|
97
103
|
|
|
98
104
|
def schema_check(*_: Any, **__: Any) -> list[Any]:
|
|
99
|
-
return ReadOnlyInterface.
|
|
105
|
+
return ReadOnlyInterface.ensure_schema_is_up_to_date(manager_cls, model)
|
|
100
106
|
|
|
101
107
|
return schema_check
|
|
102
108
|
|
|
@@ -113,18 +119,18 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
@staticmethod
|
|
116
|
-
def
|
|
122
|
+
def patch_read_only_interface_sync(
|
|
117
123
|
general_manager_classes: list[Type[GeneralManager]],
|
|
118
124
|
) -> None:
|
|
119
125
|
"""
|
|
120
126
|
Ensure the provided GeneralManager classes' ReadOnlyInterfaces synchronize their data before any Django management command is executed.
|
|
121
127
|
|
|
122
|
-
For each class in `general_manager_classes`, calls the class's `Interface.
|
|
128
|
+
For each class in `general_manager_classes`, calls the class's `Interface.sync_data()` to keep read-only data consistent. Skips synchronization when running the autoreload subprocess of `runserver`.
|
|
123
129
|
Parameters:
|
|
124
|
-
general_manager_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose `Interface` implements `
|
|
130
|
+
general_manager_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose `Interface` implements `sync_data`.
|
|
125
131
|
"""
|
|
126
132
|
"""
|
|
127
|
-
Wrap BaseCommand.run_from_argv to call `
|
|
133
|
+
Wrap BaseCommand.run_from_argv to call `sync_data()` on registered ReadOnlyInterfaces before executing the original command.
|
|
128
134
|
|
|
129
135
|
Skips synchronization when the command is `runserver` and the process is the autoreload subprocess.
|
|
130
136
|
Parameters:
|
|
@@ -133,7 +139,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
133
139
|
Returns:
|
|
134
140
|
The result returned by the original `BaseCommand.run_from_argv` call.
|
|
135
141
|
"""
|
|
136
|
-
from general_manager.interface.
|
|
142
|
+
from general_manager.interface.read_only_interface import ReadOnlyInterface
|
|
137
143
|
|
|
138
144
|
original_run_from_argv = BaseCommand.run_from_argv
|
|
139
145
|
|
|
@@ -141,7 +147,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
141
147
|
self: BaseCommand,
|
|
142
148
|
argv: list[str],
|
|
143
149
|
) -> None:
|
|
144
|
-
# Ensure
|
|
150
|
+
# Ensure sync_data is only called at real run of runserver
|
|
145
151
|
"""
|
|
146
152
|
Synchronizes all registered ReadOnlyInterface data before running a Django management command, except when running the autoreload subprocess of `runserver`.
|
|
147
153
|
|
|
@@ -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
|
-
read_only_interface.
|
|
163
|
-
|
|
164
|
-
logger.debug(
|
|
175
|
+
read_only_interface.sync_data()
|
|
176
|
+
|
|
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
|
|
@@ -169,20 +188,26 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
169
188
|
BaseCommand.run_from_argv = run_from_argv_with_sync # type: ignore[assignment]
|
|
170
189
|
|
|
171
190
|
@staticmethod
|
|
172
|
-
def
|
|
191
|
+
def initialize_general_manager_classes(
|
|
173
192
|
pending_attribute_initialization: list[Type[GeneralManager]],
|
|
174
193
|
all_classes: list[Type[GeneralManager]],
|
|
175
194
|
) -> None:
|
|
176
195
|
"""
|
|
177
196
|
Initialize GeneralManager classes' interface attributes, create attribute-based accessors, wire GraphQL connection properties between related managers, and validate each class's permission configuration.
|
|
178
197
|
|
|
179
|
-
For each class in `pending_attribute_initialization` this assigns the class's Interface attributes to its internal `_attributes` and creates property accessors for those attributes. For each class in `all_classes` this scans its Interface `input_fields` for inputs whose type is another GeneralManager subclass and adds a GraphQL property on the connected manager that resolves related objects filtered by the input attribute. Finally, validate and normalize the Permission attribute on every class via GeneralmanagerConfig.
|
|
198
|
+
For each class in `pending_attribute_initialization` this assigns the class's Interface attributes to its internal `_attributes` and creates property accessors for those attributes. For each class in `all_classes` this scans its Interface `input_fields` for inputs whose type is another GeneralManager subclass and adds a GraphQL property on the connected manager that resolves related objects filtered by the input attribute. Finally, validate and normalize the Permission attribute on every class via GeneralmanagerConfig.check_permission_class.
|
|
180
199
|
|
|
181
200
|
Parameters:
|
|
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,15 +229,21 @@ 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
|
-
attributes = general_manager_class.Interface.
|
|
237
|
+
attributes = general_manager_class.Interface.get_attributes()
|
|
210
238
|
general_manager_class._attributes = attributes
|
|
211
|
-
GeneralManagerMeta.
|
|
239
|
+
GeneralManagerMeta.create_at_properties_for_attributes(
|
|
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():
|
|
@@ -226,13 +257,13 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
226
257
|
setattr(
|
|
227
258
|
connected_manager,
|
|
228
259
|
f"{general_manager_class.__name__.lower()}_list",
|
|
229
|
-
|
|
260
|
+
graph_ql_property(resolver),
|
|
230
261
|
)
|
|
231
262
|
for general_manager_class in all_classes:
|
|
232
|
-
GeneralmanagerConfig.
|
|
263
|
+
GeneralmanagerConfig.check_permission_class(general_manager_class)
|
|
233
264
|
|
|
234
265
|
@staticmethod
|
|
235
|
-
def
|
|
266
|
+
def handle_graph_ql(
|
|
236
267
|
pending_graphql_interfaces: list[Type[GeneralManager]],
|
|
237
268
|
) -> None:
|
|
238
269
|
"""
|
|
@@ -241,10 +272,13 @@ 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
|
-
GraphQL.
|
|
247
|
-
GraphQL.
|
|
280
|
+
GraphQL.create_graphql_interface(general_manager_class)
|
|
281
|
+
GraphQL.create_graphql_mutation(general_manager_class)
|
|
248
282
|
|
|
249
283
|
query_class = type("Query", (graphene.ObjectType,), GraphQL._query_fields)
|
|
250
284
|
GraphQL._query_class = query_class
|
|
@@ -279,10 +313,10 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
279
313
|
schema_kwargs["subscription"] = GraphQL._subscription_class
|
|
280
314
|
schema = graphene.Schema(**schema_kwargs)
|
|
281
315
|
GraphQL._schema = schema
|
|
282
|
-
GeneralmanagerConfig.
|
|
316
|
+
GeneralmanagerConfig.add_graphql_url(schema)
|
|
283
317
|
|
|
284
318
|
@staticmethod
|
|
285
|
-
def
|
|
319
|
+
def add_graphql_url(schema: graphene.Schema) -> None:
|
|
286
320
|
"""
|
|
287
321
|
Add a GraphQL endpoint to the project's URL configuration and ensure the ASGI subscription route is configured.
|
|
288
322
|
|
|
@@ -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
|
|
|
@@ -498,7 +553,7 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
498
553
|
setattr(asgi_module, attr_name, wrapped_application)
|
|
499
554
|
|
|
500
555
|
@staticmethod
|
|
501
|
-
def
|
|
556
|
+
def check_permission_class(general_manager_class: Type[GeneralManager]) -> None:
|
|
502
557
|
"""
|
|
503
558
|
Validate and normalize a GeneralManager class's Permission attribute.
|
|
504
559
|
|
|
@@ -510,8 +565,8 @@ class GeneralmanagerConfig(AppConfig):
|
|
|
510
565
|
Raises:
|
|
511
566
|
InvalidPermissionClassError: If the existing Permission attribute is not a subclass of BasePermission.
|
|
512
567
|
"""
|
|
513
|
-
from general_manager.permission.
|
|
514
|
-
from general_manager.permission.
|
|
568
|
+
from general_manager.permission.base_permission import BasePermission
|
|
569
|
+
from general_manager.permission.manager_based_permission import (
|
|
515
570
|
ManagerBasedPermission,
|
|
516
571
|
)
|
|
517
572
|
|
|
@@ -14,10 +14,10 @@ from typing import (
|
|
|
14
14
|
GeneralManagerType = TypeVar("GeneralManagerType", bound="GeneralManager")
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
-
from general_manager.manager.
|
|
18
|
-
from general_manager.manager.
|
|
19
|
-
from general_manager.bucket.
|
|
20
|
-
from general_manager.interface.
|
|
17
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
18
|
+
from general_manager.manager.group_manager import GroupManager
|
|
19
|
+
from general_manager.bucket.group_bucket import GroupBucket
|
|
20
|
+
from general_manager.interface.base_interface import InterfaceBase
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Bucket(ABC, Generic[GeneralManagerType]):
|
|
@@ -243,7 +243,7 @@ class Bucket(ABC, Generic[GeneralManagerType]):
|
|
|
243
243
|
Returns:
|
|
244
244
|
GroupBucket[GeneralManagerType]: Bucket grouping items by the provided keys.
|
|
245
245
|
"""
|
|
246
|
-
from general_manager.bucket.
|
|
246
|
+
from general_manager.bucket.group_bucket import GroupBucket
|
|
247
247
|
|
|
248
248
|
return GroupBucket(self._manager_class, group_by_keys, self)
|
|
249
249
|
|
|
@@ -17,13 +17,13 @@ from typing import (
|
|
|
17
17
|
)
|
|
18
18
|
from operator import attrgetter
|
|
19
19
|
from copy import deepcopy
|
|
20
|
-
from general_manager.interface.
|
|
20
|
+
from general_manager.interface.base_interface import (
|
|
21
21
|
generalManagerClassName,
|
|
22
22
|
GeneralManagerType,
|
|
23
23
|
)
|
|
24
|
-
from general_manager.bucket.
|
|
24
|
+
from general_manager.bucket.base_bucket import Bucket
|
|
25
25
|
from general_manager.manager.input import Input
|
|
26
|
-
from general_manager.utils.
|
|
26
|
+
from general_manager.utils.filter_parser import parse_filters
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
from general_manager.api.property import GraphQLProperty
|
|
@@ -164,7 +164,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
164
164
|
Raises:
|
|
165
165
|
InvalidCalculationInterfaceError: If the manager_class.Interface does not inherit from CalculationInterface.
|
|
166
166
|
"""
|
|
167
|
-
from general_manager.interface.
|
|
167
|
+
from general_manager.interface.calculation_interface import (
|
|
168
168
|
CalculationInterface,
|
|
169
169
|
)
|
|
170
170
|
|
|
@@ -181,8 +181,8 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
181
181
|
{} if exclude_definitions is None else exclude_definitions
|
|
182
182
|
)
|
|
183
183
|
|
|
184
|
-
properties = self._manager_class.Interface.
|
|
185
|
-
possible_values = self.
|
|
184
|
+
properties = self._manager_class.Interface.get_graph_ql_properties()
|
|
185
|
+
possible_values = self.transform_properties_to_input_fields(
|
|
186
186
|
properties, self.input_fields
|
|
187
187
|
)
|
|
188
188
|
|
|
@@ -259,7 +259,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
259
259
|
IncompatibleBucketTypeError: If `other` is neither a CalculationBucket nor a compatible manager instance.
|
|
260
260
|
IncompatibleBucketManagerError: If `other` is a CalculationBucket for a different manager class.
|
|
261
261
|
"""
|
|
262
|
-
from general_manager.manager.
|
|
262
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
263
263
|
|
|
264
264
|
if isinstance(other, GeneralManager) and other.__class__ == self._manager_class:
|
|
265
265
|
return self.__or__(self.filter(id__in=[other.identification]))
|
|
@@ -322,7 +322,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
322
322
|
return f"{self.__class__.__name__}({self._manager_class.__name__}, {self.filter_definitions}, {self.exclude_definitions}, {self.sort_key}, {self.reverse})"
|
|
323
323
|
|
|
324
324
|
@staticmethod
|
|
325
|
-
def
|
|
325
|
+
def transform_properties_to_input_fields(
|
|
326
326
|
properties: dict[str, GraphQLProperty], input_fields: dict[str, Input]
|
|
327
327
|
) -> dict[str, Input]:
|
|
328
328
|
"""
|
|
@@ -420,7 +420,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
420
420
|
for combo in combinations:
|
|
421
421
|
yield self._manager_class(**combo)
|
|
422
422
|
|
|
423
|
-
def
|
|
423
|
+
def _sort_filters(self, sorted_inputs: List[str]) -> SortedFilters:
|
|
424
424
|
"""
|
|
425
425
|
Partition filters into input- and property-based buckets.
|
|
426
426
|
|
|
@@ -467,7 +467,7 @@ class CalculationBucket(Bucket[GeneralManagerType]):
|
|
|
467
467
|
|
|
468
468
|
if self._data is None:
|
|
469
469
|
sorted_inputs = self.topological_sort_inputs()
|
|
470
|
-
sorted_filters = self.
|
|
470
|
+
sorted_filters = self._sort_filters(sorted_inputs)
|
|
471
471
|
current_combinations = self._generate_input_combinations(
|
|
472
472
|
sorted_inputs,
|
|
473
473
|
sorted_filters["input_filters"],
|