GeneralManager 0.17.0__py3-none-any.whl → 0.19.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.
Files changed (68) hide show
  1. general_manager/__init__.py +11 -1
  2. general_manager/_types/api.py +0 -1
  3. general_manager/_types/bucket.py +0 -1
  4. general_manager/_types/cache.py +0 -1
  5. general_manager/_types/factory.py +0 -1
  6. general_manager/_types/general_manager.py +0 -1
  7. general_manager/_types/interface.py +0 -1
  8. general_manager/_types/manager.py +0 -1
  9. general_manager/_types/measurement.py +0 -1
  10. general_manager/_types/permission.py +0 -1
  11. general_manager/_types/rule.py +0 -1
  12. general_manager/_types/utils.py +0 -1
  13. general_manager/api/__init__.py +13 -1
  14. general_manager/api/graphql.py +356 -221
  15. general_manager/api/graphql_subscription_consumer.py +81 -78
  16. general_manager/api/mutation.py +85 -23
  17. general_manager/api/property.py +39 -13
  18. general_manager/apps.py +188 -47
  19. general_manager/bucket/__init__.py +10 -1
  20. general_manager/bucket/calculationBucket.py +155 -53
  21. general_manager/bucket/databaseBucket.py +157 -45
  22. general_manager/bucket/groupBucket.py +133 -44
  23. general_manager/cache/__init__.py +10 -1
  24. general_manager/cache/cacheDecorator.py +3 -0
  25. general_manager/cache/dependencyIndex.py +143 -45
  26. general_manager/cache/signals.py +9 -2
  27. general_manager/factory/__init__.py +10 -1
  28. general_manager/factory/autoFactory.py +55 -13
  29. general_manager/factory/factories.py +110 -40
  30. general_manager/factory/factoryMethods.py +122 -34
  31. general_manager/interface/__init__.py +10 -1
  32. general_manager/interface/baseInterface.py +129 -36
  33. general_manager/interface/calculationInterface.py +35 -18
  34. general_manager/interface/databaseBasedInterface.py +71 -45
  35. general_manager/interface/databaseInterface.py +96 -38
  36. general_manager/interface/models.py +5 -5
  37. general_manager/interface/readOnlyInterface.py +94 -20
  38. general_manager/manager/__init__.py +10 -1
  39. general_manager/manager/generalManager.py +25 -16
  40. general_manager/manager/groupManager.py +20 -6
  41. general_manager/manager/meta.py +84 -16
  42. general_manager/measurement/__init__.py +10 -1
  43. general_manager/measurement/measurement.py +289 -95
  44. general_manager/measurement/measurementField.py +42 -31
  45. general_manager/permission/__init__.py +10 -1
  46. general_manager/permission/basePermission.py +120 -38
  47. general_manager/permission/managerBasedPermission.py +72 -21
  48. general_manager/permission/mutationPermission.py +14 -9
  49. general_manager/permission/permissionChecks.py +14 -12
  50. general_manager/permission/permissionDataManager.py +24 -11
  51. general_manager/permission/utils.py +34 -6
  52. general_manager/public_api_registry.py +36 -10
  53. general_manager/rule/__init__.py +10 -1
  54. general_manager/rule/handler.py +133 -44
  55. general_manager/rule/rule.py +178 -39
  56. general_manager/utils/__init__.py +10 -1
  57. general_manager/utils/argsToKwargs.py +34 -9
  58. general_manager/utils/filterParser.py +22 -7
  59. general_manager/utils/formatString.py +1 -0
  60. general_manager/utils/pathMapping.py +23 -15
  61. general_manager/utils/public_api.py +33 -2
  62. general_manager/utils/testing.py +31 -33
  63. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
  64. generalmanager-0.19.0.dist-info/RECORD +77 -0
  65. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
  66. generalmanager-0.17.0.dist-info/RECORD +0 -77
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
  68. {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,28 @@
1
1
  """GraphQL-aware property descriptor used by GeneralManager classes."""
2
2
 
3
- from typing import Any, Callable, get_type_hints, overload, TypeVar
4
3
  import sys
4
+ from typing import Any, Callable, TypeVar, get_type_hints, overload
5
5
 
6
6
  T = TypeVar("T", bound=Callable[..., Any])
7
7
 
8
8
 
9
+ class GraphQLPropertyReturnAnnotationError(TypeError):
10
+ """Raised when a GraphQLProperty is defined without a return type annotation."""
11
+
12
+ def __init__(self) -> None:
13
+ """
14
+ Indicates a GraphQLProperty-decorated function is missing a return type annotation.
15
+
16
+ This exception is raised to signal that a property resolver intended for use with GraphQLProperty must have an explicit return type hint. The exception message is: "GraphQLProperty requires a return type hint for the property function."
17
+ """
18
+ super().__init__(
19
+ "GraphQLProperty requires a return type hint for the property function."
20
+ )
21
+
22
+
9
23
  class GraphQLProperty(property):
10
24
  """Descriptor that exposes a property with GraphQL metadata and type hints."""
25
+
11
26
  sortable: bool
12
27
  filterable: bool
13
28
  query_annotation: Any | None
@@ -22,14 +37,17 @@ class GraphQLProperty(property):
22
37
  query_annotation: Any | None = None,
23
38
  ) -> None:
24
39
  """
25
- Initialise the descriptor with GraphQL-specific configuration.
40
+ Initialize the GraphQLProperty descriptor with GraphQL-specific metadata.
26
41
 
27
42
  Parameters:
28
- fget (Callable): Underlying resolver function.
29
- doc (str | None): Optional documentation string.
30
- sortable (bool): Whether the property participates in sorting.
31
- filterable (bool): Whether the property participates in filtering.
32
- query_annotation (Any | None): Optional annotation applied to querysets.
43
+ fget (Callable[..., Any]): The resolver function to wrap; its unwrapped form must include a return type annotation.
44
+ doc (str | None): Optional documentation string exposed on the descriptor.
45
+ sortable (bool): Whether the property should be considered for sorting.
46
+ filterable (bool): Whether the property should be considered for filtering.
47
+ query_annotation (Any | None): Optional annotation to apply when querying/queryset construction.
48
+
49
+ Raises:
50
+ GraphQLPropertyReturnAnnotationError: If the underlying resolver function does not declare a return type annotation.
33
51
  """
34
52
  super().__init__(fget, doc=doc)
35
53
  self.is_graphql_resolver = True
@@ -46,17 +64,25 @@ class GraphQLProperty(property):
46
64
  ) # falls decorator Annotations durchreicht
47
65
  ann = getattr(orig, "__annotations__", {}) or {}
48
66
  if "return" not in ann:
49
- raise TypeError(
50
- "GraphQLProperty requires a return type hint for the property function."
51
- )
67
+ raise GraphQLPropertyReturnAnnotationError()
52
68
 
53
69
  def __set_name__(self, owner: type, name: str) -> None:
54
- """Store the owning class and attribute name for later introspection."""
70
+ """
71
+ Record the owner class and attribute name for the descriptor to support later introspection.
72
+
73
+ Parameters:
74
+ owner (type): The class that owns this descriptor.
75
+ name (str): The attribute name under which this descriptor is assigned.
76
+ """
55
77
  self._owner = owner
56
78
  self._name = name
57
79
 
58
80
  def _try_resolve_type_hint(self) -> None:
59
- """Resolve the return type hint of the wrapped resolver, if available."""
81
+ """
82
+ Resolve and cache the wrapped resolver's return type hint.
83
+
84
+ When successful, stores the resolved return annotation on self._graphql_type_hint; if resolution fails or cannot be determined, sets self._graphql_type_hint to None.
85
+ """
60
86
  if self._graphql_type_hint is not None:
61
87
  return
62
88
 
@@ -71,7 +97,7 @@ class GraphQLProperty(property):
71
97
 
72
98
  hints = get_type_hints(self.fget, globalns=globalns, localns=localns)
73
99
  self._graphql_type_hint = hints.get("return", None)
74
- except Exception:
100
+ except (AttributeError, KeyError, NameError, TypeError, ValueError):
75
101
  self._graphql_type_hint = None
76
102
 
77
103
  @property
general_manager/apps.py CHANGED
@@ -13,12 +13,35 @@ from general_manager.manager.meta import GeneralManagerMeta
13
13
  from general_manager.manager.input import Input
14
14
  from general_manager.api.property import graphQlProperty
15
15
  from general_manager.api.graphql import GraphQL
16
- from typing import TYPE_CHECKING, Any, Type, cast
16
+ from typing import TYPE_CHECKING, Any, Callable, Type, cast
17
17
  from django.core.checks import register
18
18
  import logging
19
19
  from django.core.management.base import BaseCommand
20
20
 
21
21
 
22
+ class MissingRootUrlconfError(RuntimeError):
23
+ """Raised when Django settings do not define ROOT_URLCONF."""
24
+
25
+ def __init__(self) -> None:
26
+ """
27
+ Initialize the MissingRootUrlconfError with a default message indicating ROOT_URLCONF is missing from Django settings.
28
+ """
29
+ super().__init__("ROOT_URLCONF not found in settings.")
30
+
31
+
32
+ class InvalidPermissionClassError(TypeError):
33
+ """Raised when a GeneralManager Permission attribute is not a BasePermission subclass."""
34
+
35
+ def __init__(self, permission_name: str) -> None:
36
+ """
37
+ Create an InvalidPermissionClassError indicating a permission is not a subclass of BasePermission.
38
+
39
+ Parameters:
40
+ permission_name (str): The name of the permission (typically the class or attribute name) that failed validation. The exception message will be "`{permission_name} must be a subclass of BasePermission.`"
41
+ """
42
+ super().__init__(f"{permission_name} must be a subclass of BasePermission.")
43
+
44
+
22
45
  if TYPE_CHECKING:
23
46
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
24
47
 
@@ -48,24 +71,45 @@ class GeneralmanagerConfig(AppConfig):
48
71
  read_only_classes: list[Type[GeneralManager]],
49
72
  ) -> None:
50
73
  """
51
- Configures synchronization and schema validation for the given read-only GeneralManager classes.
74
+ Configure synchronization and register schema checks for read-only GeneralManager classes.
52
75
 
53
- For each provided class, ensures that its data is synchronized before any Django management command executes, and registers a system check to verify that the associated schema remains up to date.
76
+ Parameters:
77
+ 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.
54
78
  """
55
79
  GeneralmanagerConfig.patchReadOnlyInterfaceSync(read_only_classes)
56
80
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
57
81
 
58
82
  logger.debug("starting to register ReadOnlyInterface schema warnings...")
83
+
84
+ def _build_schema_check(
85
+ manager_cls: Type[GeneralManager], model: Any
86
+ ) -> Callable[..., list[Any]]:
87
+ """
88
+ Builds a Django system check callable that verifies the read-only interface schema for a manager against a model is current.
89
+
90
+ Parameters:
91
+ manager_cls (Type[GeneralManager]): The GeneralManager class whose ReadOnlyInterface schema will be validated.
92
+ model (Any): The model (or model-like descriptor) to check the schema against.
93
+
94
+ Returns:
95
+ Callable[..., list[Any]]: A callable suitable for Django's system checks framework that returns a list of check messages.
96
+ """
97
+
98
+ def schema_check(*_: Any, **__: Any) -> list[Any]:
99
+ return ReadOnlyInterface.ensureSchemaIsUpToDate(manager_cls, model)
100
+
101
+ return schema_check
102
+
59
103
  for general_manager_class in read_only_classes:
60
104
  read_only_interface = cast(
61
105
  Type[ReadOnlyInterface], general_manager_class.Interface
62
106
  )
63
107
 
64
- register(
65
- lambda app_configs, model=read_only_interface._model, manager_class=general_manager_class, **kwargs: ReadOnlyInterface.ensureSchemaIsUpToDate(
66
- manager_class, model
67
- ),
68
- "general_manager",
108
+ register("general_manager")(
109
+ _build_schema_check(
110
+ general_manager_class,
111
+ read_only_interface._model,
112
+ )
69
113
  )
70
114
 
71
115
  @staticmethod
@@ -73,9 +117,21 @@ class GeneralmanagerConfig(AppConfig):
73
117
  general_manager_classes: list[Type[GeneralManager]],
74
118
  ) -> None:
75
119
  """
76
- Monkey-patches Django's management command runner to synchronize all provided read-only interfaces before executing any management command, except during autoreload subprocesses of 'runserver'.
120
+ Ensure the provided GeneralManager classes' ReadOnlyInterfaces synchronize their data before any Django management command is executed.
121
+
122
+ For each class in `general_manager_classes`, calls the class's `Interface.syncData()` to keep read-only data consistent. Skips synchronization when running the autoreload subprocess of `runserver`.
123
+ Parameters:
124
+ general_manager_classes (list[Type[GeneralManager]]): GeneralManager subclasses whose `Interface` implements `syncData`.
125
+ """
126
+ """
127
+ Wrap BaseCommand.run_from_argv to call `syncData()` on registered ReadOnlyInterfaces before executing the original command.
77
128
 
78
- For each class in `general_manager_classes`, the associated read-only interface's `syncData` method is called prior to command execution, ensuring data consistency before management operations.
129
+ Skips synchronization when the command is `runserver` and the process is the autoreload subprocess.
130
+ Parameters:
131
+ self (BaseCommand): The management command instance.
132
+ argv (list[str]): Command-line arguments for the management command.
133
+ Returns:
134
+ The result returned by the original `BaseCommand.run_from_argv` call.
79
135
  """
80
136
  from general_manager.interface.readOnlyInterface import ReadOnlyInterface
81
137
 
@@ -87,13 +143,13 @@ class GeneralmanagerConfig(AppConfig):
87
143
  ) -> None:
88
144
  # Ensure syncData is only called at real run of runserver
89
145
  """
90
- Executes a Django management command, synchronizing all registered read-only interfaces before execution unless running in an autoreload subprocess of 'runserver'.
146
+ Synchronizes all registered ReadOnlyInterface data before running a Django management command, except when running the autoreload subprocess of `runserver`.
91
147
 
92
148
  Parameters:
93
- argv (list): Command-line arguments for the management command.
149
+ argv (list[str]): The management command `argv`, including the program name and command.
94
150
 
95
151
  Returns:
96
- The result of the original management command execution.
152
+ The value returned by the original `BaseCommand.run_from_argv` invocation.
97
153
  """
98
154
  run_main = os.environ.get("RUN_MAIN") == "true"
99
155
  command = argv[1] if len(argv) > 1 else None
@@ -110,7 +166,7 @@ class GeneralmanagerConfig(AppConfig):
110
166
  result = original_run_from_argv(self, argv)
111
167
  return result
112
168
 
113
- setattr(BaseCommand, "run_from_argv", run_from_argv_with_sync)
169
+ BaseCommand.run_from_argv = run_from_argv_with_sync # type: ignore[assignment]
114
170
 
115
171
  @staticmethod
116
172
  def initializeGeneralManagerClasses(
@@ -118,16 +174,40 @@ class GeneralmanagerConfig(AppConfig):
118
174
  all_classes: list[Type[GeneralManager]],
119
175
  ) -> None:
120
176
  """
121
- Initializes attributes and establishes dynamic relationships for GeneralManager classes.
177
+ Initialize GeneralManager classes' interface attributes, create attribute-based accessors, wire GraphQL connection properties between related managers, and validate each class's permission configuration.
122
178
 
123
- For each class pending attribute initialization, assigns interface attributes and creates property accessors. Then, for all registered GeneralManager classes, connects input fields referencing other GeneralManager subclasses by adding GraphQL properties to enable filtered access to related objects.
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.checkPermissionClass.
180
+
181
+ Parameters:
182
+ pending_attribute_initialization (list[type[GeneralManager]]): GeneralManager classes whose Interface attributes need to be initialized and whose attribute properties should be created.
183
+ all_classes (list[type[GeneralManager]]): All registered GeneralManager classes to inspect for input-field connections and to validate permissions.
124
184
  """
125
185
  logger.debug("Initializing GeneralManager classes...")
126
186
 
187
+ def _build_connection_resolver(
188
+ attribute_key: str, manager_cls: Type[GeneralManager]
189
+ ) -> Callable[[object], Any]:
190
+ """
191
+ Create a resolver that queries the given GeneralManager class by matching an attribute to a provided value.
192
+
193
+ Parameters:
194
+ attribute_key (str): Name of the attribute to filter on.
195
+ manager_cls (Type[GeneralManager]): GeneralManager subclass to query.
196
+
197
+ Returns:
198
+ Callable[[object], Any]: A callable that accepts a value and returns the result of filtering `manager_cls` where `attribute_key` equals that value.
199
+ """
200
+
201
+ def resolver(value: object) -> Any:
202
+ return manager_cls.filter(**{attribute_key: value})
203
+
204
+ resolver.__annotations__ = {"return": manager_cls}
205
+ return resolver
206
+
127
207
  logger.debug("starting to create attributes for GeneralManager classes...")
128
208
  for general_manager_class in pending_attribute_initialization:
129
209
  attributes = general_manager_class.Interface.getAttributes()
130
- setattr(general_manager_class, "_attributes", attributes)
210
+ general_manager_class._attributes = attributes
131
211
  GeneralManagerMeta.createAtPropertiesForAttributes(
132
212
  attributes.keys(), general_manager_class
133
213
  )
@@ -140,15 +220,13 @@ class GeneralmanagerConfig(AppConfig):
140
220
  attribute.type, GeneralManager
141
221
  ):
142
222
  connected_manager = attribute.type
143
- func = lambda x, attribute_name=attribute_name: general_manager_class.filter(
144
- **{attribute_name: x}
223
+ resolver = _build_connection_resolver(
224
+ attribute_name, general_manager_class
145
225
  )
146
-
147
- func.__annotations__ = {"return": general_manager_class}
148
226
  setattr(
149
227
  connected_manager,
150
228
  f"{general_manager_class.__name__.lower()}_list",
151
- graphQlProperty(func),
229
+ graphQlProperty(resolver),
152
230
  )
153
231
  for general_manager_class in all_classes:
154
232
  GeneralmanagerConfig.checkPermissionClass(general_manager_class)
@@ -158,10 +236,10 @@ class GeneralmanagerConfig(AppConfig):
158
236
  pending_graphql_interfaces: list[Type[GeneralManager]],
159
237
  ) -> None:
160
238
  """
161
- Create GraphQL interfaces and mutations for the given manager classes, build the GraphQL schema, and add the GraphQL endpoint to the URL configuration.
162
-
239
+ Generate GraphQL types, assemble the GraphQL schema for the given manager classes, and expose the GraphQL endpoint in the project's URL configuration.
240
+
163
241
  Parameters:
164
- pending_graphql_interfaces (list[Type[GeneralManager]]): GeneralManager classes that require GraphQL interface and mutation generation.
242
+ 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.
165
243
  """
166
244
  logger.debug("Starting to create GraphQL interfaces and mutations...")
167
245
  for general_manager_class in pending_graphql_interfaces:
@@ -206,19 +284,19 @@ class GeneralmanagerConfig(AppConfig):
206
284
  @staticmethod
207
285
  def addGraphqlUrl(schema: graphene.Schema) -> None:
208
286
  """
209
- Adds a GraphQL endpoint to the Django URL configuration using the provided schema.
287
+ Add a GraphQL endpoint to the project's URL configuration and ensure the ASGI subscription route is configured.
210
288
 
211
289
  Parameters:
212
- schema: The GraphQL schema to use for the endpoint.
290
+ schema (graphene.Schema): GraphQL schema to serve at the configured GRAPHQL_URL.
213
291
 
214
292
  Raises:
215
- Exception: If the ROOT_URLCONF setting is not defined in Django settings.
293
+ MissingRootUrlconfError: If ROOT_URLCONF is not defined in Django settings.
216
294
  """
217
295
  logging.debug("Adding GraphQL URL to Django settings...")
218
296
  root_url_conf_path = getattr(settings, "ROOT_URLCONF", None)
219
297
  graph_ql_url = getattr(settings, "GRAPHQL_URL", "graphql")
220
298
  if not root_url_conf_path:
221
- raise Exception("ROOT_URLCONF not found in settings")
299
+ raise MissingRootUrlconfError()
222
300
  urlconf = import_module(root_url_conf_path)
223
301
  urlconf.urlpatterns.append(
224
302
  path(
@@ -230,6 +308,13 @@ class GeneralmanagerConfig(AppConfig):
230
308
 
231
309
  @staticmethod
232
310
  def _ensure_asgi_subscription_route(graphql_url: str) -> None:
311
+ """
312
+ Ensure GraphQL websocket subscription route is integrated into the project's ASGI application when ASGI is configured.
313
+
314
+ If settings.ASGI_APPLICATION is absent or invalid, the function logs and returns without making changes. When the ASGI application module can be imported, delegates to GeneralmanagerConfig._finalize_asgi_module to add or wire WebSocket routes for GraphQL subscriptions at the given URL. If the ASGI module cannot be imported during Django's application population phase, the function arranges a deferred finalization so the ASGI module will be finalized once it is loaded; other import failures are logged and cause a no-op return.
315
+ Parameters:
316
+ graphql_url (str): URL path at which the GraphQL websocket subscription endpoint should be exposed (e.g., "/graphql/").
317
+ """
233
318
  asgi_path = getattr(settings, "ASGI_APPLICATION", None)
234
319
  if not asgi_path:
235
320
  logger.debug("ASGI_APPLICATION not configured; skipping websocket setup.")
@@ -249,7 +334,10 @@ class GeneralmanagerConfig(AppConfig):
249
334
  except RuntimeError as exc:
250
335
  if "populate() isn't reentrant" not in str(exc):
251
336
  logger.warning(
252
- "Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
337
+ "Unable to import ASGI module '%s': %s",
338
+ module_path,
339
+ exc,
340
+ exc_info=True,
253
341
  )
254
342
  return
255
343
 
@@ -262,10 +350,26 @@ class GeneralmanagerConfig(AppConfig):
262
350
  return
263
351
 
264
352
  def finalize(module: Any) -> None:
265
- GeneralmanagerConfig._finalize_asgi_module(module, attr_name, graphql_url)
353
+ """
354
+ Finalize integration of GraphQL subscription routing into the given ASGI module using the surrounding attribute name and GraphQL URL.
355
+
356
+ Ensure the ASGI module exposes websocket_urlpatterns and add a websocket route for the GraphQL subscription endpoint if missing; modify or wrap the module's application mapping or ASGI application as needed to include an authenticated websocket route for subscriptions.
357
+
358
+ Parameters:
359
+ module (Any): The imported ASGI module object to be modified in-place to support GraphQL websocket subscriptions.
360
+ """
361
+ GeneralmanagerConfig._finalize_asgi_module(
362
+ module, attr_name, graphql_url
363
+ )
266
364
 
267
365
  class _Loader(importlib.abc.Loader):
268
366
  def __init__(self, original_loader: importlib.abc.Loader) -> None:
367
+ """
368
+ Initialize the wrapper with the given original importlib loader.
369
+
370
+ Parameters:
371
+ original_loader (importlib.abc.Loader): The underlying loader that this wrapper will delegate to.
372
+ """
269
373
  self._original_loader = original_loader
270
374
 
271
375
  def create_module(self, spec): # type: ignore[override]
@@ -284,18 +388,33 @@ class GeneralmanagerConfig(AppConfig):
284
388
  self._processed = False
285
389
 
286
390
  def find_spec(self, fullname, path, target=None): # type: ignore[override]
391
+ """
392
+ Return a ModuleSpec for the wrapped loader the first time the specified module is requested.
393
+
394
+ If `fullname` matches the loader's target module name and this loader has not yet produced a spec, mark the loader as processed, create a new spec using the wrapped loader, copy `submodule_search_locations` from the original `spec` if present, and return the new spec; otherwise return `None`.
395
+
396
+ Parameters:
397
+ fullname (str): Fully-qualified name of the module being imported; matched against the loader's target module name.
398
+ path (Sequence[str] | None): Import path for package search (not used by this finder).
399
+ target (ModuleSpec | None): Optional existing spec suggested by the import machinery (ignored).
400
+
401
+ Returns:
402
+ ModuleSpec | None: The created ModuleSpec when `fullname` matches and has not been processed, `None` otherwise.
403
+ """
287
404
  if fullname != module_path or self._processed:
288
405
  return None
289
406
  self._processed = True
290
407
  new_spec = util.spec_from_loader(fullname, wrapped_loader)
291
408
  if new_spec is not None:
292
- new_spec.submodule_search_locations = spec.submodule_search_locations
409
+ new_spec.submodule_search_locations = (
410
+ spec.submodule_search_locations
411
+ )
293
412
  return new_spec
294
413
 
295
414
  finder = _Finder()
296
415
  sys.meta_path.insert(0, finder)
297
416
  return
298
- except Exception as exc: # pragma: no cover - defensive
417
+ except ImportError as exc: # pragma: no cover - defensive
299
418
  logger.warning(
300
419
  "Unable to import ASGI module '%s': %s", module_path, exc, exc_info=True
301
420
  )
@@ -304,14 +423,29 @@ class GeneralmanagerConfig(AppConfig):
304
423
  GeneralmanagerConfig._finalize_asgi_module(asgi_module, attr_name, graphql_url)
305
424
 
306
425
  @staticmethod
307
- def _finalize_asgi_module(asgi_module: Any, attr_name: str, graphql_url: str) -> None:
426
+ def _finalize_asgi_module(
427
+ asgi_module: Any, attr_name: str, graphql_url: str
428
+ ) -> None:
429
+ """
430
+ Ensure the ASGI module exposes a websocket route for GraphQL subscriptions and integrate it into the ASGI application.
431
+
432
+ If Channels and the GraphQL subscription consumer are available, this function appends a websocket URL pattern for the GraphQL subscriptions to asgi_module.websocket_urlpatterns (creating the list if needed) and then wires that websocket route into the module's ASGI application. If the module exposes an application mapping (application.application_mapping is a dict) the websocket entry is added there; otherwise the existing application is wrapped in a ProtocolTypeRouter that routes websocket traffic through Channels' AuthMiddlewareStack. If optional dependencies are missing, or websocket_urlpatterns cannot be appended, the function returns without modifying the ASGI module.
433
+
434
+ Parameters:
435
+ asgi_module (module): The imported ASGI module object referenced by ASGI_APPLICATION in settings.
436
+ attr_name (str): Attribute name on asgi_module that holds the ASGI application (e.g., "application").
437
+ graphql_url (str): URL path (relative, may include leading/trailing slashes) at which the GraphQL HTTP endpoint is mounted; used to build the websocket route for subscriptions.
438
+ """
308
439
  try:
309
440
  from channels.auth import AuthMiddlewareStack # type: ignore[import-untyped]
310
441
  from channels.routing import ProtocolTypeRouter, URLRouter # type: ignore[import-untyped]
311
442
  from general_manager.api.graphql_subscription_consumer import (
312
443
  GraphQLSubscriptionConsumer,
313
444
  )
314
- except Exception as exc: # pragma: no cover - optional dependency
445
+ except (
446
+ ImportError,
447
+ RuntimeError,
448
+ ) as exc: # pragma: no cover - optional dependency
315
449
  logger.debug(
316
450
  "Channels or GraphQL subscription consumer unavailable (%s); skipping websocket setup.",
317
451
  exc,
@@ -321,7 +455,7 @@ class GeneralmanagerConfig(AppConfig):
321
455
  websocket_patterns = getattr(asgi_module, "websocket_urlpatterns", None)
322
456
  if websocket_patterns is None:
323
457
  websocket_patterns = []
324
- setattr(asgi_module, "websocket_urlpatterns", websocket_patterns)
458
+ asgi_module.websocket_urlpatterns = websocket_patterns
325
459
 
326
460
  if not hasattr(websocket_patterns, "append"):
327
461
  logger.warning(
@@ -338,17 +472,16 @@ class GeneralmanagerConfig(AppConfig):
338
472
  for route in websocket_patterns
339
473
  )
340
474
  if not route_exists:
341
- websocket_route = re_path(pattern, GraphQLSubscriptionConsumer.as_asgi()) # type: ignore[arg-type]
342
- setattr(websocket_route, "_general_manager_graphql_ws", True)
475
+ websocket_route = re_path(pattern, GraphQLSubscriptionConsumer.as_asgi()) # type: ignore[arg-type]
476
+ websocket_route._general_manager_graphql_ws = True
343
477
  websocket_patterns.append(websocket_route)
344
478
 
345
479
  application = getattr(asgi_module, attr_name, None)
346
480
  if application is None:
347
481
  return
348
482
 
349
- if (
350
- hasattr(application, "application_mapping")
351
- and isinstance(application.application_mapping, dict)
483
+ if hasattr(application, "application_mapping") and isinstance(
484
+ application.application_mapping, dict
352
485
  ):
353
486
  application.application_mapping["websocket"] = AuthMiddlewareStack(
354
487
  URLRouter(list(websocket_patterns))
@@ -367,8 +500,15 @@ class GeneralmanagerConfig(AppConfig):
367
500
  @staticmethod
368
501
  def checkPermissionClass(general_manager_class: Type[GeneralManager]) -> None:
369
502
  """
370
- Checks if the class has a Permission attribute and if it is a subclass of BasePermission.
371
- If so, it sets the Permission attribute on the class.
503
+ Validate and normalize a GeneralManager class's Permission attribute.
504
+
505
+ If the class defines a Permission attribute, ensure it is a subclass of BasePermission and leave it assigned; if it is not a subclass, raise InvalidPermissionClassError. If the class does not define Permission, assign ManagerBasedPermission as the default.
506
+
507
+ Parameters:
508
+ general_manager_class (Type[GeneralManager]): GeneralManager subclass whose Permission attribute will be validated or initialized.
509
+
510
+ Raises:
511
+ InvalidPermissionClassError: If the existing Permission attribute is not a subclass of BasePermission.
372
512
  """
373
513
  from general_manager.permission.basePermission import BasePermission
374
514
  from general_manager.permission.managerBasedPermission import (
@@ -377,10 +517,11 @@ class GeneralmanagerConfig(AppConfig):
377
517
 
378
518
  if hasattr(general_manager_class, "Permission"):
379
519
  permission = general_manager_class.Permission
380
- if not issubclass(permission, BasePermission):
381
- raise TypeError(
382
- f"{permission.__name__} must be a subclass of BasePermission"
383
- )
520
+ if not (
521
+ isinstance(permission, type) and issubclass(permission, BasePermission)
522
+ ):
523
+ permission_name = getattr(permission, "__name__", repr(permission))
524
+ raise InvalidPermissionClassError(permission_name)
384
525
  general_manager_class.Permission = permission
385
526
  else:
386
527
  general_manager_class.Permission = ManagerBasedPermission
@@ -12,10 +12,19 @@ __all__ = list(BUCKET_EXPORTS)
12
12
  _MODULE_MAP = BUCKET_EXPORTS
13
13
 
14
14
  if TYPE_CHECKING:
15
- from general_manager._types.bucket import * # noqa: F401,F403
15
+ from general_manager._types.bucket import * # noqa: F403
16
16
 
17
17
 
18
18
  def __getattr__(name: str) -> Any:
19
+ """
20
+ Dynamically resolve and return a named bucket export from this module's public API.
21
+
22
+ Parameters:
23
+ name (str): The attribute name to resolve from the module's exports.
24
+
25
+ Returns:
26
+ Any: The resolved export object for `name`.
27
+ """
19
28
  return resolve_export(
20
29
  name,
21
30
  module_all=__all__,