tellaro-query-language 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.
- tellaro_query_language-0.1.0.dist-info/LICENSE +21 -0
- tellaro_query_language-0.1.0.dist-info/METADATA +401 -0
- tellaro_query_language-0.1.0.dist-info/RECORD +56 -0
- tellaro_query_language-0.1.0.dist-info/WHEEL +4 -0
- tellaro_query_language-0.1.0.dist-info/entry_points.txt +7 -0
- tql/__init__.py +47 -0
- tql/analyzer.py +385 -0
- tql/cache/__init__.py +7 -0
- tql/cache/base.py +25 -0
- tql/cache/memory.py +63 -0
- tql/cache/redis.py +68 -0
- tql/core.py +929 -0
- tql/core_components/README.md +92 -0
- tql/core_components/__init__.py +20 -0
- tql/core_components/file_operations.py +113 -0
- tql/core_components/opensearch_operations.py +869 -0
- tql/core_components/stats_operations.py +200 -0
- tql/core_components/validation_operations.py +599 -0
- tql/evaluator.py +379 -0
- tql/evaluator_components/README.md +131 -0
- tql/evaluator_components/__init__.py +17 -0
- tql/evaluator_components/field_access.py +176 -0
- tql/evaluator_components/special_expressions.py +296 -0
- tql/evaluator_components/value_comparison.py +315 -0
- tql/exceptions.py +160 -0
- tql/geoip_normalizer.py +233 -0
- tql/mutator_analyzer.py +830 -0
- tql/mutators/__init__.py +222 -0
- tql/mutators/base.py +78 -0
- tql/mutators/dns.py +316 -0
- tql/mutators/encoding.py +218 -0
- tql/mutators/geo.py +363 -0
- tql/mutators/list.py +212 -0
- tql/mutators/network.py +163 -0
- tql/mutators/security.py +225 -0
- tql/mutators/string.py +165 -0
- tql/opensearch.py +78 -0
- tql/opensearch_components/README.md +130 -0
- tql/opensearch_components/__init__.py +17 -0
- tql/opensearch_components/field_mapping.py +399 -0
- tql/opensearch_components/lucene_converter.py +305 -0
- tql/opensearch_components/query_converter.py +775 -0
- tql/opensearch_mappings.py +309 -0
- tql/opensearch_stats.py +451 -0
- tql/parser.py +1363 -0
- tql/parser_components/README.md +72 -0
- tql/parser_components/__init__.py +20 -0
- tql/parser_components/ast_builder.py +162 -0
- tql/parser_components/error_analyzer.py +101 -0
- tql/parser_components/field_extractor.py +112 -0
- tql/parser_components/grammar.py +473 -0
- tql/post_processor.py +737 -0
- tql/scripts.py +124 -0
- tql/stats_evaluator.py +444 -0
- tql/stats_transformer.py +184 -0
- tql/validators.py +110 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Value comparison operations for TQL evaluator.
|
|
2
|
+
|
|
3
|
+
This module handles all value comparison operations including type conversions,
|
|
4
|
+
operator implementations, and special cases like CIDR matching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ipaddress
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValueComparator:
|
|
13
|
+
"""Handles value comparison operations for TQL evaluation."""
|
|
14
|
+
|
|
15
|
+
# Sentinel value to distinguish missing fields from None values
|
|
16
|
+
_MISSING_FIELD = object()
|
|
17
|
+
|
|
18
|
+
def compare_values(self, field_value: Any, operator: str, expected_value: Any) -> bool: # noqa: C901
|
|
19
|
+
"""Compare a field value against an expected value using the given operator.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
field_value: Value from the record
|
|
23
|
+
operator: Comparison operator
|
|
24
|
+
expected_value: Expected value from the query
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Boolean result of comparison
|
|
28
|
+
"""
|
|
29
|
+
# Handle missing fields
|
|
30
|
+
if field_value is self._MISSING_FIELD:
|
|
31
|
+
if operator in ["exists"]:
|
|
32
|
+
return False
|
|
33
|
+
elif operator in ["not_exists"]:
|
|
34
|
+
return True # Field doesn't exist, so "not exists" is true
|
|
35
|
+
# For negated string operators, missing fields should return True
|
|
36
|
+
# (e.g., if field doesn't exist, it doesn't contain/start with/end with the value)
|
|
37
|
+
elif operator in ["not_contains", "not_startswith", "not_endswith", "not_regexp"]:
|
|
38
|
+
return True
|
|
39
|
+
# For not_cidr, missing fields should return False (can't check CIDR on missing IP)
|
|
40
|
+
elif operator in ["cidr", "not_cidr"]:
|
|
41
|
+
return False
|
|
42
|
+
# Note: for is_not operations, missing fields are treated as non-matching
|
|
43
|
+
else:
|
|
44
|
+
# Missing fields return False for all other operators
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Handle None field values (field exists but is None)
|
|
48
|
+
if field_value is None:
|
|
49
|
+
if operator in ["exists"]:
|
|
50
|
+
return True # Field exists, even if value is None
|
|
51
|
+
elif operator in ["is"]:
|
|
52
|
+
# Check for null comparison - expected_value can be None or "null"
|
|
53
|
+
return expected_value is None or (isinstance(expected_value, str) and expected_value.lower() == "null")
|
|
54
|
+
else:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
# Convert numeric strings to numbers for comparison
|
|
58
|
+
field_value = self._convert_numeric(field_value)
|
|
59
|
+
expected_value = self._convert_numeric(expected_value)
|
|
60
|
+
|
|
61
|
+
# Convert boolean strings to booleans for comparison
|
|
62
|
+
if isinstance(expected_value, str) and expected_value.lower() in ["true", "false"]:
|
|
63
|
+
expected_value = expected_value.lower() == "true"
|
|
64
|
+
if isinstance(field_value, str) and field_value.lower() in ["true", "false"]:
|
|
65
|
+
field_value = field_value.lower() == "true"
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if operator in ["eq", "="]:
|
|
69
|
+
return field_value == expected_value
|
|
70
|
+
elif operator in ["ne", "!="]:
|
|
71
|
+
return field_value != expected_value
|
|
72
|
+
elif operator in ["gt", ">"]:
|
|
73
|
+
return field_value > expected_value
|
|
74
|
+
elif operator in ["gte", ">="]:
|
|
75
|
+
return field_value >= expected_value
|
|
76
|
+
elif operator in ["lt", "<"]:
|
|
77
|
+
return field_value < expected_value
|
|
78
|
+
elif operator in ["lte", "<="]:
|
|
79
|
+
return field_value <= expected_value
|
|
80
|
+
elif operator == "contains":
|
|
81
|
+
# Unwrap single-element lists for string operators
|
|
82
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
83
|
+
expected_value = expected_value[0]
|
|
84
|
+
# Handle list fields by checking if expected value is in the list
|
|
85
|
+
if isinstance(field_value, list):
|
|
86
|
+
# For lists, check if expected value is in the list
|
|
87
|
+
return expected_value in field_value
|
|
88
|
+
else:
|
|
89
|
+
return str(expected_value) in str(field_value)
|
|
90
|
+
elif operator == "startswith":
|
|
91
|
+
# Unwrap single-element lists for string operators
|
|
92
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
93
|
+
expected_value = expected_value[0]
|
|
94
|
+
return str(field_value).startswith(str(expected_value))
|
|
95
|
+
elif operator == "endswith":
|
|
96
|
+
# Unwrap single-element lists for string operators
|
|
97
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
98
|
+
expected_value = expected_value[0]
|
|
99
|
+
return str(field_value).endswith(str(expected_value))
|
|
100
|
+
elif operator == "in":
|
|
101
|
+
if isinstance(expected_value, list):
|
|
102
|
+
if len(expected_value) == 1 and isinstance(field_value, list):
|
|
103
|
+
# This is likely a reversed 'in' case: 'value' in field_list
|
|
104
|
+
# Check if the single expected value is in the field list
|
|
105
|
+
converted_expected = self._convert_numeric(expected_value[0])
|
|
106
|
+
return converted_expected in field_value
|
|
107
|
+
else:
|
|
108
|
+
# Standard case: field_value in list
|
|
109
|
+
# Convert list elements to appropriate types for comparison
|
|
110
|
+
converted_list = [self._convert_numeric(val) for val in expected_value]
|
|
111
|
+
return field_value in converted_list
|
|
112
|
+
else:
|
|
113
|
+
return field_value == expected_value
|
|
114
|
+
elif operator == "regexp":
|
|
115
|
+
# Unwrap single-element lists for string operators
|
|
116
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
117
|
+
expected_value = expected_value[0]
|
|
118
|
+
return bool(re.search(str(expected_value), str(field_value)))
|
|
119
|
+
elif operator == "cidr":
|
|
120
|
+
# Unwrap single-element lists for CIDR
|
|
121
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
122
|
+
expected_value = expected_value[0]
|
|
123
|
+
return self._check_cidr(field_value, expected_value)
|
|
124
|
+
elif operator == "exists":
|
|
125
|
+
return True # If we got here, field exists
|
|
126
|
+
elif operator == "is":
|
|
127
|
+
# Handle null comparison specially
|
|
128
|
+
if isinstance(expected_value, str) and expected_value.lower() == "null":
|
|
129
|
+
return field_value is None
|
|
130
|
+
# Handle boolean and other literal comparisons
|
|
131
|
+
return field_value is expected_value
|
|
132
|
+
elif operator == "between":
|
|
133
|
+
# between requires a list with two values
|
|
134
|
+
if isinstance(expected_value, list) and len(expected_value) == 2:
|
|
135
|
+
# Convert string values to appropriate numeric types if needed
|
|
136
|
+
val1 = self._convert_numeric(expected_value[0])
|
|
137
|
+
val2 = self._convert_numeric(expected_value[1])
|
|
138
|
+
|
|
139
|
+
# Allow values in any order (determine lower and upper bounds)
|
|
140
|
+
lower_bound = min(val1, val2)
|
|
141
|
+
upper_bound = max(val1, val2)
|
|
142
|
+
|
|
143
|
+
# Perform range check
|
|
144
|
+
return lower_bound <= field_value <= upper_bound
|
|
145
|
+
else:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
# Negated operators - return the opposite of the base operator
|
|
149
|
+
elif operator == "not_exists":
|
|
150
|
+
# Field should not exist (handled earlier for missing fields)
|
|
151
|
+
return False # If we got here, field exists, so return False
|
|
152
|
+
elif operator == "is_not":
|
|
153
|
+
# Handle null comparison specially
|
|
154
|
+
if isinstance(expected_value, str) and expected_value.lower() == "null":
|
|
155
|
+
return field_value is not None
|
|
156
|
+
# Handle boolean and other literal comparisons
|
|
157
|
+
return field_value is not expected_value
|
|
158
|
+
elif operator == "not_in":
|
|
159
|
+
if isinstance(expected_value, list):
|
|
160
|
+
# Convert list elements to appropriate types for comparison
|
|
161
|
+
converted_list = [self._convert_numeric(val) for val in expected_value]
|
|
162
|
+
return field_value not in converted_list
|
|
163
|
+
else:
|
|
164
|
+
return field_value != expected_value
|
|
165
|
+
elif operator == "not_contains":
|
|
166
|
+
# Unwrap single-element lists for string operators
|
|
167
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
168
|
+
expected_value = expected_value[0]
|
|
169
|
+
return str(expected_value) not in str(field_value)
|
|
170
|
+
elif operator == "not_startswith":
|
|
171
|
+
# Unwrap single-element lists for string operators
|
|
172
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
173
|
+
expected_value = expected_value[0]
|
|
174
|
+
return not str(field_value).startswith(str(expected_value))
|
|
175
|
+
elif operator == "not_endswith":
|
|
176
|
+
# Unwrap single-element lists for string operators
|
|
177
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
178
|
+
expected_value = expected_value[0]
|
|
179
|
+
return not str(field_value).endswith(str(expected_value))
|
|
180
|
+
elif operator == "not_regexp":
|
|
181
|
+
# Unwrap single-element lists for string operators
|
|
182
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
183
|
+
expected_value = expected_value[0]
|
|
184
|
+
return not bool(re.search(str(expected_value), str(field_value)))
|
|
185
|
+
elif operator == "not_cidr":
|
|
186
|
+
# Unwrap single-element lists for CIDR
|
|
187
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
188
|
+
expected_value = expected_value[0]
|
|
189
|
+
return not self._check_cidr(field_value, expected_value)
|
|
190
|
+
elif operator == "not_between":
|
|
191
|
+
# not between requires a list with two values
|
|
192
|
+
if isinstance(expected_value, list) and len(expected_value) == 2:
|
|
193
|
+
# Convert string values to appropriate numeric types if needed
|
|
194
|
+
val1 = self._convert_numeric(expected_value[0])
|
|
195
|
+
val2 = self._convert_numeric(expected_value[1])
|
|
196
|
+
|
|
197
|
+
# Allow values in any order (determine lower and upper bounds)
|
|
198
|
+
lower_bound = min(val1, val2)
|
|
199
|
+
upper_bound = max(val1, val2)
|
|
200
|
+
|
|
201
|
+
# Perform range check (opposite of between)
|
|
202
|
+
return not lower_bound <= field_value <= upper_bound
|
|
203
|
+
else:
|
|
204
|
+
return False
|
|
205
|
+
elif operator == "any":
|
|
206
|
+
# ANY operator - matches if the value equals any element (for arrays)
|
|
207
|
+
# or equals the value (for single values)
|
|
208
|
+
# Handle case where expected_value might be wrapped in a list
|
|
209
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
210
|
+
expected_value = expected_value[0]
|
|
211
|
+
|
|
212
|
+
if isinstance(field_value, (list, tuple, set)):
|
|
213
|
+
# For arrays, check if expected value is in the array
|
|
214
|
+
return expected_value in field_value
|
|
215
|
+
else:
|
|
216
|
+
# For single values, just check equality
|
|
217
|
+
return field_value == expected_value
|
|
218
|
+
elif operator == "all":
|
|
219
|
+
# ALL operator - for arrays, all elements must equal the value
|
|
220
|
+
# For single values, it's just equality
|
|
221
|
+
# Handle case where expected_value might be wrapped in a list
|
|
222
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
223
|
+
expected_value = expected_value[0]
|
|
224
|
+
|
|
225
|
+
if isinstance(field_value, (list, tuple, set)):
|
|
226
|
+
# For arrays, all elements must equal the expected value
|
|
227
|
+
return all(elem == expected_value for elem in field_value) if field_value else False
|
|
228
|
+
else:
|
|
229
|
+
# For single values, just check equality
|
|
230
|
+
return field_value == expected_value
|
|
231
|
+
elif operator == "not_any":
|
|
232
|
+
# NOT ANY - the value should not equal any element
|
|
233
|
+
# Handle case where expected_value might be wrapped in a list
|
|
234
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
235
|
+
expected_value = expected_value[0]
|
|
236
|
+
|
|
237
|
+
if isinstance(field_value, (list, tuple, set)):
|
|
238
|
+
# For arrays, expected value should not be in the array
|
|
239
|
+
return expected_value not in field_value
|
|
240
|
+
else:
|
|
241
|
+
# For single values, check inequality
|
|
242
|
+
return field_value != expected_value
|
|
243
|
+
elif operator == "not_all":
|
|
244
|
+
# NOT ALL - at least one element doesn't equal the value
|
|
245
|
+
# Handle case where expected_value might be wrapped in a list
|
|
246
|
+
if isinstance(expected_value, list) and len(expected_value) == 1:
|
|
247
|
+
expected_value = expected_value[0]
|
|
248
|
+
|
|
249
|
+
if isinstance(field_value, (list, tuple, set)):
|
|
250
|
+
# For arrays, at least one element must not equal the expected value
|
|
251
|
+
# This is true if ANY element doesn't match
|
|
252
|
+
return any(elem != expected_value for elem in field_value) if field_value else True
|
|
253
|
+
else:
|
|
254
|
+
# For single values, NOT ALL means the opposite of ALL
|
|
255
|
+
# If the single value matches, then ALL match, so NOT ALL is false
|
|
256
|
+
return field_value != expected_value
|
|
257
|
+
else:
|
|
258
|
+
raise ValueError(f"Unknown operator: {operator}")
|
|
259
|
+
except (TypeError, ValueError):
|
|
260
|
+
# Type mismatch or conversion error
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def _convert_numeric(self, value: Any) -> Any:
|
|
264
|
+
"""Convert string numbers and booleans to appropriate types.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
value: Value to convert
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Converted value (int, float, bool, or original)
|
|
271
|
+
"""
|
|
272
|
+
if isinstance(value, str):
|
|
273
|
+
# Try to convert to int
|
|
274
|
+
try:
|
|
275
|
+
# Check if it's a valid integer
|
|
276
|
+
if "." not in value and "e" not in value.lower() and "E" not in value:
|
|
277
|
+
return int(value)
|
|
278
|
+
except ValueError:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
# Try to convert to float
|
|
282
|
+
try:
|
|
283
|
+
return float(value)
|
|
284
|
+
except ValueError:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# Try to convert to boolean
|
|
288
|
+
if value.lower() == "true":
|
|
289
|
+
return True
|
|
290
|
+
elif value.lower() == "false":
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
def _check_cidr(self, ip_value: Any, cidr: str) -> bool:
|
|
296
|
+
"""Check if an IP address matches a CIDR pattern.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
ip_value: IP address to check
|
|
300
|
+
cidr: CIDR pattern
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
True if IP is in CIDR range
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
# Convert IP value to string if needed
|
|
307
|
+
ip_str = str(ip_value)
|
|
308
|
+
# Create network from CIDR
|
|
309
|
+
network = ipaddress.ip_network(cidr, strict=False)
|
|
310
|
+
# Check if IP is in network
|
|
311
|
+
ip = ipaddress.ip_address(ip_str)
|
|
312
|
+
return ip in network
|
|
313
|
+
except (ValueError, TypeError):
|
|
314
|
+
# Invalid IP or CIDR
|
|
315
|
+
return False
|
tql/exceptions.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""TQL exception classes.
|
|
2
|
+
|
|
3
|
+
This module defines custom exceptions used throughout the TQL library.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TQLError(Exception):
|
|
10
|
+
"""Base exception class for all TQL errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
position: Optional[int] = None,
|
|
16
|
+
query: Optional[str] = None,
|
|
17
|
+
suggestions: Optional[List[str]] = None,
|
|
18
|
+
context: Optional[Dict[str, Any]] = None,
|
|
19
|
+
):
|
|
20
|
+
"""Initialize TQL error with enhanced context.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message: Primary error message
|
|
24
|
+
position: Character position where error occurred
|
|
25
|
+
query: Original query string
|
|
26
|
+
suggestions: List of suggestions or examples
|
|
27
|
+
context: Additional context information
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.position = position
|
|
31
|
+
self.query = query
|
|
32
|
+
self.suggestions = suggestions or []
|
|
33
|
+
self.context = context or {}
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
"""Format error message with position and suggestions."""
|
|
37
|
+
lines = []
|
|
38
|
+
|
|
39
|
+
# Main error message with position
|
|
40
|
+
if self.position is not None:
|
|
41
|
+
lines.append(f"{self.__class__.__name__} at position {self.position}: {super().__str__()}")
|
|
42
|
+
|
|
43
|
+
# Show query with position indicator
|
|
44
|
+
if self.query:
|
|
45
|
+
lines.append(f"Query: {self.query}")
|
|
46
|
+
# Add position indicator
|
|
47
|
+
if 0 <= self.position <= len(self.query):
|
|
48
|
+
lines.append(" " * (7 + self.position) + "^")
|
|
49
|
+
else:
|
|
50
|
+
lines.append(f"{self.__class__.__name__}: {super().__str__()}")
|
|
51
|
+
|
|
52
|
+
# Add suggestions
|
|
53
|
+
if self.suggestions:
|
|
54
|
+
if len(self.suggestions) == 1:
|
|
55
|
+
lines.append(f"Did you mean: {self.suggestions[0]}?")
|
|
56
|
+
else:
|
|
57
|
+
lines.append("Suggestions:")
|
|
58
|
+
for suggestion in self.suggestions:
|
|
59
|
+
lines.append(f" - {suggestion}")
|
|
60
|
+
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TQLSyntaxError(TQLError):
|
|
65
|
+
"""Raised when TQL query has syntax errors."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TQLParseError(TQLError):
|
|
69
|
+
"""Raised when there's an error parsing a TQL query."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TQLTypeError(TQLError):
|
|
73
|
+
"""Raised when an operator is incompatible with a field's data type."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self, field: str, field_type: str, operator: str, valid_operators: Optional[List[str]] = None, **kwargs
|
|
77
|
+
):
|
|
78
|
+
"""Initialize type error with field and operator context."""
|
|
79
|
+
message = f"Cannot apply operator '{operator}' to field '{field}' of type '{field_type}'. "
|
|
80
|
+
|
|
81
|
+
if operator in [">", ">=", "<", "<="] and field_type in ["keyword", "text"]:
|
|
82
|
+
message += (
|
|
83
|
+
"Numeric comparison operators (>, >=, <, <=) require numeric field types "
|
|
84
|
+
"(integer, long, float, double). "
|
|
85
|
+
)
|
|
86
|
+
if valid_operators:
|
|
87
|
+
message += f"Consider using: {', '.join(valid_operators)} for {field_type} fields."
|
|
88
|
+
elif valid_operators:
|
|
89
|
+
message += f"Valid operators for {field_type} fields: {', '.join(valid_operators)}"
|
|
90
|
+
|
|
91
|
+
super().__init__(message, **kwargs)
|
|
92
|
+
self.field = field
|
|
93
|
+
self.field_type = field_type
|
|
94
|
+
self.operator = operator
|
|
95
|
+
self.valid_operators = valid_operators
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TQLFieldError(TQLError):
|
|
99
|
+
"""Raised when referencing invalid or non-existent fields."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, field: str, available_fields: Optional[List[str]] = None, **kwargs):
|
|
102
|
+
"""Initialize field error with available fields context."""
|
|
103
|
+
message = f"Unknown field '{field}'."
|
|
104
|
+
|
|
105
|
+
if available_fields:
|
|
106
|
+
message += f"\nAvailable fields: {', '.join(sorted(available_fields))}"
|
|
107
|
+
|
|
108
|
+
# Simple suggestion based on string similarity
|
|
109
|
+
suggestions = []
|
|
110
|
+
field_lower = field.lower()
|
|
111
|
+
for available in available_fields:
|
|
112
|
+
if field_lower in available.lower() or available.lower() in field_lower:
|
|
113
|
+
suggestions.append(f"{available}")
|
|
114
|
+
|
|
115
|
+
if suggestions and "suggestions" not in kwargs:
|
|
116
|
+
kwargs["suggestions"] = suggestions[:3] # Limit to top 3 suggestions
|
|
117
|
+
|
|
118
|
+
super().__init__(message, **kwargs)
|
|
119
|
+
self.field = field
|
|
120
|
+
self.available_fields = available_fields
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TQLValueError(TQLError):
|
|
124
|
+
"""Raised when provided values don't match expected formats."""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TQLOperatorError(TQLError):
|
|
128
|
+
"""Raised when operators are used incorrectly."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TQLExecutionError(TQLError):
|
|
132
|
+
"""Raised when there's an error executing a TQL query."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TQLValidationError(TQLError):
|
|
136
|
+
"""Raised when a TQL query fails validation."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TQLUnsupportedOperationError(TQLError):
|
|
140
|
+
"""Raised when attempting to use unsupported operations with a backend."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TQLConfigError(TQLError):
|
|
144
|
+
"""Raised when there's a configuration error."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TQLMutatorError(TQLError):
|
|
148
|
+
"""Raised when there's an error applying a mutator."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, mutator_name: str, field_name: str, value_type: str, message: Optional[str] = None, **kwargs):
|
|
151
|
+
"""Initialize mutator error with context."""
|
|
152
|
+
if not message:
|
|
153
|
+
message = (
|
|
154
|
+
f"Cannot apply mutator '{mutator_name}' to field '{field_name}' with value of type '{value_type}'."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
super().__init__(message, **kwargs)
|
|
158
|
+
self.mutator_name = mutator_name
|
|
159
|
+
self.field_name = field_name
|
|
160
|
+
self.value_type = value_type
|