GeneralManager 0.17.0__py3-none-any.whl → 0.18.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.

Files changed (67) 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/dependencyIndex.py +143 -45
  25. general_manager/cache/signals.py +9 -2
  26. general_manager/factory/__init__.py +10 -1
  27. general_manager/factory/autoFactory.py +55 -13
  28. general_manager/factory/factories.py +110 -40
  29. general_manager/factory/factoryMethods.py +122 -34
  30. general_manager/interface/__init__.py +10 -1
  31. general_manager/interface/baseInterface.py +129 -36
  32. general_manager/interface/calculationInterface.py +35 -18
  33. general_manager/interface/databaseBasedInterface.py +71 -45
  34. general_manager/interface/databaseInterface.py +96 -38
  35. general_manager/interface/models.py +5 -5
  36. general_manager/interface/readOnlyInterface.py +94 -20
  37. general_manager/manager/__init__.py +10 -1
  38. general_manager/manager/generalManager.py +25 -16
  39. general_manager/manager/groupManager.py +20 -6
  40. general_manager/manager/meta.py +84 -16
  41. general_manager/measurement/__init__.py +10 -1
  42. general_manager/measurement/measurement.py +289 -95
  43. general_manager/measurement/measurementField.py +42 -31
  44. general_manager/permission/__init__.py +10 -1
  45. general_manager/permission/basePermission.py +120 -38
  46. general_manager/permission/managerBasedPermission.py +72 -21
  47. general_manager/permission/mutationPermission.py +14 -9
  48. general_manager/permission/permissionChecks.py +14 -12
  49. general_manager/permission/permissionDataManager.py +24 -11
  50. general_manager/permission/utils.py +34 -6
  51. general_manager/public_api_registry.py +36 -10
  52. general_manager/rule/__init__.py +10 -1
  53. general_manager/rule/handler.py +133 -44
  54. general_manager/rule/rule.py +178 -39
  55. general_manager/utils/__init__.py +10 -1
  56. general_manager/utils/argsToKwargs.py +34 -9
  57. general_manager/utils/filterParser.py +22 -7
  58. general_manager/utils/formatString.py +1 -0
  59. general_manager/utils/pathMapping.py +23 -15
  60. general_manager/utils/public_api.py +33 -2
  61. general_manager/utils/testing.py +31 -33
  62. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/METADATA +2 -1
  63. generalmanager-0.18.0.dist-info/RECORD +77 -0
  64. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
  65. generalmanager-0.17.0.dist-info/RECORD +0 -77
  66. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
  67. {generalmanager-0.17.0.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
@@ -10,43 +10,53 @@ import pint
10
10
  from general_manager.measurement.measurement import Measurement, ureg, currency_units
11
11
  from django.db.backends.base.base import BaseDatabaseWrapper
12
12
  from django.db.models import Lookup, Transform
13
- from typing import Any, ClassVar, cast
13
+ from typing import Any, cast
14
+
15
+
16
+ class MeasurementFieldNotEditableError(ValidationError):
17
+ """Raised when attempting to modify a non-editable MeasurementField."""
18
+
19
+ def __init__(self, field_name: str) -> None:
20
+ """
21
+ Initialize the exception indicating an attempt to assign to a non-editable measurement field.
22
+
23
+ Parameters:
24
+ field_name (str): Name of the field that was attempted to be modified; used to compose the error message.
25
+ """
26
+ super().__init__(f"{field_name} is not editable.")
14
27
 
15
28
 
16
29
  class MeasurementField(models.Field):
17
30
  description = "Stores a measurement (value + unit) but exposes a single field API"
18
31
 
19
- empty_values: ClassVar[tuple[object, ...]] = (None, "", [], (), {})
32
+ empty_values: tuple[object, ...] = (None, "", [], (), {})
20
33
 
21
34
  def __init__(
22
35
  self,
23
36
  base_unit: str,
24
- *args: object,
37
+ *args: Any,
25
38
  null: bool = False,
26
39
  blank: bool = False,
27
40
  editable: bool = True,
28
- **kwargs: object,
41
+ **kwargs: Any,
29
42
  ) -> None:
30
43
  """
31
44
  Configure a measurement field backed by separate value and unit columns.
32
45
 
33
46
  Parameters:
34
- base_unit (str): Canonical unit used when normalising stored measurements.
35
- args (tuple): Positional arguments forwarded to the base `Field` implementation.
36
- null (bool): If True, the measurement may be stored as NULL.
37
- blank (bool): If True, forms may submit an empty value.
38
- editable (bool): If False, assignments through the API raise a validation error.
39
- kwargs (dict): Additional keyword arguments forwarded to the base `Field`.
40
-
41
- Returns:
42
- None
47
+ base_unit (str): Canonical unit used to normalise stored measurements.
48
+ *args: Positional arguments forwarded to the base Field implementation.
49
+ null (bool): If True, the measurement may be stored as NULL in the database.
50
+ blank (bool): If True, forms may accept an empty value for this field.
51
+ editable (bool): If False, assignments through the model API are rejected.
52
+ **kwargs: Additional keyword arguments forwarded to the base Field implementation.
43
53
  """
44
54
  self.base_unit = base_unit
45
55
  self.base_dimension = ureg.parse_expression(self.base_unit).dimensionality
46
56
 
47
57
  self.editable = editable
48
- self.value_field: models.DecimalField[Any]
49
- self.unit_field: models.CharField[Any]
58
+ self.value_field: models.Field[Any, Any]
59
+ self.unit_field: models.Field[Any, Any]
50
60
  if null:
51
61
  self.value_field = models.DecimalField(
52
62
  max_digits=30,
@@ -78,7 +88,13 @@ class MeasurementField(models.Field):
78
88
  blank=blank,
79
89
  )
80
90
 
81
- super().__init__(*args, null=null, blank=blank, editable=editable, **kwargs)
91
+ options: dict[str, Any] = {
92
+ **kwargs,
93
+ "null": null,
94
+ "blank": blank,
95
+ "editable": editable,
96
+ }
97
+ super().__init__(*args, **options)
82
98
 
83
99
  def contribute_to_class(
84
100
  self,
@@ -88,16 +104,13 @@ class MeasurementField(models.Field):
88
104
  **kwargs: object,
89
105
  ) -> None:
90
106
  """
91
- Attach the measurement field and its backing columns to a Django model.
107
+ Attach the measurement field and its backing value and unit fields to the model and install the descriptor.
92
108
 
93
109
  Parameters:
94
- cls (type[models.Model]): Model class receiving the field.
95
- name (str): Attribute name of the field on the model.
96
- private_only (bool): Whether the field should be treated as private.
97
- kwargs (dict): Additional options forwarded to the base implementation.
98
-
99
- Returns:
100
- None
110
+ cls: Model class receiving the field.
111
+ name: Attribute name to use on the model for this field.
112
+ private_only: Whether the field should be treated as private.
113
+ kwargs: Additional options forwarded to the base implementation.
101
114
  """
102
115
  super().contribute_to_class(cls, name, private_only=private_only, **kwargs)
103
116
  self.concrete = False
@@ -289,20 +302,18 @@ class MeasurementField(models.Field):
289
302
  value: Measurement | str | None,
290
303
  ) -> None:
291
304
  """
292
- Assign a measurement to the model instance after validating compatibility.
305
+ Set a measurement on a model instance after validating editability, type, and unit compatibility.
293
306
 
294
307
  Parameters:
295
308
  instance (models.Model): Model instance receiving the value.
296
- value (Measurement | str | None): Measurement value supplied by the caller.
297
-
298
- Returns:
299
- None
309
+ value (Measurement | str | None): A Measurement, a string parseable to a Measurement, or None to clear the field.
300
310
 
301
311
  Raises:
302
- ValidationError: If the field is not editable, the value is invalid, or the units are incompatible.
312
+ MeasurementFieldNotEditableError: If the field is not editable.
313
+ ValidationError: If the value is not a Measurement (or valid parseable string), if currency unit rules are violated, or if the unit is incompatible with the field's base unit.
303
314
  """
304
315
  if not self.editable:
305
- raise ValidationError(f"{self.name} is not editable.")
316
+ raise MeasurementFieldNotEditableError(self.name)
306
317
  if value is None:
307
318
  setattr(instance, self.value_attr, None)
308
319
  setattr(instance, self.unit_attr, None)
@@ -12,10 +12,19 @@ __all__ = list(PERMISSION_EXPORTS)
12
12
  _MODULE_MAP = PERMISSION_EXPORTS
13
13
 
14
14
  if TYPE_CHECKING:
15
- from general_manager._types.permission import * # noqa: F401,F403
15
+ from general_manager._types.permission import * # noqa: F403
16
16
 
17
17
 
18
18
  def __getattr__(name: str) -> Any:
19
+ """
20
+ Resolve and return a lazily-exposed public attribute for this module.
21
+
22
+ Parameters:
23
+ name (str): The attribute name being accessed on the module.
24
+
25
+ Returns:
26
+ Any: The resolved attribute value associated with `name`.
27
+ """
19
28
  return resolve_export(
20
29
  name,
21
30
  module_all=__all__,
@@ -2,20 +2,43 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from abc import ABC, abstractmethod
5
- from typing import TYPE_CHECKING, Any, Literal
6
- from general_manager.permission.permissionChecks import (
7
- permission_functions,
8
- permission_filter,
9
- )
5
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
6
+ from general_manager.permission.permissionChecks import permission_functions
10
7
 
11
- from django.contrib.auth.models import AnonymousUser, AbstractUser
8
+ from django.contrib.auth.models import AnonymousUser, AbstractBaseUser, AbstractUser
12
9
  from general_manager.permission.permissionDataManager import PermissionDataManager
13
- from general_manager.permission.utils import validatePermissionString
10
+ from general_manager.permission.utils import (
11
+ validatePermissionString,
12
+ PermissionNotFoundError,
13
+ )
14
+ import logging
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from general_manager.manager.generalManager import GeneralManager
17
18
  from general_manager.manager.meta import GeneralManagerMeta
18
19
 
20
+ logger = logging.getLogger(__name__)
21
+
22
+ UserLike: TypeAlias = AbstractBaseUser | AnonymousUser
23
+
24
+
25
+ class PermissionCheckError(PermissionError):
26
+ """Raised when permission evaluation fails for a user."""
27
+
28
+ def __init__(self, user: UserLike, errors: list[str]) -> None:
29
+ """
30
+ Initialize a PermissionCheckError carrying the requesting user's identity and permission failure details.
31
+
32
+ Parameters:
33
+ user (UserLike): The user for whom permission evaluation failed; if the user has an `id`, it is included in the error message, otherwise the user is labeled "anonymous".
34
+ errors (list[str]): A list of error messages describing individual permission failures.
35
+ """
36
+ user_id = getattr(user, "id", None)
37
+ user_label = "anonymous" if user_id is None else f"id={user_id}"
38
+ super().__init__(
39
+ f"Permission denied for user {user_label} with errors: {errors}."
40
+ )
41
+
19
42
 
20
43
  class BasePermission(ABC):
21
44
  """Abstract base class defining CRUD permission checks for managers."""
@@ -23,7 +46,7 @@ class BasePermission(ABC):
23
46
  def __init__(
24
47
  self,
25
48
  instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
26
- request_user: AbstractUser | AnonymousUser,
49
+ request_user: UserLike,
27
50
  ) -> None:
28
51
  """Initialise the permission context for a specific manager and user."""
29
52
  self._instance = instance
@@ -35,7 +58,7 @@ class BasePermission(ABC):
35
58
  return self._instance
36
59
 
37
60
  @property
38
- def request_user(self) -> AbstractUser | AnonymousUser:
61
+ def request_user(self) -> UserLike:
39
62
  """Return the user being evaluated for permission checks."""
40
63
  return self._request_user
41
64
 
@@ -44,9 +67,21 @@ class BasePermission(ABC):
44
67
  cls,
45
68
  data: dict[str, Any],
46
69
  manager: type[GeneralManager],
47
- request_user: AbstractUser | AnonymousUser | Any,
70
+ request_user: UserLike | Any,
48
71
  ) -> None:
49
- """Validate create permissions for the supplied payload."""
72
+ """
73
+ Validate that the requesting user is allowed to create each attribute in the provided payload.
74
+
75
+ Checks create permission for every key in `data` using the given `manager`. If any attribute is not permitted, raises a PermissionCheckError that includes the evaluated user and a list of denial messages.
76
+
77
+ Parameters:
78
+ data (dict[str, Any]): Mapping of attribute names to the values intended for creation.
79
+ manager (type[GeneralManager]): Manager class that defines the model/schema against which permissions are checked.
80
+ request_user (UserLike | Any): User instance or user id (will be resolved to a user or AnonymousUser).
81
+
82
+ Raises:
83
+ PermissionCheckError: If one or more attributes in `data` are denied for the resolved `request_user`.
84
+ """
50
85
  request_user = cls.getUserWithId(request_user)
51
86
  errors = []
52
87
  permission_data = PermissionDataManager(permission_data=data, manager=manager)
@@ -54,22 +89,31 @@ class BasePermission(ABC):
54
89
  for key in data.keys():
55
90
  is_allowed = Permission.checkPermission("create", key)
56
91
  if not is_allowed:
57
- errors.append(
92
+ logger.debug(
58
93
  f"Permission denied for {key} with value {data[key]} for user {request_user}"
59
94
  )
95
+ errors.append(f"Create permission denied for attribute '{key}'")
60
96
  if errors:
61
- raise PermissionError(
62
- f"Permission denied for user {request_user} with errors: {errors}"
63
- )
97
+ raise PermissionCheckError(request_user, errors)
64
98
 
65
99
  @classmethod
66
100
  def checkUpdatePermission(
67
101
  cls,
68
102
  data: dict[str, Any],
69
103
  old_manager_instance: GeneralManager,
70
- request_user: AbstractUser | AnonymousUser | Any,
104
+ request_user: UserLike | Any,
71
105
  ) -> None:
72
- """Validate update permissions for the supplied payload."""
106
+ """
107
+ Validate whether the request_user can update the given fields on an existing manager instance.
108
+
109
+ Parameters:
110
+ data (dict[str, Any]): Mapping of attribute names to new values to be applied.
111
+ old_manager_instance (GeneralManager): Existing manager instance whose current state is used to evaluate update permissions.
112
+ request_user (UserLike | Any): User instance or user id; non-user values will be resolved to a User or AnonymousUser via getUserWithId.
113
+
114
+ Raises:
115
+ PermissionCheckError: Raised with a list of error messages when one or more fields are not permitted to be updated.
116
+ """
73
117
  request_user = cls.getUserWithId(request_user)
74
118
 
75
119
  errors = []
@@ -80,21 +124,31 @@ class BasePermission(ABC):
80
124
  for key in data.keys():
81
125
  is_allowed = Permission.checkPermission("update", key)
82
126
  if not is_allowed:
83
- errors.append(
127
+ logger.debug(
84
128
  f"Permission denied for {key} with value {data[key]} for user {request_user}"
85
129
  )
130
+ errors.append(f"Update permission denied for attribute '{key}'")
86
131
  if errors:
87
- raise PermissionError(
88
- f"Permission denied for user {request_user} with errors: {errors}"
89
- )
132
+ raise PermissionCheckError(request_user, errors)
90
133
 
91
134
  @classmethod
92
135
  def checkDeletePermission(
93
136
  cls,
94
137
  manager_instance: GeneralManager,
95
- request_user: AbstractUser | AnonymousUser | Any,
138
+ request_user: UserLike | Any,
96
139
  ) -> None:
97
- """Validate delete permissions for the supplied manager instance."""
140
+ """
141
+ Validate that the request_user has delete permission for every attribute of the given manager instance.
142
+
143
+ This resolves the provided request_user to a User/AnonymousUser, evaluates delete permission for each attribute present on manager_instance, collects any denied attributes into error messages, and raises PermissionCheckError if any permissions are denied.
144
+
145
+ Parameters:
146
+ manager_instance (GeneralManager): The manager object whose attributes will be checked for delete permission.
147
+ request_user (UserLike | Any): The user (or user id) to evaluate; non-user values will be resolved to AnonymousUser.
148
+
149
+ Raises:
150
+ PermissionCheckError: If one or more attributes are not permitted for deletion by request_user. The exception carries the user and the list of denial messages.
151
+ """
98
152
  request_user = cls.getUserWithId(request_user)
99
153
 
100
154
  errors = []
@@ -103,22 +157,32 @@ class BasePermission(ABC):
103
157
  for key in manager_instance.__dict__.keys():
104
158
  is_allowed = Permission.checkPermission("delete", key)
105
159
  if not is_allowed:
106
- errors.append(
160
+ logger.debug(
107
161
  f"Permission denied for {key} with value {getattr(manager_instance, key)} for user {request_user}"
108
162
  )
163
+ errors.append(f"Delete permission denied for attribute '{key}'")
109
164
  if errors:
110
- raise PermissionError(
111
- f"Permission denied for user {request_user} with errors: {errors}"
112
- )
165
+ raise PermissionCheckError(request_user, errors)
113
166
 
114
167
  @staticmethod
115
168
  def getUserWithId(
116
- user: Any | AbstractUser | AnonymousUser,
117
- ) -> AbstractUser | AnonymousUser:
118
- """Return a ``User`` instance given a primary key or user object."""
119
- from django.contrib.auth.models import User
169
+ user: Any | UserLike,
170
+ ) -> UserLike:
171
+ """
172
+ Resolve a user identifier or user-like object to a Django User or AnonymousUser instance.
173
+
174
+ If the input is already an AbstractBaseUser or AnonymousUser, it is returned unchanged. If the input is a primary key (or other value used to look up a User by id), the corresponding User is returned; if no such User exists, an AnonymousUser is returned.
175
+
176
+ Parameters:
177
+ user (Any | UserLike): A user object or a value to look up a User by primary key.
178
+
179
+ Returns:
180
+ UserLike: The resolved User instance, or an AnonymousUser when no matching User is found.
181
+ """
182
+ from django.contrib.auth import get_user_model
120
183
 
121
- if isinstance(user, (AbstractUser, AnonymousUser)):
184
+ User = get_user_model()
185
+ if isinstance(user, (AbstractBaseUser, AnonymousUser)):
122
186
  return user
123
187
  try:
124
188
  return User.objects.get(id=user)
@@ -129,14 +193,14 @@ class BasePermission(ABC):
129
193
  def checkPermission(
130
194
  self,
131
195
  action: Literal["create", "read", "update", "delete"],
132
- attriubte: str,
196
+ attribute: str,
133
197
  ) -> bool:
134
198
  """
135
199
  Determine whether the given action is permitted on the specified attribute.
136
200
 
137
201
  Parameters:
138
202
  action (Literal["create", "read", "update", "delete"]): Operation being checked.
139
- attriubte (str): Attribute name subject to the permission check.
203
+ attribute (str): Attribute name subject to the permission check.
140
204
 
141
205
  Returns:
142
206
  bool: True when the action is allowed.
@@ -152,13 +216,27 @@ class BasePermission(ABC):
152
216
  def _getPermissionFilter(
153
217
  self, permission: str
154
218
  ) -> dict[Literal["filter", "exclude"], dict[str, str]]:
155
- """Resolve a filter definition for the given permission string."""
219
+ """
220
+ Resolve the filter/exclude constraints associated with a permission expression.
221
+
222
+ Parameters:
223
+ permission (str): Permission expression of the form "<function_name>[:config,...]"; the leading name selects a permission function and the optional colon-separated values are passed as configuration.
224
+
225
+ Returns:
226
+ dict: A mapping with keys "filter" and "exclude", each a dict[str, str] describing query constraints to apply. If the resolved permission provides no constraints, both will be empty dicts.
227
+
228
+ Raises:
229
+ PermissionNotFoundError: If no permission function matches the leading name in `permission`.
230
+ """
156
231
  permission_function, *config = permission.split(":")
157
232
  if permission_function not in permission_functions:
158
- raise ValueError(f"Permission {permission} not found")
233
+ raise PermissionNotFoundError(permission)
159
234
  permission_filter = permission_functions[permission_function][
160
235
  "permission_filter"
161
- ](self.request_user, config)
236
+ ](
237
+ cast(AbstractUser | AnonymousUser, self.request_user),
238
+ config,
239
+ )
162
240
  if permission_filter is None:
163
241
  return {"filter": {}, "exclude": {}}
164
242
  return permission_filter
@@ -176,4 +254,8 @@ class BasePermission(ABC):
176
254
  Returns:
177
255
  bool: True when every sub-permission evaluates to True for the current user.
178
256
  """
179
- return validatePermissionString(permission, self.instance, self.request_user)
257
+ return validatePermissionString(
258
+ permission,
259
+ self.instance,
260
+ cast(AbstractUser | AnonymousUser, self.request_user),
261
+ )
@@ -19,6 +19,47 @@ type permission_type = Literal[
19
19
  ]
20
20
 
21
21
 
22
+ class InvalidBasedOnConfigurationError(ValueError):
23
+ """Raised when the configured `__based_on__` attribute is missing or invalid."""
24
+
25
+ def __init__(self, attribute_name: str) -> None:
26
+ """
27
+ Initialize the exception for an invalid or missing based-on configuration attribute.
28
+
29
+ Parameters:
30
+ attribute_name (str): Name of the configured `__based_on__` attribute that is missing or invalid.
31
+ """
32
+ super().__init__(
33
+ f"Based on configuration '{attribute_name}' is not valid or does not exist."
34
+ )
35
+
36
+
37
+ class InvalidBasedOnTypeError(TypeError):
38
+ """Raised when the `__based_on__` attribute does not resolve to a GeneralManager."""
39
+
40
+ def __init__(self, attribute_name: str) -> None:
41
+ """
42
+ Initialize the exception indicating that the configured based-on attribute does not resolve to a GeneralManager.
43
+
44
+ Parameters:
45
+ attribute_name (str): Name of the configured based-on attribute that failed type validation; included in the exception message.
46
+ """
47
+ super().__init__(f"Based on object {attribute_name} is not a GeneralManager.")
48
+
49
+
50
+ class UnknownPermissionActionError(ValueError):
51
+ """Raised when an unsupported permission action is encountered."""
52
+
53
+ def __init__(self, action: str) -> None:
54
+ """
55
+ Initialize the exception for an unsupported permission action.
56
+
57
+ Parameters:
58
+ action (str): The permission action name that is not recognized; used to build the exception message "Action {action} not found."
59
+ """
60
+ super().__init__(f"Action {action} not found.")
61
+
62
+
22
63
  class notExistent:
23
64
  pass
24
65
 
@@ -72,33 +113,33 @@ class ManagerBasedPermission(BasePermission):
72
113
 
73
114
  def __getBasedOnPermission(self) -> Optional[BasePermission]:
74
115
  """
75
- Retrieve the permission object referenced by ``__based_on__`` when configured.
116
+ Resolve and return a BasePermission instance from the manager attribute named by the class-level `__based_on__` configuration.
117
+
118
+ If `__based_on__` is None or not configured on this class, returns None. If the referenced attribute exists on the target instance but is None, resets permissions to skip based-on evaluation and returns None. If the referenced attribute resolves to a manager that exposes a valid `Permission` subclass, constructs and returns that permission with the corresponding manager instance and the current request user.
76
119
 
77
120
  Returns:
78
- BasePermission | None: Permission instance for the related object, if applicable.
121
+ BasePermission | None: The resolved permission instance for the related manager, or `None` when no based-on permission applies.
79
122
 
80
123
  Raises:
81
- ValueError: If the configured attribute does not exist on the instance.
82
- TypeError: If the attribute does not resolve to a `GeneralManager`.
124
+ InvalidBasedOnConfigurationError: If the configured `__based_on__` attribute does not exist on the target instance.
125
+ InvalidBasedOnTypeError: If the configured attribute exists but does not resolve to a `GeneralManager` or subclass.
83
126
  """
84
127
  from general_manager.manager.generalManager import GeneralManager
85
128
 
86
- __based_on__ = getattr(self, "__based_on__")
129
+ __based_on__ = self.__based_on__
87
130
  if __based_on__ is None:
88
131
  return None
89
132
 
90
133
  basis_object = getattr(self.instance, __based_on__, notExistent)
91
134
  if basis_object is notExistent:
92
- raise ValueError(
93
- f"Based on configuration '{__based_on__}' is not valid or does not exist."
94
- )
135
+ raise InvalidBasedOnConfigurationError(__based_on__)
95
136
  if basis_object is None:
96
137
  self.__setPermissions(skip_based_on=True)
97
138
  return None
98
139
  if not isinstance(basis_object, GeneralManager) and not (
99
140
  isinstance(basis_object, type) and issubclass(basis_object, GeneralManager)
100
141
  ):
101
- raise TypeError(f"Based on object {__based_on__} is not a GeneralManager")
142
+ raise InvalidBasedOnTypeError(__based_on__)
102
143
 
103
144
  Permission = getattr(basis_object, "Permission", None)
104
145
 
@@ -126,21 +167,24 @@ class ManagerBasedPermission(BasePermission):
126
167
  def checkPermission(
127
168
  self,
128
169
  action: permission_type,
129
- attriubte: str,
170
+ attribute: str,
130
171
  ) -> bool:
131
172
  """
132
- Determine whether the user has permission to perform ``action`` on ``attribute``.
173
+ Determine whether the request user is allowed to perform a CRUD action on a specific attribute.
133
174
 
134
175
  Parameters:
135
- action (permission_type): CRUD operation being evaluated.
136
- attriubte (str): Attribute name subject to the permission check.
176
+ action (permission_type): CRUD action to evaluate ("create", "read", "update", "delete").
177
+ attribute (str): Name of the attribute to check permission for.
137
178
 
138
179
  Returns:
139
- bool: True when the action is permitted.
180
+ bool: True if the action is permitted on the attribute, False otherwise.
181
+
182
+ Raises:
183
+ UnknownPermissionActionError: If `action` is not one of "create", "read", "update", or "delete".
140
184
  """
141
185
  if (
142
186
  self.__based_on_permission
143
- and not self.__based_on_permission.checkPermission(action, attriubte)
187
+ and not self.__based_on_permission.checkPermission(action, attribute)
144
188
  ):
145
189
  return False
146
190
 
@@ -153,11 +197,11 @@ class ManagerBasedPermission(BasePermission):
153
197
  elif action == "delete":
154
198
  permissions = self.__delete__
155
199
  else:
156
- raise ValueError(f"Action {action} not found")
200
+ raise UnknownPermissionActionError(action)
157
201
 
158
202
  has_attribute_permissions = (
159
- attriubte in self.__attribute_permissions
160
- and action in self.__attribute_permissions[attriubte]
203
+ attribute in self.__attribute_permissions
204
+ and action in self.__attribute_permissions[attribute]
161
205
  )
162
206
 
163
207
  if not has_attribute_permissions:
@@ -167,7 +211,7 @@ class ManagerBasedPermission(BasePermission):
167
211
  attribute_permission = True
168
212
  else:
169
213
  attribute_permission = self.__checkSpecificPermission(
170
- self.__attribute_permissions[attriubte][action]
214
+ self.__attribute_permissions[attribute][action]
171
215
  )
172
216
 
173
217
  permission = self.__checkSpecificPermission(permissions)
@@ -189,8 +233,15 @@ class ManagerBasedPermission(BasePermission):
189
233
  def getPermissionFilter(
190
234
  self,
191
235
  ) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
192
- """Return queryset filters inferred from class-level permission configuration."""
193
- __based_on__ = getattr(self, "__based_on__")
236
+ """
237
+ Builds queryset filter and exclude mappings derived from this permission configuration.
238
+
239
+ If a based-on permission exists, its filters and excludes are included with each key prefixed by the name in __based_on__. Then appends filters produced from this class's read permissions via _getPermissionFilter.
240
+
241
+ Returns:
242
+ list[dict[Literal["filter", "exclude"], dict[str, str]]]: A list of dictionaries each containing "filter" and "exclude" mappings where keys are queryset lookups and values are lookup values.
243
+ """
244
+ __based_on__ = self.__based_on__
194
245
  filters: list[dict[Literal["filter", "exclude"], dict[str, str]]] = []
195
246
 
196
247
  if self.__based_on_permission is not None:
@@ -3,7 +3,10 @@
3
3
  from __future__ import annotations
4
4
  from django.contrib.auth.models import AbstractUser, AnonymousUser
5
5
  from typing import Any
6
- from general_manager.permission.basePermission import BasePermission
6
+ from general_manager.permission.basePermission import (
7
+ BasePermission,
8
+ PermissionCheckError,
9
+ )
7
10
 
8
11
  from general_manager.permission.permissionDataManager import PermissionDataManager
9
12
  from general_manager.permission.utils import validatePermissionString
@@ -57,14 +60,14 @@ class MutationPermission:
57
60
  request_user: AbstractUser | AnonymousUser | Any,
58
61
  ) -> None:
59
62
  """
60
- Validate that ``request_user`` may execute the mutation for the provided data.
63
+ Validate that the given user is authorized to perform the mutation described by `data`.
61
64
 
62
65
  Parameters:
63
- data (dict[str, Any]): Mutation payload.
64
- request_user (AbstractUser | AnonymousUser | Any): User or user ID.
66
+ data (dict[str, Any]): Mutation payload mapping field names to values.
67
+ request_user (AbstractUser | AnonymousUser | Any): A user object or a user identifier; if an identifier is provided it will be resolved to a user.
65
68
 
66
69
  Raises:
67
- PermissionError: If any field-level permission check fails.
70
+ PermissionCheckError: Raised with the `request_user` and a list of field-level error messages when one or more fields fail their permission checks.
68
71
  """
69
72
  errors = []
70
73
  if not isinstance(request_user, (AbstractUser, AnonymousUser)):
@@ -76,20 +79,22 @@ class MutationPermission:
76
79
  f"Permission denied for {key} with value {data[key]} for user {request_user}"
77
80
  )
78
81
  if errors:
79
- raise PermissionError(f"Permission denied with errors: {errors}")
82
+ raise PermissionCheckError(request_user, errors)
80
83
 
81
84
  def checkPermission(
82
85
  self,
83
86
  attribute: str,
84
87
  ) -> bool:
85
88
  """
86
- Evaluate permissions for a specific attribute within the mutation payload.
89
+ Determine whether the request user is allowed to modify a specific attribute in the mutation payload.
90
+
91
+ Updates the instance's cached overall permission result based on the class-level mutate permissions.
87
92
 
88
93
  Parameters:
89
- attribute (str): Attribute name being validated.
94
+ attribute (str): Name of the attribute to validate.
90
95
 
91
96
  Returns:
92
- bool: True when permitted, False otherwise.
97
+ True if modification of the attribute is allowed, False otherwise.
93
98
  """
94
99
 
95
100
  has_attribute_permissions = attribute in self.__attribute_permissions