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.
- rule_engine/__init__.py +65 -0
- rule_engine/_vendor/__init__.py +4 -0
- rule_engine/_vendor/ply/LICENSE +32 -0
- rule_engine/_vendor/ply/__init__.py +5 -0
- rule_engine/_vendor/ply/lex.py +1098 -0
- rule_engine/_vendor/ply/yacc.py +3433 -0
- rule_engine/ast/__init__.py +92 -0
- rule_engine/ast/base.py +343 -0
- rule_engine/ast/binary/__init__.py +103 -0
- rule_engine/ast/binary/arithmetic.py +211 -0
- rule_engine/ast/binary/base.py +112 -0
- rule_engine/ast/binary/comparison.py +154 -0
- rule_engine/ast/expression/__init__.py +35 -0
- rule_engine/ast/expression/access.py +434 -0
- rule_engine/ast/expression/control.py +256 -0
- rule_engine/ast/expression/resolution.py +220 -0
- rule_engine/ast/literal.py +183 -0
- rule_engine/builtins.py +230 -0
- rule_engine/debug_ast.py +65 -0
- rule_engine/debug_repl.py +159 -0
- rule_engine/engine/__init__.py +34 -0
- rule_engine/engine/_attribute_resolver.py +353 -0
- rule_engine/engine/context.py +438 -0
- rule_engine/engine/rule.py +148 -0
- rule_engine/errors.py +359 -0
- rule_engine/parser/__init__.py +614 -0
- rule_engine/parser/base.py +86 -0
- rule_engine/parser/utilities.py +114 -0
- rule_engine/suggestions.py +105 -0
- rule_engine/types/__init__.py +61 -0
- rule_engine/types/_object.py +318 -0
- rule_engine/types/coercion.py +141 -0
- rule_engine/types/dataclass.py +149 -0
- rule_engine/types/datatype.py +398 -0
- rule_engine/types/definitions.py +454 -0
- rule_engine/types/sqlalchemy.py +171 -0
- rule_engine-5.0.0.dist-info/METADATA +126 -0
- rule_engine-5.0.0.dist-info/RECORD +41 -0
- rule_engine-5.0.0.dist-info/WHEEL +5 -0
- rule_engine-5.0.0.dist-info/licenses/LICENSE +28 -0
- 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
|