expression-py 0.1.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.
expression/helpers.py ADDED
@@ -0,0 +1,169 @@
1
+ # Copyright (C) 2026 Jakub T. Jankiewicz <https://jcu.bi/>
2
+ #
3
+ # This file is part of expression.py.
4
+ #
5
+ # expression.py is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # expression.py is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with expression.py. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import json
19
+ import re
20
+
21
+
22
+ class Typed(dict):
23
+ pass
24
+
25
+
26
+ def with_type(value, typ=None):
27
+ if isinstance(value, Typed) and set(value.keys()) == {'type', 'value'}:
28
+ return value
29
+ if typ is not None:
30
+ t = typ
31
+ elif isinstance(value, bool):
32
+ t = 'boolean'
33
+ elif isinstance(value, int):
34
+ t = 'integer'
35
+ elif isinstance(value, float):
36
+ t = 'double'
37
+ elif isinstance(value, str):
38
+ t = 'string'
39
+ elif isinstance(value, (list, dict)):
40
+ t = 'array'
41
+ elif value is None:
42
+ t = 'NULL'
43
+ else:
44
+ t = type(value).__name__
45
+ return Typed(type=t, value=value)
46
+
47
+
48
+ def is_typed(value):
49
+ return isinstance(value, Typed) and set(value.keys()) == {'type', 'value'}
50
+
51
+
52
+ def is_type(typ, value):
53
+ return is_typed(value) and value['type'] == typ
54
+
55
+
56
+ def is_number(value):
57
+ return is_type('double', value) or is_type('integer', value)
58
+
59
+
60
+ def is_string_type(value):
61
+ return is_type('string', value)
62
+
63
+
64
+ def is_array_type(value):
65
+ return is_type('array', value)
66
+
67
+
68
+ def to_array(value):
69
+ if is_array_type(value):
70
+ return list(value['value'])
71
+ return [value['value']]
72
+
73
+
74
+ def validate_types(types, operation, obj):
75
+ if not is_typed(obj):
76
+ raise Exception(f"Internal error: Invalid object {obj}")
77
+ t = obj['type']
78
+ if t not in types:
79
+ if len(types) == 1:
80
+ valid = types[0]
81
+ else:
82
+ valid = 'any of ' + ', '.join(types)
83
+ raise Exception(
84
+ f"Invalid operand to {operation} operation expecting {valid} got {t}"
85
+ )
86
+
87
+
88
+ def validate_number(operation, obj):
89
+ validate_types(['double', 'integer'], operation, obj)
90
+
91
+
92
+ def maybe_regex(value):
93
+ if re.match(r'^(\W).*\1[imsxUXJ]*$', value, re.DOTALL):
94
+ return with_type(value, 'regex')
95
+ return with_type(value, 'string')
96
+
97
+
98
+ def loose_equal(a, b):
99
+ if type(a) is type(b):
100
+ return a == b
101
+ try:
102
+ if isinstance(a, str) and isinstance(b, (int, float)):
103
+ return float(a) == b
104
+ if isinstance(b, str) and isinstance(a, (int, float)):
105
+ return a == float(b)
106
+ except (ValueError, TypeError):
107
+ pass
108
+ return a == b
109
+
110
+
111
+ def do_check_equal(left, right, fn):
112
+ a = right['value']
113
+ b = left['value']
114
+ if is_array_type(right) or is_array_type(left):
115
+ return with_type(
116
+ fn(
117
+ json.dumps(a, separators=(',', ':')),
118
+ json.dumps(b, separators=(',', ':')),
119
+ )
120
+ )
121
+ return with_type(fn(a, b))
122
+
123
+
124
+ def do_match(left, right, variables):
125
+ validate_types(['string'], '=~', left)
126
+ validate_types(['regex', 'string'], '=~', right)
127
+ regex_str = right['value']
128
+ flags_str = ''
129
+ if regex_str.startswith('/'):
130
+ last_slash = regex_str.rfind('/')
131
+ if last_slash > 0:
132
+ flags_str = regex_str[last_slash + 1:]
133
+ regex_str = regex_str[1:last_slash]
134
+ flags = 0
135
+ if 'i' in flags_str:
136
+ flags |= re.IGNORECASE
137
+ if 'm' in flags_str:
138
+ flags |= re.MULTILINE
139
+ if 's' in flags_str:
140
+ flags |= re.DOTALL
141
+ if 'x' in flags_str:
142
+ flags |= re.VERBOSE
143
+ try:
144
+ match = re.search(regex_str, left['value'], flags)
145
+ except re.error:
146
+ raise Exception(f"Invalid regular expression: {right['value']}")
147
+ old_keys = [k for k in variables if k.startswith('$')]
148
+ for k in old_keys:
149
+ del variables[k]
150
+ if match:
151
+ variables['$0'] = match.group(0)
152
+ for i, g in enumerate(match.groups(), 1):
153
+ if g is not None:
154
+ variables[f'${i}'] = g
155
+ return with_type(True)
156
+ return with_type(False)
157
+
158
+
159
+ def do_power(base, exp):
160
+ validate_number('**', base)
161
+ validate_number('**', exp)
162
+ result = base['value'] ** exp['value']
163
+ if (
164
+ isinstance(result, float)
165
+ and result == int(result)
166
+ and isinstance(base['value'], int)
167
+ ):
168
+ result = int(result)
169
+ return with_type(result)