GeneralManager 0.14.1__py3-none-any.whl → 0.15.1__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 (62) hide show
  1. general_manager/__init__.py +49 -0
  2. general_manager/api/__init__.py +36 -0
  3. general_manager/api/graphql.py +92 -43
  4. general_manager/api/mutation.py +35 -10
  5. general_manager/api/property.py +26 -3
  6. general_manager/apps.py +23 -16
  7. general_manager/bucket/__init__.py +32 -0
  8. general_manager/bucket/baseBucket.py +76 -64
  9. general_manager/bucket/calculationBucket.py +188 -108
  10. general_manager/bucket/databaseBucket.py +130 -49
  11. general_manager/bucket/groupBucket.py +113 -60
  12. general_manager/cache/__init__.py +38 -0
  13. general_manager/cache/cacheDecorator.py +29 -17
  14. general_manager/cache/cacheTracker.py +34 -15
  15. general_manager/cache/dependencyIndex.py +117 -33
  16. general_manager/cache/modelDependencyCollector.py +17 -8
  17. general_manager/cache/signals.py +17 -6
  18. general_manager/factory/__init__.py +34 -5
  19. general_manager/factory/autoFactory.py +57 -60
  20. general_manager/factory/factories.py +39 -14
  21. general_manager/factory/factoryMethods.py +38 -1
  22. general_manager/interface/__init__.py +36 -0
  23. general_manager/interface/baseInterface.py +71 -27
  24. general_manager/interface/calculationInterface.py +18 -10
  25. general_manager/interface/databaseBasedInterface.py +102 -71
  26. general_manager/interface/databaseInterface.py +66 -20
  27. general_manager/interface/models.py +10 -4
  28. general_manager/interface/readOnlyInterface.py +44 -30
  29. general_manager/manager/__init__.py +36 -3
  30. general_manager/manager/generalManager.py +73 -47
  31. general_manager/manager/groupManager.py +72 -17
  32. general_manager/manager/input.py +23 -15
  33. general_manager/manager/meta.py +53 -53
  34. general_manager/measurement/__init__.py +37 -2
  35. general_manager/measurement/measurement.py +135 -58
  36. general_manager/measurement/measurementField.py +161 -61
  37. general_manager/permission/__init__.py +32 -1
  38. general_manager/permission/basePermission.py +29 -12
  39. general_manager/permission/managerBasedPermission.py +32 -26
  40. general_manager/permission/mutationPermission.py +32 -3
  41. general_manager/permission/permissionChecks.py +9 -1
  42. general_manager/permission/permissionDataManager.py +49 -15
  43. general_manager/permission/utils.py +14 -3
  44. general_manager/rule/__init__.py +27 -1
  45. general_manager/rule/handler.py +90 -5
  46. general_manager/rule/rule.py +40 -27
  47. general_manager/utils/__init__.py +44 -2
  48. general_manager/utils/argsToKwargs.py +17 -9
  49. general_manager/utils/filterParser.py +29 -30
  50. general_manager/utils/formatString.py +2 -0
  51. general_manager/utils/jsonEncoder.py +14 -1
  52. general_manager/utils/makeCacheKey.py +18 -12
  53. general_manager/utils/noneToZero.py +8 -6
  54. general_manager/utils/pathMapping.py +92 -29
  55. general_manager/utils/public_api.py +49 -0
  56. general_manager/utils/testing.py +135 -69
  57. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/METADATA +10 -2
  58. generalmanager-0.15.1.dist-info/RECORD +62 -0
  59. generalmanager-0.14.1.dist-info/RECORD +0 -58
  60. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/WHEEL +0 -0
  61. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/licenses/LICENSE +0 -0
  62. {generalmanager-0.14.1.dist-info → generalmanager-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ """Custom Django model field storing values as unit-aware measurements."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from django.db import models
@@ -6,50 +8,96 @@ from django.db.models.expressions import Col
6
8
  from decimal import Decimal
7
9
  import pint
8
10
  from general_manager.measurement.measurement import Measurement, ureg, currency_units
11
+ from django.db.backends.base.base import BaseDatabaseWrapper
12
+ from django.db.models import Lookup, Transform
13
+ from typing import Any, ClassVar, cast
9
14
 
10
15
 
11
16
  class MeasurementField(models.Field):
12
17
  description = "Stores a measurement (value + unit) but exposes a single field API"
13
18
 
14
- empty_values = (None, "", [], (), {})
19
+ empty_values: ClassVar[tuple[object, ...]] = (None, "", [], (), {})
15
20
 
16
21
  def __init__(
17
- self, base_unit: str, *args, null=False, blank=False, editable=True, **kwargs
18
- ):
22
+ self,
23
+ base_unit: str,
24
+ *args: object,
25
+ null: bool = False,
26
+ blank: bool = False,
27
+ editable: bool = True,
28
+ **kwargs: object,
29
+ ) -> None:
19
30
  """
20
- Initialize a MeasurementField to store a numeric value and its unit with unit-aware validation.
31
+ Configure a measurement field backed by separate value and unit columns.
21
32
 
22
33
  Parameters:
23
- base_unit (str): The canonical unit for the measurement, used for conversions and validation.
24
- null (bool, optional): Whether the field allows NULL values. Defaults to False.
25
- blank (bool, optional): Whether the field allows blank values. Defaults to False.
26
- editable (bool, optional): Whether the field is editable in forms and admin. Defaults to True.
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`.
27
40
 
28
- The field internally manages a DecimalField for the value and a CharField for the unit, both configured according to the provided options.
41
+ Returns:
42
+ None
29
43
  """
30
44
  self.base_unit = base_unit
31
45
  self.base_dimension = ureg.parse_expression(self.base_unit).dimensionality
32
46
 
33
- nb = {}
34
- if null:
35
- nb["null"] = True
36
- if blank:
37
- nb["blank"] = True
38
-
39
47
  self.editable = editable
40
- self.value_field = models.DecimalField(
41
- max_digits=30, decimal_places=10, db_index=True, editable=editable, **nb
42
- )
43
- self.unit_field = models.CharField(max_length=30, editable=editable, **nb)
48
+ self.value_field: models.DecimalField[Any]
49
+ self.unit_field: models.CharField[Any]
50
+ if null:
51
+ self.value_field = models.DecimalField(
52
+ max_digits=30,
53
+ decimal_places=10,
54
+ db_index=True,
55
+ editable=editable,
56
+ null=True,
57
+ blank=blank,
58
+ )
59
+ self.unit_field = models.CharField(
60
+ max_length=30,
61
+ editable=editable,
62
+ null=True,
63
+ blank=blank,
64
+ )
65
+ else:
66
+ self.value_field = models.DecimalField(
67
+ max_digits=30,
68
+ decimal_places=10,
69
+ db_index=True,
70
+ editable=editable,
71
+ null=False,
72
+ blank=blank,
73
+ )
74
+ self.unit_field = models.CharField(
75
+ max_length=30,
76
+ editable=editable,
77
+ null=False,
78
+ blank=blank,
79
+ )
44
80
 
45
81
  super().__init__(*args, null=null, blank=blank, editable=editable, **kwargs)
46
82
 
47
- def contribute_to_class(self, cls, name, private_only=False, **kwargs):
48
- # Register myself first (so opts.get_field('height') works)
83
+ def contribute_to_class(
84
+ self,
85
+ cls: type[models.Model],
86
+ name: str,
87
+ private_only: bool = False,
88
+ **kwargs: object,
89
+ ) -> None:
49
90
  """
50
- Registers the MeasurementField with the model class and attaches internal value and unit fields.
91
+ Attach the measurement field and its backing columns to a Django model.
92
+
93
+ 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.
51
98
 
52
- This method sets up the composite field by creating and adding separate fields for the numeric value and unit to the model class, ensuring they are not duplicated. It also overrides the model attribute with the MeasurementField descriptor itself to manage access and assignment.
99
+ Returns:
100
+ None
53
101
  """
54
102
  super().contribute_to_class(cls, name, private_only=private_only, **kwargs)
55
103
  self.concrete = False
@@ -76,47 +124,72 @@ class MeasurementField(models.Field):
76
124
  setattr(cls, name, self)
77
125
 
78
126
  # ---- ORM Delegation ----
79
- def get_col(self, alias, output_field=None):
127
+ def get_col(
128
+ self,
129
+ alias: str,
130
+ output_field: models.Field[object, object] | None = None,
131
+ ) -> Col:
80
132
  """
81
- Returns a Django ORM column expression for the internal value field, enabling queries on the numeric part of the measurement.
133
+ Produce a column expression referencing the underlying value field.
134
+
135
+ Parameters:
136
+ alias (str): Table alias used within the query.
137
+ output_field (models.Field | None): Optional output field override.
138
+
139
+ Returns:
140
+ Col: ORM expression targeting the numeric component.
82
141
  """
83
142
  return Col(alias, self.value_field, output_field or self.value_field) # type: ignore
84
143
 
85
- def get_lookup(self, lookup_name):
144
+ def get_lookup(self, lookup_name: str) -> type[Lookup]:
86
145
  """
87
- Return the lookup class for the specified lookup name, delegating to the internal value field.
146
+ Retrieve a lookup class from the underlying decimal field.
88
147
 
89
148
  Parameters:
90
- lookup_name (str): The name of the lookup to retrieve.
149
+ lookup_name (str): Name of the lookup to resolve.
91
150
 
92
151
  Returns:
93
- The lookup class corresponding to the given name, as provided by the internal decimal value field.
152
+ type[models.Lookup]: Lookup class implementing the requested comparison.
94
153
  """
95
- return self.value_field.get_lookup(lookup_name)
154
+ return cast(type[Lookup], self.value_field.get_lookup(lookup_name))
96
155
 
97
- def get_transform(self, lookup_name) -> models.Transform | None:
156
+ def get_transform(
157
+ self,
158
+ lookup_name: str,
159
+ ) -> type[Transform] | None:
98
160
  """
99
- Delegates retrieval of a transform operation to the internal value field.
161
+ Return a transform callable provided by the underlying decimal field.
162
+
163
+ Parameters:
164
+ lookup_name (str): Name of the transform to resolve.
100
165
 
101
166
  Returns:
102
- The transform corresponding to the given lookup name, or None if not found.
167
+ models.Transform | None: Transform class when available; otherwise None.
103
168
  """
104
- return self.value_field.get_transform(lookup_name)
169
+ transform = self.value_field.get_transform(lookup_name)
170
+ return cast(type[Transform] | None, transform)
105
171
 
106
- def db_type(self, connection) -> None: # type: ignore
172
+ def db_type(self, connection: BaseDatabaseWrapper) -> None: # type: ignore[override]
107
173
  """
108
- Return None to indicate that MeasurementField does not correspond to a single database column.
174
+ Signal to Django that the field does not map to a single column.
175
+
176
+ Parameters:
177
+ connection (BaseDatabaseWrapper): Database connection used for schema generation.
109
178
 
110
- This field manages its data using separate internal fields and does not require a direct database type.
179
+ Returns:
180
+ None
111
181
  """
112
182
  return None
113
183
 
114
184
  def run_validators(self, value: Measurement | None) -> None:
115
185
  """
116
- Runs all validators on the provided Measurement value if it is not None.
186
+ Execute all configured validators when a measurement is provided.
117
187
 
118
188
  Parameters:
119
- value (Measurement | None): The measurement to validate, or None to skip validation.
189
+ value (Measurement | None): Measurement instance that should satisfy field validators.
190
+
191
+ Returns:
192
+ None
120
193
  """
121
194
  if value is None:
122
195
  return
@@ -127,40 +200,46 @@ class MeasurementField(models.Field):
127
200
  self, value: Measurement | None, model_instance: models.Model | None = None
128
201
  ) -> Measurement | None:
129
202
  """
130
- Validates and cleans a Measurement value for use in the model field.
131
-
132
- Runs field-level validation and all configured validators on the provided value, returning it unchanged if valid.
203
+ Validate a measurement value before it is saved to the model.
133
204
 
134
205
  Parameters:
135
- value (Measurement | None): The measurement value to validate and clean.
136
- model_instance (models.Model | None): The model instance this value is associated with, if any.
206
+ value (Measurement | None): Measurement provided by forms or assignment.
207
+ model_instance (models.Model | None): Instance associated with the field, when available.
137
208
 
138
209
  Returns:
139
210
  Measurement | None: The validated measurement value, or None if the input was None.
211
+
212
+ Raises:
213
+ ValidationError: If validation fails due to null/blank constraints or validator errors.
140
214
  """
141
215
  self.validate(value, model_instance)
142
216
  self.run_validators(value)
143
217
  return value
144
218
 
145
- def to_python(self, value):
219
+ def to_python(self, value: Measurement | str | None) -> Measurement | str | None:
146
220
  """
147
- Returns the input value unchanged.
221
+ Convert database values back into Python objects.
222
+
223
+ Parameters:
224
+ value (Any): Value retrieved from the database.
148
225
 
149
- This method is required by Django custom fields to convert database values to Python objects, but no conversion is performed for this field.
226
+ Returns:
227
+ Any: Original value without modification.
150
228
  """
151
229
  return value
152
230
 
153
- def get_prep_value(self, value):
231
+ def get_prep_value(self, value: Measurement | str | None) -> Decimal | None:
154
232
  """
155
- Prepare a value for database storage by converting a Measurement to its decimal magnitude in the base unit.
233
+ Serialise a measurement for storage by converting it to the base unit magnitude.
156
234
 
157
- If the input is a string, it is parsed into a Measurement. If the value cannot be converted to the base unit due to dimensionality mismatch, a ValidationError is raised. Only Measurement instances or None are accepted.
235
+ Parameters:
236
+ value (Measurement | str | None): Value provided by the model or form.
158
237
 
159
238
  Returns:
160
- Decimal: The numeric value of the measurement in the base unit, or None if the input is None.
239
+ Decimal | None: Decimal magnitude in the base unit, or None when no value is supplied.
161
240
 
162
241
  Raises:
163
- ValidationError: If the value is not a Measurement or cannot be converted to the base unit.
242
+ ValidationError: If the value cannot be interpreted as a compatible measurement.
164
243
  """
165
244
  if value is None:
166
245
  return None
@@ -182,12 +261,14 @@ class MeasurementField(models.Field):
182
261
  self, instance: models.Model | None, owner: None = None
183
262
  ) -> MeasurementField | Measurement | None:
184
263
  """
185
- Retrieve the measurement value from the model instance, reconstructing it as a `Measurement` object with the stored unit.
264
+ Resolve the field value on an instance, reconstructing the measurement when possible.
265
+
266
+ Parameters:
267
+ instance (models.Model | None): Model instance owning the field, or None when accessed on the class.
268
+ owner (type[models.Model] | None): Model class owning the descriptor.
186
269
 
187
270
  Returns:
188
- Measurement: The measurement with its original unit if both value and unit are present.
189
- None: If either the value or unit is missing.
190
- MeasurementField: If accessed from the class rather than an instance.
271
+ MeasurementField | Measurement | None: Descriptor when accessed on the class, reconstructed measurement for instances, or None when incomplete.
191
272
  """
192
273
  if instance is None:
193
274
  return self
@@ -202,11 +283,23 @@ class MeasurementField(models.Field):
202
283
  qty_orig = qty_base
203
284
  return Measurement(qty_orig.magnitude, str(qty_orig.units))
204
285
 
205
- def __set__(self, instance, value):
286
+ def __set__(
287
+ self,
288
+ instance: models.Model,
289
+ value: Measurement | str | None,
290
+ ) -> None:
206
291
  """
207
- Assigns a measurement value to the model instance, validating type, unit compatibility, and editability.
292
+ Assign a measurement to the model instance after validating compatibility.
293
+
294
+ Parameters:
295
+ instance (models.Model): Model instance receiving the value.
296
+ value (Measurement | str | None): Measurement value supplied by the caller.
297
+
298
+ Returns:
299
+ None
208
300
 
209
- If the value is a string, attempts to parse it as a Measurement. Ensures the unit matches the expected base unit's dimensionality or currency status. Stores the numeric value (converted to the base unit) and the original unit string in the instance. Raises ValidationError if the value is invalid or incompatible.
301
+ Raises:
302
+ ValidationError: If the field is not editable, the value is invalid, or the units are incompatible.
210
303
  """
211
304
  if not self.editable:
212
305
  raise ValidationError(f"{self.name} is not editable.")
@@ -253,10 +346,17 @@ class MeasurementField(models.Field):
253
346
  self, value: Measurement | None, model_instance: models.Model | None = None
254
347
  ) -> None:
255
348
  """
256
- Validates a measurement value against null and blank constraints and applies all field validators.
349
+ Enforce null/blank constraints and run validators on the provided value.
350
+
351
+ Parameters:
352
+ value (Measurement | None): Measurement value under validation.
353
+ model_instance (models.Model | None): Instance owning the field; unused but provided for API compatibility.
354
+
355
+ Returns:
356
+ None
257
357
 
258
358
  Raises:
259
- ValidationError: If the value is None and the field does not allow nulls, or if the value is blank and the field does not allow blanks, or if any validator fails.
359
+ ValidationError: If the value violates constraint or validator requirements.
260
360
  """
261
361
  if value is None:
262
362
  if not self.null:
@@ -1 +1,32 @@
1
- from .managerBasedPermission import ManagerBasedPermission
1
+ """Permission helpers for GeneralManager mutations and actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from general_manager.utils.public_api import build_module_dir, resolve_export
8
+
9
+ __all__ = [
10
+ "BasePermission",
11
+ "ManagerBasedPermission",
12
+ "MutationPermission",
13
+ ]
14
+
15
+ _MODULE_MAP = {
16
+ "BasePermission": ("general_manager.permission.basePermission", "BasePermission"),
17
+ "ManagerBasedPermission": ("general_manager.permission.managerBasedPermission", "ManagerBasedPermission"),
18
+ "MutationPermission": ("general_manager.permission.mutationPermission", "MutationPermission"),
19
+ }
20
+
21
+
22
+ def __getattr__(name: str) -> Any:
23
+ return resolve_export(
24
+ name,
25
+ module_all=__all__,
26
+ module_map=_MODULE_MAP,
27
+ module_globals=globals(),
28
+ )
29
+
30
+
31
+ def __dir__() -> list[str]:
32
+ return build_module_dir(module_all=__all__, module_globals=globals())
@@ -1,3 +1,5 @@
1
+ """Base permission contract used by GeneralManager instances."""
2
+
1
3
  from __future__ import annotations
2
4
  from abc import ABC, abstractmethod
3
5
  from typing import TYPE_CHECKING, Any, Literal
@@ -16,21 +18,25 @@ if TYPE_CHECKING:
16
18
 
17
19
 
18
20
  class BasePermission(ABC):
21
+ """Abstract base class defining CRUD permission checks for managers."""
19
22
 
20
23
  def __init__(
21
24
  self,
22
25
  instance: PermissionDataManager | GeneralManager | GeneralManagerMeta,
23
26
  request_user: AbstractUser | AnonymousUser,
24
27
  ) -> None:
28
+ """Initialise the permission context for a specific manager and user."""
25
29
  self._instance = instance
26
30
  self._request_user = request_user
27
31
 
28
32
  @property
29
33
  def instance(self) -> PermissionDataManager | GeneralManager | GeneralManagerMeta:
34
+ """Return the object against which permission checks are performed."""
30
35
  return self._instance
31
36
 
32
37
  @property
33
38
  def request_user(self) -> AbstractUser | AnonymousUser:
39
+ """Return the user being evaluated for permission checks."""
34
40
  return self._request_user
35
41
 
36
42
  @classmethod
@@ -40,6 +46,7 @@ class BasePermission(ABC):
40
46
  manager: type[GeneralManager],
41
47
  request_user: AbstractUser | AnonymousUser | Any,
42
48
  ) -> None:
49
+ """Validate create permissions for the supplied payload."""
43
50
  request_user = cls.getUserWithId(request_user)
44
51
  errors = []
45
52
  permission_data = PermissionDataManager(permission_data=data, manager=manager)
@@ -62,6 +69,7 @@ class BasePermission(ABC):
62
69
  old_manager_instance: GeneralManager,
63
70
  request_user: AbstractUser | AnonymousUser | Any,
64
71
  ) -> None:
72
+ """Validate update permissions for the supplied payload."""
65
73
  request_user = cls.getUserWithId(request_user)
66
74
 
67
75
  errors = []
@@ -86,6 +94,7 @@ class BasePermission(ABC):
86
94
  manager_instance: GeneralManager,
87
95
  request_user: AbstractUser | AnonymousUser | Any,
88
96
  ) -> None:
97
+ """Validate delete permissions for the supplied manager instance."""
89
98
  request_user = cls.getUserWithId(request_user)
90
99
 
91
100
  errors = []
@@ -106,9 +115,7 @@ class BasePermission(ABC):
106
115
  def getUserWithId(
107
116
  user: Any | AbstractUser | AnonymousUser,
108
117
  ) -> AbstractUser | AnonymousUser:
109
- """
110
- Returns the user with the given id
111
- """
118
+ """Return a ``User`` instance given a primary key or user object."""
112
119
  from django.contrib.auth.models import User
113
120
 
114
121
  if isinstance(user, (AbstractUser, AnonymousUser)):
@@ -124,22 +131,28 @@ class BasePermission(ABC):
124
131
  action: Literal["create", "read", "update", "delete"],
125
132
  attriubte: str,
126
133
  ) -> bool:
134
+ """
135
+ Determine whether the given action is permitted on the specified attribute.
136
+
137
+ Parameters:
138
+ action (Literal["create", "read", "update", "delete"]): Operation being checked.
139
+ attriubte (str): Attribute name subject to the permission check.
140
+
141
+ Returns:
142
+ bool: True when the action is allowed.
143
+ """
127
144
  raise NotImplementedError
128
145
 
129
146
  def getPermissionFilter(
130
147
  self,
131
148
  ) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
132
- """
133
- Returns the filter for the permission
134
- """
149
+ """Return the filter/exclude constraints associated with this permission."""
135
150
  raise NotImplementedError
136
151
 
137
152
  def _getPermissionFilter(
138
153
  self, permission: str
139
154
  ) -> dict[Literal["filter", "exclude"], dict[str, str]]:
140
- """
141
- Returns the filter for the permission
142
- """
155
+ """Resolve a filter definition for the given permission string."""
143
156
  permission_function, *config = permission.split(":")
144
157
  if permission_function not in permission_functions:
145
158
  raise ValueError(f"Permission {permission} not found")
@@ -155,8 +168,12 @@ class BasePermission(ABC):
155
168
  permission: str,
156
169
  ) -> bool:
157
170
  """
158
- Validates a permission string which can be a combination of multiple permissions
159
- separated by "&" (e.g. "isAuthenticated&isMatchingKeyAccount").
160
- This means that all sub_permissions must be true.
171
+ Validate complex permission expressions joined by ``&`` operators.
172
+
173
+ Parameters:
174
+ permission (str): Permission expression (for example, ``isAuthenticated&isMatchingKeyAccount``).
175
+
176
+ Returns:
177
+ bool: True when every sub-permission evaluates to True for the current user.
161
178
  """
162
179
  return validatePermissionString(permission, self.instance, self.request_user)
@@ -1,3 +1,5 @@
1
+ """Default permission implementation leveraging manager configuration."""
2
+
1
3
  from __future__ import annotations
2
4
  from typing import TYPE_CHECKING, Literal, Optional, Dict
3
5
  from general_manager.permission.basePermission import BasePermission
@@ -22,6 +24,8 @@ class notExistent:
22
24
 
23
25
 
24
26
  class ManagerBasedPermission(BasePermission):
27
+ """Permission implementation driven by class-level configuration lists."""
28
+
25
29
  __based_on__: Optional[str] = None
26
30
  __read__: list[str]
27
31
  __create__: list[str]
@@ -34,9 +38,11 @@ class ManagerBasedPermission(BasePermission):
34
38
  request_user: AbstractUser,
35
39
  ) -> None:
36
40
  """
37
- Initializes the ManagerBasedPermission with a manager instance and the requesting user.
38
-
39
- Configures default CRUD permissions, collects attribute-specific permissions, and sets up any related "based on" permission for cascading checks.
41
+ Initialise the permission object and gather default and attribute-level rules.
42
+
43
+ Parameters:
44
+ instance (PermissionDataManager | GeneralManager): Target data used for permission evaluation.
45
+ request_user (AbstractUser): User whose permissions are being checked.
40
46
  """
41
47
  super().__init__(instance, request_user)
42
48
  self.__setPermissions()
@@ -51,12 +57,7 @@ class ManagerBasedPermission(BasePermission):
51
57
  }
52
58
 
53
59
  def __setPermissions(self, skip_based_on: bool = False) -> None:
54
-
55
- """
56
- Assigns default permission lists for CRUD actions based on the presence of a related permission attribute.
57
-
58
- If the permission is based on another attribute and `skip_based_on` is False, all default permissions are set to empty lists. Otherwise, read permissions default to `["public"]` and write permissions to `["isAuthenticated"]`. Class-level overrides are respected if present.
59
- """
60
+ """Populate CRUD permissions using class-level defaults and overrides."""
60
61
  default_read = ["public"]
61
62
  default_write = ["isAuthenticated"]
62
63
 
@@ -71,14 +72,14 @@ class ManagerBasedPermission(BasePermission):
71
72
 
72
73
  def __getBasedOnPermission(self) -> Optional[BasePermission]:
73
74
  """
74
- Retrieves the permission object associated with the `__based_on__` attribute, if present and valid.
75
-
75
+ Retrieve the permission object referenced by ``__based_on__`` when configured.
76
+
76
77
  Returns:
77
- An instance of the related `BasePermission` subclass if the `__based_on__` attribute exists on the instance and its `Permission` class is a subclass of `BasePermission`; otherwise, returns `None`.
78
-
78
+ BasePermission | None: Permission instance for the related object, if applicable.
79
+
79
80
  Raises:
80
- ValueError: If the `__based_on__` attribute is missing from the instance.
81
- TypeError: If the `__based_on__` attribute is not a `GeneralManager` or its subclass.
81
+ ValueError: If the configured attribute does not exist on the instance.
82
+ TypeError: If the attribute does not resolve to a `GeneralManager`.
82
83
  """
83
84
  from general_manager.manager.generalManager import GeneralManager
84
85
 
@@ -115,6 +116,7 @@ class ManagerBasedPermission(BasePermission):
115
116
  def __getAttributePermissions(
116
117
  self,
117
118
  ) -> dict[str, dict[permission_type, list[str]]]:
119
+ """Collect attribute-level permission overrides defined on the class."""
118
120
  attribute_permissions = {}
119
121
  for attribute in self.__class__.__dict__:
120
122
  if not attribute.startswith("__"):
@@ -126,6 +128,16 @@ class ManagerBasedPermission(BasePermission):
126
128
  action: permission_type,
127
129
  attriubte: str,
128
130
  ) -> bool:
131
+ """
132
+ Determine whether the user has permission to perform ``action`` on ``attribute``.
133
+
134
+ Parameters:
135
+ action (permission_type): CRUD operation being evaluated.
136
+ attriubte (str): Attribute name subject to the permission check.
137
+
138
+ Returns:
139
+ bool: True when the action is permitted.
140
+ """
129
141
  if (
130
142
  self.__based_on_permission
131
143
  and not self.__based_on_permission.checkPermission(action, attriubte)
@@ -166,11 +178,7 @@ class ManagerBasedPermission(BasePermission):
166
178
  self,
167
179
  permissions: list[str],
168
180
  ) -> bool:
169
- """
170
- Return True if no permissions are required or if at least one permission string is valid for the user.
171
-
172
- If the permissions list is empty, access is granted. Otherwise, returns True if any permission string in the list is validated for the user; returns False if none are valid.
173
- """
181
+ """Return True if any permission expression in the list evaluates to True."""
174
182
  if not permissions:
175
183
  return True
176
184
  for permission in permissions:
@@ -181,17 +189,15 @@ class ManagerBasedPermission(BasePermission):
181
189
  def getPermissionFilter(
182
190
  self,
183
191
  ) -> list[dict[Literal["filter", "exclude"], dict[str, str]]]:
184
- """
185
- Returns the filter for the permission
186
- """
192
+ """Return queryset filters inferred from class-level permission configuration."""
187
193
  __based_on__ = getattr(self, "__based_on__")
188
194
  filters: list[dict[Literal["filter", "exclude"], dict[str, str]]] = []
189
195
 
190
196
  if self.__based_on_permission is not None:
191
197
  base_permissions = self.__based_on_permission.getPermissionFilter()
192
- for permission in base_permissions:
193
- filter = permission.get("filter", {})
194
- exclude = permission.get("exclude", {})
198
+ for base_permission in base_permissions:
199
+ filter = base_permission.get("filter", {})
200
+ exclude = base_permission.get("exclude", {})
195
201
  filters.append(
196
202
  {
197
203
  "filter": {