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/__init__.py +350 -0
- expression/grammar.peg +191 -0
- expression/helpers.py +169 -0
- expression/parser.py +968 -0
- expression/tokenizer.py +171 -0
- expression_py-0.1.0.dist-info/METADATA +350 -0
- expression_py-0.1.0.dist-info/RECORD +10 -0
- expression_py-0.1.0.dist-info/WHEEL +5 -0
- expression_py-0.1.0.dist-info/licenses/LICENSE +674 -0
- expression_py-0.1.0.dist-info/top_level.txt +1 -0
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)
|