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,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)
|