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,211 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/binary/arithmetic.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 datetime
|
|
34
|
+
import functools
|
|
35
|
+
import operator
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
from ... import errors
|
|
39
|
+
from ...types import DataType, coerce_value
|
|
40
|
+
from ...types import _DataTypeDef
|
|
41
|
+
|
|
42
|
+
from ..base import _assert_is_bytes, _assert_is_natural_number, _assert_is_numeric, _assert_is_string, _assert_not_nullable, _is_reduced
|
|
43
|
+
from .base import BinaryExpressionBase
|
|
44
|
+
|
|
45
|
+
class AddExpression(BinaryExpressionBase):
|
|
46
|
+
"""A class for representing addition expressions from the grammar text."""
|
|
47
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.BYTES, DataType.FLOAT, DataType.STRING, DataType.DATETIME, DataType.TIMEDELTA)
|
|
48
|
+
result_type: _DataTypeDef = DataType.UNDEFINED
|
|
49
|
+
|
|
50
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
51
|
+
super(AddExpression, self).__init__(*args, **kwargs)
|
|
52
|
+
_assert_not_nullable(self.left.result_type, role="left operand of '+'")
|
|
53
|
+
_assert_not_nullable(self.right.result_type, role="right operand of '+'")
|
|
54
|
+
if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
|
|
55
|
+
if self.left.result_type == DataType.DATETIME:
|
|
56
|
+
if self.right.result_type != DataType.TIMEDELTA:
|
|
57
|
+
raise errors.EvaluationError('data type mismatch')
|
|
58
|
+
self.result_type = self.left.result_type
|
|
59
|
+
elif self.left.result_type == DataType.TIMEDELTA:
|
|
60
|
+
if self.right.result_type not in (DataType.DATETIME, DataType.TIMEDELTA):
|
|
61
|
+
raise errors.EvaluationError('data type mismatch')
|
|
62
|
+
self.result_type = self.right.result_type
|
|
63
|
+
elif self.left.result_type != self.right.result_type:
|
|
64
|
+
raise errors.EvaluationError('data type mismatch')
|
|
65
|
+
else:
|
|
66
|
+
self.result_type = self.left.result_type
|
|
67
|
+
|
|
68
|
+
def _op_add(self, thing: Any) -> Any:
|
|
69
|
+
left_value = self.left.evaluate(thing)
|
|
70
|
+
right_value = self.right.evaluate(thing)
|
|
71
|
+
if isinstance(left_value, datetime.datetime):
|
|
72
|
+
if not isinstance(right_value, datetime.timedelta):
|
|
73
|
+
raise errors.EvaluationError('data type mismatch (not a timedelta value)')
|
|
74
|
+
elif isinstance(left_value, datetime.timedelta):
|
|
75
|
+
if not isinstance(right_value, (datetime.timedelta, datetime.datetime)):
|
|
76
|
+
raise errors.EvaluationError('data type mismatch (not a datetime or timedelta value)')
|
|
77
|
+
elif isinstance(left_value, bytes) or isinstance(right_value, bytes):
|
|
78
|
+
_assert_is_bytes(left_value, right_value)
|
|
79
|
+
elif isinstance(left_value, str) or isinstance(right_value, str):
|
|
80
|
+
_assert_is_string(left_value, right_value)
|
|
81
|
+
else:
|
|
82
|
+
_assert_is_numeric(left_value, right_value)
|
|
83
|
+
return operator.add(left_value, right_value)
|
|
84
|
+
|
|
85
|
+
class SubtractExpression(BinaryExpressionBase):
|
|
86
|
+
"""
|
|
87
|
+
A class for representing subtraction expressions from the grammar text.
|
|
88
|
+
|
|
89
|
+
.. versionadded:: 3.5.0
|
|
90
|
+
"""
|
|
91
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.FLOAT, DataType.DATETIME, DataType.TIMEDELTA)
|
|
92
|
+
result_type: _DataTypeDef = DataType.UNDEFINED
|
|
93
|
+
|
|
94
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
95
|
+
super(SubtractExpression, self).__init__(*args, **kwargs)
|
|
96
|
+
_assert_not_nullable(self.left.result_type, role="left operand of '-'")
|
|
97
|
+
_assert_not_nullable(self.right.result_type, role="right operand of '-'")
|
|
98
|
+
if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
|
|
99
|
+
if self.left.result_type == DataType.DATETIME:
|
|
100
|
+
if self.right.result_type == DataType.DATETIME:
|
|
101
|
+
self.result_type = DataType.TIMEDELTA
|
|
102
|
+
elif self.right.result_type == DataType.TIMEDELTA:
|
|
103
|
+
self.result_type = DataType.DATETIME
|
|
104
|
+
else:
|
|
105
|
+
raise errors.EvaluationError('data type mismatch')
|
|
106
|
+
elif self.left.result_type == DataType.TIMEDELTA:
|
|
107
|
+
if self.right.result_type != DataType.TIMEDELTA:
|
|
108
|
+
raise errors.EvaluationError('data type mismatch')
|
|
109
|
+
self.result_type = self.left.result_type
|
|
110
|
+
elif self.left.result_type != self.right.result_type:
|
|
111
|
+
raise errors.EvaluationError('data type mismatch')
|
|
112
|
+
else:
|
|
113
|
+
self.result_type = self.left.result_type
|
|
114
|
+
|
|
115
|
+
def _op_sub(self, thing: Any) -> Any:
|
|
116
|
+
left_value = self.left.evaluate(thing)
|
|
117
|
+
right_value = self.right.evaluate(thing)
|
|
118
|
+
if isinstance(left_value, datetime.datetime):
|
|
119
|
+
if not isinstance(right_value, (datetime.datetime, datetime.timedelta)):
|
|
120
|
+
raise errors.EvaluationError('data type mismatch (not a datetime or timedelta value)')
|
|
121
|
+
elif isinstance(left_value, datetime.timedelta):
|
|
122
|
+
if not isinstance(right_value, datetime.timedelta):
|
|
123
|
+
raise errors.EvaluationError('data type mismatch (not a timedelta value)')
|
|
124
|
+
else:
|
|
125
|
+
_assert_is_numeric(left_value, right_value)
|
|
126
|
+
return operator.sub(left_value, right_value)
|
|
127
|
+
|
|
128
|
+
class ArithmeticExpression(BinaryExpressionBase):
|
|
129
|
+
"""A class for representing arithmetic expressions from the grammar text such as multiplication and division."""
|
|
130
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.FLOAT,)
|
|
131
|
+
result_type: _DataTypeDef = DataType.FLOAT
|
|
132
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
133
|
+
super(ArithmeticExpression, self).__init__(*args, **kwargs)
|
|
134
|
+
_assert_not_nullable(self.left.result_type, role='left arithmetic operand')
|
|
135
|
+
_assert_not_nullable(self.right.result_type, role='right arithmetic operand')
|
|
136
|
+
|
|
137
|
+
def __op_arithmetic(self, op: Callable[[Any, Any], Any], thing: Any) -> Any:
|
|
138
|
+
left_value = self.left.evaluate(thing)
|
|
139
|
+
_assert_is_numeric(left_value)
|
|
140
|
+
right_value = self.right.evaluate(thing)
|
|
141
|
+
_assert_is_numeric(right_value)
|
|
142
|
+
try:
|
|
143
|
+
result = op(left_value, right_value)
|
|
144
|
+
except ZeroDivisionError:
|
|
145
|
+
raise errors.ArithmeticError('arithmetic error: division by zero') from None
|
|
146
|
+
except ArithmeticError:
|
|
147
|
+
raise errors.ArithmeticError('arithmetic error') from None
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
_op_fdiv = functools.partialmethod(__op_arithmetic, operator.floordiv)
|
|
151
|
+
_op_tdiv = functools.partialmethod(__op_arithmetic, operator.truediv)
|
|
152
|
+
_op_mod = functools.partialmethod(__op_arithmetic, operator.mod)
|
|
153
|
+
_op_mul = functools.partialmethod(__op_arithmetic, operator.mul)
|
|
154
|
+
_op_pow = functools.partialmethod(__op_arithmetic, operator.pow)
|
|
155
|
+
|
|
156
|
+
class BitwiseExpression(BinaryExpressionBase):
|
|
157
|
+
"""
|
|
158
|
+
A class for representing bitwise arithmetic expressions from the grammar text such as XOR and shifting operations.
|
|
159
|
+
"""
|
|
160
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.FLOAT, DataType.SET)
|
|
161
|
+
result_type: _DataTypeDef = DataType.UNDEFINED
|
|
162
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
163
|
+
super(BitwiseExpression, self).__init__(*args, **kwargs)
|
|
164
|
+
_assert_not_nullable(self.left.result_type, role='left bitwise operand')
|
|
165
|
+
_assert_not_nullable(self.right.result_type, role='right bitwise operand')
|
|
166
|
+
# don't use DataType.is_compatible, because for sets the member type isn't important
|
|
167
|
+
if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
|
|
168
|
+
if self.left.result_type.__class__ != self.right.result_type.__class__:
|
|
169
|
+
raise errors.EvaluationError('data type mismatch')
|
|
170
|
+
if self.left.result_type == DataType.FLOAT:
|
|
171
|
+
if _is_reduced(self.left):
|
|
172
|
+
_assert_is_natural_number(self.left.evaluate(None))
|
|
173
|
+
self.result_type = DataType.FLOAT
|
|
174
|
+
if self.right.result_type == DataType.FLOAT:
|
|
175
|
+
if _is_reduced(self.right):
|
|
176
|
+
_assert_is_natural_number(self.right.evaluate(None))
|
|
177
|
+
self.result_type = DataType.FLOAT
|
|
178
|
+
if DataType.is_type(self.left.result_type, DataType.SET) or DataType.is_type(self.right.result_type, DataType.SET):
|
|
179
|
+
self.result_type = DataType.SET # this discards the member type info
|
|
180
|
+
|
|
181
|
+
def _op_bitwise(self, op: Callable[[Any, Any], Any], thing: Any) -> Any:
|
|
182
|
+
left = self.left.evaluate(thing)
|
|
183
|
+
if DataType.from_value(left) == DataType.FLOAT:
|
|
184
|
+
return self._op_bitwise_float(op, thing, left)
|
|
185
|
+
elif DataType.is_type(DataType.from_value(left), DataType.SET):
|
|
186
|
+
return self._op_bitwise_set(op, thing, left)
|
|
187
|
+
raise errors.EvaluationError('data type mismatch')
|
|
188
|
+
|
|
189
|
+
def _op_bitwise_float(self, op: Callable[[Any, Any], Any], thing: Any, left: Any) -> Any:
|
|
190
|
+
_assert_is_natural_number(left)
|
|
191
|
+
right = self.right.evaluate(thing)
|
|
192
|
+
_assert_is_natural_number(right)
|
|
193
|
+
return coerce_value(op(int(left), int(right)))
|
|
194
|
+
|
|
195
|
+
def _op_bitwise_set(self, op: Callable[[Any, Any], Any], thing: Any, left: Any) -> Any:
|
|
196
|
+
right = self.right.evaluate(thing)
|
|
197
|
+
if not DataType.is_compatible(DataType.from_value(right), DataType.SET):
|
|
198
|
+
raise errors.EvaluationError('data type mismatch')
|
|
199
|
+
return op(left, right)
|
|
200
|
+
|
|
201
|
+
_op_bwand = functools.partialmethod(_op_bitwise, operator.and_)
|
|
202
|
+
_op_bwor = functools.partialmethod(_op_bitwise, operator.or_)
|
|
203
|
+
_op_bwxor = functools.partialmethod(_op_bitwise, operator.xor)
|
|
204
|
+
|
|
205
|
+
class BitwiseShiftExpression(BitwiseExpression):
|
|
206
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.FLOAT,)
|
|
207
|
+
result_type: _DataTypeDef = DataType.FLOAT
|
|
208
|
+
def _op_bitwise_shift(self, *args: Any, **kwargs: Any) -> Any:
|
|
209
|
+
return self._op_bitwise(*args, **kwargs)
|
|
210
|
+
_op_bwlsh = functools.partialmethod(_op_bitwise_shift, operator.lshift)
|
|
211
|
+
_op_bwrsh = functools.partialmethod(_op_bitwise_shift, operator.rshift)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/binary/base.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, Callable
|
|
34
|
+
|
|
35
|
+
from ... import errors
|
|
36
|
+
from ...types import DataType
|
|
37
|
+
from ...types import _DataTypeDef
|
|
38
|
+
|
|
39
|
+
from ..base import ExpressionBase, LiteralExpressionBase, _is_reduced
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from ...engine.context import Context
|
|
43
|
+
|
|
44
|
+
class BinaryExpressionBase(ExpressionBase):
|
|
45
|
+
"""
|
|
46
|
+
A base class for representing complex expressions composed of a left side and a right side, separated by an
|
|
47
|
+
operator.
|
|
48
|
+
"""
|
|
49
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.TIMEDELTA, DataType.FLOAT, DataType.MAPPING, DataType.NULL, DataType.SET, DataType.STRING)
|
|
50
|
+
"""
|
|
51
|
+
A tuple containing the compatible data types that the left and right expressions must return. This can for example
|
|
52
|
+
be used to indicate that arithmetic operations are compatible with :py:attr:`~.DataType.FLOAT` but not
|
|
53
|
+
:py:attr:`~.DataType.STRING` values.
|
|
54
|
+
"""
|
|
55
|
+
result_type: _DataTypeDef = DataType.BOOLEAN
|
|
56
|
+
def __init__(self, context: 'Context', type_: str, left: ExpressionBase, right: ExpressionBase) -> None:
|
|
57
|
+
"""
|
|
58
|
+
:param context: The context to use for evaluating the expression.
|
|
59
|
+
:type context: :py:class:`~rule_engine.engine.Context`
|
|
60
|
+
:param str type_: The grammar type of operator at the center of the expression. Subclasses must define operator
|
|
61
|
+
methods to handle evaluation based on this value.
|
|
62
|
+
:param left: The expression to the left of the operator.
|
|
63
|
+
:type left: :py:class:`.ExpressionBase`
|
|
64
|
+
:param right: The expression to the right of the operator.
|
|
65
|
+
:type right: :py:class:`.ExpressionBase`
|
|
66
|
+
"""
|
|
67
|
+
self.context = context
|
|
68
|
+
type_ = type_.lower()
|
|
69
|
+
self.type = type_
|
|
70
|
+
evaluator = getattr(self, '_op_' + type_, None)
|
|
71
|
+
if evaluator is None:
|
|
72
|
+
raise errors.EngineError('unsupported operator: ' + type_)
|
|
73
|
+
self._evaluator: Callable[[Any], Any] = evaluator
|
|
74
|
+
self._assert_type_is_compatible(left)
|
|
75
|
+
self.left = left
|
|
76
|
+
self._assert_type_is_compatible(right)
|
|
77
|
+
self.right = right
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def build(cls, context: 'Context', type_: str, left: ExpressionBase, right: ExpressionBase) -> ExpressionBase: # type: ignore[override]
|
|
81
|
+
left_built = left.build()
|
|
82
|
+
assert isinstance(left_built, ExpressionBase)
|
|
83
|
+
right_built = right.build()
|
|
84
|
+
assert isinstance(right_built, ExpressionBase)
|
|
85
|
+
reduced = cls(context, type_, left_built, right_built).reduce()
|
|
86
|
+
assert isinstance(reduced, ExpressionBase)
|
|
87
|
+
return reduced
|
|
88
|
+
|
|
89
|
+
def _assert_type_is_compatible(self, value: ExpressionBase) -> None:
|
|
90
|
+
if value.result_type == DataType.UNDEFINED:
|
|
91
|
+
return
|
|
92
|
+
if any(DataType.is_compatible(dt, value.result_type) for dt in self.compatible_types):
|
|
93
|
+
return
|
|
94
|
+
raise errors.EvaluationError('data type mismatch')
|
|
95
|
+
|
|
96
|
+
def __repr__(self) -> str:
|
|
97
|
+
return "<{} type={!r} >".format(self.__class__.__name__, self.type)
|
|
98
|
+
|
|
99
|
+
def evaluate(self, thing: Any) -> Any:
|
|
100
|
+
return self._evaluator(thing)
|
|
101
|
+
|
|
102
|
+
def reduce(self) -> ExpressionBase:
|
|
103
|
+
if not _is_reduced(self.left, self.right):
|
|
104
|
+
return self
|
|
105
|
+
return LiteralExpressionBase.from_value(self.context, self.evaluate(None))
|
|
106
|
+
|
|
107
|
+
def to_graphviz(self, digraph: Any, *args: Any, **kwargs: Any) -> None:
|
|
108
|
+
digraph.node(str(id(self)), "{}\ntype={!r}".format(self.__class__.__name__, self.type))
|
|
109
|
+
self.left.to_graphviz(digraph, *args, **kwargs)
|
|
110
|
+
self.right.to_graphviz(digraph, *args, **kwargs)
|
|
111
|
+
digraph.edge(str(id(self)), str(id(self.left)), label='left')
|
|
112
|
+
digraph.edge(str(id(self)), str(id(self.right)), label='right')
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/binary/comparison.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 functools
|
|
34
|
+
import operator
|
|
35
|
+
import re
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
from ... import errors
|
|
39
|
+
from ...types import DataType, coerce_value
|
|
40
|
+
from ...types import _DataTypeDef
|
|
41
|
+
|
|
42
|
+
from ..base import _assert_not_nullable
|
|
43
|
+
from ..literal import StringExpression
|
|
44
|
+
from .base import BinaryExpressionBase
|
|
45
|
+
|
|
46
|
+
class LogicExpression(BinaryExpressionBase):
|
|
47
|
+
"""A class for representing logical expressions from the grammar text such as "and" and "or"."""
|
|
48
|
+
def _op_and(self, thing: Any) -> bool:
|
|
49
|
+
return bool(self.left.evaluate(thing) and self.right.evaluate(thing))
|
|
50
|
+
|
|
51
|
+
def _op_or(self, thing: Any) -> bool:
|
|
52
|
+
return bool(self.left.evaluate(thing) or self.right.evaluate(thing))
|
|
53
|
+
|
|
54
|
+
class ComparisonExpression(BinaryExpressionBase):
|
|
55
|
+
"""A class for representing comparison expressions from the grammar text such as equality checks."""
|
|
56
|
+
compatible_types: tuple[_DataTypeDef, ...] = BinaryExpressionBase.compatible_types + (DataType.OBJECT,)
|
|
57
|
+
def _op_eq(self, thing: Any) -> bool:
|
|
58
|
+
left_value = self.left.evaluate(thing)
|
|
59
|
+
right_value = self.right.evaluate(thing)
|
|
60
|
+
if type(left_value) is not type(right_value):
|
|
61
|
+
return False
|
|
62
|
+
return operator.eq(left_value, right_value)
|
|
63
|
+
|
|
64
|
+
def _op_ne(self, thing: Any) -> bool:
|
|
65
|
+
left_value = self.left.evaluate(thing)
|
|
66
|
+
right_value = self.right.evaluate(thing)
|
|
67
|
+
if type(left_value) is not type(right_value):
|
|
68
|
+
return True
|
|
69
|
+
return operator.ne(left_value, right_value)
|
|
70
|
+
|
|
71
|
+
class ArithmeticComparisonExpression(ComparisonExpression):
|
|
72
|
+
"""
|
|
73
|
+
A class for representing arithmetic comparison expressions from the grammar text such as less-than-or-equal-to and
|
|
74
|
+
greater-than.
|
|
75
|
+
"""
|
|
76
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.TIMEDELTA, DataType.FLOAT, DataType.NULL, DataType.STRING)
|
|
77
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
78
|
+
super(ArithmeticComparisonExpression, self).__init__(*args, **kwargs)
|
|
79
|
+
_assert_not_nullable(self.left.result_type, role='left ordered-comparison operand')
|
|
80
|
+
_assert_not_nullable(self.right.result_type, role='right ordered-comparison operand')
|
|
81
|
+
if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
|
|
82
|
+
if self.left.result_type != self.right.result_type:
|
|
83
|
+
raise errors.EvaluationError('data type mismatch')
|
|
84
|
+
|
|
85
|
+
def __op_arithmetic(self, op: Callable[[Any, Any], Any], thing: Any) -> Any:
|
|
86
|
+
left_value = self.left.evaluate(thing)
|
|
87
|
+
right_value = self.right.evaluate(thing)
|
|
88
|
+
return self.__op_arithmetic_values(op, left_value, right_value)
|
|
89
|
+
|
|
90
|
+
def __op_arithmetic_arrays(self, op: Callable[[Any, Any], Any], left_value: Any, right_value: Any) -> Any:
|
|
91
|
+
for subleft_value, subright_value in zip(left_value, right_value):
|
|
92
|
+
if self.__op_arithmetic_values(operator.ne, subleft_value, subright_value):
|
|
93
|
+
return self.__op_arithmetic_values(op, subleft_value, subright_value)
|
|
94
|
+
if len(left_value) != len(right_value):
|
|
95
|
+
return self.__op_arithmetic_values(op, len(left_value), len(right_value))
|
|
96
|
+
return op in (operator.ge, operator.le)
|
|
97
|
+
|
|
98
|
+
def __op_arithmetic_values(self, op: Callable[[Any, Any], Any], left_value: Any, right_value: Any) -> Any:
|
|
99
|
+
if left_value is None and right_value is None:
|
|
100
|
+
return op in (operator.ge, operator.le)
|
|
101
|
+
elif isinstance(left_value, tuple) and isinstance(right_value, tuple):
|
|
102
|
+
return self.__op_arithmetic_arrays(op, left_value, right_value)
|
|
103
|
+
elif type(left_value) is not type(right_value):
|
|
104
|
+
raise errors.EvaluationError('data type mismatch')
|
|
105
|
+
return op(left_value, right_value)
|
|
106
|
+
|
|
107
|
+
_op_ge = functools.partialmethod(__op_arithmetic, operator.ge)
|
|
108
|
+
_op_gt = functools.partialmethod(__op_arithmetic, operator.gt)
|
|
109
|
+
_op_le = functools.partialmethod(__op_arithmetic, operator.le)
|
|
110
|
+
_op_lt = functools.partialmethod(__op_arithmetic, operator.lt)
|
|
111
|
+
|
|
112
|
+
class FuzzyComparisonExpression(ComparisonExpression):
|
|
113
|
+
"""
|
|
114
|
+
A class for representing regular expression comparison expressions from the grammar text such as search and does not
|
|
115
|
+
match.
|
|
116
|
+
"""
|
|
117
|
+
compatible_types: tuple[_DataTypeDef, ...] = (DataType.NULL, DataType.STRING)
|
|
118
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
119
|
+
super(FuzzyComparisonExpression, self).__init__(*args, **kwargs)
|
|
120
|
+
_assert_not_nullable(self.left.result_type, role='left regex operand')
|
|
121
|
+
_assert_not_nullable(self.right.result_type, role='right regex operand')
|
|
122
|
+
if isinstance(self.right, StringExpression):
|
|
123
|
+
self._right = self._compile_regex(self.right.evaluate(None))
|
|
124
|
+
|
|
125
|
+
def _compile_regex(self, regex: str) -> re.Pattern[str]:
|
|
126
|
+
try:
|
|
127
|
+
result = re.compile(regex, flags=self.context.regex_flags)
|
|
128
|
+
except re.error as error:
|
|
129
|
+
raise errors.RegexSyntaxError('invalid regular expression', error=error, value=regex) from None
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def __op_regex(self, regex_function: str, modifier: Callable[[Any, Any], Any], thing: Any) -> Any:
|
|
133
|
+
left = self.left.evaluate(thing)
|
|
134
|
+
if not isinstance(left, str) and left is not None:
|
|
135
|
+
raise errors.EvaluationError('data type mismatch')
|
|
136
|
+
if isinstance(self.right, StringExpression):
|
|
137
|
+
regex = self._right
|
|
138
|
+
else:
|
|
139
|
+
regex = self.right.evaluate(thing)
|
|
140
|
+
if isinstance(regex, str):
|
|
141
|
+
regex = self._compile_regex(regex)
|
|
142
|
+
elif regex is not None:
|
|
143
|
+
raise errors.EvaluationError('data type mismatch')
|
|
144
|
+
if left is None or regex is None:
|
|
145
|
+
return not modifier(left, regex)
|
|
146
|
+
match = getattr(regex, regex_function)(left)
|
|
147
|
+
if match is not None:
|
|
148
|
+
self.context._tls.regex_groups = coerce_value(match.groups())
|
|
149
|
+
return modifier(match, None)
|
|
150
|
+
|
|
151
|
+
_op_eq_fzm = functools.partialmethod(__op_regex, 'match', operator.is_not)
|
|
152
|
+
_op_eq_fzs = functools.partialmethod(__op_regex, 'search', operator.is_not)
|
|
153
|
+
_op_ne_fzm = functools.partialmethod(__op_regex, 'match', operator.is_)
|
|
154
|
+
_op_ne_fzs = functools.partialmethod(__op_regex, 'search', operator.is_)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/ast/expression/__init__.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 .access import ContainsExpression, GetAttributeExpression, GetItemExpression, GetSliceExpression
|
|
34
|
+
from .control import ComprehensionExpression, TernaryExpression, UnaryExpression
|
|
35
|
+
from .resolution import FunctionCallExpression, SymbolExpression
|