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,353 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # rule_engine/engine/_attribute_resolver.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 binascii
34
+ import collections
35
+ import datetime
36
+ import decimal
37
+ import functools
38
+ import math
39
+ import re
40
+ from typing import Any, Callable, Iterable, Sequence, Sized
41
+
42
+ from .. import errors
43
+ from .. import parser
44
+ from .. import types
45
+ from ..suggestions import suggest_symbol
46
+ from ..types import _CollectionDataTypeDef, _DataTypeDef
47
+
48
+ def _float_op(value: decimal.Decimal, op: Callable[[decimal.Decimal], Any]) -> Any:
49
+ if value.is_nan() or value.is_infinite():
50
+ return value
51
+ return op(value)
52
+
53
+ def _value_to_ary_result_type(object_type: _DataTypeDef) -> _DataTypeDef:
54
+ if object_type == types.DataType.BYTES:
55
+ return types.DataType.ARRAY(types.DataType.FLOAT)
56
+ elif object_type == types.DataType.STRING:
57
+ return types.DataType.ARRAY(types.DataType.STRING)
58
+ assert isinstance(object_type, _CollectionDataTypeDef)
59
+ return types.DataType.ARRAY(object_type.value_type)
60
+
61
+ def _value_to_set_result_type(object_type: _DataTypeDef) -> _DataTypeDef:
62
+ if object_type == types.DataType.BYTES:
63
+ return types.DataType.SET(types.DataType.FLOAT)
64
+ elif object_type == types.DataType.STRING:
65
+ return types.DataType.SET(types.DataType.STRING)
66
+ assert isinstance(object_type, _CollectionDataTypeDef)
67
+ return types.DataType.SET(object_type.value_type)
68
+
69
+ def _value_with_result_type(name: str, object_type: _DataTypeDef) -> _DataTypeDef:
70
+ return types.DataType.FUNCTION(name, argument_types=(object_type,), return_type=types.DataType.BOOLEAN)
71
+
72
+ class _AttributeResolverFunction(object):
73
+ __slots__ = ('function', 'type_resolver')
74
+ def __init__(
75
+ self,
76
+ function: Callable[..., Any],
77
+ *,
78
+ result_type: _DataTypeDef,
79
+ type_resolver: Callable[[_DataTypeDef], _DataTypeDef] | Any
80
+ ) -> None:
81
+ self.function = function
82
+ if result_type and result_type is not types.DataType.UNDEFINED:
83
+ if not types.DataType.is_definition(result_type):
84
+ raise TypeError('result_type must be a types.DataType definition')
85
+ if type_resolver:
86
+ raise ValueError('both result_type and type_resolver can not be specified')
87
+ type_resolver = functools.partial(self._type_resolver, result_type)
88
+ elif not type_resolver:
89
+ type_resolver = functools.partial(self._type_resolver, types.DataType.UNDEFINED)
90
+ self.type_resolver: Callable[[_DataTypeDef], _DataTypeDef] = type_resolver
91
+
92
+ @staticmethod
93
+ def _type_resolver(result_type: _DataTypeDef, _object_type: _DataTypeDef) -> _DataTypeDef:
94
+ return result_type
95
+
96
+ def resolve_type(self, object_type: _DataTypeDef) -> _DataTypeDef:
97
+ return self.type_resolver(object_type)
98
+
99
+ class _AttributeResolver(object):
100
+ class attribute(object):
101
+ __slots__ = ('types', 'name', 'result_type', 'type_resolver')
102
+ type_map: dict[_DataTypeDef, dict[str, _AttributeResolverFunction]] = collections.defaultdict(dict)
103
+ def __init__(
104
+ self,
105
+ name: str,
106
+ *data_types: _DataTypeDef,
107
+ result_type: _DataTypeDef = types.DataType.UNDEFINED,
108
+ type_resolver: Callable[[_DataTypeDef], _DataTypeDef] | Any = errors.UNDEFINED
109
+ ) -> None:
110
+ self.types = data_types
111
+ self.name = name
112
+ self.result_type = result_type
113
+ self.type_resolver = type_resolver
114
+
115
+ def __call__(self, function: Callable[..., Any]) -> Callable[..., Any]:
116
+ for type_ in self.types:
117
+ self.type_map[type_][self.name] = _AttributeResolverFunction(function, result_type=self.result_type, type_resolver=self.type_resolver)
118
+ return function
119
+
120
+ def __call__(self, thing: Any, object_: Any, name: str) -> Any:
121
+ try:
122
+ object_type = types.DataType.from_value(object_)
123
+ except TypeError:
124
+ # if the object can't be mapped to a supported type, raise a resolution error
125
+ raise errors.AttributeResolutionError(name, object_, thing=thing) from None
126
+ resolver = self._get_resolver(object_type, name, thing=thing)
127
+ value = resolver.function(self, object_)
128
+ value = types.coerce_value(value)
129
+ value_type = types.DataType.from_value(value)
130
+ expected_value_type = resolver.resolve_type(value_type)
131
+ if types.DataType.is_compatible(expected_value_type, value_type):
132
+ return value
133
+ raise errors.AttributeTypeError(name, object_, is_value=value, is_type=value_type, expected_type=expected_value_type)
134
+
135
+ def _get_resolver(self, object_type: _DataTypeDef, name: str, thing: Any = errors.UNDEFINED) -> _AttributeResolverFunction:
136
+ for data_type, attribute_resolvers in self.attribute.type_map.items():
137
+ if types.DataType.is_compatible(data_type, object_type):
138
+ break
139
+ else:
140
+ raise errors.AttributeResolutionError(name, object_type, thing=thing)
141
+ resolver = attribute_resolvers.get(name)
142
+ if resolver is None:
143
+ raise errors.AttributeResolutionError(name, object_type, thing=thing, suggestion=suggest_symbol(name, attribute_resolvers.keys()))
144
+ return resolver
145
+
146
+ def resolve_type(self, object_type: _DataTypeDef, name: str) -> _DataTypeDef:
147
+ """
148
+ The method to use for resolving the data type of an attribute.
149
+
150
+ :param object_type: The data type of the object that *name* is an attribute of.
151
+ :param str name: The name of the attribute to retrieve the data type of.
152
+ :return: The data type of the specified attribute.
153
+ """
154
+ return self._get_resolver(object_type, name).resolve_type(object_type)
155
+
156
+ @attribute('decode', types.DataType.BYTES, result_type=types.DataType.FUNCTION('decode', return_type=types.DataType.STRING, argument_types=(types.DataType.STRING,)))
157
+ def bytes_decode(self, value: bytes) -> Callable[..., str]:
158
+ return functools.partial(self._bytes_decode, value)
159
+
160
+ @classmethod
161
+ def _bytes_decode(cls, value: bytes, encoding: str) -> str:
162
+ encoding = encoding.lower()
163
+ if encoding == 'base16' or encoding == 'hex':
164
+ return binascii.b2a_hex(value).decode()
165
+ elif encoding == 'base64':
166
+ return binascii.b2a_base64(value).decode().strip()
167
+ try:
168
+ return value.decode(encoding)
169
+ except LookupError as error:
170
+ raise errors.FunctionCallError("invalid encoding name {}".format(encoding), error=error, function_name='decode')
171
+
172
+ @attribute('to_epoch', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
173
+ def datetime_to_epoch(self, value: datetime.datetime) -> float:
174
+ return value.timestamp()
175
+
176
+ @attribute('date', types.DataType.DATETIME, result_type=types.DataType.DATETIME)
177
+ def datetime_date(self, value: datetime.datetime) -> datetime.datetime:
178
+ return value.replace(hour=0, minute=0, second=0, microsecond=0)
179
+
180
+ @attribute('day', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
181
+ def datetime_day(self, value: datetime.datetime) -> int:
182
+ return value.day
183
+
184
+ @attribute('hour', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
185
+ def datetime_hour(self, value: datetime.datetime) -> int:
186
+ return value.hour
187
+
188
+ @attribute('microsecond', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
189
+ def datetime_microsecond(self, value: datetime.datetime) -> int:
190
+ return value.microsecond
191
+
192
+ @attribute('millisecond', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
193
+ def datetime_millisecond(self, value: datetime.datetime) -> float:
194
+ return value.microsecond / 1000
195
+
196
+ @attribute('minute', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
197
+ def datetime_minute(self, value: datetime.datetime) -> int:
198
+ return value.minute
199
+
200
+ @attribute('month', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
201
+ def datetime_month(self, value: datetime.datetime) -> int:
202
+ return value.month
203
+
204
+ @attribute('second', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
205
+ def datetime_second(self, value: datetime.datetime) -> int:
206
+ return value.second
207
+
208
+ @attribute('weekday', types.DataType.DATETIME, result_type=types.DataType.STRING)
209
+ def datetime_weekday(self, value: datetime.datetime) -> str:
210
+ # use strftime %A so the value is localized
211
+ return value.strftime('%A')
212
+
213
+ @attribute('year', types.DataType.DATETIME, result_type=types.DataType.FLOAT)
214
+ def datetime_year(self, value: datetime.datetime) -> int:
215
+ return value.year
216
+
217
+ @attribute('zone_name', types.DataType.DATETIME, result_type=types.DataType.STRING)
218
+ def datetime_zone_name(self, value: datetime.datetime) -> str | None:
219
+ return value.tzname()
220
+
221
+ @attribute('ceiling', types.DataType.FLOAT, result_type=types.DataType.FLOAT)
222
+ def float_ceiling(self, value: decimal.Decimal) -> decimal.Decimal | int:
223
+ return _float_op(value, math.ceil)
224
+
225
+ @attribute('floor', types.DataType.FLOAT, result_type=types.DataType.FLOAT)
226
+ def float_floor(self, value: decimal.Decimal) -> decimal.Decimal | int:
227
+ return _float_op(value, math.floor)
228
+
229
+ @attribute('is_nan', types.DataType.FLOAT, result_type=types.DataType.BOOLEAN)
230
+ def float_is_nan(self, value: decimal.Decimal) -> bool:
231
+ return math.isnan(value)
232
+
233
+ @attribute('to_flt', types.DataType.FLOAT, result_type=types.DataType.FLOAT)
234
+ def float_to_flt(self, value: decimal.Decimal) -> decimal.Decimal:
235
+ return value
236
+
237
+ @attribute('to_int', types.DataType.FLOAT, result_type=types.DataType.FLOAT)
238
+ def float_to_int(self, value: decimal.Decimal) -> decimal.Decimal:
239
+ if not types.is_integer_number(value):
240
+ raise errors.EvaluationError('data type mismatch (not an integer number)')
241
+ return value
242
+
243
+ @attribute('keys', types.DataType.MAPPING, result_type=types.DataType.ARRAY)
244
+ def mapping_keys(self, value: dict[Any, Any]) -> tuple[Any, ...]:
245
+ return tuple(value.keys())
246
+
247
+ @attribute('values', types.DataType.MAPPING, result_type=types.DataType.ARRAY)
248
+ def mapping_values(self, value: dict[Any, Any]) -> tuple[Any, ...]:
249
+ return tuple(value.values())
250
+
251
+ @attribute('as_lower', types.DataType.STRING, result_type=types.DataType.STRING)
252
+ def string_as_lower(self, value: str) -> str:
253
+ return value.lower()
254
+
255
+ @attribute('as_upper', types.DataType.STRING, result_type=types.DataType.STRING)
256
+ def string_as_upper(self, value: str) -> str:
257
+ return value.upper()
258
+
259
+ @attribute('encode', types.DataType.STRING, result_type=types.DataType.FUNCTION('encode', return_type=types.DataType.BYTES, argument_types=(types.DataType.STRING,)))
260
+ def string_encode(self, value: str) -> Callable[..., bytes]:
261
+ return functools.partial(self._string_encode, value)
262
+
263
+ @classmethod
264
+ def _string_encode(cls, value: str, encoding: str) -> bytes:
265
+ encoding = encoding.lower()
266
+ try:
267
+ if encoding == 'base16' or encoding == 'hex':
268
+ return binascii.a2b_hex(value.encode())
269
+ elif encoding == 'base64':
270
+ return binascii.a2b_base64(value.encode())
271
+ except binascii.Error as error:
272
+ raise errors.FunctionCallError("error converting to {}".format(encoding), error=error, function_name='encode')
273
+ try:
274
+ return value.encode(encoding)
275
+ except LookupError as error:
276
+ raise errors.FunctionCallError("invalid encoding name {}".format(encoding), error=error, function_name='encode')
277
+
278
+ @attribute('to_flt', types.DataType.STRING, result_type=types.DataType.FLOAT)
279
+ def string_to_flt(self, value: str) -> decimal.Decimal:
280
+ value = value.strip()
281
+ if re.match(r'-?inf', value):
282
+ return decimal.Decimal(value)
283
+ match = re.match(r'^(' + parser.Parser.get_token_regex('FLOAT') + ')$', value)
284
+ if match is None:
285
+ return decimal.Decimal('nan')
286
+ return parser.literal_eval(match.group(0))
287
+
288
+ @attribute('to_int', types.DataType.STRING, result_type=types.DataType.FLOAT)
289
+ def string_to_int(self, value: str) -> decimal.Decimal:
290
+ result = self.string_to_flt(value)
291
+ if not types.is_integer_number(result):
292
+ raise errors.EvaluationError('data type mismatch (not an integer number)')
293
+ return result
294
+
295
+ @attribute('days', types.DataType.TIMEDELTA, result_type=types.DataType.FLOAT)
296
+ def timedelta_days(self, value: datetime.timedelta) -> int:
297
+ return value.days
298
+
299
+ @attribute('seconds', types.DataType.TIMEDELTA, result_type=types.DataType.FLOAT)
300
+ def timedelta_seconds(self, value: datetime.timedelta) -> int:
301
+ return value.seconds
302
+
303
+ @attribute('microseconds', types.DataType.TIMEDELTA, result_type=types.DataType.FLOAT)
304
+ def timedelta_microseconds(self, value: datetime.timedelta) -> int:
305
+ return value.microseconds
306
+
307
+ @attribute('total_seconds', types.DataType.TIMEDELTA, result_type=types.DataType.FLOAT)
308
+ def timedelta_total_seconds(self, value: datetime.timedelta) -> float:
309
+ return value.total_seconds()
310
+
311
+ @attribute('ends_with', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.STRING, type_resolver=functools.partial(_value_with_result_type, 'ends_with'))
312
+ def value_ends_with(self, value: Sequence[Any]) -> Callable[..., bool]:
313
+ return functools.partial(self._value_ends_with, value)
314
+
315
+ def _value_ends_with(self, value: Sequence[Any], suffix: Sequence[Any]) -> bool:
316
+ return value[-len(suffix):] == suffix
317
+
318
+ @attribute('is_empty', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.STRING, types.DataType.MAPPING, types.DataType.SET, result_type=types.DataType.BOOLEAN)
319
+ def value_is_empty(self, value: Sized) -> bool:
320
+ return len(value) == 0
321
+
322
+ @attribute('length', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.STRING, types.DataType.MAPPING, types.DataType.SET, result_type=types.DataType.FLOAT)
323
+ def value_length(self, value: Sized) -> int:
324
+ return len(value)
325
+
326
+ @attribute('starts_with', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.STRING, type_resolver=functools.partial(_value_with_result_type, 'starts_with'))
327
+ def value_starts_with(self, value: Sequence[Any]) -> Callable[..., bool]:
328
+ return functools.partial(self._value_starts_with, value)
329
+
330
+ def _value_starts_with(self, value: Sequence[Any], prefix: Sequence[Any]) -> bool:
331
+ return value[:len(prefix)] == prefix
332
+
333
+ @attribute('to_ary', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.SET, types.DataType.STRING, type_resolver=_value_to_ary_result_type)
334
+ def value_to_ary(self, value: Iterable[Any]) -> tuple[Any, ...]:
335
+ return tuple(value)
336
+
337
+ @attribute('to_str', types.DataType.FLOAT, types.DataType.STRING, result_type=types.DataType.STRING)
338
+ def value_to_str(self, value: decimal.Decimal | str) -> str:
339
+ if isinstance(value, str):
340
+ return value
341
+ # keep the string representations consistent for nan, inf, -inf
342
+ if value.is_nan():
343
+ return 'nan'
344
+ elif value.is_infinite():
345
+ if value.is_signed():
346
+ return '-inf'
347
+ else:
348
+ return 'inf'
349
+ return str(value)
350
+
351
+ @attribute('to_set', types.DataType.ARRAY, types.DataType.BYTES, types.DataType.SET, types.DataType.STRING, type_resolver=_value_to_set_result_type)
352
+ def value_to_set(self, value: Iterable[Any]) -> set[Any]:
353
+ return set(value)