rule-engine 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. rule_engine/__init__.py +65 -0
  2. rule_engine/_vendor/__init__.py +4 -0
  3. rule_engine/_vendor/ply/LICENSE +32 -0
  4. rule_engine/_vendor/ply/__init__.py +5 -0
  5. rule_engine/_vendor/ply/lex.py +1098 -0
  6. rule_engine/_vendor/ply/yacc.py +3433 -0
  7. rule_engine/ast/__init__.py +92 -0
  8. rule_engine/ast/base.py +343 -0
  9. rule_engine/ast/binary/__init__.py +103 -0
  10. rule_engine/ast/binary/arithmetic.py +211 -0
  11. rule_engine/ast/binary/base.py +112 -0
  12. rule_engine/ast/binary/comparison.py +154 -0
  13. rule_engine/ast/expression/__init__.py +35 -0
  14. rule_engine/ast/expression/access.py +434 -0
  15. rule_engine/ast/expression/control.py +256 -0
  16. rule_engine/ast/expression/resolution.py +220 -0
  17. rule_engine/ast/literal.py +183 -0
  18. rule_engine/builtins.py +230 -0
  19. rule_engine/debug_ast.py +65 -0
  20. rule_engine/debug_repl.py +159 -0
  21. rule_engine/engine/__init__.py +34 -0
  22. rule_engine/engine/_attribute_resolver.py +353 -0
  23. rule_engine/engine/context.py +438 -0
  24. rule_engine/engine/rule.py +148 -0
  25. rule_engine/errors.py +359 -0
  26. rule_engine/parser/__init__.py +614 -0
  27. rule_engine/parser/base.py +86 -0
  28. rule_engine/parser/utilities.py +114 -0
  29. rule_engine/suggestions.py +105 -0
  30. rule_engine/types/__init__.py +61 -0
  31. rule_engine/types/_object.py +318 -0
  32. rule_engine/types/coercion.py +141 -0
  33. rule_engine/types/dataclass.py +149 -0
  34. rule_engine/types/datatype.py +398 -0
  35. rule_engine/types/definitions.py +454 -0
  36. rule_engine/types/sqlalchemy.py +171 -0
  37. rule_engine-5.0.0.dist-info/METADATA +126 -0
  38. rule_engine-5.0.0.dist-info/RECORD +41 -0
  39. rule_engine-5.0.0.dist-info/WHEEL +5 -0
  40. rule_engine-5.0.0.dist-info/licenses/LICENSE +28 -0
  41. rule_engine-5.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)
@@ -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