GeneralManager 0.17.0__py3-none-any.whl → 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general_manager/__init__.py +11 -1
- general_manager/_types/api.py +0 -1
- general_manager/_types/bucket.py +0 -1
- general_manager/_types/cache.py +0 -1
- general_manager/_types/factory.py +0 -1
- general_manager/_types/general_manager.py +0 -1
- general_manager/_types/interface.py +0 -1
- general_manager/_types/manager.py +0 -1
- general_manager/_types/measurement.py +0 -1
- general_manager/_types/permission.py +0 -1
- general_manager/_types/rule.py +0 -1
- general_manager/_types/utils.py +0 -1
- general_manager/api/__init__.py +13 -1
- general_manager/api/graphql.py +356 -221
- general_manager/api/graphql_subscription_consumer.py +81 -78
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +188 -47
- general_manager/bucket/__init__.py +10 -1
- general_manager/bucket/calculationBucket.py +155 -53
- general_manager/bucket/databaseBucket.py +157 -45
- general_manager/bucket/groupBucket.py +133 -44
- general_manager/cache/__init__.py +10 -1
- general_manager/cache/cacheDecorator.py +3 -0
- general_manager/cache/dependencyIndex.py +143 -45
- general_manager/cache/signals.py +9 -2
- general_manager/factory/__init__.py +10 -1
- general_manager/factory/autoFactory.py +55 -13
- general_manager/factory/factories.py +110 -40
- general_manager/factory/factoryMethods.py +122 -34
- general_manager/interface/__init__.py +10 -1
- general_manager/interface/baseInterface.py +129 -36
- general_manager/interface/calculationInterface.py +35 -18
- general_manager/interface/databaseBasedInterface.py +71 -45
- general_manager/interface/databaseInterface.py +96 -38
- general_manager/interface/models.py +5 -5
- general_manager/interface/readOnlyInterface.py +94 -20
- general_manager/manager/__init__.py +10 -1
- general_manager/manager/generalManager.py +25 -16
- general_manager/manager/groupManager.py +20 -6
- general_manager/manager/meta.py +84 -16
- general_manager/measurement/__init__.py +10 -1
- general_manager/measurement/measurement.py +289 -95
- general_manager/measurement/measurementField.py +42 -31
- general_manager/permission/__init__.py +10 -1
- general_manager/permission/basePermission.py +120 -38
- general_manager/permission/managerBasedPermission.py +72 -21
- general_manager/permission/mutationPermission.py +14 -9
- general_manager/permission/permissionChecks.py +14 -12
- general_manager/permission/permissionDataManager.py +24 -11
- general_manager/permission/utils.py +34 -6
- general_manager/public_api_registry.py +36 -10
- general_manager/rule/__init__.py +10 -1
- general_manager/rule/handler.py +133 -44
- general_manager/rule/rule.py +178 -39
- general_manager/utils/__init__.py +10 -1
- general_manager/utils/argsToKwargs.py +34 -9
- general_manager/utils/filterParser.py +22 -7
- general_manager/utils/formatString.py +1 -0
- general_manager/utils/pathMapping.py +23 -15
- general_manager/utils/public_api.py +33 -2
- general_manager/utils/testing.py +31 -33
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/METADATA +3 -1
- generalmanager-0.19.0.dist-info/RECORD +77 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.17.0.dist-info/RECORD +0 -77
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.17.0.dist-info → generalmanager-0.19.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
|
-
|
|
154
|
+
Initialize the interface using the provided identification inputs.
|
|
68
155
|
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
171
|
+
Convert positional and keyword inputs into a validated identification mapping for the interface's input fields.
|
|
86
172
|
|
|
87
173
|
Parameters:
|
|
88
|
-
*args
|
|
89
|
-
**kwargs
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
291
|
+
raise InvalidPossibleValuesTypeError(name)
|
|
206
292
|
|
|
207
293
|
if value not in allowed_values:
|
|
208
|
-
raise
|
|
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
|
-
"""
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
85
|
+
_name: generalManagerClassName, attrs: attributes, interface: interfaceBaseClass
|
|
76
86
|
) -> tuple[attributes, interfaceBaseClass, None]:
|
|
77
87
|
"""
|
|
78
|
-
Prepare
|
|
88
|
+
Prepare and attach a generated Interface subclass into the attributes for a GeneralManager class before its creation.
|
|
79
89
|
|
|
80
90
|
Parameters:
|
|
81
|
-
|
|
82
|
-
attrs (attributes):
|
|
83
|
-
interface (interfaceBaseClass): Base interface
|
|
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]:
|
|
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
|
-
|
|
117
|
+
_model: relatedClass,
|
|
108
118
|
) -> None:
|
|
109
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
The Python type associated with the specified input field.
|
|
132
149
|
|
|
133
150
|
Raises:
|
|
134
|
-
KeyError: If
|
|
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(
|
|
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
|
|
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
|
-
|
|
67
|
+
Initialize the interface and load its underlying model instance.
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
201
|
+
Builds a mapping of model attribute names to their type metadata for the interface.
|
|
190
202
|
|
|
191
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
322
|
+
Builds a mapping of attribute names to accessor callables for a DBBasedInterface instance.
|
|
303
323
|
|
|
304
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
477
|
-
attrs (attributes): Attribute dictionary updated with
|
|
478
|
-
interface (interfaceBaseClass): Interface definition used to derive the model.
|
|
479
|
-
base_model_class (type[GeneralManagerBasisModel]): Base Django model
|
|
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]:
|
|
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 =
|
|
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
|
-
|
|
513
|
-
# full_clean
|
|
514
|
-
|
|
515
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
|