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.
Files changed (56) hide show
  1. tellaro_query_language-0.1.0.dist-info/LICENSE +21 -0
  2. tellaro_query_language-0.1.0.dist-info/METADATA +401 -0
  3. tellaro_query_language-0.1.0.dist-info/RECORD +56 -0
  4. tellaro_query_language-0.1.0.dist-info/WHEEL +4 -0
  5. tellaro_query_language-0.1.0.dist-info/entry_points.txt +7 -0
  6. tql/__init__.py +47 -0
  7. tql/analyzer.py +385 -0
  8. tql/cache/__init__.py +7 -0
  9. tql/cache/base.py +25 -0
  10. tql/cache/memory.py +63 -0
  11. tql/cache/redis.py +68 -0
  12. tql/core.py +929 -0
  13. tql/core_components/README.md +92 -0
  14. tql/core_components/__init__.py +20 -0
  15. tql/core_components/file_operations.py +113 -0
  16. tql/core_components/opensearch_operations.py +869 -0
  17. tql/core_components/stats_operations.py +200 -0
  18. tql/core_components/validation_operations.py +599 -0
  19. tql/evaluator.py +379 -0
  20. tql/evaluator_components/README.md +131 -0
  21. tql/evaluator_components/__init__.py +17 -0
  22. tql/evaluator_components/field_access.py +176 -0
  23. tql/evaluator_components/special_expressions.py +296 -0
  24. tql/evaluator_components/value_comparison.py +315 -0
  25. tql/exceptions.py +160 -0
  26. tql/geoip_normalizer.py +233 -0
  27. tql/mutator_analyzer.py +830 -0
  28. tql/mutators/__init__.py +222 -0
  29. tql/mutators/base.py +78 -0
  30. tql/mutators/dns.py +316 -0
  31. tql/mutators/encoding.py +218 -0
  32. tql/mutators/geo.py +363 -0
  33. tql/mutators/list.py +212 -0
  34. tql/mutators/network.py +163 -0
  35. tql/mutators/security.py +225 -0
  36. tql/mutators/string.py +165 -0
  37. tql/opensearch.py +78 -0
  38. tql/opensearch_components/README.md +130 -0
  39. tql/opensearch_components/__init__.py +17 -0
  40. tql/opensearch_components/field_mapping.py +399 -0
  41. tql/opensearch_components/lucene_converter.py +305 -0
  42. tql/opensearch_components/query_converter.py +775 -0
  43. tql/opensearch_mappings.py +309 -0
  44. tql/opensearch_stats.py +451 -0
  45. tql/parser.py +1363 -0
  46. tql/parser_components/README.md +72 -0
  47. tql/parser_components/__init__.py +20 -0
  48. tql/parser_components/ast_builder.py +162 -0
  49. tql/parser_components/error_analyzer.py +101 -0
  50. tql/parser_components/field_extractor.py +112 -0
  51. tql/parser_components/grammar.py +473 -0
  52. tql/post_processor.py +737 -0
  53. tql/scripts.py +124 -0
  54. tql/stats_evaluator.py +444 -0
  55. tql/stats_transformer.py +184 -0
  56. 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