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
rule_engine/builtins.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/builtins.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 collections.abc
|
|
35
|
+
import datetime
|
|
36
|
+
import decimal
|
|
37
|
+
import functools
|
|
38
|
+
import math
|
|
39
|
+
import random
|
|
40
|
+
from typing import Any, Callable, Iterable, Iterator, Mapping
|
|
41
|
+
|
|
42
|
+
from . import errors
|
|
43
|
+
from . import types
|
|
44
|
+
from .parser.utilities import parse_datetime, parse_float, parse_timedelta
|
|
45
|
+
|
|
46
|
+
import dateutil.tz
|
|
47
|
+
|
|
48
|
+
def _builtin_filter(function: Callable[[Any], Any], iterable: Iterable[Any]) -> tuple[Any, ...]:
|
|
49
|
+
return tuple(filter(function, iterable))
|
|
50
|
+
|
|
51
|
+
def _builtin_map(function: Callable[[Any], Any], iterable: Iterable[Any]) -> tuple[Any, ...]:
|
|
52
|
+
return tuple(map(function, iterable))
|
|
53
|
+
|
|
54
|
+
def _builtin_parse_datetime(builtins: 'Builtins', string: str) -> datetime.datetime:
|
|
55
|
+
return parse_datetime(string, builtins.timezone)
|
|
56
|
+
|
|
57
|
+
def _builtin_random(boundary: Any = None) -> Any:
|
|
58
|
+
if boundary:
|
|
59
|
+
if not types.is_natural_number(boundary):
|
|
60
|
+
raise errors.FunctionCallError('argument #1 (boundary) must be a natural number')
|
|
61
|
+
return random.randint(0, int(boundary))
|
|
62
|
+
return random.random()
|
|
63
|
+
|
|
64
|
+
def _builtin_now(builtins: 'Builtins') -> datetime.datetime:
|
|
65
|
+
return datetime.datetime.now(tz=builtins.timezone)
|
|
66
|
+
|
|
67
|
+
def _builtin_today(builtins: 'Builtins') -> datetime.datetime:
|
|
68
|
+
return _builtin_now(builtins).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
69
|
+
|
|
70
|
+
def _builtin_parse_datetime_generator(builtins: 'Builtins') -> 'functools.partial[datetime.datetime]':
|
|
71
|
+
return functools.partial(_builtin_parse_datetime, builtins)
|
|
72
|
+
|
|
73
|
+
def _builtin_range(start: Any, stop: Any = None, step: Any = None) -> list[int]:
|
|
74
|
+
if not types.is_integer_number(start):
|
|
75
|
+
raise errors.FunctionCallError('argument #1 (start) must be an integer number')
|
|
76
|
+
if stop:
|
|
77
|
+
if not types.is_integer_number(stop):
|
|
78
|
+
raise errors.FunctionCallError('argument #2 (stop) must be an integer number')
|
|
79
|
+
if step:
|
|
80
|
+
if not types.is_integer_number(step):
|
|
81
|
+
raise errors.FunctionCallError('argument #3 (step) must be an integer number')
|
|
82
|
+
return list(range(int(start), int(stop), int(step)))
|
|
83
|
+
return list(range(int(start), int(stop)))
|
|
84
|
+
return list(range(int(start)))
|
|
85
|
+
|
|
86
|
+
def _builtins_split(string: str, sep: str | None = None, maxsplit: Any = None) -> tuple[str, ...]:
|
|
87
|
+
if maxsplit is None:
|
|
88
|
+
maxsplit = -1
|
|
89
|
+
elif types.is_natural_number(maxsplit):
|
|
90
|
+
maxsplit = int(maxsplit)
|
|
91
|
+
else:
|
|
92
|
+
raise errors.FunctionCallError('argument #3 (maxsplit) must be a natural number')
|
|
93
|
+
return tuple(string.split(sep=sep, maxsplit=maxsplit))
|
|
94
|
+
|
|
95
|
+
class BuiltinValueGenerator(object):
|
|
96
|
+
"""
|
|
97
|
+
A class used as a wrapper for builtin values to differentiate between a value that is a function and a value that
|
|
98
|
+
should be generated by calling a function. A value that is generated by calling a function is useful for determining
|
|
99
|
+
the value during evaluation for things like the current time.
|
|
100
|
+
|
|
101
|
+
.. versionadded:: 4.0.0
|
|
102
|
+
"""
|
|
103
|
+
__slots__ = ('callable',)
|
|
104
|
+
callable: Callable[['Builtins'], Any]
|
|
105
|
+
def __init__(self, callable: Callable[['Builtins'], Any]) -> None:
|
|
106
|
+
self.callable = callable
|
|
107
|
+
|
|
108
|
+
def __call__(self, builtins: 'Builtins') -> Any:
|
|
109
|
+
return self.callable(builtins)
|
|
110
|
+
|
|
111
|
+
class Builtins(collections.abc.Mapping):
|
|
112
|
+
"""
|
|
113
|
+
A class to define and provide variables to within the builtin context of rules. These can be accessed by specifying
|
|
114
|
+
a symbol name with the ``$`` prefix.
|
|
115
|
+
"""
|
|
116
|
+
scope_name = 'built-in'
|
|
117
|
+
"""The identity name of the scope for builtin symbols."""
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
values: Mapping[str, Any],
|
|
121
|
+
namespace: str | None = None,
|
|
122
|
+
timezone: datetime.tzinfo | None = None,
|
|
123
|
+
value_types: Mapping[str, 'types._DataTypeDef'] | None = None
|
|
124
|
+
) -> None:
|
|
125
|
+
"""
|
|
126
|
+
:param dict values: A mapping of string keys to be used as symbol names with values of either Python literals or
|
|
127
|
+
a function which will be called when the symbol is accessed. When using a function, it will be passed a
|
|
128
|
+
single argument, which is the instance of :py:class:`Builtins`.
|
|
129
|
+
:param str namespace: The namespace of the variables to resolve.
|
|
130
|
+
:param timezone: A timezone to use when resolving timestamps.
|
|
131
|
+
:type timezone: :py:class:`~datetime.tzinfo`
|
|
132
|
+
:param dict value_types: A mapping of the values to their datatypes.
|
|
133
|
+
|
|
134
|
+
.. versionchanged:: 2.3.0
|
|
135
|
+
Added the *value_types* parameter.
|
|
136
|
+
"""
|
|
137
|
+
self.__values = values
|
|
138
|
+
self.__value_types = value_types or {}
|
|
139
|
+
self.namespace = namespace
|
|
140
|
+
self.timezone = timezone or dateutil.tz.tzlocal()
|
|
141
|
+
|
|
142
|
+
def resolve_type(self, name: str) -> 'types._DataTypeDef':
|
|
143
|
+
"""
|
|
144
|
+
The method to use for resolving the data type of a builtin symbol.
|
|
145
|
+
|
|
146
|
+
:param str name: The name of the symbol to retrieve the data type of.
|
|
147
|
+
:return: The data type of the symbol or :py:attr:`~rule_engine.types.DataType.UNDEFINED`.
|
|
148
|
+
"""
|
|
149
|
+
return self.__value_types.get(name, types.DataType.UNDEFINED)
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
return "<{} namespace={!r} keys={!r} timezone={!r} >".format(self.__class__.__name__, self.namespace, tuple(self.keys()), self.timezone)
|
|
153
|
+
|
|
154
|
+
def __getitem__(self, name: str) -> Any:
|
|
155
|
+
value = self.__values[name]
|
|
156
|
+
if isinstance(value, collections.abc.Mapping):
|
|
157
|
+
if self.namespace is None:
|
|
158
|
+
namespace = name
|
|
159
|
+
else:
|
|
160
|
+
namespace = self.namespace + '.' + name
|
|
161
|
+
return self.__class__(value, namespace=namespace, timezone=self.timezone)
|
|
162
|
+
elif callable(value) and isinstance(value, BuiltinValueGenerator):
|
|
163
|
+
value = value(self)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def __iter__(self) -> Iterator[str]:
|
|
167
|
+
return iter(self.__values)
|
|
168
|
+
|
|
169
|
+
def __len__(self) -> int:
|
|
170
|
+
return len(self.__values)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_defaults(cls, values: Mapping[str, Any] | None = None, **kwargs: Any) -> 'Builtins':
|
|
174
|
+
"""Initialize a :py:class:`Builtins` instance with a set of default values."""
|
|
175
|
+
now = BuiltinValueGenerator(_builtin_now)
|
|
176
|
+
# there may be errors here if the decimal.Context precision exceeds what is provided by the math constants
|
|
177
|
+
default_values = {
|
|
178
|
+
# mathematical constants
|
|
179
|
+
'e': decimal.Decimal(repr(math.e)),
|
|
180
|
+
'pi': decimal.Decimal(repr(math.pi)),
|
|
181
|
+
# timestamps
|
|
182
|
+
'now': now,
|
|
183
|
+
'today': BuiltinValueGenerator(_builtin_today),
|
|
184
|
+
# functions
|
|
185
|
+
'abs': abs,
|
|
186
|
+
'any': any,
|
|
187
|
+
'all': all,
|
|
188
|
+
'sum': sum,
|
|
189
|
+
'map': _builtin_map,
|
|
190
|
+
'max': max,
|
|
191
|
+
'min': min,
|
|
192
|
+
'filter': _builtin_filter,
|
|
193
|
+
'parse_datetime': BuiltinValueGenerator(_builtin_parse_datetime_generator),
|
|
194
|
+
'parse_float': parse_float,
|
|
195
|
+
'parse_timedelta': parse_timedelta,
|
|
196
|
+
'random': _builtin_random,
|
|
197
|
+
'range': _builtin_range,
|
|
198
|
+
'split': _builtins_split
|
|
199
|
+
}
|
|
200
|
+
default_values.update(values or {})
|
|
201
|
+
default_value_types = {
|
|
202
|
+
# mathematical constants
|
|
203
|
+
'e': types.DataType.FLOAT,
|
|
204
|
+
'pi': types.DataType.FLOAT,
|
|
205
|
+
# timestamps
|
|
206
|
+
'now': types.DataType.DATETIME,
|
|
207
|
+
'today': types.DataType.DATETIME,
|
|
208
|
+
# functions
|
|
209
|
+
'abs': types.DataType.FUNCTION('abs', return_type=types.DataType.FLOAT, argument_types=(types.DataType.FLOAT,)),
|
|
210
|
+
'all': types.DataType.FUNCTION('all', return_type=types.DataType.BOOLEAN, argument_types=(types.DataType.ARRAY,)),
|
|
211
|
+
'any': types.DataType.FUNCTION('any', return_type=types.DataType.BOOLEAN, argument_types=(types.DataType.ARRAY,)),
|
|
212
|
+
'sum': types.DataType.FUNCTION('sum', return_type=types.DataType.FLOAT, argument_types=(types.DataType.ARRAY(types.DataType.FLOAT),)),
|
|
213
|
+
'map': types.DataType.FUNCTION('map', return_type=types.DataType.ARRAY, argument_types=(types.DataType.FUNCTION, types.DataType.ARRAY)),
|
|
214
|
+
'max': types.DataType.FUNCTION('max', return_type=types.DataType.FLOAT, argument_types=(types.DataType.ARRAY(types.DataType.FLOAT),)),
|
|
215
|
+
'min': types.DataType.FUNCTION('min', return_type=types.DataType.FLOAT, argument_types=(types.DataType.ARRAY(types.DataType.FLOAT),)),
|
|
216
|
+
'filter': types.DataType.FUNCTION('filter', return_type=types.DataType.ARRAY, argument_types=(types.DataType.FUNCTION, types.DataType.ARRAY)),
|
|
217
|
+
'parse_datetime': types.DataType.FUNCTION('parse_datetime', return_type=types.DataType.DATETIME, argument_types=(types.DataType.STRING,)),
|
|
218
|
+
'parse_float': types.DataType.FUNCTION('parse_float', return_type=types.DataType.FLOAT, argument_types=(types.DataType.STRING,)),
|
|
219
|
+
'parse_timedelta': types.DataType.FUNCTION('parse_timedelta', return_type=types.DataType.TIMEDELTA, argument_types=(types.DataType.STRING,)),
|
|
220
|
+
'random': types.DataType.FUNCTION('random', return_type=types.DataType.FLOAT, argument_types=(types.DataType.FLOAT,), minimum_arguments=0),
|
|
221
|
+
'range': types.DataType.FUNCTION('range', return_type=types.DataType.ARRAY(types.DataType.FLOAT), argument_types=(types.DataType.FLOAT, types.DataType.FLOAT, types.DataType.FLOAT,), minimum_arguments=1),
|
|
222
|
+
'split': types.DataType.FUNCTION(
|
|
223
|
+
'split',
|
|
224
|
+
return_type=types.DataType.ARRAY(types.DataType.STRING),
|
|
225
|
+
argument_types=(types.DataType.STRING, types.DataType.STRING, types.DataType.FLOAT),
|
|
226
|
+
minimum_arguments=1
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
default_value_types.update(kwargs.pop('value_types', {}))
|
|
230
|
+
return cls(default_values, value_types=default_value_types, **kwargs)
|
rule_engine/debug_ast.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/debug_ast.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 argparse
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
from . import __version__
|
|
37
|
+
from . import engine
|
|
38
|
+
|
|
39
|
+
def _print_written(file_path: str) -> None:
|
|
40
|
+
size = os.stat(file_path).st_size
|
|
41
|
+
print("wrote {:,} bytes to {}".format(size, file_path))
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
parser = argparse.ArgumentParser(description='Rule Engine: Debug AST', conflict_handler='resolve')
|
|
45
|
+
parser.add_argument('output', help='output files')
|
|
46
|
+
parser.add_argument('-t', '--text', dest='rule_text', help='the rule text to debug')
|
|
47
|
+
parser.add_argument('-v', '--version', action='version', version=parser.prog + ' Version: ' + __version__)
|
|
48
|
+
arguments = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
rule_text = arguments.rule_text
|
|
51
|
+
if not rule_text:
|
|
52
|
+
rule_text = input('rule > ')
|
|
53
|
+
|
|
54
|
+
rule = engine.Rule(rule_text)
|
|
55
|
+
digraph = rule.to_graphviz()
|
|
56
|
+
|
|
57
|
+
digraph.save(arguments.output + '.gv')
|
|
58
|
+
_print_written(arguments.output + '.gv')
|
|
59
|
+
|
|
60
|
+
digraph.render(arguments.output)
|
|
61
|
+
_print_written(arguments.output + '.pdf')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == '__main__':
|
|
65
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/debug_repl.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 argparse
|
|
34
|
+
import code
|
|
35
|
+
import os
|
|
36
|
+
import pprint
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
import textwrap
|
|
40
|
+
import traceback
|
|
41
|
+
|
|
42
|
+
from . import __version__
|
|
43
|
+
from . import engine
|
|
44
|
+
from . import errors
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from IPython import embed
|
|
48
|
+
from IPython.core import ultratb
|
|
49
|
+
from prompt_toolkit import PromptSession
|
|
50
|
+
except ImportError:
|
|
51
|
+
has_development_dependencies = False
|
|
52
|
+
else:
|
|
53
|
+
has_development_dependencies = True
|
|
54
|
+
|
|
55
|
+
from typing import Any
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
parser = argparse.ArgumentParser(description='Rule Engine: Debug REPL', conflict_handler='resolve')
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
'--debug',
|
|
61
|
+
action='store_true',
|
|
62
|
+
default=False,
|
|
63
|
+
help='enable debugging output'
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
'--edit-console',
|
|
67
|
+
action='store_true',
|
|
68
|
+
default=False,
|
|
69
|
+
help='edit the environment (via an interactive console)'
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
'--edit-file',
|
|
73
|
+
metavar='<path>',
|
|
74
|
+
type=argparse.FileType('r'),
|
|
75
|
+
help='edit the environment (via a file)'
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument('-v', '--version', action='version', version=parser.prog + ' Version: ' + __version__)
|
|
78
|
+
arguments = parser.parse_args()
|
|
79
|
+
|
|
80
|
+
if not has_development_dependencies:
|
|
81
|
+
parser.error('development dependencies are not installed, install them with: uv sync --group dev')
|
|
82
|
+
|
|
83
|
+
if os.environ.get('NO_COLOR'):
|
|
84
|
+
color_scheme = 'nocolor'
|
|
85
|
+
else:
|
|
86
|
+
color_scheme = 'neutral'
|
|
87
|
+
|
|
88
|
+
context: engine.Context = engine.Context()
|
|
89
|
+
thing: Any = None
|
|
90
|
+
if arguments.edit_console or arguments.edit_file:
|
|
91
|
+
console = code.InteractiveConsole({
|
|
92
|
+
'context': context,
|
|
93
|
+
'thing': thing
|
|
94
|
+
})
|
|
95
|
+
if arguments.edit_file:
|
|
96
|
+
print('executing: ' + arguments.edit_file.name)
|
|
97
|
+
compiled = code.compile_command(
|
|
98
|
+
arguments.edit_file.read(),
|
|
99
|
+
filename=arguments.edit_file.name,
|
|
100
|
+
symbol='exec'
|
|
101
|
+
)
|
|
102
|
+
assert compiled is not None
|
|
103
|
+
console.runcode(compiled)
|
|
104
|
+
context = console.locals['context']
|
|
105
|
+
thing = console.locals['thing']
|
|
106
|
+
if arguments.edit_console:
|
|
107
|
+
namespace: dict[str, Any] = {'context': context, 'thing': thing}
|
|
108
|
+
print("Starting IPython shell...")
|
|
109
|
+
print("Edit the \'context\' and \'thing\' objects as necessary")
|
|
110
|
+
embed(colors=color_scheme, user_ns=namespace)
|
|
111
|
+
context = namespace.get('context', context)
|
|
112
|
+
thing = namespace.get('thing', thing)
|
|
113
|
+
debugging = arguments.debug
|
|
114
|
+
session: PromptSession[str] = PromptSession()
|
|
115
|
+
color_tb = ultratb.FormattedTB(theme_name=color_scheme)
|
|
116
|
+
if color_scheme == 'nocolor':
|
|
117
|
+
exc_name_color = ''
|
|
118
|
+
normal_color = ''
|
|
119
|
+
else:
|
|
120
|
+
exc_name_color = '\033[31m'
|
|
121
|
+
normal_color = '\033[0m'
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
try:
|
|
125
|
+
rule_text = session.prompt('rule > ')
|
|
126
|
+
except (EOFError, KeyboardInterrupt):
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
match = re.match(r'\s*#!\s*debug\s*=\s*(\w+)', rule_text)
|
|
130
|
+
if match:
|
|
131
|
+
debugging = match.group(1).lower() != 'false'
|
|
132
|
+
print('# debugging = ' + str(debugging).lower())
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
rule = engine.Rule(rule_text, context=context)
|
|
137
|
+
result = rule.evaluate(thing)
|
|
138
|
+
except errors.EngineError as error:
|
|
139
|
+
print(f"{exc_name_color}{error.__class__.__name__}:{normal_color} {error.message}", file=sys.stderr)
|
|
140
|
+
if isinstance(error, (errors.AttributeResolutionError, errors.SymbolResolutionError)) and error.suggestion:
|
|
141
|
+
print(f"Did you mean '{error.suggestion}'?", file=sys.stderr)
|
|
142
|
+
elif isinstance(error, errors.RegexSyntaxError):
|
|
143
|
+
print(f" Regex: {error.error.pattern!r}", file=sys.stderr)
|
|
144
|
+
print(f" Details: {error.error.msg} at position {error.error.pos}", file=sys.stderr)
|
|
145
|
+
elif isinstance(error, errors.FunctionCallError):
|
|
146
|
+
print(f" Function: {error.function_name!r}", file=sys.stderr)
|
|
147
|
+
if debugging and error.error:
|
|
148
|
+
inner_exception = color_tb.text(error.error.__class__, error.error, error.error.__traceback__)
|
|
149
|
+
print(textwrap.indent(inner_exception, ' ' * 2), file=sys.stderr)
|
|
150
|
+
if debugging:
|
|
151
|
+
print(color_tb.text(error.__class__, error, error.__traceback__), file=sys.stderr)
|
|
152
|
+
except Exception as error:
|
|
153
|
+
print(color_tb.text(error.__class__, error, error.__traceback__), file=sys.stderr)
|
|
154
|
+
else:
|
|
155
|
+
print('result: ')
|
|
156
|
+
pprint.pprint(result, indent=4)
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# rule_engine/engine/__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 .context import resolve_attribute, resolve_item, type_resolver_from_dataclass, type_resolver_from_dict, type_resolver_from_sqlalchemy, Context
|
|
34
|
+
from .rule import Rule, DebugRule
|