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
|
@@ -35,32 +35,34 @@ class PermissionDict(TypedDict):
|
|
|
35
35
|
|
|
36
36
|
permission_functions: dict[str, PermissionDict] = {
|
|
37
37
|
"public": {
|
|
38
|
-
"permission_method": lambda
|
|
39
|
-
"permission_filter": lambda
|
|
38
|
+
"permission_method": lambda _instance, _user, _config: True,
|
|
39
|
+
"permission_filter": lambda _user, _config: None,
|
|
40
40
|
},
|
|
41
41
|
"matches": {
|
|
42
|
-
"permission_method": lambda instance,
|
|
42
|
+
"permission_method": lambda instance, _user, config: getattr(
|
|
43
|
+
instance, config[0]
|
|
44
|
+
)
|
|
43
45
|
== config[1],
|
|
44
|
-
"permission_filter": lambda
|
|
46
|
+
"permission_filter": lambda _user, config: {"filter": {config[0]: config[1]}},
|
|
45
47
|
},
|
|
46
48
|
"ends_with": {
|
|
47
|
-
"permission_method": lambda instance,
|
|
49
|
+
"permission_method": lambda instance, _user, config: getattr(
|
|
48
50
|
instance, config[0]
|
|
49
51
|
).endswith(config[1]),
|
|
50
|
-
"permission_filter": lambda
|
|
52
|
+
"permission_filter": lambda _user, config: {
|
|
51
53
|
"filter": {f"{config[0]}__endswith": config[1]}
|
|
52
54
|
},
|
|
53
55
|
},
|
|
54
56
|
"isAdmin": {
|
|
55
|
-
"permission_method": lambda
|
|
56
|
-
"permission_filter": lambda
|
|
57
|
+
"permission_method": lambda _instance, user, _config: user.is_staff,
|
|
58
|
+
"permission_filter": lambda _user, _config: None,
|
|
57
59
|
},
|
|
58
60
|
"isSelf": {
|
|
59
|
-
"permission_method": lambda instance, user,
|
|
60
|
-
"permission_filter": lambda user,
|
|
61
|
+
"permission_method": lambda instance, user, _config: instance.creator == user, # type: ignore
|
|
62
|
+
"permission_filter": lambda user, _config: {"filter": {"creator_id": user.id}}, # type: ignore
|
|
61
63
|
},
|
|
62
64
|
"isAuthenticated": {
|
|
63
|
-
"permission_method": lambda
|
|
64
|
-
"permission_filter": lambda
|
|
65
|
+
"permission_method": lambda _instance, user, _config: user.is_authenticated,
|
|
66
|
+
"permission_filter": lambda _user, _config: None,
|
|
65
67
|
},
|
|
66
68
|
}
|
|
@@ -6,6 +6,21 @@ from django.contrib.auth.models import AbstractUser
|
|
|
6
6
|
|
|
7
7
|
from general_manager.manager.generalManager import GeneralManager
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
class InvalidPermissionDataError(TypeError):
|
|
11
|
+
"""Raised when the permission data manager receives unsupported input."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Exception raised when a permission data input is not a dict or a GeneralManager instance.
|
|
16
|
+
|
|
17
|
+
The exception carries the message: "permission_data must be either a dict or an instance of GeneralManager."
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(
|
|
20
|
+
"permission_data must be either a dict or an instance of GeneralManager."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
9
24
|
GeneralManagerData = TypeVar("GeneralManagerData", bound=GeneralManager)
|
|
10
25
|
|
|
11
26
|
|
|
@@ -18,14 +33,14 @@ class PermissionDataManager(Generic[GeneralManagerData]):
|
|
|
18
33
|
manager: Optional[type[GeneralManagerData]] = None,
|
|
19
34
|
) -> None:
|
|
20
35
|
"""
|
|
21
|
-
|
|
36
|
+
Wrap a mapping or GeneralManager instance to expose permission-related fields via attribute access.
|
|
22
37
|
|
|
23
38
|
Parameters:
|
|
24
|
-
permission_data (dict[str,
|
|
25
|
-
manager (type[GeneralManager] | None):
|
|
39
|
+
permission_data (dict[str, object] | GeneralManager): Either a dict mapping field names to values or a GeneralManager instance whose attributes provide field values.
|
|
40
|
+
manager (type[GeneralManager] | None): When `permission_data` is a dict, the manager class associated with that data; otherwise ignored.
|
|
26
41
|
|
|
27
42
|
Raises:
|
|
28
|
-
|
|
43
|
+
InvalidPermissionDataError: If `permission_data` is neither a dict nor an instance of GeneralManager.
|
|
29
44
|
"""
|
|
30
45
|
self.getData: Callable[[str], object]
|
|
31
46
|
self._permission_data = permission_data
|
|
@@ -47,9 +62,7 @@ class PermissionDataManager(Generic[GeneralManagerData]):
|
|
|
47
62
|
self.getData = dict_getter
|
|
48
63
|
self._manager = manager
|
|
49
64
|
else:
|
|
50
|
-
raise
|
|
51
|
-
"permission_data must be either a dict or an instance of GeneralManager"
|
|
52
|
-
)
|
|
65
|
+
raise InvalidPermissionDataError()
|
|
53
66
|
|
|
54
67
|
@classmethod
|
|
55
68
|
def forUpdate(
|
|
@@ -58,14 +71,14 @@ class PermissionDataManager(Generic[GeneralManagerData]):
|
|
|
58
71
|
update_data: dict[str, object],
|
|
59
72
|
) -> PermissionDataManager:
|
|
60
73
|
"""
|
|
61
|
-
Create a
|
|
74
|
+
Create a PermissionDataManager representing `base_data` with `update_data` applied.
|
|
62
75
|
|
|
63
76
|
Parameters:
|
|
64
|
-
base_data (
|
|
65
|
-
update_data (dict[str,
|
|
77
|
+
base_data (GeneralManagerData): Existing manager instance whose data will serve as the base.
|
|
78
|
+
update_data (dict[str, object]): Fields to add or override on the base data.
|
|
66
79
|
|
|
67
80
|
Returns:
|
|
68
|
-
PermissionDataManager: Wrapper exposing merged data
|
|
81
|
+
PermissionDataManager: Wrapper exposing the merged data where keys in `update_data` override those from `base_data`.
|
|
69
82
|
"""
|
|
70
83
|
merged_data: dict[str, object] = {**dict(base_data), **update_data}
|
|
71
84
|
return cls(merged_data, base_data.__class__)
|
|
@@ -10,29 +10,57 @@ from general_manager.manager.generalManager import GeneralManager
|
|
|
10
10
|
from general_manager.manager.meta import GeneralManagerMeta
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class PermissionNotFoundError(ValueError):
|
|
14
|
+
"""Raised when a referenced permission function is not registered."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, permission: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Exception raised when a referenced permission function cannot be found.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
permission (str): The permission identifier that was not found; used to format the exception message.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(f"Permission {permission} not found.")
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
def validatePermissionString(
|
|
14
27
|
permission: str,
|
|
15
28
|
data: PermissionDataManager | GeneralManager | GeneralManagerMeta,
|
|
16
29
|
request_user: AbstractUser | AnonymousUser,
|
|
17
30
|
) -> bool:
|
|
18
31
|
"""
|
|
19
|
-
Evaluate a compound permission expression joined by
|
|
32
|
+
Evaluate a compound permission expression joined by '&' operators.
|
|
20
33
|
|
|
21
34
|
Parameters:
|
|
22
|
-
permission (str): Permission expression (for example,
|
|
23
|
-
data (PermissionDataManager | GeneralManager | GeneralManagerMeta): Object
|
|
24
|
-
request_user (AbstractUser | AnonymousUser): User
|
|
35
|
+
permission (str): Permission expression where sub-permissions are joined with '&'. Individual sub-permissions may include ':'-separated configuration parts (for example, "isAuthenticated&admin:level").
|
|
36
|
+
data (PermissionDataManager | GeneralManager | GeneralManagerMeta): Object passed to each permission function.
|
|
37
|
+
request_user (AbstractUser | AnonymousUser): User for whom permissions are evaluated.
|
|
25
38
|
|
|
26
39
|
Returns:
|
|
27
|
-
|
|
40
|
+
`true` if every sub-permission evaluates to True, `false` otherwise.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
PermissionNotFoundError: If a referenced permission function is not registered.
|
|
28
44
|
"""
|
|
29
45
|
|
|
30
46
|
def _validateSinglePermission(
|
|
31
47
|
permission: str,
|
|
32
48
|
) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Evaluate a single sub-permission expression against the registered permission functions.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
permission (str): A single permission fragment in the form "permission_name[:config...]" where parts after the first colon are passed as configuration.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
bool: `true` if the referenced permission function grants the permission, `false` otherwise.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
PermissionNotFoundError: If no registered permission function matches the `permission_name`.
|
|
60
|
+
"""
|
|
33
61
|
permission_function, *config = permission.split(":")
|
|
34
62
|
if permission_function not in permission_functions:
|
|
35
|
-
raise
|
|
63
|
+
raise PermissionNotFoundError(permission)
|
|
36
64
|
|
|
37
65
|
return permission_functions[permission_function]["permission_method"](
|
|
38
66
|
data, request_user, config
|
|
@@ -18,9 +18,18 @@ GENERAL_MANAGER_EXPORTS: LazyExportMap = {
|
|
|
18
18
|
"graphQlMutation": ("general_manager.api.mutation", "graphQlMutation"),
|
|
19
19
|
"GeneralManager": ("general_manager.manager.generalManager", "GeneralManager"),
|
|
20
20
|
"Input": ("general_manager.manager.input", "Input"),
|
|
21
|
-
"CalculationInterface": (
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
"CalculationInterface": (
|
|
22
|
+
"general_manager.interface.calculationInterface",
|
|
23
|
+
"CalculationInterface",
|
|
24
|
+
),
|
|
25
|
+
"DatabaseInterface": (
|
|
26
|
+
"general_manager.interface.databaseInterface",
|
|
27
|
+
"DatabaseInterface",
|
|
28
|
+
),
|
|
29
|
+
"ReadOnlyInterface": (
|
|
30
|
+
"general_manager.interface.readOnlyInterface",
|
|
31
|
+
"ReadOnlyInterface",
|
|
32
|
+
),
|
|
24
33
|
"ManagerBasedPermission": (
|
|
25
34
|
"general_manager.permission.managerBasedPermission",
|
|
26
35
|
"ManagerBasedPermission",
|
|
@@ -45,7 +54,10 @@ FACTORY_EXPORTS: LazyExportMap = {
|
|
|
45
54
|
"LazyProjectName": ("general_manager.factory.factoryMethods", "LazyProjectName"),
|
|
46
55
|
"LazyDateToday": ("general_manager.factory.factoryMethods", "LazyDateToday"),
|
|
47
56
|
"LazyDateBetween": ("general_manager.factory.factoryMethods", "LazyDateBetween"),
|
|
48
|
-
"LazyDateTimeBetween": (
|
|
57
|
+
"LazyDateTimeBetween": (
|
|
58
|
+
"general_manager.factory.factoryMethods",
|
|
59
|
+
"LazyDateTimeBetween",
|
|
60
|
+
),
|
|
49
61
|
"LazyInteger": ("general_manager.factory.factoryMethods", "LazyInteger"),
|
|
50
62
|
"LazyDecimal": ("general_manager.factory.factoryMethods", "LazyDecimal"),
|
|
51
63
|
"LazyChoice": ("general_manager.factory.factoryMethods", "LazyChoice"),
|
|
@@ -54,7 +66,10 @@ FACTORY_EXPORTS: LazyExportMap = {
|
|
|
54
66
|
"LazyUUID": ("general_manager.factory.factoryMethods", "LazyUUID"),
|
|
55
67
|
"LazyFakerName": ("general_manager.factory.factoryMethods", "LazyFakerName"),
|
|
56
68
|
"LazyFakerEmail": ("general_manager.factory.factoryMethods", "LazyFakerEmail"),
|
|
57
|
-
"LazyFakerSentence": (
|
|
69
|
+
"LazyFakerSentence": (
|
|
70
|
+
"general_manager.factory.factoryMethods",
|
|
71
|
+
"LazyFakerSentence",
|
|
72
|
+
),
|
|
58
73
|
"LazyFakerAddress": ("general_manager.factory.factoryMethods", "LazyFakerAddress"),
|
|
59
74
|
"LazyFakerUrl": ("general_manager.factory.factoryMethods", "LazyFakerUrl"),
|
|
60
75
|
}
|
|
@@ -64,7 +79,10 @@ MEASUREMENT_EXPORTS: LazyExportMap = {
|
|
|
64
79
|
"Measurement": ("general_manager.measurement.measurement", "Measurement"),
|
|
65
80
|
"ureg": ("general_manager.measurement.measurement", "ureg"),
|
|
66
81
|
"currency_units": ("general_manager.measurement.measurement", "currency_units"),
|
|
67
|
-
"MeasurementField": (
|
|
82
|
+
"MeasurementField": (
|
|
83
|
+
"general_manager.measurement.measurementField",
|
|
84
|
+
"MeasurementField",
|
|
85
|
+
),
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
|
|
@@ -73,7 +91,10 @@ UTILS_EXPORTS: LazyExportMap = {
|
|
|
73
91
|
"args_to_kwargs": ("general_manager.utils.argsToKwargs", "args_to_kwargs"),
|
|
74
92
|
"make_cache_key": ("general_manager.utils.makeCacheKey", "make_cache_key"),
|
|
75
93
|
"parse_filters": ("general_manager.utils.filterParser", "parse_filters"),
|
|
76
|
-
"create_filter_function": (
|
|
94
|
+
"create_filter_function": (
|
|
95
|
+
"general_manager.utils.filterParser",
|
|
96
|
+
"create_filter_function",
|
|
97
|
+
),
|
|
77
98
|
"snake_to_pascal": ("general_manager.utils.formatString", "snake_to_pascal"),
|
|
78
99
|
"snake_to_camel": ("general_manager.utils.formatString", "snake_to_camel"),
|
|
79
100
|
"pascal_to_snake": ("general_manager.utils.formatString", "pascal_to_snake"),
|
|
@@ -89,7 +110,10 @@ PERMISSION_EXPORTS: LazyExportMap = {
|
|
|
89
110
|
"general_manager.permission.managerBasedPermission",
|
|
90
111
|
"ManagerBasedPermission",
|
|
91
112
|
),
|
|
92
|
-
"MutationPermission": (
|
|
113
|
+
"MutationPermission": (
|
|
114
|
+
"general_manager.permission.mutationPermission",
|
|
115
|
+
"MutationPermission",
|
|
116
|
+
),
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
|
|
@@ -124,7 +148,10 @@ CACHE_EXPORTS: LazyExportMap = {
|
|
|
124
148
|
BUCKET_EXPORTS: LazyExportMap = {
|
|
125
149
|
"Bucket": ("general_manager.bucket.baseBucket", "Bucket"),
|
|
126
150
|
"DatabaseBucket": ("general_manager.bucket.databaseBucket", "DatabaseBucket"),
|
|
127
|
-
"CalculationBucket": (
|
|
151
|
+
"CalculationBucket": (
|
|
152
|
+
"general_manager.bucket.calculationBucket",
|
|
153
|
+
"CalculationBucket",
|
|
154
|
+
),
|
|
128
155
|
"GroupBucket": ("general_manager.bucket.groupBucket", "GroupBucket"),
|
|
129
156
|
}
|
|
130
157
|
|
|
@@ -157,4 +184,3 @@ EXPORT_REGISTRY: Mapping[str, LazyExportMap] = {
|
|
|
157
184
|
"general_manager.manager": MANAGER_EXPORTS,
|
|
158
185
|
"general_manager.rule": RULE_EXPORTS,
|
|
159
186
|
}
|
|
160
|
-
|
general_manager/rule/__init__.py
CHANGED
|
@@ -12,10 +12,19 @@ __all__ = list(RULE_EXPORTS)
|
|
|
12
12
|
_MODULE_MAP = RULE_EXPORTS
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from general_manager._types.rule import * # noqa:
|
|
15
|
+
from general_manager._types.rule import * # noqa: F403
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def __getattr__(name: str) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Dynamically resolve a missing module attribute using the module's export registry.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
name (str): The attribute name being accessed on the module.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The attribute value associated with `name` from the module's export registry, or a fallback value if the name cannot be resolved.
|
|
27
|
+
"""
|
|
19
28
|
return resolve_export(
|
|
20
29
|
name,
|
|
21
30
|
module_all=__all__,
|
general_manager/rule/handler.py
CHANGED
|
@@ -9,6 +9,71 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from general_manager.rule.rule import Rule
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class InvalidFunctionNodeError(ValueError):
|
|
13
|
+
"""Raised when a rule handler receives an invalid AST node for its function."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, function_name: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialize the exception for an invalid left-hand AST node used with a function call.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
function_name (str): Name of the function with the invalid left node; stored on the exception and used to form the message "Invalid left node for {function_name}() function."
|
|
21
|
+
"""
|
|
22
|
+
self.function_name = function_name
|
|
23
|
+
super().__init__(f"Invalid left node for {function_name}() function.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidLenThresholdError(TypeError):
|
|
27
|
+
"""Raised when len() comparisons use a non-numeric threshold."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Exception raised when a len() threshold is not a numeric value.
|
|
32
|
+
|
|
33
|
+
Initializes the exception with a default message indicating invalid arguments for the len function.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__("Invalid arguments for len function.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidNumericThresholdError(TypeError):
|
|
39
|
+
"""Raised when aggregate handlers use a non-numeric threshold."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, function_name: str) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Create an InvalidFunctionNodeError with a formatted message for the given function name.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
function_name (str): Name of the aggregate function (e.g., "sum", "max", "min") included in the message.
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(f"Invalid arguments for {function_name} function.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NonEmptyIterableError(ValueError):
|
|
52
|
+
"""Raised when an aggregate function expects a non-empty iterable."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, function_name: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize the error indicating an aggregate function received an empty iterable.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
function_name (str): Name of the aggregate function (e.g., 'sum', 'max', 'len') used to build the error message.
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(f"{function_name} expects a non-empty iterable.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NumericIterableError(TypeError):
|
|
65
|
+
"""Raised when an aggregate function expects numeric elements."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, function_name: str) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Initialize the exception indicating that a function expected an iterable of numeric values.
|
|
70
|
+
|
|
71
|
+
Parameters:
|
|
72
|
+
function_name (str): Name of the function included in the exception message (message: "<function_name> expects an iterable of numbers.").
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(f"{function_name} expects an iterable of numbers.")
|
|
75
|
+
|
|
76
|
+
|
|
12
77
|
class BaseRuleHandler(ABC):
|
|
13
78
|
"""Define the protocol for generating rule-specific error messages."""
|
|
14
79
|
|
|
@@ -55,6 +120,23 @@ class FunctionHandler(BaseRuleHandler, ABC):
|
|
|
55
120
|
var_values: Dict[str, Optional[object]],
|
|
56
121
|
rule: Rule,
|
|
57
122
|
) -> Dict[str, str]:
|
|
123
|
+
"""
|
|
124
|
+
Handle a comparison AST node whose left side is a function call and delegate analysis to the subclass aggregate method.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
node (ast.AST): The AST node to inspect; processing only occurs if it is an `ast.Compare`.
|
|
128
|
+
left (Optional[ast.expr]): Original left operand from the rule (may be unused by this handler).
|
|
129
|
+
right (Optional[ast.expr]): Original right operand from the rule (may be unused by this handler).
|
|
130
|
+
op (Optional[ast.cmpop]): Comparison operator from the rule; used to determine operator symbol.
|
|
131
|
+
var_values (Dict[str, Optional[object]]): Mapping of variable names to their resolved values for message construction.
|
|
132
|
+
rule (Rule): Rule instance used to obtain the textual operator symbol and any rule-specific context.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dict[str, str]: Mapping from variable name to generated error message; empty if `node` is not an `ast.Compare`.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
InvalidFunctionNodeError: If the compare left-hand side is not a function call with at least one argument.
|
|
139
|
+
"""
|
|
58
140
|
if not isinstance(node, ast.Compare):
|
|
59
141
|
return {}
|
|
60
142
|
compare_node = node
|
|
@@ -64,7 +146,7 @@ class FunctionHandler(BaseRuleHandler, ABC):
|
|
|
64
146
|
op_symbol = rule._get_op_symbol(op)
|
|
65
147
|
|
|
66
148
|
if not (isinstance(left_node, ast.Call) and left_node.args):
|
|
67
|
-
raise
|
|
149
|
+
raise InvalidFunctionNodeError(self.function_name)
|
|
68
150
|
arg_node = left_node.args[0]
|
|
69
151
|
|
|
70
152
|
return self.aggregate(
|
|
@@ -112,20 +194,20 @@ class LenHandler(FunctionHandler):
|
|
|
112
194
|
rule: Rule,
|
|
113
195
|
) -> Dict[str, str]:
|
|
114
196
|
"""
|
|
115
|
-
|
|
197
|
+
Produce an error message for a len() comparison against a numeric threshold.
|
|
116
198
|
|
|
117
199
|
Parameters:
|
|
118
|
-
arg_node (ast.expr): AST node representing the
|
|
119
|
-
right_node (ast.expr):
|
|
120
|
-
op_symbol (str):
|
|
121
|
-
var_values (
|
|
122
|
-
rule (Rule):
|
|
200
|
+
arg_node (ast.expr): AST node representing the value passed to `len`.
|
|
201
|
+
right_node (ast.expr): AST node representing the comparison threshold.
|
|
202
|
+
op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=").
|
|
203
|
+
var_values (Dict[str, Optional[object]]): Runtime values for variables keyed by name.
|
|
204
|
+
rule (Rule): Rule helper used to resolve node names and evaluate nodes.
|
|
123
205
|
|
|
124
206
|
Returns:
|
|
125
|
-
|
|
207
|
+
Dict[str, str]: A single-entry mapping from the variable name to a human-readable violation message.
|
|
126
208
|
|
|
127
209
|
Raises:
|
|
128
|
-
|
|
210
|
+
InvalidLenThresholdError: If the comparison threshold cannot be interpreted as a number.
|
|
129
211
|
"""
|
|
130
212
|
|
|
131
213
|
var_name = rule._get_node_name(arg_node)
|
|
@@ -134,7 +216,7 @@ class LenHandler(FunctionHandler):
|
|
|
134
216
|
# --- Hier der Typ-Guard für right_value ---
|
|
135
217
|
raw = rule._eval_node(right_node)
|
|
136
218
|
if not isinstance(raw, (int, float)):
|
|
137
|
-
raise
|
|
219
|
+
raise InvalidLenThresholdError()
|
|
138
220
|
right_value: int | float = raw
|
|
139
221
|
|
|
140
222
|
if op_symbol == ">":
|
|
@@ -171,35 +253,38 @@ class SumHandler(FunctionHandler):
|
|
|
171
253
|
rule: Rule,
|
|
172
254
|
) -> Dict[str, str]:
|
|
173
255
|
"""
|
|
174
|
-
|
|
256
|
+
Evaluate the sum of the iterable referenced by `arg_node` and produce a descriptive error message when the sum does not satisfy the comparison against the provided threshold.
|
|
175
257
|
|
|
176
258
|
Parameters:
|
|
177
|
-
arg_node (ast.expr): AST node
|
|
178
|
-
right_node (ast.expr):
|
|
179
|
-
op_symbol (str):
|
|
180
|
-
var_values (
|
|
181
|
-
rule (Rule):
|
|
259
|
+
arg_node (ast.expr): AST node identifying the iterable variable whose elements will be summed.
|
|
260
|
+
right_node (ast.expr): AST node representing the threshold value to compare against.
|
|
261
|
+
op_symbol (str): Comparison operator symbol (e.g., ">", "<=", "==") used to form the message.
|
|
262
|
+
var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated runtime values.
|
|
263
|
+
rule (Rule): Rule helper used to resolve node names and evaluate AST nodes.
|
|
182
264
|
|
|
183
265
|
Returns:
|
|
184
|
-
|
|
266
|
+
Dict[str, str]: A mapping containing a single entry: the variable name mapped to a human-readable message
|
|
267
|
+
describing how the computed sum relates to the threshold (too small, too large, or must equal).
|
|
185
268
|
|
|
186
269
|
Raises:
|
|
187
|
-
|
|
270
|
+
NonEmptyIterableError: If the referenced iterable is missing or empty.
|
|
271
|
+
NumericIterableError: If the iterable contains non-numeric elements.
|
|
272
|
+
InvalidNumericThresholdError: If the threshold evaluated from `right_node` is not numeric.
|
|
188
273
|
"""
|
|
189
274
|
|
|
190
275
|
# Name und Wert holen
|
|
191
276
|
var_name = rule._get_node_name(arg_node)
|
|
192
277
|
raw_iter = var_values.get(var_name)
|
|
193
|
-
if not isinstance(raw_iter, (list, tuple)):
|
|
194
|
-
raise
|
|
278
|
+
if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
|
|
279
|
+
raise NonEmptyIterableError("sum")
|
|
195
280
|
if not all(isinstance(x, (int, float)) for x in raw_iter):
|
|
196
|
-
raise
|
|
281
|
+
raise NumericIterableError("sum")
|
|
197
282
|
total = sum(raw_iter)
|
|
198
283
|
|
|
199
284
|
# Schwellenwert aus dem rechten Knoten
|
|
200
285
|
raw = rule._eval_node(right_node)
|
|
201
286
|
if not isinstance(raw, (int, float)):
|
|
202
|
-
raise
|
|
287
|
+
raise InvalidNumericThresholdError("sum")
|
|
203
288
|
right_value = raw
|
|
204
289
|
|
|
205
290
|
# Message formulieren
|
|
@@ -229,33 +314,35 @@ class MaxHandler(FunctionHandler):
|
|
|
229
314
|
rule: Rule,
|
|
230
315
|
) -> Dict[str, str]:
|
|
231
316
|
"""
|
|
232
|
-
Compare the maximum element of an iterable against the
|
|
317
|
+
Compare the maximum element of an iterable variable against a numeric threshold and produce an error message when the comparison fails.
|
|
233
318
|
|
|
234
319
|
Parameters:
|
|
235
|
-
arg_node (ast.expr): AST node
|
|
236
|
-
right_node (ast.expr):
|
|
237
|
-
op_symbol (str):
|
|
238
|
-
var_values (
|
|
239
|
-
rule (Rule):
|
|
320
|
+
arg_node (ast.expr): AST node identifying the iterable variable passed to `max`.
|
|
321
|
+
right_node (ast.expr): AST node that evaluates to the numeric threshold to compare against.
|
|
322
|
+
op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=", "==") used to shape the message.
|
|
323
|
+
var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated runtime values.
|
|
324
|
+
rule (Rule): Rule helper used to resolve node names and evaluate `right_node`.
|
|
240
325
|
|
|
241
326
|
Returns:
|
|
242
|
-
|
|
327
|
+
Dict[str, str]: Single-item mapping from the variable name to a human-readable message describing the max value and the threshold comparison.
|
|
243
328
|
|
|
244
329
|
Raises:
|
|
245
|
-
|
|
330
|
+
NonEmptyIterableError: If the resolved iterable is not a non-empty list or tuple.
|
|
331
|
+
NumericIterableError: If any element of the iterable is not an int or float.
|
|
332
|
+
InvalidNumericThresholdError: If the evaluated threshold is not an int or float.
|
|
246
333
|
"""
|
|
247
334
|
|
|
248
335
|
var_name = rule._get_node_name(arg_node)
|
|
249
336
|
raw_iter = var_values.get(var_name)
|
|
250
337
|
if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
|
|
251
|
-
raise
|
|
338
|
+
raise NonEmptyIterableError("max")
|
|
252
339
|
if not all(isinstance(x, (int, float)) for x in raw_iter):
|
|
253
|
-
raise
|
|
340
|
+
raise NumericIterableError("max")
|
|
254
341
|
current = max(raw_iter)
|
|
255
342
|
|
|
256
343
|
raw = rule._eval_node(right_node)
|
|
257
344
|
if not isinstance(raw, (int, float)):
|
|
258
|
-
raise
|
|
345
|
+
raise InvalidNumericThresholdError("max")
|
|
259
346
|
right_value = raw
|
|
260
347
|
|
|
261
348
|
if op_symbol in (">", ">="):
|
|
@@ -280,33 +367,35 @@ class MinHandler(FunctionHandler):
|
|
|
280
367
|
rule: Rule,
|
|
281
368
|
) -> Dict[str, str]:
|
|
282
369
|
"""
|
|
283
|
-
Compare the minimum element of an iterable against
|
|
370
|
+
Compare the minimum element of an iterable against a numeric threshold and produce an error message describing the violation.
|
|
284
371
|
|
|
285
372
|
Parameters:
|
|
286
|
-
arg_node (ast.expr): AST node
|
|
287
|
-
right_node (ast.expr):
|
|
288
|
-
op_symbol (str):
|
|
289
|
-
var_values (
|
|
290
|
-
rule (Rule):
|
|
373
|
+
arg_node (ast.expr): AST node for the iterable argument passed to `min`.
|
|
374
|
+
right_node (ast.expr): AST node for the threshold value to compare against.
|
|
375
|
+
op_symbol (str): Comparison operator symbol (e.g., ">", ">=", "<", "<=", "==").
|
|
376
|
+
var_values (Dict[str, Optional[object]]): Mapping of variable names to their evaluated values.
|
|
377
|
+
rule (Rule): Rule instance used to evaluate AST nodes and obtain variable names.
|
|
291
378
|
|
|
292
379
|
Returns:
|
|
293
|
-
dict[str, str]: Mapping
|
|
380
|
+
dict[str, str]: Mapping with the variable name as key and the generated error message as value.
|
|
294
381
|
|
|
295
382
|
Raises:
|
|
296
|
-
|
|
383
|
+
NonEmptyIterableError: If the iterable is not a non-empty list/tuple.
|
|
384
|
+
NumericIterableError: If the iterable contains non-numeric elements.
|
|
385
|
+
InvalidNumericThresholdError: If the evaluated threshold is not numeric.
|
|
297
386
|
"""
|
|
298
387
|
|
|
299
388
|
var_name = rule._get_node_name(arg_node)
|
|
300
389
|
raw_iter = var_values.get(var_name)
|
|
301
390
|
if not isinstance(raw_iter, (list, tuple)) or len(raw_iter) == 0:
|
|
302
|
-
raise
|
|
391
|
+
raise NonEmptyIterableError("min")
|
|
303
392
|
if not all(isinstance(x, (int, float)) for x in raw_iter):
|
|
304
|
-
raise
|
|
393
|
+
raise NumericIterableError("min")
|
|
305
394
|
current = min(raw_iter)
|
|
306
395
|
|
|
307
396
|
raw = rule._eval_node(right_node)
|
|
308
397
|
if not isinstance(raw, (int, float)):
|
|
309
|
-
raise
|
|
398
|
+
raise InvalidNumericThresholdError("min")
|
|
310
399
|
right_value = raw
|
|
311
400
|
|
|
312
401
|
if op_symbol in (">", ">="):
|