GeneralManager 0.16.1__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.
- 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 +897 -147
- general_manager/api/graphql_subscription_consumer.py +432 -0
- general_manager/api/mutation.py +85 -23
- general_manager/api/property.py +39 -13
- general_manager/apps.py +336 -40
- 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/dependencyIndex.py +303 -53
- 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 +21 -7
- 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 +49 -42
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/METADATA +3 -1
- generalmanager-0.18.0.dist-info/RECORD +77 -0
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/licenses/LICENSE +1 -1
- generalmanager-0.16.1.dist-info/RECORD +0 -76
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/WHEEL +0 -0
- {generalmanager-0.16.1.dist-info → generalmanager-0.18.0.dist-info}/top_level.txt +0 -0
general_manager/rule/rule.py
CHANGED
|
@@ -5,16 +5,8 @@ import ast
|
|
|
5
5
|
import inspect
|
|
6
6
|
import re
|
|
7
7
|
import textwrap
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
ClassVar,
|
|
11
|
-
Dict,
|
|
12
|
-
Generic,
|
|
13
|
-
List,
|
|
14
|
-
Optional,
|
|
15
|
-
TypeVar,
|
|
16
|
-
cast,
|
|
17
|
-
)
|
|
8
|
+
from typing import Callable, Dict, Generic, List, Optional, Tuple, TypeVar
|
|
9
|
+
from decimal import Decimal
|
|
18
10
|
|
|
19
11
|
from django.conf import settings
|
|
20
12
|
from django.utils.module_loading import import_string
|
|
@@ -30,6 +22,48 @@ from general_manager.manager.generalManager import GeneralManager
|
|
|
30
22
|
|
|
31
23
|
GeneralManagerType = TypeVar("GeneralManagerType", bound=GeneralManager)
|
|
32
24
|
|
|
25
|
+
NOTEXISTENT = object()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NonexistentAttributeError(AttributeError):
|
|
29
|
+
"""Raised when a referenced attribute does not exist on the GeneralManager instance."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, attribute: str) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initialize the exception indicating that a referenced attribute does not exist.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
attribute (str): The name of the nonexistent attribute; this name will be included in the exception message.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(f"The attribute '{attribute}' does not exist.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MissingErrorTemplateVariableError(ValueError):
|
|
42
|
+
"""Raised when a custom error template omits required variables."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, missing: List[str]) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Initialize the exception indicating that one or more variables referenced by a rule are missing from a custom error template.
|
|
47
|
+
|
|
48
|
+
Parameters:
|
|
49
|
+
missing (List[str]): Names of the variables that are required by the template but were not found; these names will be included in the exception message.
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"The custom error message does not contain all used variables: {missing}."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ErrorMessageGenerationError(ValueError):
|
|
57
|
+
"""Raised when generating an error message before evaluating any input."""
|
|
58
|
+
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Exception raised when no input is available for generating an error message.
|
|
62
|
+
|
|
63
|
+
This exception is initialized with the default message "No input provided for error message generation."
|
|
64
|
+
"""
|
|
65
|
+
super().__init__("No input provided for error message generation.")
|
|
66
|
+
|
|
33
67
|
|
|
34
68
|
class Rule(Generic[GeneralManagerType]):
|
|
35
69
|
"""
|
|
@@ -44,6 +78,9 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
44
78
|
_ignore_if_none: bool
|
|
45
79
|
_last_result: Optional[bool]
|
|
46
80
|
_last_input: Optional[GeneralManagerType]
|
|
81
|
+
_last_args: Dict[str, object]
|
|
82
|
+
_param_names: Tuple[str, ...]
|
|
83
|
+
_primary_param: Optional[str]
|
|
47
84
|
_tree: ast.AST
|
|
48
85
|
_variables: List[str]
|
|
49
86
|
_handlers: Dict[str, BaseRuleHandler]
|
|
@@ -54,11 +91,24 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
54
91
|
custom_error_message: Optional[str] = None,
|
|
55
92
|
ignore_if_none: bool = True,
|
|
56
93
|
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Initialize a Rule that wraps a predicate function, captures its source AST to discover referenced variables, and prepares handlers for generating error messages.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
func (Callable[[GeneralManagerType], bool]): Predicate that evaluates a GeneralManager instance.
|
|
99
|
+
custom_error_message (Optional[str]): Optional template used to format generated error messages; placeholders must match variables referenced by the predicate.
|
|
100
|
+
ignore_if_none (bool): If True, evaluation will be skipped (result recorded as None) when any referenced variable resolves to None.
|
|
101
|
+
"""
|
|
57
102
|
self._func = func
|
|
58
103
|
self._custom_error_message = custom_error_message
|
|
59
104
|
self._ignore_if_none = ignore_if_none
|
|
60
105
|
self._last_result = None
|
|
61
106
|
self._last_input = None
|
|
107
|
+
self._last_args = {}
|
|
108
|
+
|
|
109
|
+
parameters = inspect.signature(func).parameters
|
|
110
|
+
self._param_names = tuple(parameters.keys())
|
|
111
|
+
self._primary_param = self._param_names[0] if self._param_names else None
|
|
62
112
|
|
|
63
113
|
# 1) Extract source, strip decorators, and dedent
|
|
64
114
|
src = textwrap.dedent(inspect.getsource(func))
|
|
@@ -67,7 +117,7 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
67
117
|
self._tree = ast.parse(src)
|
|
68
118
|
for parent in ast.walk(self._tree):
|
|
69
119
|
for child in ast.iter_child_nodes(parent):
|
|
70
|
-
|
|
120
|
+
child.parent = parent # type: ignore
|
|
71
121
|
|
|
72
122
|
# 3) Extract referenced variables
|
|
73
123
|
self._variables = self._extract_variables()
|
|
@@ -108,15 +158,21 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
108
158
|
|
|
109
159
|
def evaluate(self, x: GeneralManagerType) -> Optional[bool]:
|
|
110
160
|
"""
|
|
111
|
-
|
|
161
|
+
Evaluate the rule's predicate against a GeneralManager instance and record evaluation context.
|
|
162
|
+
|
|
163
|
+
Binds the primary parameter to the provided input, extracts referenced variable values, and sets the last evaluation result to the predicate outcome. If `ignore_if_none` is true and any referenced variable value is `None`, the evaluation is skipped and the last result is set to `None`.
|
|
112
164
|
|
|
113
165
|
Parameters:
|
|
114
166
|
x (GeneralManagerType): Manager instance supplied to the predicate.
|
|
115
167
|
|
|
116
168
|
Returns:
|
|
117
|
-
|
|
169
|
+
`True` if the predicate evaluates to true, `False` if it evaluates to false, `None` if evaluation was skipped because a referenced value was `None` and `ignore_if_none` is enabled.
|
|
118
170
|
"""
|
|
119
171
|
self._last_input = x
|
|
172
|
+
self._last_args = {}
|
|
173
|
+
if self._primary_param is not None:
|
|
174
|
+
self._last_args[self._primary_param] = x
|
|
175
|
+
|
|
120
176
|
vals = self._extract_variable_values(x)
|
|
121
177
|
if self._ignore_if_none and any(v is None for v in vals.values()):
|
|
122
178
|
self._last_result = None
|
|
@@ -127,13 +183,10 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
127
183
|
|
|
128
184
|
def validateCustomErrorMessage(self) -> None:
|
|
129
185
|
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
None
|
|
186
|
+
Validate that a provided custom error message template includes placeholders for every variable referenced by the rule.
|
|
134
187
|
|
|
135
188
|
Raises:
|
|
136
|
-
|
|
189
|
+
MissingErrorTemplateVariableError: If one or more extracted variables are not present as `{name}` placeholders in the custom template.
|
|
137
190
|
"""
|
|
138
191
|
if not self._custom_error_message:
|
|
139
192
|
return
|
|
@@ -141,24 +194,22 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
141
194
|
vars_in_msg = set(re.findall(r"{([^}]+)}", self._custom_error_message))
|
|
142
195
|
missing = [v for v in self._variables if v not in vars_in_msg]
|
|
143
196
|
if missing:
|
|
144
|
-
raise
|
|
145
|
-
f"The custom error message does not contain all used variables: {missing}"
|
|
146
|
-
)
|
|
197
|
+
raise MissingErrorTemplateVariableError(missing)
|
|
147
198
|
|
|
148
199
|
def getErrorMessage(self) -> Optional[Dict[str, str]]:
|
|
149
200
|
"""
|
|
150
|
-
|
|
201
|
+
Constructs error messages for the last failed evaluation and returns them keyed by variable name.
|
|
151
202
|
|
|
152
203
|
Returns:
|
|
153
|
-
dict[str, str] | None: Mapping
|
|
204
|
+
dict[str, str] | None: Mapping from each referenced variable name to its error message, or `None` if the predicate passed or was not evaluated.
|
|
154
205
|
|
|
155
206
|
Raises:
|
|
156
|
-
|
|
207
|
+
ErrorMessageGenerationError: If called before any input has been evaluated.
|
|
157
208
|
"""
|
|
158
209
|
if self._last_result or self._last_result is None:
|
|
159
210
|
return None
|
|
160
211
|
if self._last_input is None:
|
|
161
|
-
raise
|
|
212
|
+
raise ErrorMessageGenerationError()
|
|
162
213
|
|
|
163
214
|
# Validate and substitute template placeholders
|
|
164
215
|
self.validateCustomErrorMessage()
|
|
@@ -176,20 +227,48 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
176
227
|
return errors or None
|
|
177
228
|
|
|
178
229
|
def _extract_variables(self) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
Collects the dotted attribute names referenced on the rule's parameters.
|
|
232
|
+
|
|
233
|
+
Scans the predicate's AST and returns a sorted list of attribute access paths that originate from any of the predicate's parameter names (for example, "user.name" or "order.total"). If the predicate has no parameters, returns an empty list.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List[str]: Sorted list of dotted variable names referenced from the predicate's parameters.
|
|
237
|
+
"""
|
|
238
|
+
param_names = set(self._param_names)
|
|
239
|
+
if not param_names:
|
|
240
|
+
return []
|
|
241
|
+
|
|
179
242
|
class VarVisitor(ast.NodeVisitor):
|
|
180
|
-
|
|
243
|
+
def __init__(self, params: set[str]) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Initialize visitor state with a set of parameter names and an empty collection for discovered variables.
|
|
246
|
+
|
|
247
|
+
Parameters:
|
|
248
|
+
params (set[str]): Names of function parameters to consider when extracting referenced variables.
|
|
249
|
+
"""
|
|
250
|
+
self.vars: set[str] = set()
|
|
251
|
+
self.params = params
|
|
181
252
|
|
|
182
253
|
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Record dotted attribute accesses that originate from allowed parameter names.
|
|
256
|
+
|
|
257
|
+
Visits an ast.Attribute node, collects the dotted name components (e.g., "a.b.c") if the base is an ast.Name present in self.params, and adds the joined name to self.vars. Continues generic traversal after handling the node.
|
|
258
|
+
|
|
259
|
+
Parameters:
|
|
260
|
+
node (ast.Attribute): The attribute node being visited.
|
|
261
|
+
"""
|
|
183
262
|
parts: list[str] = []
|
|
184
263
|
curr: ast.AST = node
|
|
185
264
|
while isinstance(curr, ast.Attribute):
|
|
186
265
|
parts.append(curr.attr)
|
|
187
266
|
curr = curr.value
|
|
188
|
-
if isinstance(curr, ast.Name) and curr.id
|
|
267
|
+
if isinstance(curr, ast.Name) and curr.id in self.params:
|
|
189
268
|
self.vars.add(".".join(reversed(parts)))
|
|
190
269
|
self.generic_visit(node)
|
|
191
270
|
|
|
192
|
-
visitor = VarVisitor()
|
|
271
|
+
visitor = VarVisitor(param_names)
|
|
193
272
|
visitor.visit(self._tree)
|
|
194
273
|
return sorted(visitor.vars)
|
|
195
274
|
|
|
@@ -200,15 +279,27 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
200
279
|
for var in self._variables:
|
|
201
280
|
obj: object = x # type: ignore
|
|
202
281
|
for part in var.split("."):
|
|
203
|
-
obj = getattr(obj, part)
|
|
282
|
+
obj = getattr(obj, part, NOTEXISTENT)
|
|
283
|
+
if obj is NOTEXISTENT:
|
|
284
|
+
raise NonexistentAttributeError(var)
|
|
204
285
|
if obj is None:
|
|
205
286
|
break
|
|
206
287
|
out[var] = obj
|
|
207
288
|
return out
|
|
208
289
|
|
|
209
290
|
def _extract_comparisons(self) -> list[ast.Compare]:
|
|
291
|
+
"""
|
|
292
|
+
Collect all comparison (ast.Compare) nodes from the predicate AST.
|
|
293
|
+
|
|
294
|
+
Searches the rule's parsed AST stored on self._tree and returns every ast.Compare node found in traversal order.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
comps (list[ast.Compare]): The list of comparison nodes present in the predicate AST.
|
|
298
|
+
"""
|
|
299
|
+
|
|
210
300
|
class CompVisitor(ast.NodeVisitor):
|
|
211
|
-
|
|
301
|
+
def __init__(self) -> None:
|
|
302
|
+
self.comps: list[ast.Compare] = []
|
|
212
303
|
|
|
213
304
|
def visit_Compare(self, node: ast.Compare) -> None:
|
|
214
305
|
self.comps.append(node)
|
|
@@ -234,6 +325,17 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
234
325
|
def _generate_error_messages(
|
|
235
326
|
self, var_values: Dict[str, Optional[object]]
|
|
236
327
|
) -> Dict[str, str]:
|
|
328
|
+
"""
|
|
329
|
+
Generate human-readable error messages for each referenced variable using the resolved variable values.
|
|
330
|
+
|
|
331
|
+
Given a mapping of variable names to their evaluated values, produce a dictionary mapping each variable to an explanatory error message derived from the rule's predicate. If specialized rule handlers are registered for particular function calls in the predicate, those handlers will be used to produce messages for the affected variables. When the predicate contains boolean combinations and the last evaluation failed, all referenced variables receive a combined invalid-combination message. If no explicit comparisons are present in the predicate, a generic combination-invalid message is returned for every referenced variable.
|
|
332
|
+
|
|
333
|
+
Parameters:
|
|
334
|
+
var_values (Dict[str, Optional[object]]): Mapping from variable names (as extracted from the predicate) to their resolved values for the last-evaluated input.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dict[str, str]: Mapping from variable name to its generated error message.
|
|
338
|
+
"""
|
|
237
339
|
errors: Dict[str, str] = {}
|
|
238
340
|
comparisons = self._extract_comparisons()
|
|
239
341
|
logical = self._contains_logical_ops()
|
|
@@ -241,7 +343,7 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
241
343
|
if comparisons:
|
|
242
344
|
for cmp in comparisons:
|
|
243
345
|
left, rights, ops = cmp.left, cmp.comparators, cmp.ops
|
|
244
|
-
for right, op in zip(rights, ops):
|
|
346
|
+
for right, op in zip(rights, ops, strict=False):
|
|
245
347
|
# Special handler?
|
|
246
348
|
if isinstance(left, ast.Call):
|
|
247
349
|
fn = self._get_node_name(left.func)
|
|
@@ -270,7 +372,7 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
270
372
|
combo = ", ".join(f"[{v}]" for v in self._variables)
|
|
271
373
|
msg = f"{combo} combination is not valid"
|
|
272
374
|
for v in self._variables:
|
|
273
|
-
errors
|
|
375
|
+
errors.setdefault(v, msg) # keep specific messages
|
|
274
376
|
|
|
275
377
|
return errors
|
|
276
378
|
|
|
@@ -295,6 +397,17 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
295
397
|
}.get(type(op), "?")
|
|
296
398
|
|
|
297
399
|
def _get_node_name(self, node: ast.AST) -> str:
|
|
400
|
+
"""
|
|
401
|
+
Produce a concise, human-readable name for the given AST node.
|
|
402
|
+
|
|
403
|
+
For attribute access returns the dotted attribute path (e.g., "a.b.c"); for a Name node returns its identifier; for a Call node returns "func(arg1, arg2)" using the same naming rules for subnodes; for Constant nodes or nodes that cannot be represented returns an empty string.
|
|
404
|
+
|
|
405
|
+
Parameters:
|
|
406
|
+
node (ast.AST): The AST node to derive a readable name from.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
str: The human-readable name or an empty string if no sensible name can be produced.
|
|
410
|
+
"""
|
|
298
411
|
if isinstance(node, ast.Attribute):
|
|
299
412
|
parts: list[str] = []
|
|
300
413
|
curr: ast.AST = node
|
|
@@ -313,16 +426,42 @@ class Rule(Generic[GeneralManagerType]):
|
|
|
313
426
|
try:
|
|
314
427
|
# ast.unparse returns a string representation
|
|
315
428
|
return ast.unparse(node)
|
|
316
|
-
except
|
|
429
|
+
except (AttributeError, ValueError, TypeError):
|
|
317
430
|
return ""
|
|
318
431
|
|
|
319
432
|
def _eval_node(self, node: ast.expr) -> Optional[object]:
|
|
320
|
-
"""
|
|
433
|
+
"""
|
|
434
|
+
Evaluate an AST expression against the Rule's last-evaluated input and bound argument context.
|
|
435
|
+
|
|
436
|
+
Parameters:
|
|
437
|
+
node (ast.expr): The AST expression node to evaluate.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
result (Optional[object]): The evaluated value of the expression when resolvable; `None` if the node is not an expression or cannot be evaluated in the current context.
|
|
441
|
+
|
|
442
|
+
Description:
|
|
443
|
+
Supported evaluations:
|
|
444
|
+
- Attribute access: resolves chained attributes from the last input or resolved base value.
|
|
445
|
+
- Name: returns a bound argument value if present, otherwise looks up the name in the predicate function's global namespace.
|
|
446
|
+
- Unary negative: evaluates numeric negation for integer, float, or Decimal operands.
|
|
447
|
+
If the expression cannot be resolved (including missing intermediate attributes or unsupported node kinds), `None` is returned.
|
|
448
|
+
"""
|
|
321
449
|
if not isinstance(node, ast.expr):
|
|
322
450
|
return None
|
|
323
451
|
try:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
return ast.literal_eval(node)
|
|
453
|
+
except (ValueError, TypeError):
|
|
454
|
+
if isinstance(node, ast.Attribute):
|
|
455
|
+
base = self._eval_node(node.value)
|
|
456
|
+
if base is None:
|
|
457
|
+
return None
|
|
458
|
+
return getattr(base, node.attr, None)
|
|
459
|
+
if isinstance(node, ast.Name):
|
|
460
|
+
if node.id in self._last_args:
|
|
461
|
+
return self._last_args[node.id]
|
|
462
|
+
return self._func.__globals__.get(node.id)
|
|
463
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
|
464
|
+
operand = self._eval_node(node.operand)
|
|
465
|
+
if isinstance(operand, (int, float, Decimal)):
|
|
466
|
+
return -operand
|
|
467
|
+
return None
|
|
@@ -12,10 +12,19 @@ __all__ = list(UTILS_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = UTILS_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.utils import * # noqa:
|
|
15
|
+
from general_manager._types.utils import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Resolve and return a lazily exported attribute from the module's public API.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): The attribute name being accessed on the module.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Any: The resolved export object corresponding to `name` as defined by the module's public API mapping.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
from typing import Iterable, Mapping
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
class TooManyArgumentsError(TypeError):
|
|
5
|
+
"""Raised when more positional arguments are supplied than available keys."""
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
"""
|
|
9
|
+
Initialize the TooManyArgumentsError instance.
|
|
10
|
+
|
|
11
|
+
Sets the exception message to "More positional arguments than keys provided."
|
|
12
|
+
"""
|
|
13
|
+
super().__init__("More positional arguments than keys provided.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConflictingKeywordError(TypeError):
|
|
17
|
+
"""Raised when generated keyword arguments conflict with existing kwargs."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize ConflictingKeywordError with the standard message "Conflicts in existing kwargs."
|
|
22
|
+
"""
|
|
23
|
+
super().__init__("Conflicts in existing kwargs.")
|
|
24
|
+
|
|
25
|
+
|
|
4
26
|
def args_to_kwargs(
|
|
5
27
|
args: tuple[object, ...],
|
|
6
28
|
keys: Iterable[str],
|
|
7
29
|
existing_kwargs: Mapping[str, object] | None = None,
|
|
8
30
|
) -> dict[str, object]:
|
|
9
31
|
"""
|
|
10
|
-
|
|
32
|
+
Map positional arguments to the given keys and merge the result with an optional existing kwargs mapping.
|
|
11
33
|
|
|
12
34
|
Parameters:
|
|
13
|
-
args (tuple[
|
|
14
|
-
keys (Iterable[
|
|
15
|
-
existing_kwargs (
|
|
35
|
+
args (tuple[object, ...]): Positional values to assign to keys in order.
|
|
36
|
+
keys (Iterable[str]): Keys to assign the positional values to.
|
|
37
|
+
existing_kwargs (Mapping[str, object] | None): Optional mapping of keyword arguments to merge into the result.
|
|
16
38
|
|
|
17
39
|
Returns:
|
|
18
|
-
dict[
|
|
40
|
+
dict[str, object]: A dictionary containing the mapped keys for the provided positional arguments plus all entries from `existing_kwargs` (if given).
|
|
19
41
|
|
|
20
42
|
Raises:
|
|
21
|
-
|
|
43
|
+
TooManyArgumentsError: If more positional arguments are provided than keys.
|
|
44
|
+
ConflictingKeywordError: If `existing_kwargs` contains a key that was already produced from `args` and `keys`.
|
|
22
45
|
"""
|
|
23
46
|
keys = list(keys)
|
|
24
47
|
if len(args) > len(keys):
|
|
25
|
-
raise
|
|
48
|
+
raise TooManyArgumentsError()
|
|
26
49
|
|
|
27
|
-
kwargs: dict[str, object] = {
|
|
50
|
+
kwargs: dict[str, object] = {
|
|
51
|
+
key: value for key, value in zip(keys, args, strict=False)
|
|
52
|
+
}
|
|
28
53
|
if existing_kwargs and any(key in kwargs for key in existing_kwargs):
|
|
29
|
-
raise
|
|
54
|
+
raise ConflictingKeywordError()
|
|
30
55
|
if existing_kwargs:
|
|
31
56
|
kwargs.update(existing_kwargs)
|
|
32
57
|
|
|
@@ -5,21 +5,36 @@ from typing import Any, Callable
|
|
|
5
5
|
from general_manager.manager.input import Input
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class UnknownInputFieldError(ValueError):
|
|
9
|
+
"""Raised when a filter references an unknown input field."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, field_name: str) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Initialize the UnknownInputFieldError with a message indicating which input field was not recognized.
|
|
14
|
+
|
|
15
|
+
Parameters:
|
|
16
|
+
field_name (str): Name of the input field referenced in the filter that is not defined.
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(f"Unknown input field '{field_name}' in filter.")
|
|
19
|
+
|
|
20
|
+
|
|
8
21
|
def parse_filters(
|
|
9
22
|
filter_kwargs: dict[str, Any], possible_values: dict[str, Input]
|
|
10
23
|
) -> dict[str, dict]:
|
|
11
24
|
"""
|
|
12
|
-
Parse raw filter keyword arguments into structured criteria
|
|
25
|
+
Parse raw filter keyword arguments into structured criteria aligned with configured input fields.
|
|
13
26
|
|
|
14
27
|
Parameters:
|
|
15
|
-
filter_kwargs (dict[str, Any]):
|
|
16
|
-
possible_values (dict[str, Input]):
|
|
28
|
+
filter_kwargs (dict[str, Any]): Mapping of filter expressions keyed by "<field>[__lookup]".
|
|
29
|
+
possible_values (dict[str, Input]): Mapping of field names to Input definitions used for casting and type information.
|
|
17
30
|
|
|
18
31
|
Returns:
|
|
19
|
-
dict[str, dict
|
|
32
|
+
dict[str, dict]: Mapping from input field name to a dictionary containing either:
|
|
33
|
+
- "filter_kwargs": dict of lookup names to values for bucket (GeneralManager) fields, or
|
|
34
|
+
- "filter_funcs": list of callables that evaluate non-bucket field conditions.
|
|
20
35
|
|
|
21
36
|
Raises:
|
|
22
|
-
|
|
37
|
+
UnknownInputFieldError: If a filter references a field name not present in `possible_values`.
|
|
23
38
|
"""
|
|
24
39
|
from general_manager.manager.generalManager import GeneralManager
|
|
25
40
|
|
|
@@ -28,7 +43,7 @@ def parse_filters(
|
|
|
28
43
|
parts = kwarg.split("__")
|
|
29
44
|
field_name = parts[0]
|
|
30
45
|
if field_name not in possible_values:
|
|
31
|
-
raise
|
|
46
|
+
raise UnknownInputFieldError(field_name)
|
|
32
47
|
input_field = possible_values[field_name]
|
|
33
48
|
|
|
34
49
|
lookup = "__".join(parts[1:]) if len(parts) > 1 else ""
|
|
@@ -131,5 +146,5 @@ def apply_lookup(value_to_check: Any, lookup: str, filter_value: Any) -> bool:
|
|
|
131
146
|
return value_to_check in filter_value
|
|
132
147
|
else:
|
|
133
148
|
return False
|
|
134
|
-
except TypeError
|
|
149
|
+
except TypeError:
|
|
135
150
|
return False
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Utilities for tracing relationships between GeneralManager classes."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import TYPE_CHECKING, Any, cast, get_args
|
|
4
|
+
from typing import TYPE_CHECKING, Any, ClassVar, cast, get_args
|
|
5
5
|
from general_manager.manager.meta import GeneralManagerMeta
|
|
6
6
|
from general_manager.api.property import GraphQLProperty
|
|
7
7
|
|
|
@@ -13,19 +13,27 @@ type PathStart = str
|
|
|
13
13
|
type PathDestination = str
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class MissingStartInstanceError(ValueError):
|
|
17
|
+
"""Raised when attempting to traverse a path without a starting instance."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Create the MissingStartInstanceError with its default message.
|
|
22
|
+
|
|
23
|
+
This initializer constructs the exception with the message: "Cannot call goTo on a PathMap without a start instance."
|
|
24
|
+
"""
|
|
25
|
+
super().__init__("Cannot call goTo on a PathMap without a start instance.")
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
class PathMap:
|
|
17
29
|
"""Maintain cached traversal paths between GeneralManager classes."""
|
|
18
30
|
|
|
19
31
|
instance: PathMap
|
|
20
|
-
mapping: dict[tuple[PathStart, PathDestination], PathTracer] = {}
|
|
32
|
+
mapping: ClassVar[dict[tuple[PathStart, PathDestination], PathTracer]] = {}
|
|
21
33
|
|
|
22
|
-
def __new__(cls, *args:
|
|
34
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> PathMap:
|
|
23
35
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Parameters:
|
|
27
|
-
args (tuple): Positional arguments ignored by the constructor.
|
|
28
|
-
kwargs (dict): Keyword arguments ignored by the constructor.
|
|
36
|
+
Obtain the singleton PathMap, initializing the path mapping on first instantiation.
|
|
29
37
|
|
|
30
38
|
Returns:
|
|
31
39
|
PathMap: The singleton PathMap instance.
|
|
@@ -106,16 +114,16 @@ class PathMap:
|
|
|
106
114
|
self, path_destination: PathDestination | type[GeneralManager] | str
|
|
107
115
|
) -> GeneralManager | Bucket | None:
|
|
108
116
|
"""
|
|
109
|
-
|
|
117
|
+
Traverse the cached path from the configured start to the given destination.
|
|
110
118
|
|
|
111
119
|
Parameters:
|
|
112
|
-
path_destination (PathDestination | type[GeneralManager] | str):
|
|
120
|
+
path_destination (PathDestination | type[GeneralManager] | str): Destination specified as a GeneralManager class, a destination name, or a PathDestination identifier.
|
|
113
121
|
|
|
114
122
|
Returns:
|
|
115
|
-
GeneralManager | Bucket | None: The resolved
|
|
123
|
+
GeneralManager | Bucket | None: The resolved GeneralManager instance, a Bucket of instances reached by the path, or `None` if no cached path exists.
|
|
116
124
|
|
|
117
125
|
Raises:
|
|
118
|
-
|
|
126
|
+
MissingStartInstanceError: If the cached path requires a concrete start instance but the PathMap was constructed without one.
|
|
119
127
|
"""
|
|
120
128
|
if isinstance(path_destination, type):
|
|
121
129
|
path_destination = path_destination.__name__
|
|
@@ -124,15 +132,15 @@ class PathMap:
|
|
|
124
132
|
if not tracer:
|
|
125
133
|
return None
|
|
126
134
|
if self.start_instance is None:
|
|
127
|
-
raise
|
|
135
|
+
raise MissingStartInstanceError()
|
|
128
136
|
return tracer.traversePath(self.start_instance)
|
|
129
137
|
|
|
130
138
|
def getAllConnected(self) -> set[str]:
|
|
131
139
|
"""
|
|
132
|
-
|
|
140
|
+
Return the set of destination class names that are reachable from the configured start.
|
|
133
141
|
|
|
134
142
|
Returns:
|
|
135
|
-
set[str]:
|
|
143
|
+
set[str]: Destination class names reachable from the current start_class_name.
|
|
136
144
|
"""
|
|
137
145
|
connected_classes: set[str] = set()
|
|
138
146
|
for path_tuple, path_obj in self.mapping.items():
|
|
@@ -5,6 +5,23 @@ from __future__ import annotations
|
|
|
5
5
|
from importlib import import_module
|
|
6
6
|
from typing import Any, Iterable, Mapping, MutableMapping, overload
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
class MissingExportError(AttributeError):
|
|
10
|
+
"""Raised when a requested export is not defined in the public API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, module_name: str, attribute: str) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Initialize the MissingExportError with the originating module name and the missing attribute.
|
|
15
|
+
|
|
16
|
+
Constructs the exception message "module 'module_name' has no attribute 'attribute'".
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
module_name (str): Name of the module where the attribute was expected.
|
|
20
|
+
attribute (str): Name of the missing attribute.
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(f"module {module_name!r} has no attribute {attribute!r}")
|
|
23
|
+
|
|
24
|
+
|
|
8
25
|
ModuleTarget = tuple[str, str]
|
|
9
26
|
ModuleMap = Mapping[str, str | ModuleTarget]
|
|
10
27
|
|
|
@@ -30,9 +47,23 @@ def resolve_export(
|
|
|
30
47
|
module_map: ModuleMap,
|
|
31
48
|
module_globals: MutableMapping[str, Any],
|
|
32
49
|
) -> Any:
|
|
33
|
-
"""
|
|
50
|
+
"""
|
|
51
|
+
Resolve and cache a lazily-loaded export for a package __init__ module.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
name (str): The public export name to resolve.
|
|
55
|
+
module_all (Iterable[str]): Iterable of names declared in the module's __all__; used to validate that `name` is an allowed export.
|
|
56
|
+
module_map (ModuleMap): Mapping from public export names to target module paths or (module path, attribute) pairs used to locate the actual object.
|
|
57
|
+
module_globals (MutableMapping[str, Any]): The module's globals dict; the resolved value will be stored here under `name`.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Any: The resolved attribute value for `name`.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
MissingExportError: If `name` is not present in `module_all`.
|
|
64
|
+
"""
|
|
34
65
|
if name not in module_all:
|
|
35
|
-
raise
|
|
66
|
+
raise MissingExportError(module_globals["__name__"], name)
|
|
36
67
|
module_path, attr_name = _normalize_target(name, module_map[name])
|
|
37
68
|
module = import_module(module_path)
|
|
38
69
|
value = getattr(module, attr_name)
|