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
@@ -55,23 +55,109 @@ class AttributeTypedDict(TypedDict):
55
55
  is_derived: bool
56
56
 
57
57
 
58
+ class UnexpectedInputArgumentsError(TypeError):
59
+ """Raised when parseInputFields receives keyword arguments not defined by the interface."""
60
+
61
+ def __init__(self, extra_args: Iterable[str]) -> None:
62
+ """
63
+ Initialize the exception with a message listing unexpected input argument names.
64
+
65
+ Parameters:
66
+ extra_args (Iterable[str]): Names of the unexpected keyword arguments to include in the error message.
67
+ """
68
+ extras = ", ".join(extra_args)
69
+ super().__init__(f"Unexpected arguments: {extras}.")
70
+
71
+
72
+ class MissingInputArgumentsError(TypeError):
73
+ """Raised when required interface inputs are not supplied."""
74
+
75
+ def __init__(self, missing_args: Iterable[str]) -> None:
76
+ """
77
+ Initialize the exception for missing required input arguments.
78
+
79
+ Parameters:
80
+ missing_args (Iterable[str]): Names of required input arguments that were not provided; these will be joined into the exception message.
81
+ """
82
+ missing = ", ".join(missing_args)
83
+ super().__init__(f"Missing required arguments: {missing}.")
84
+
85
+
86
+ class CircularInputDependencyError(ValueError):
87
+ """Raised when input fields declare circular dependencies."""
88
+
89
+ def __init__(self, unresolved: Iterable[str]) -> None:
90
+ """
91
+ Initialize the CircularInputDependencyError with the names of inputs involved in the cycle.
92
+
93
+ Parameters:
94
+ unresolved (Iterable[str]): Iterable of input names that form the detected circular dependency.
95
+ """
96
+ names = ", ".join(unresolved)
97
+ super().__init__(f"Circular dependency detected among inputs: {names}.")
98
+
99
+
100
+ class InvalidInputTypeError(TypeError):
101
+ """Raised when an input value does not match its declared type."""
102
+
103
+ def __init__(self, name: str, provided: type, expected: type) -> None:
104
+ """
105
+ Initialize the InvalidInputTypeError with a message describing a type mismatch for a named input.
106
+
107
+ Parameters:
108
+ name (str): The name of the input field with the invalid type.
109
+ provided (type): The actual type that was provided.
110
+ expected (type): The type that was expected.
111
+ """
112
+ super().__init__(f"Invalid type for {name}: {provided}, expected: {expected}.")
113
+
114
+
115
+ class InvalidPossibleValuesTypeError(TypeError):
116
+ """Raised when an input's possible_values configuration is not callable or iterable."""
117
+
118
+ def __init__(self, name: str) -> None:
119
+ """
120
+ Exception raised when an input's `possible_values` configuration is neither callable nor iterable.
121
+
122
+ Parameters:
123
+ name (str): The input field name whose `possible_values` is invalid; included in the exception message.
124
+ """
125
+ super().__init__(f"Invalid type for possible_values of input {name}.")
126
+
127
+
128
+ class InvalidInputValueError(ValueError):
129
+ """Raised when a provided input value is not within the allowed set."""
130
+
131
+ def __init__(self, name: str, value: object, allowed: Iterable[object]) -> None:
132
+ """
133
+ Initialize the exception with a message describing an invalid input value for a specific field.
134
+
135
+ Parameters:
136
+ name (str): The name of the input field that received the invalid value.
137
+ value (object): The value that was provided and deemed invalid.
138
+ allowed (Iterable[object]): An iterable of permitted values for the field; used to include allowed options in the exception message.
139
+ """
140
+ super().__init__(
141
+ f"Invalid value for {name}: {value}, allowed: {list(allowed)}."
142
+ )
143
+
144
+
58
145
  class InterfaceBase(ABC):
59
146
  """Common base API for interfaces backing GeneralManager classes."""
60
147
 
61
- _parent_class: Type[GeneralManager]
148
+ _parent_class: ClassVar[Type["GeneralManager"]]
62
149
  _interface_type: ClassVar[str]
63
- input_fields: dict[str, Input]
150
+ input_fields: ClassVar[dict[str, "Input"]]
64
151
 
65
152
  def __init__(self, *args: Any, **kwargs: Any) -> None:
66
153
  """
67
- Construct the interface using the supplied identification arguments.
154
+ Initialize the interface using the provided identification inputs.
68
155
 
69
- Parameters:
70
- *args: Positional arguments passed to the interface constructor.
71
- **kwargs: Keyword arguments passed to the interface constructor.
156
+ Positional arguments are mapped to the interface's declared input fields by position; keyword arguments are matched by name. Inputs are validated and normalized according to the interface's input field definitions and the resulting normalized identification is stored on the instance as `self.identification`.
72
157
 
73
- Returns:
74
- None
158
+ Parameters:
159
+ *args: Positional identification values corresponding to the interface's input field order.
160
+ **kwargs: Named identification values matching the interface's input field names.
75
161
  """
76
162
  identification = self.parseInputFieldsToIdentification(*args, **kwargs)
77
163
  self.identification = self.formatIdentification(identification)
@@ -82,18 +168,22 @@ class InterfaceBase(ABC):
82
168
  **kwargs: Any,
83
169
  ) -> dict[str, Any]:
84
170
  """
85
- Parse raw arguments into a validated identification mapping.
171
+ Convert positional and keyword inputs into a validated identification mapping for the interface's input fields.
86
172
 
87
173
  Parameters:
88
- *args (Any): Positional arguments matched to the interface's input field order.
89
- **kwargs (dict[str, Any]): Keyword arguments supplied by the caller.
174
+ *args: Positional arguments matched, in order, to the interface's defined input fields.
175
+ **kwargs: Keyword arguments supplying input values by name.
90
176
 
91
177
  Returns:
92
- dict[str, Any]: Mapping of input field names to validated values.
178
+ dict[str, Any]: Mapping of input field names to their validated values.
93
179
 
94
180
  Raises:
95
- TypeError: If required inputs are missing, unexpected inputs are provided, or a value fails type checking.
96
- ValueError: If circular dependencies prevent resolution of the inputs.
181
+ UnexpectedInputArgumentsError: If extra keyword arguments are provided that do not match any input field (after allowing keys suffixed with "_id").
182
+ MissingInputArgumentsError: If one or more required input fields are not provided.
183
+ CircularInputDependencyError: If input fields declare dependencies that form a cycle and cannot be resolved.
184
+ InvalidInputTypeError: If a provided value does not match the declared type for an input.
185
+ InvalidPossibleValuesTypeError: If an input's `possible_values` configuration is neither callable nor iterable.
186
+ InvalidInputValueError: If a provided value is not in the allowed set defined by an input's `possible_values`.
97
187
  """
98
188
  identification: dict[str, Any] = {}
99
189
  kwargs = cast(
@@ -106,11 +196,11 @@ class InterfaceBase(ABC):
106
196
  if extra_arg.replace("_id", "") in self.input_fields.keys():
107
197
  kwargs[extra_arg.replace("_id", "")] = kwargs.pop(extra_arg)
108
198
  else:
109
- raise TypeError(f"Unexpected arguments: {', '.join(extra_args)}")
199
+ raise UnexpectedInputArgumentsError(extra_args)
110
200
 
111
201
  missing_args = set(self.input_fields.keys()) - set(kwargs.keys())
112
202
  if missing_args:
113
- raise TypeError(f"Missing required arguments: {', '.join(missing_args)}")
203
+ raise MissingInputArgumentsError(missing_args)
114
204
 
115
205
  # process input fields with dependencies
116
206
  processed: set[str] = set()
@@ -129,9 +219,7 @@ class InterfaceBase(ABC):
129
219
  if not progress_made:
130
220
  # detect circular dependencies
131
221
  unresolved = set(self.input_fields.keys()) - processed
132
- raise ValueError(
133
- f"Circular dependency detected among inputs: {', '.join(unresolved)}"
134
- )
222
+ raise CircularInputDependencyError(unresolved)
135
223
  return identification
136
224
 
137
225
  @staticmethod
@@ -169,25 +257,23 @@ class InterfaceBase(ABC):
169
257
  self, name: str, value: Any, identification: dict[str, Any]
170
258
  ) -> None:
171
259
  """
172
- Validate a single input value against its definition.
260
+ Validate a single input value against its declared Input definition.
173
261
 
174
- Parameters:
175
- name (str): Input field name being processed.
176
- value (Any): Value provided by the caller.
177
- identification (dict[str, Any]): Partially resolved identification mapping used to evaluate dependencies.
262
+ Checks that the provided value matches the declared Python type and, when DEBUG is enabled, verifies the value is allowed by the input's `possible_values` (which may be an iterable or a callable that receives dependent input values).
178
263
 
179
- Returns:
180
- None
264
+ Parameters:
265
+ name: The input field name being validated.
266
+ value: The value to validate.
267
+ identification: Partially resolved identification mapping used to supply dependent input values when evaluating `possible_values`.
181
268
 
182
269
  Raises:
183
- TypeError: If the value has the wrong type or possible values are misconfigured.
184
- ValueError: If the value is not permitted by the configured `possible_values`.
270
+ InvalidInputTypeError: If `value` is not an instance of the input's declared `type`.
271
+ InvalidPossibleValuesTypeError: If `possible_values` is neither callable nor iterable.
272
+ InvalidInputValueError: If `value` is not contained in the evaluated `possible_values` (only checked when DEBUG is true).
185
273
  """
186
274
  input_field = self.input_fields[name]
187
275
  if not isinstance(value, input_field.type):
188
- raise TypeError(
189
- f"Invalid type for {name}: {type(value)}, expected: {input_field.type}"
190
- )
276
+ raise InvalidInputTypeError(name, type(value), input_field.type)
191
277
  if settings.DEBUG:
192
278
  # `possible_values` can be a callable or an iterable
193
279
  possible_values = input_field.possible_values
@@ -202,16 +288,23 @@ class InterfaceBase(ABC):
202
288
  elif isinstance(possible_values, Iterable):
203
289
  allowed_values = possible_values
204
290
  else:
205
- raise TypeError(f"Invalid type for possible_values of input {name}")
291
+ raise InvalidPossibleValuesTypeError(name)
206
292
 
207
293
  if value not in allowed_values:
208
- raise ValueError(
209
- f"Invalid value for {name}: {value}, allowed: {allowed_values}"
210
- )
294
+ raise InvalidInputValueError(name, value, allowed_values)
211
295
 
212
296
  @classmethod
213
297
  def create(cls, *args: Any, **kwargs: Any) -> Any:
214
- """Create a new record via the underlying data source."""
298
+ """
299
+ Create a new managed record in the underlying data store using the interface's inputs.
300
+
301
+ Parameters:
302
+ *args: Positional input values corresponding to the interface's defined input fields.
303
+ **kwargs: Input values provided by name; unexpected extra keywords will be rejected.
304
+
305
+ Returns:
306
+ The created record or a manager-specific representation of the newly created entity.
307
+ """
215
308
  raise NotImplementedError
216
309
 
217
310
  def update(self, *args: Any, **kwargs: Any) -> Any:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from datetime import datetime
5
- from typing import Any
5
+ from typing import Any, ClassVar
6
6
  from general_manager.interface.baseInterface import (
7
7
  InterfaceBase,
8
8
  classPostCreationMethod,
@@ -21,10 +21,20 @@ from general_manager.bucket.calculationBucket import CalculationBucket
21
21
 
22
22
  class CalculationInterface(InterfaceBase):
23
23
  """Interface exposing calculation inputs without persisting data."""
24
- _interface_type = "calculation"
25
- input_fields: dict[str, Input]
24
+
25
+ _interface_type: ClassVar[str] = "calculation"
26
+ input_fields: ClassVar[dict[str, Input]]
26
27
 
27
28
  def getData(self, search_date: datetime | None = None) -> Any:
29
+ """
30
+ Indicates that calculation interfaces do not provide stored data.
31
+
32
+ Parameters:
33
+ search_date (datetime | None): Date for which data would be requested.
34
+
35
+ Raises:
36
+ NotImplementedError: Always raised with the message "Calculations do not store data."
37
+ """
28
38
  raise NotImplementedError("Calculations do not store data.")
29
39
 
30
40
  @classmethod
@@ -72,18 +82,18 @@ class CalculationInterface(InterfaceBase):
72
82
 
73
83
  @staticmethod
74
84
  def _preCreate(
75
- name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
85
+ _name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
76
86
  ) -> tuple[attributes, interfaceBaseClass, None]:
77
87
  """
78
- Prepare interface attributes prior to GeneralManager class creation.
88
+ Prepare and attach a generated Interface subclass into the attributes for a GeneralManager class before its creation.
79
89
 
80
90
  Parameters:
81
- name (generalManagerClassName): Name of the new manager class.
82
- attrs (attributes): Attribute dictionary for the manager being created.
83
- interface (interfaceBaseClass): Base interface definition.
91
+ _name (generalManagerClassName): Name of the manager class being created.
92
+ attrs (attributes): Mutable attribute dictionary for the manager class under construction; will be modified to include the generated Interface and interface type.
93
+ interface (interfaceBaseClass): Base interface class from which the generated Interface subclass is derived.
84
94
 
85
95
  Returns:
86
- tuple[attributes, interfaceBaseClass, None]: Updated attributes, interface class, and related model (None).
96
+ tuple[attributes, interfaceBaseClass, None]: The updated attributes dict, the newly created Interface subclass, and None for the related model.
87
97
  """
88
98
  input_fields: dict[str, Input[Any]] = {}
89
99
  for key, value in vars(interface).items():
@@ -104,9 +114,19 @@ class CalculationInterface(InterfaceBase):
104
114
  def _postCreate(
105
115
  new_class: newlyCreatedGeneralManagerClass,
106
116
  interface_class: newlyCreatedInterfaceClass,
107
- model: relatedClass,
117
+ _model: relatedClass,
108
118
  ) -> None:
109
- """Link the generated interface to the manager class after creation."""
119
+ """
120
+ Link the generated interface class to its manager class after creation.
121
+
122
+ Parameters:
123
+ new_class: The newly created GeneralManager class to attach.
124
+ interface_class: The generated interface class that will reference the manager.
125
+ _model: Unused placeholder for the related model class; ignored.
126
+
127
+ Description:
128
+ Sets `interface_class._parent_class` to `new_class` so the interface knows its owning manager.
129
+ """
110
130
  interface_class._parent_class = new_class
111
131
 
112
132
  @classmethod
@@ -122,18 +142,15 @@ class CalculationInterface(InterfaceBase):
122
142
  @classmethod
123
143
  def getFieldType(cls, field_name: str) -> type:
124
144
  """
125
- Return the Python type of the specified input field.
126
-
127
- Parameters:
128
- field_name (str): The name of the input field.
145
+ Get the Python type for an input field.
129
146
 
130
147
  Returns:
131
- type: The Python type associated with the input field.
148
+ The Python type associated with the specified input field.
132
149
 
133
150
  Raises:
134
- KeyError: If the specified field name does not exist in input_fields.
151
+ KeyError: If `field_name` is not present in `cls.input_fields`.
135
152
  """
136
153
  field = cls.input_fields.get(field_name)
137
154
  if field is None:
138
- raise KeyError(f"Field '{field_name}' not found in input fields.")
155
+ raise KeyError(field_name)
139
156
  return field.type
@@ -1,7 +1,7 @@
1
1
  """Database-backed interface implementation for GeneralManager classes."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Type, Any, Callable, TYPE_CHECKING, TypeVar, Generic, cast
4
+ from typing import Any, Callable, ClassVar, Generic, TYPE_CHECKING, TypeVar, Type, cast
5
5
  from django.db import models
6
6
 
7
7
  from datetime import datetime, date, time, timedelta
@@ -39,11 +39,23 @@ modelsModel = TypeVar("modelsModel", bound=models.Model)
39
39
  MODEL_TYPE = TypeVar("MODEL_TYPE", bound=GeneralManagerBasisModel)
40
40
 
41
41
 
42
+ class DuplicateFieldNameError(ValueError):
43
+ """Raised when a dynamically generated field name conflicts with an existing one."""
44
+
45
+ def __init__(self) -> None:
46
+ """
47
+ Initialize the DuplicateFieldNameError with a default descriptive message.
48
+
49
+ This exception indicates a conflict where a dynamically generated field name duplicates an existing name; the default message is "Field name already exists."
50
+ """
51
+ super().__init__("Field name already exists.")
52
+
53
+
42
54
  class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
43
55
  """Interface implementation that persists data using Django ORM models."""
44
56
 
45
57
  _model: Type[MODEL_TYPE]
46
- input_fields: dict[str, Input] = {"id": Input(int)}
58
+ input_fields: ClassVar[dict[str, Input]] = {"id": Input(int)}
47
59
 
48
60
  def __init__(
49
61
  self,
@@ -52,15 +64,15 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
52
64
  **kwargs: Any,
53
65
  ) -> None:
54
66
  """
55
- Build the interface and hydrate the underlying model instance.
67
+ Initialize the interface and load its underlying model instance.
56
68
 
57
- Parameters:
58
- *args (list[Any]): Positional identification arguments forwarded to the parent interface.
59
- search_date (datetime | None): When provided, load historical data for the given timestamp.
60
- **kwargs (Any): Keyword identification arguments forwarded to the parent interface.
69
+ Positional and keyword arguments are forwarded to the parent interface to establish identification.
70
+ search_date, when provided, causes the instance to be resolved from historical records at or before that timestamp; if omitted, the current database record is loaded.
61
71
 
62
- Returns:
63
- None
72
+ Parameters:
73
+ *args: Positional identification arguments forwarded to the parent interface.
74
+ search_date (datetime | None): Timestamp to select a historical record; `None` to use the current record.
75
+ **kwargs: Keyword identification arguments forwarded to the parent interface.
64
76
  """
65
77
  super().__init__(*args, **kwargs)
66
78
  self.pk = self.identification["id"]
@@ -186,12 +198,20 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
186
198
  @classmethod
187
199
  def getAttributeTypes(cls) -> dict[str, AttributeTypedDict]:
188
200
  """
189
- Return a dictionary mapping each model attribute name to its type information and metadata.
201
+ Builds a mapping of model attribute names to their type metadata for the interface.
190
202
 
191
- Includes standard fields, custom fields, foreign keys, many-to-many, and reverse relation fields, excluding GenericForeignKey fields. For each attribute, provides its Python type (translated from Django field types when possible), required and editable status, whether it is derived, and its default value. For related models with a general manager class, the type is set to that class.
203
+ Produces entries for model fields, custom measurement-like fields, foreign-key relations, many-to-many relations, and reverse one-to-many relations. For related models that expose a general manager class, the attribute type is that manager class; many-to-many and reverse relation attributes are exposed with a "_list" suffix. GenericForeignKey fields are omitted.
192
204
 
193
205
  Returns:
194
- dict[str, AttributeTypedDict]: Mapping of attribute names to their type information and metadata.
206
+ dict[str, AttributeTypedDict]: Mapping from attribute name to metadata with keys:
207
+ - `type`: the attribute's Python type or general-manager class for related models (common Django field classes are translated to built-in Python types),
208
+ - `is_derived`: `True` for attributes computed from relations, `False` for direct model fields,
209
+ - `is_required`: `True` if the attribute must be present (e.g., field null is False and no default),
210
+ - `is_editable`: `True` if the field is editable on the model,
211
+ - `default`: the field's default value or `None` when not applicable.
212
+
213
+ Raises:
214
+ DuplicateFieldNameError: if a generated attribute name collides with an existing attribute name.
195
215
  """
196
216
  TRANSLATION: dict[Type[models.Field[Any, Any]], type] = {
197
217
  models.fields.BigAutoField: int,
@@ -253,7 +273,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
253
273
  if hasattr(field, "default"):
254
274
  default = field.default # type: ignore
255
275
  fields[field_name] = {
256
- "type": related_model,
276
+ "type": cast(type, related_model),
257
277
  "is_derived": False,
258
278
  "is_required": not field.null,
259
279
  "is_editable": field.editable,
@@ -268,7 +288,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
268
288
  if field_call not in fields:
269
289
  field_name = field_call
270
290
  else:
271
- raise ValueError("Field name already exists.")
291
+ raise DuplicateFieldNameError()
272
292
  field = cls._model._meta.get_field(field_name)
273
293
  related_model = cls._model._meta.get_field(field_name).related_model
274
294
  if related_model == "self":
@@ -284,7 +304,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
284
304
 
285
305
  if related_model is not None:
286
306
  fields[f"{field_name}_list"] = {
287
- "type": related_model,
307
+ "type": cast(type, related_model),
288
308
  "is_required": False,
289
309
  "is_derived": not bool(field.many_to_many),
290
310
  "is_editable": bool(field.many_to_many and field.editable),
@@ -299,12 +319,15 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
299
319
  @classmethod
300
320
  def getAttributes(cls) -> dict[str, Callable[[DBBasedInterface], Any]]:
301
321
  """
302
- Return a mapping of attribute names to callables that extract values from a DBBasedInterface instance.
322
+ Builds a mapping of attribute names to accessor callables for a DBBasedInterface instance.
303
323
 
304
- The returned dictionary includes accessors for custom fields, standard model fields, foreign keys, many-to-many relations, and reverse relations. For related models with a general manager class, the accessor returns an instance or queryset of that class; otherwise, it returns the related object or queryset directly. Raises a ValueError if a field name conflict is detected.
324
+ Includes accessors for custom fields, standard model fields, foreign-key relations, many-to-many relations, and reverse relations. For relations whose related model exposes a _general_manager_class, the accessor yields the corresponding GeneralManager instance (for single relations) or a filtered manager/queryset (for multi-relations); otherwise the accessor yields the related model instance or a queryset directly.
305
325
 
306
326
  Returns:
307
- dict[str, Callable[[DBBasedInterface], Any]]: Mapping of attribute names to callables for retrieving values from a DBBasedInterface instance.
327
+ dict[str, Callable[[DBBasedInterface], Any]]: Mapping from attribute name to a callable that accepts a DBBasedInterface and returns that attribute's value.
328
+
329
+ Raises:
330
+ DuplicateFieldNameError: If a generated attribute name conflicts with an existing attribute name.
308
331
  """
309
332
  from general_manager.manager.generalManager import GeneralManager
310
333
 
@@ -332,7 +355,9 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
332
355
  Type[GeneralManager], related_model._general_manager_class
333
356
  )
334
357
  field_values[f"{field_name}"] = (
335
- lambda self, field_name=field_name, manager_class=generalManagerClass: (
358
+ lambda self,
359
+ field_name=field_name,
360
+ manager_class=generalManagerClass: (
336
361
  manager_class(getattr(self._instance, field_name).pk)
337
362
  if getattr(self._instance, field_name)
338
363
  else None
@@ -353,7 +378,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
353
378
  if field_call not in field_values:
354
379
  field_name = field_call
355
380
  else:
356
- raise ValueError("Field name already exists.")
381
+ raise DuplicateFieldNameError()
357
382
  if hasattr(
358
383
  cls._model._meta.get_field(field_name).related_model,
359
384
  "_general_manager_class",
@@ -368,12 +393,17 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
368
393
  if f.related_model == cls._model
369
394
  ]
370
395
 
371
- field_values[
372
- f"{field_name}_list"
373
- ] = lambda self, field_name=field_name, related_fields=related_fields: self._instance._meta.get_field(
374
- field_name
375
- ).related_model._general_manager_class.filter(
376
- **{related_field.name: self.pk for related_field in related_fields}
396
+ field_values[f"{field_name}_list"] = (
397
+ lambda self,
398
+ field_name=field_name,
399
+ related_fields=related_fields: self._instance._meta.get_field(
400
+ field_name
401
+ ).related_model._general_manager_class.filter(
402
+ **{
403
+ related_field.name: self.pk
404
+ for related_field in related_fields
405
+ }
406
+ )
377
407
  )
378
408
  else:
379
409
  field_values[f"{field_name}_list"] = (
@@ -468,18 +498,16 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
468
498
  ) -> tuple[attributes, interfaceBaseClass, relatedClass]:
469
499
  # Collect fields defined directly on the interface class
470
500
  """
471
- Dynamically generates a Django model class, its associated interface class, and a factory class from an interface definition.
472
-
473
- This method collects fields and metadata from the provided interface class, creates a new Django model inheriting from the specified base model class, attaches custom validation rules if present, and constructs corresponding interface and factory classes. The updated attributes dictionary, the new interface class, and the newly created model class are returned for integration into the general manager framework.
501
+ Create a Django model class, a corresponding interface subclass, and a Factory class from an interface definition.
474
502
 
475
503
  Parameters:
476
- name (generalManagerClassName): Name for the dynamically created model class.
477
- attrs (attributes): Attribute dictionary updated with interface and factory definitions.
478
- interface (interfaceBaseClass): Interface definition used to derive the model.
479
- base_model_class (type[GeneralManagerBasisModel]): Base Django model class (defaults to GeneralManagerModel).
504
+ name (generalManagerClassName): Name to assign to the generated Django model class.
505
+ attrs (attributes): Attribute dictionary to be updated with the generated Interface and Factory entries.
506
+ interface (interfaceBaseClass): Interface definition used to derive the model and interface subclass.
507
+ base_model_class (type[GeneralManagerBasisModel]): Base class for the generated Django model (defaults to GeneralManagerModel).
480
508
 
481
509
  Returns:
482
- tuple[attributes, interfaceBaseClass, relatedClass]: Updated attributes, interface class, and the generated model.
510
+ tuple[attributes, interfaceBaseClass, relatedClass]: A tuple containing the updated attributes dictionary, the newly created interface subclass, and the generated Django model class.
483
511
  """
484
512
  model_fields: dict[str, Any] = {}
485
513
  meta_class = None
@@ -500,7 +528,7 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
500
528
  model_fields["Meta"] = meta_class
501
529
 
502
530
  if hasattr(meta_class, "rules"):
503
- rules = getattr(meta_class, "rules")
531
+ rules = meta_class.rules
504
532
  delattr(meta_class, "rules")
505
533
 
506
534
  # Create the concrete Django model dynamically
@@ -509,13 +537,13 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
509
537
  type(name, (base_model_class,), model_fields),
510
538
  )
511
539
  if meta_class and rules:
512
- setattr(model._meta, "rules", rules)
513
- # full_clean Methode hinzufügen
514
- setattr(model, "full_clean", getFullCleanMethode(model))
515
- # Interface-Typ bestimmen
540
+ model._meta.rules = rules # type: ignore[attr-defined]
541
+ # add full_clean method
542
+ model.full_clean = getFullCleanMethode(model) # type: ignore[assignment]
543
+ # Determine interface type
516
544
  attrs["_interface_type"] = interface._interface_type
517
545
  interface_cls = type(interface.__name__, (interface,), {})
518
- setattr(interface_cls, "_model", model)
546
+ interface_cls._model = model # type: ignore[attr-defined]
519
547
  attrs["Interface"] = interface_cls
520
548
 
521
549
  # Build the associated factory class
@@ -550,19 +578,17 @@ class DBBasedInterface(InterfaceBase, Generic[MODEL_TYPE]):
550
578
  model (relatedClass): Django model linked to the manager.
551
579
  """
552
580
  interface_class._parent_class = new_class
553
- setattr(model, "_general_manager_class", new_class)
581
+ model._general_manager_class = new_class # type: ignore
554
582
 
555
583
  @classmethod
556
584
  def handleInterface(
557
585
  cls,
558
586
  ) -> tuple[classPreCreationMethod, classPostCreationMethod]:
559
587
  """
560
- Returns the pre- and post-creation hooks for initializing the interface.
561
-
562
- The pre-creation method is called before the GeneralManager class is created to allow customization, while the post-creation method is called after creation to finalize setup.
588
+ Provide hooks invoked before and after dynamic interface class creation.
563
589
 
564
590
  Returns:
565
- tuple[classPreCreationMethod, classPostCreationMethod]: Hooks used during manager class creation.
591
+ tuple[classPreCreationMethod, classPostCreationMethod]: A pair (pre_create, post_create) where `pre_create` is invoked before the manager class is created to allow customization, and `post_create` is invoked after creation to finalize setup.
566
592
  """
567
593
  return cls._preCreate, cls._postCreate
568
594