rule-engine 5.0.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.
Files changed (41) hide show
  1. rule_engine/__init__.py +65 -0
  2. rule_engine/_vendor/__init__.py +4 -0
  3. rule_engine/_vendor/ply/LICENSE +32 -0
  4. rule_engine/_vendor/ply/__init__.py +5 -0
  5. rule_engine/_vendor/ply/lex.py +1098 -0
  6. rule_engine/_vendor/ply/yacc.py +3433 -0
  7. rule_engine/ast/__init__.py +92 -0
  8. rule_engine/ast/base.py +343 -0
  9. rule_engine/ast/binary/__init__.py +103 -0
  10. rule_engine/ast/binary/arithmetic.py +211 -0
  11. rule_engine/ast/binary/base.py +112 -0
  12. rule_engine/ast/binary/comparison.py +154 -0
  13. rule_engine/ast/expression/__init__.py +35 -0
  14. rule_engine/ast/expression/access.py +434 -0
  15. rule_engine/ast/expression/control.py +256 -0
  16. rule_engine/ast/expression/resolution.py +220 -0
  17. rule_engine/ast/literal.py +183 -0
  18. rule_engine/builtins.py +230 -0
  19. rule_engine/debug_ast.py +65 -0
  20. rule_engine/debug_repl.py +159 -0
  21. rule_engine/engine/__init__.py +34 -0
  22. rule_engine/engine/_attribute_resolver.py +353 -0
  23. rule_engine/engine/context.py +438 -0
  24. rule_engine/engine/rule.py +148 -0
  25. rule_engine/errors.py +359 -0
  26. rule_engine/parser/__init__.py +614 -0
  27. rule_engine/parser/base.py +86 -0
  28. rule_engine/parser/utilities.py +114 -0
  29. rule_engine/suggestions.py +105 -0
  30. rule_engine/types/__init__.py +61 -0
  31. rule_engine/types/_object.py +318 -0
  32. rule_engine/types/coercion.py +141 -0
  33. rule_engine/types/dataclass.py +149 -0
  34. rule_engine/types/datatype.py +398 -0
  35. rule_engine/types/definitions.py +454 -0
  36. rule_engine/types/sqlalchemy.py +171 -0
  37. rule_engine-5.0.0.dist-info/METADATA +126 -0
  38. rule_engine-5.0.0.dist-info/RECORD +41 -0
  39. rule_engine-5.0.0.dist-info/WHEEL +5 -0
  40. rule_engine-5.0.0.dist-info/licenses/LICENSE +28 -0
  41. rule_engine-5.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # rule_engine/ast/expression/control.py
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are
8
+ # met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above
13
+ # copyright notice, this list of conditions and the following disclaimer
14
+ # in the documentation and/or other materials provided with the
15
+ # distribution.
16
+ # * Neither the name of the project nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ import collections
34
+ import datetime
35
+ import functools
36
+ import operator
37
+ from typing import TYPE_CHECKING, Any, Callable
38
+
39
+ from ... import errors
40
+ from ...types import DataType, is_numeric
41
+ from ...types import _DataTypeDef
42
+
43
+ from ..base import (
44
+ Assignment,
45
+ ExpressionBase,
46
+ LiteralExpressionBase,
47
+ _assert_not_nullable,
48
+ _is_reduced,
49
+ _propagate_nullable,
50
+ _resolve_type,
51
+ )
52
+ from ..literal import BooleanExpression, FloatExpression, TimedeltaExpression
53
+
54
+ if TYPE_CHECKING:
55
+ from ...engine.context import Context
56
+
57
+ class ComprehensionExpression(ExpressionBase):
58
+ result_type: _DataTypeDef = DataType.ARRAY
59
+ def __init__(
60
+ self,
61
+ context: 'Context',
62
+ result: ExpressionBase,
63
+ variable: str,
64
+ iterable: ExpressionBase,
65
+ condition: ExpressionBase | None = None
66
+ ) -> None:
67
+ self.context = context
68
+ self.result = result
69
+ self.variable = variable
70
+ self.iterable = iterable
71
+ self.condition = condition
72
+ self.result_type = DataType.ARRAY(self.result.result_type)
73
+
74
+ @classmethod
75
+ def build( # type: ignore[override]
76
+ cls,
77
+ context: 'Context',
78
+ result: ExpressionBase,
79
+ variable: str,
80
+ iterable: ExpressionBase,
81
+ condition: ExpressionBase | None = None
82
+ ) -> ExpressionBase:
83
+ iterable_built = iterable.build()
84
+ assert isinstance(iterable_built, ExpressionBase)
85
+ if iterable_built.result_type is not DataType.UNDEFINED and not iterable_built.result_type.is_iterable:
86
+ raise errors.EvaluationError('data type mismatch (comprehension requires an iterable)')
87
+ resolved_iterable_type = _resolve_type(iterable_built.result_type, context)
88
+ assignment = Assignment(variable, value_type=getattr(resolved_iterable_type, 'iterable_type', DataType.UNDEFINED))
89
+ with context.assignments(assignment):
90
+ if condition is not None:
91
+ condition_built = condition.build()
92
+ assert isinstance(condition_built, ExpressionBase)
93
+ condition = condition_built
94
+ result_built = result.build()
95
+ assert isinstance(result_built, ExpressionBase)
96
+ result = result_built
97
+ reduced = cls(context, result, variable, iterable_built, condition=condition).reduce()
98
+ assert isinstance(reduced, ExpressionBase)
99
+ return reduced
100
+
101
+ def __repr__(self) -> str:
102
+ return "<{0} iterable={1!r} result={2!r} condition={3!r} >".format(self.__class__.__name__, self.iterable, self.result, self.condition)
103
+
104
+ def evaluate(self, thing: Any) -> Any:
105
+ output_array: 'collections.deque[Any]' = collections.deque()
106
+ input_iterable = self.iterable.evaluate(thing)
107
+ if not DataType.from_value(input_iterable).is_iterable:
108
+ raise errors.EvaluationError('data type mismatch (comprehension requires an iterable)')
109
+ for value in input_iterable:
110
+ assignment = Assignment(self.variable, value=value)
111
+ with self.context.assignments(assignment):
112
+ if self.condition is None or self.condition.evaluate(thing):
113
+ output_array.append(self.result.evaluate(thing))
114
+ return tuple(output_array)
115
+
116
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
117
+ digraph.node(str(id(self)), "{}\nvariable={!r}".format(self.__class__.__name__, self.variable))
118
+ self.result.to_graphviz(digraph, *args, **kwargs)
119
+ digraph.edge(str(id(self)), str(id(self.result)), label='result')
120
+ self.iterable.to_graphviz(digraph, *args, **kwargs)
121
+ digraph.edge(str(id(self)), str(id(self.iterable)), label='iterable')
122
+ if self.condition is not None:
123
+ self.condition.to_graphviz(digraph, *args, **kwargs)
124
+ digraph.edge(str(id(self)), str(id(self.condition)), label='condition')
125
+
126
+ class TernaryExpression(ExpressionBase):
127
+ """
128
+ A class for representing ternary expressions from the grammar text. These involve evaluating :py:attr:`.condition`
129
+ before evaluating either :py:attr:`.case_true` or :py:attr:`.case_false` based on the results.
130
+ """
131
+ def __init__(self, context: 'Context', condition: ExpressionBase, case_true: ExpressionBase, case_false: ExpressionBase) -> None:
132
+ """
133
+ :param context: The context to use for evaluating the expression.
134
+ :type context: :py:class:`~rule_engine.engine.Context`
135
+ :param condition: The condition expression whose evaluation determines whether the *case_true* or *case_false*
136
+ expression is evaluated.
137
+ :param case_true: The expression that's evaluated when *condition* is True.
138
+ :param case_false:The expression that's evaluated when *condition* is False.
139
+ """
140
+ self.context = context
141
+ self.condition = condition
142
+ self.case_true = case_true
143
+ self.case_false = case_false
144
+ true_type = DataType.NULLABLE.unwrap(self.case_true.result_type)
145
+ false_type = DataType.NULLABLE.unwrap(self.case_false.result_type)
146
+ if true_type == false_type:
147
+ self.result_type = true_type
148
+ elif DataType.is_type(true_type, DataType.ARRAY) and DataType.is_type(false_type, DataType.ARRAY):
149
+ self.result_type = DataType.ARRAY
150
+ # todo: the other compound types should be checked here as well.
151
+ self.result_type = _propagate_nullable(self.result_type, self.case_true.result_type, self.case_false.result_type)
152
+
153
+ @classmethod
154
+ def build(cls, context: 'Context', condition: ExpressionBase, case_true: ExpressionBase, case_false: ExpressionBase) -> ExpressionBase: # type: ignore[override]
155
+ condition_built = condition.build()
156
+ assert isinstance(condition_built, ExpressionBase)
157
+ case_true_built = case_true.build()
158
+ assert isinstance(case_true_built, ExpressionBase)
159
+ case_false_built = case_false.build()
160
+ assert isinstance(case_false_built, ExpressionBase)
161
+ reduced = cls(context, condition_built, case_true_built, case_false_built).reduce()
162
+ assert isinstance(reduced, ExpressionBase)
163
+ return reduced
164
+
165
+ def evaluate(self, thing: Any) -> Any:
166
+ case = (self.case_true if self.condition.evaluate(thing) else self.case_false)
167
+ return case.evaluate(thing)
168
+
169
+ def reduce(self) -> ExpressionBase:
170
+ if not _is_reduced(self.condition):
171
+ return self
172
+ assert isinstance(self.condition, LiteralExpressionBase)
173
+ reduced_condition = bool(self.condition.value)
174
+ reduced = self.case_true.reduce() if reduced_condition else self.case_false.reduce()
175
+ assert isinstance(reduced, ExpressionBase)
176
+ return reduced
177
+
178
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
179
+ super(TernaryExpression, self).to_graphviz(digraph, *args, **kwargs)
180
+ self.condition.to_graphviz(digraph, *args, **kwargs)
181
+ self.case_true.to_graphviz(digraph, *args, **kwargs)
182
+ self.case_false.to_graphviz(digraph, *args, **kwargs)
183
+ digraph.edge(str(id(self)), str(id(self.condition)), label='condition')
184
+ digraph.edge(str(id(self)), str(id(self.case_true)), label='true case')
185
+ digraph.edge(str(id(self)), str(id(self.case_false)), label='false case')
186
+
187
+ class UnaryExpression(ExpressionBase):
188
+ """
189
+ A class for representing unary expressions from the grammar text. These involve a single operator on the left side.
190
+ """
191
+ def __init__(self, context: 'Context', type_: str, right: ExpressionBase) -> None:
192
+ """
193
+ :param context: The context to use for evaluating the expression.
194
+ :type context: :py:class:`~rule_engine.engine.Context`
195
+ :param str type_: The grammar type of operator to the left of the expression.
196
+ :param right: The expression to the right of the operator.
197
+ :type right: :py:class:`~.ExpressionBase`
198
+ """
199
+ self.context = context
200
+ type_ = type_.lower()
201
+ self.type = type_
202
+ if type_ == 'not':
203
+ self.result_type = DataType.BOOLEAN
204
+ elif type_ == 'uminus':
205
+ _assert_not_nullable(right.result_type, role='unary minus operand')
206
+ self.result_type = right.result_type
207
+ else:
208
+ raise ValueError('unknown unary expression type')
209
+ self._evaluator = getattr(self, '_op_' + type_)
210
+ self.right = right
211
+
212
+ @classmethod
213
+ def build(cls, context: 'Context', type_: str, right: ExpressionBase) -> ExpressionBase: # type: ignore[override]
214
+ right_built = right.build()
215
+ assert isinstance(right_built, ExpressionBase)
216
+ reduced = cls(context, type_, right_built).reduce()
217
+ assert isinstance(reduced, ExpressionBase)
218
+ return reduced
219
+
220
+ def __repr__(self) -> str:
221
+ return "<{} type={!r} >".format(self.__class__.__name__, self.type)
222
+
223
+ def evaluate(self, thing: Any) -> Any:
224
+ return self._evaluator(thing)
225
+
226
+ def __op(self, op: Callable[[Any], Any], thing: Any) -> Any:
227
+ return op(self.right.evaluate(thing))
228
+
229
+ _op_not = functools.partialmethod(__op, operator.not_)
230
+
231
+ def __op_arithmetic(self, op: Callable[[Any], Any], thing: Any) -> Any:
232
+ right = self.right.evaluate(thing)
233
+ if not is_numeric(right) and not isinstance(right, datetime.timedelta):
234
+ raise errors.EvaluationError('data type mismatch (not a numeric or timedelta value)')
235
+ return op(right)
236
+
237
+ _op_uminus = functools.partialmethod(__op_arithmetic, operator.neg)
238
+
239
+ def reduce(self) -> ExpressionBase:
240
+ type_ = self.type.lower()
241
+ if not _is_reduced(self.right):
242
+ return self
243
+ if type_ == 'not':
244
+ return BooleanExpression(self.context, self.evaluate(None))
245
+ elif type_ == 'uminus':
246
+ if isinstance(self.right, FloatExpression):
247
+ return FloatExpression(self.context, self.evaluate(None))
248
+ elif isinstance(self.right, TimedeltaExpression):
249
+ return TimedeltaExpression(self.context, self.evaluate(None))
250
+ raise errors.EvaluationError('data type mismatch (not a float or timedelta expression)')
251
+ raise errors.EngineError('unsupported unary expression type')
252
+
253
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
254
+ digraph.node(str(id(self)), "{}\ntype={!r}".format(self.__class__.__name__, self.type.lower()))
255
+ self.right.to_graphviz(digraph, *args, **kwargs)
256
+ digraph.edge(str(id(self)), str(id(self.right)))
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # rule_engine/ast/expression/resolution.py
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are
8
+ # met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above
13
+ # copyright notice, this list of conditions and the following disclaimer
14
+ # in the documentation and/or other materials provided with the
15
+ # distribution.
16
+ # * Neither the name of the project nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ from typing import TYPE_CHECKING, Any, Iterable
34
+
35
+ from ... import errors
36
+ from ...types import DataType
37
+ from ...types import _CollectionDataTypeDef, _DataTypeDef, _FunctionDataTypeDef, _MappingDataTypeDef
38
+
39
+ from ..base import ExpressionBase, LiteralExpressionBase, _is_reduced
40
+
41
+ if TYPE_CHECKING:
42
+ from ...engine.context import Context
43
+
44
+ class SymbolExpression(ExpressionBase):
45
+ """
46
+ A class representing a symbol name to be resolved at evaluation time with the help of a
47
+ :py:class:`~rule_engine.engine.Context` object.
48
+ """
49
+ __slots__ = ('name', 'result_type', 'scope')
50
+ def __init__(self, context: 'Context', name: str, scope: str | None = None) -> None:
51
+ """
52
+ :param context: The context to use for evaluating the expression.
53
+ :type context: :py:class:`~rule_engine.engine.Context`
54
+ :param str name: The name of the symbol. This will be resolved with a given context object on the specified
55
+ *thing*.
56
+ :param str scope: The optional scope to use while resolving the symbol.
57
+ """
58
+ context.symbols.add(name)
59
+ self.context = context
60
+ self.name = name
61
+ type_hint = context.resolve_type(name, scope=scope)
62
+ if type_hint is not None:
63
+ self.result_type = type_hint
64
+ self.scope = scope
65
+
66
+ def __repr__(self) -> str:
67
+ return "<{0} name={1!r} >".format(self.__class__.__name__, self.name)
68
+
69
+ def evaluate(self, thing: Any) -> Any:
70
+ try:
71
+ value = self.context.resolve(thing, self.name, scope=self.scope)
72
+ except errors.SymbolResolutionError:
73
+ default_value = self.context.default_value
74
+ if default_value is errors.UNDEFINED:
75
+ raise
76
+ value = default_value
77
+ value = self._new_value(value, verify_type=False)
78
+
79
+ # if the expected result type is undefined, return the value
80
+ if self.result_type == DataType.UNDEFINED:
81
+ return value
82
+
83
+ # NULLABLE(T) is T-or-None at runtime; None is always valid, and any other value is checked against the
84
+ # unwrapped inner type
85
+ effective_type: _DataTypeDef = self.result_type
86
+ if DataType.is_type(effective_type, DataType.NULLABLE):
87
+ if value is None:
88
+ return value
89
+ effective_type = effective_type.inner_type
90
+
91
+ # OBJECT values are opaque to DataType.from_value; trust the schema annotation and delegate attribute-level
92
+ # type checking to GetAttributeExpression
93
+ if DataType.is_type(effective_type, DataType.OBJECT):
94
+ return value
95
+
96
+ # use DataType.from_value to raise a TypeError if value is not of a
97
+ # compatible data type
98
+ value_type = DataType.from_value(value)
99
+
100
+ # if the type is the expected result type, return the value
101
+ if DataType.is_compatible(value_type, effective_type):
102
+ if effective_type.is_scalar:
103
+ return value
104
+ assert isinstance(effective_type, (_CollectionDataTypeDef, _MappingDataTypeDef))
105
+ assert isinstance(value_type, (_CollectionDataTypeDef, _MappingDataTypeDef))
106
+ if effective_type.value_type == DataType.UNDEFINED:
107
+ return value
108
+ if value_type.value_type == DataType.UNDEFINED:
109
+ return value
110
+ if effective_type.value_type != DataType.NULL and not effective_type.value_type_nullable and any(v is None for v in value):
111
+ raise errors.SymbolTypeError(self.name, is_value=value, is_type=value_type, expected_type=self.result_type)
112
+ if DataType.is_compatible(effective_type.value_type, value_type.value_type):
113
+ return value
114
+
115
+ # if the type is null, return the value (treat null as a special case)
116
+ if value_type == DataType.NULL:
117
+ return value
118
+
119
+ raise errors.SymbolTypeError(self.name, is_value=value, is_type=value_type, expected_type=self.result_type)
120
+
121
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
122
+ digraph.node(str(id(self)), "{}\nname={!r}".format(self.__class__.__name__, self.name))
123
+
124
+ class FunctionCallExpression(ExpressionBase):
125
+ __slots__ = ('function', 'arguments',)
126
+ arguments: tuple[ExpressionBase, ...]
127
+ def __init__(self, context: 'Context', function: ExpressionBase, arguments: Iterable[ExpressionBase]) -> None:
128
+ self.context = context
129
+ self.function = function
130
+ argument_tuple: tuple[ExpressionBase, ...] = tuple(arguments)
131
+ if self.function.result_type != DataType.UNDEFINED:
132
+ function_type = self.function.result_type
133
+ self._validate_function(function_type, argument_tuple)
134
+ assert isinstance(function_type, _FunctionDataTypeDef)
135
+ self.result_type = function_type.return_type
136
+ self.arguments = argument_tuple
137
+
138
+ @classmethod
139
+ def build(cls, context: 'Context', function: ExpressionBase, arguments: Iterable[ExpressionBase]) -> ExpressionBase: # type: ignore[override]
140
+ function_built = function.build()
141
+ assert isinstance(function_built, ExpressionBase)
142
+ built_args: list[ExpressionBase] = []
143
+ for argument in arguments:
144
+ argument_built = argument.build()
145
+ assert isinstance(argument_built, ExpressionBase)
146
+ built_args.append(argument_built)
147
+ reduced = cls(context, function_built, tuple(built_args)).reduce()
148
+ assert isinstance(reduced, ExpressionBase)
149
+ return reduced
150
+
151
+ def reduce(self) -> ExpressionBase:
152
+ if not _is_reduced(self.function, *self.arguments):
153
+ return self
154
+ return LiteralExpressionBase.from_value(self.context, self.evaluate(None))
155
+
156
+ def evaluate(self, thing: Any) -> Any:
157
+ function = self.function.evaluate(thing)
158
+ if not callable(function):
159
+ raise errors.EvaluationError('data type mismatch (not a callable value)')
160
+ arguments = tuple(argument.evaluate(thing) for argument in self.arguments)
161
+ function_name: str | None = '<unknown>'
162
+ if self.function.result_type != DataType.UNDEFINED:
163
+ function_type = self.function.result_type
164
+ assert isinstance(function_type, _FunctionDataTypeDef)
165
+ function_name = function_type.value_name
166
+ self._validate_function(function_type, arguments)
167
+ elif hasattr(function, '__name__'):
168
+ function_name = function.__name__ + '?'
169
+ try:
170
+ result = function(*arguments)
171
+ except errors.FunctionCallError as error:
172
+ error.function_name = function_name
173
+ raise error
174
+ except Exception as error:
175
+ raise errors.FunctionCallError('function call failed', error=error, function_name=function_name) from None
176
+ result = self._new_value(result)
177
+ if not DataType.is_compatible(DataType.from_value(result), self.result_type):
178
+ raise errors.FunctionCallError('function call failed (data type mismatch on returned value)', function_name=function_name)
179
+ return result
180
+
181
+ def _validate_function(self, function_type: _DataTypeDef, arguments: 'tuple[Any, ...]') -> None:
182
+ if not isinstance(function_type, _FunctionDataTypeDef):
183
+ raise errors.EvaluationError('data type mismatch (not a callable value)')
184
+ if function_type.minimum_arguments is not DataType.UNDEFINED:
185
+ assert isinstance(function_type.minimum_arguments, int)
186
+ if len(arguments) < function_type.minimum_arguments:
187
+ raise errors.FunctionCallError(
188
+ "expected at least {} positional arguments".format(function_type.minimum_arguments),
189
+ function_name=function_type.value_name
190
+ )
191
+ if function_type.argument_types is not DataType.UNDEFINED:
192
+ assert isinstance(function_type.argument_types, tuple)
193
+ if len(arguments) > len(function_type.argument_types):
194
+ raise errors.FunctionCallError(
195
+ "expected at most {} positional arguments".format(len(function_type.argument_types)),
196
+ function_name=function_type.value_name
197
+ )
198
+ for pos, (arg1, arg2_type) in enumerate(zip(arguments, function_type.argument_types), 1):
199
+ if isinstance(arg1, ExpressionBase):
200
+ arg1_type = arg1.result_type
201
+ else:
202
+ arg1_type = DataType.from_value(arg1)
203
+ if not DataType.is_compatible(arg1_type, arg2_type):
204
+ raise errors.FunctionCallError(
205
+ "data type mismatch (argument #{})".format(pos),
206
+ function_name=function_type.value_name
207
+ )
208
+ if DataType.is_type(arg1_type, DataType.NULLABLE) and not DataType.is_type(arg2_type, DataType.NULLABLE) and arg2_type != DataType.UNDEFINED:
209
+ raise errors.FunctionCallError(
210
+ "data type mismatch (argument #{} is nullable; discharge with '??' or use '&.' / '&[' for safe navigation)".format(pos),
211
+ function_name=function_type.value_name
212
+ )
213
+
214
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
215
+ super(FunctionCallExpression, self).to_graphviz(digraph, *args, **kwargs)
216
+ self.function.to_graphviz(digraph, *args, **kwargs)
217
+ digraph.edge(str(id(self)), str(id(self.function)), label='function')
218
+ for idx, argument in enumerate(self.arguments, 1):
219
+ argument.to_graphviz(digraph, *args, **kwargs)
220
+ digraph.edge(str(id(self)), str(id(argument)), label="argument #{}".format(idx))
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # rule_engine/ast/literal.py
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are
8
+ # met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright
11
+ # notice, this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above
13
+ # copyright notice, this list of conditions and the following disclaimer
14
+ # in the documentation and/or other materials provided with the
15
+ # distribution.
16
+ # * Neither the name of the project nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ import collections
34
+ from typing import TYPE_CHECKING, Any, Iterable
35
+
36
+ from .. import errors
37
+ from ..parser.utilities import parse_datetime, parse_float, parse_timedelta
38
+ from ..types import DataType, coerce_value
39
+ from ..types import _DataTypeDef
40
+
41
+ from .base import ExpressionBase, LiteralExpressionBase, _is_reduced, _iterable_member_value_type
42
+
43
+ if TYPE_CHECKING:
44
+ from ..engine.context import Context
45
+
46
+ ################################################################################
47
+ # Literal Expressions
48
+ ################################################################################
49
+ class _CollectionMixin(object):
50
+ # these attributes are provided by the LiteralExpressionBase half of the subclasses
51
+ result_type: _DataTypeDef
52
+ value: Any
53
+ def evaluate(self, thing: Any) -> Any:
54
+ return self.result_type.python_type(member.evaluate(thing) for member in self.value)
55
+
56
+ @property
57
+ def is_reduced(self) -> bool: # type: ignore[override]
58
+ return _is_reduced(*self.value)
59
+
60
+ def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
61
+ super(_CollectionMixin, self).to_graphviz(digraph, *args, **kwargs) # type: ignore[misc]
62
+ for member in self.value:
63
+ member.to_graphviz(digraph, *args, **kwargs)
64
+ digraph.edge(str(id(self)), str(id(member)))
65
+
66
+ class ArrayExpression(_CollectionMixin, LiteralExpressionBase): # type: ignore[override]
67
+ """Literal array expressions containing 0 or more sub-expressions."""
68
+ result_type: _DataTypeDef = DataType.ARRAY
69
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
70
+ super(ArrayExpression, self).__init__(*args, **kwargs)
71
+ self.result_type = DataType.ARRAY(value_type=_iterable_member_value_type(self.value))
72
+
73
+ @classmethod
74
+ def build(cls, context: 'Context', value: Iterable[ExpressionBase]) -> 'ArrayExpression': # type: ignore[override]
75
+ return cls(context, [member.build() for member in value])
76
+
77
+ class BooleanExpression(LiteralExpressionBase):
78
+ """Literal boolean expressions representing True or False."""
79
+ result_type: _DataTypeDef = DataType.BOOLEAN
80
+
81
+ class BytesExpression(LiteralExpressionBase):
82
+ """
83
+ Literal bytes expressions representing a binary string. This expression type always evaluates to true when not empty.
84
+ """
85
+ result_type: _DataTypeDef = DataType.BYTES
86
+
87
+ class DatetimeExpression(LiteralExpressionBase):
88
+ """
89
+ Literal datetime expressions representing a specific point in time. This expression type always evaluates to true.
90
+ """
91
+ result_type: _DataTypeDef = DataType.DATETIME
92
+ @classmethod
93
+ def from_string(cls, context: 'Context', string: str) -> 'DatetimeExpression':
94
+ dt = parse_datetime(string, default_timezone=context.default_timezone)
95
+ return cls(context, dt)
96
+
97
+ class TimedeltaExpression(LiteralExpressionBase):
98
+ """
99
+ Literal timedelta expressions representing an offset from a specific point in time.
100
+
101
+ .. versionadded:: 3.5.0
102
+ """
103
+ result_type: _DataTypeDef = DataType.TIMEDELTA
104
+ @classmethod
105
+ def from_string(cls, context: 'Context', string: str) -> 'TimedeltaExpression':
106
+ return cls(context, parse_timedelta(string))
107
+
108
+ class FloatExpression(LiteralExpressionBase):
109
+ """Literal float expressions representing numerical values."""
110
+ result_type: _DataTypeDef = DataType.FLOAT
111
+ def __init__(self, context: 'Context', value: Any, **kwargs: Any) -> None:
112
+ value = coerce_value(value)
113
+ super(FloatExpression, self).__init__(context, value, **kwargs)
114
+
115
+ @classmethod
116
+ def from_string(cls, context: 'Context', string: str) -> 'FloatExpression':
117
+ return cls(context, parse_float(string))
118
+
119
+ class FunctionExpression(LiteralExpressionBase):
120
+ """Literal mapping expression representing a function."""
121
+ # there's no syntax for defining functions, but this is required by the parser when a method is referred to on a
122
+ # literal value, e.g. `b"41".decode('utf-8')`
123
+ result_type: _DataTypeDef = DataType.FUNCTION
124
+
125
+ class MappingExpression(LiteralExpressionBase):
126
+ """Literal mapping expression representing a set of associations between keys and values."""
127
+ result_type: _DataTypeDef = DataType.MAPPING
128
+ def __init__(self, context: 'Context', value: Any, **kwargs: Any) -> None:
129
+ if isinstance(value, dict):
130
+ value = tuple(value.items())
131
+ super(MappingExpression, self).__init__(context, value, **kwargs)
132
+ self.result_type = DataType.MAPPING(
133
+ key_type=_iterable_member_value_type(key for key, _ in self.value),
134
+ value_type=_iterable_member_value_type(value for _, value in self.value)
135
+ )
136
+
137
+ @classmethod
138
+ def build(cls, context: 'Context', value: Any) -> 'MappingExpression': # type: ignore[override]
139
+ value = collections.OrderedDict(value)
140
+ value = collections.OrderedDict((k.build(), v.build()) for k, v in value.items())
141
+ return cls(context, value)
142
+
143
+ def evaluate(self, thing: Any) -> Any:
144
+ mapping: 'collections.OrderedDict[Any, Any]' = collections.OrderedDict()
145
+ for key, value in self.value:
146
+ key = key.evaluate(thing)
147
+ key_type = DataType.from_value(key)
148
+ if key_type.is_compound and not DataType.is_type(key_type, DataType.ARRAY):
149
+ raise errors.EngineError("the {} data type may not be used for mapping keys".format(key_type.name))
150
+ mapping[key] = value
151
+ # defer value evaluation to avoid evaluating values of duplicate keys
152
+ for key, value in mapping.items():
153
+ mapping[key] = value.evaluate(thing)
154
+ return mapping
155
+
156
+ @property
157
+ def is_reduced(self) -> bool: # type: ignore[override]
158
+ return all(_is_reduced(key, value) for key, value in self.value)
159
+
160
+ class NullExpression(LiteralExpressionBase):
161
+ """Literal null expressions representing null values. This expression type always evaluates to false."""
162
+ result_type: _DataTypeDef = DataType.NULL
163
+ def __init__(self, context: 'Context', value: Any = None) -> None:
164
+ # all of the literal expressions take a value
165
+ if value is not None:
166
+ raise TypeError('value must be None')
167
+ super(NullExpression, self).__init__(context, value=None)
168
+
169
+ class SetExpression(_CollectionMixin, LiteralExpressionBase): # type: ignore[override]
170
+ """Literal set expressions containing 0 or more sub-expressions."""
171
+ result_type: _DataTypeDef = DataType.SET
172
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
173
+ super(SetExpression, self).__init__(*args, **kwargs)
174
+ self.result_type = DataType.SET(value_type=_iterable_member_value_type(self.value))
175
+
176
+ @classmethod
177
+ def build(cls, context: 'Context', value: Iterable[ExpressionBase]) -> 'SetExpression': # type: ignore[override]
178
+ built = set(member.build() for member in value)
179
+ return cls(context, built)
180
+
181
+ class StringExpression(LiteralExpressionBase):
182
+ """Literal string expressions representing an array of characters."""
183
+ result_type: _DataTypeDef = DataType.STRING