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
tql/evaluator.py ADDED
@@ -0,0 +1,379 @@
1
+ """TQL query evaluator.
2
+
3
+ This module provides the TQLEvaluator class for executing TQL queries against
4
+ data records in memory.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from .evaluator_components import FieldAccessor, SpecialExpressionEvaluator, ValueComparator
10
+ from .mutators import apply_mutators
11
+
12
+
13
+ class TQLEvaluator:
14
+ """Evaluates TQL queries against data records.
15
+
16
+ This class takes parsed TQL ASTs and evaluates them against Python
17
+ dictionaries representing data records.
18
+ """
19
+
20
+ # Sentinel value to distinguish missing fields from None values
21
+ _MISSING_FIELD = object()
22
+
23
+ def __init__(self):
24
+ """Initialize the evaluator."""
25
+ # Initialize component evaluators
26
+ self.field_accessor = FieldAccessor()
27
+ self.value_comparator = ValueComparator()
28
+ # Pass sentinel value to components
29
+ self.field_accessor._MISSING_FIELD = self._MISSING_FIELD
30
+ self.value_comparator._MISSING_FIELD = self._MISSING_FIELD
31
+ # Initialize special expression evaluator with callbacks
32
+ self.special_evaluator = SpecialExpressionEvaluator(self._get_field_value, self._evaluate_node)
33
+ self.special_evaluator._MISSING_FIELD = self._MISSING_FIELD
34
+
35
+ def evaluate(
36
+ self, ast: Dict[str, Any], records: List[Dict[str, Any]], field_mappings: Optional[Dict[str, str]] = None
37
+ ) -> List[Dict[str, Any]]:
38
+ """Evaluate a TQL query against a list of records.
39
+
40
+ Args:
41
+ ast: The parsed TQL query AST
42
+ records: List of dictionaries to evaluate against
43
+ field_mappings: Optional field name mappings
44
+
45
+ Returns:
46
+ List of records that match the query
47
+ """
48
+ results = []
49
+ for record in records:
50
+ if self.evaluate_single(ast, record, field_mappings):
51
+ results.append(record)
52
+ return results
53
+
54
+ def evaluate_single(
55
+ self, ast: Dict[str, Any], record: Dict[str, Any], field_mappings: Optional[Dict[str, str]] = None
56
+ ) -> bool:
57
+ """Evaluate a TQL query against a single record.
58
+
59
+ Args:
60
+ ast: The parsed TQL query AST
61
+ record: Dictionary to evaluate against
62
+ field_mappings: Optional field name mappings
63
+
64
+ Returns:
65
+ True if the record matches the query
66
+ """
67
+ field_mappings = field_mappings or {}
68
+ return self._evaluate_node(ast, record, field_mappings)
69
+
70
+ def _evaluate_node(self, node: Any, record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
71
+ """Evaluate a single AST node against a record.
72
+
73
+ Args:
74
+ node: AST node to evaluate
75
+ record: Record to evaluate against
76
+ field_mappings: Field name mappings
77
+
78
+ Returns:
79
+ Boolean result of evaluation
80
+ """
81
+ if isinstance(node, dict):
82
+ node_type = node.get("type")
83
+
84
+ if node_type == "comparison":
85
+ return self._evaluate_comparison(node, record, field_mappings)
86
+ elif node_type == "logical_op":
87
+ return self._evaluate_logical_op(node, record, field_mappings)
88
+ elif node_type == "unary_op":
89
+ return self._evaluate_unary_op(node, record, field_mappings)
90
+ elif node_type == "collection_op":
91
+ return self._evaluate_collection_op(node, record, field_mappings)
92
+ elif node_type == "geo_expr":
93
+ return self.special_evaluator.evaluate_geo_expr(node, record, field_mappings)
94
+ elif node_type == "nslookup_expr":
95
+ return self.special_evaluator.evaluate_nslookup_expr(node, record, field_mappings)
96
+
97
+ # Unknown node type
98
+ return False
99
+
100
+ def _evaluate_comparison(
101
+ self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
102
+ ) -> bool:
103
+ """Evaluate a comparison operation.
104
+
105
+ Args:
106
+ node: Comparison node with field, operator, and value
107
+ record: Record to evaluate against
108
+ field_mappings: Field name mappings
109
+
110
+ Returns:
111
+ Boolean result of comparison
112
+ """
113
+ field_name = node["field"]
114
+ operator = node["operator"]
115
+ expected_value = node["value"]
116
+ field_mutators = node.get("field_mutators", [])
117
+ value_mutators = node.get("value_mutators", [])
118
+ type_hint = node.get("type_hint")
119
+
120
+ # Apply field mapping
121
+ actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
122
+
123
+ # Get the field value
124
+ field_value = self._get_field_value(record, actual_field)
125
+
126
+ # Apply field mutators if any
127
+ if field_mutators and field_value is not self._MISSING_FIELD:
128
+ field_value = apply_mutators(field_value, field_mutators, field_name, record)
129
+
130
+ # Apply value mutators if any
131
+ if value_mutators:
132
+ expected_value = apply_mutators(expected_value, value_mutators, field_name, record)
133
+
134
+ # Apply type hint if specified
135
+ if type_hint and field_value is not self._MISSING_FIELD:
136
+ field_value = self.field_accessor.apply_type_hint(
137
+ field_value, type_hint, field_name, operator, field_mappings
138
+ )
139
+
140
+ # Perform the comparison
141
+ return self.value_comparator.compare_values(field_value, operator, expected_value)
142
+
143
+ def _evaluate_logical_op(
144
+ self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
145
+ ) -> bool:
146
+ """Evaluate a logical operation (AND/OR).
147
+
148
+ Args:
149
+ node: Logical operation node
150
+ record: Record to evaluate against
151
+ field_mappings: Field name mappings
152
+
153
+ Returns:
154
+ Boolean result of logical operation
155
+ """
156
+ operator = node["operator"]
157
+ left = node["left"]
158
+ right = node["right"]
159
+
160
+ if operator == "and":
161
+ # Short-circuit evaluation for AND
162
+ left_result = self._evaluate_node(left, record, field_mappings)
163
+ if not left_result:
164
+ return False
165
+ return self._evaluate_node(right, record, field_mappings)
166
+ elif operator == "or":
167
+ # Short-circuit evaluation for OR
168
+ left_result = self._evaluate_node(left, record, field_mappings)
169
+ if left_result:
170
+ return True
171
+ return self._evaluate_node(right, record, field_mappings)
172
+ else:
173
+ raise ValueError(f"Unknown logical operator: {operator}")
174
+
175
+ def _evaluate_unary_op(self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
176
+ """Evaluate a unary operation (NOT).
177
+
178
+ Args:
179
+ node: Unary operation node
180
+ record: Record to evaluate against
181
+ field_mappings: Field name mappings
182
+
183
+ Returns:
184
+ Boolean result of unary operation
185
+ """
186
+ operator = node["operator"]
187
+ operand = node["operand"]
188
+
189
+ if operator == "not":
190
+ # Handle special optimizations for NOT operations
191
+ # NOT (field exists) and NOT (field is null) need special handling
192
+
193
+ # First check if the operand would fail due to missing fields
194
+ # But NOT of an exists check on a missing field should still be True
195
+ if self._is_exists_operation(operand):
196
+ # NOT EXISTS check - return opposite of exists
197
+ return not self._evaluate_node(operand, record, field_mappings)
198
+ elif self._is_null_operation(operand):
199
+ # NOT (field IS NULL) check
200
+ return not self._evaluate_node(operand, record, field_mappings)
201
+ elif self._is_not_null_operation(operand):
202
+ # NOT (field IS NOT NULL) - double negative
203
+ # This should only be True if the field exists with a null value
204
+ # For missing fields, it should remain False
205
+ # field = operand.get("field") # Not needed - handled by _operand_has_missing_fields
206
+ if self._operand_has_missing_fields(operand, record, field_mappings):
207
+ # Missing field - NOT (IS NOT NULL) is False
208
+ return False
209
+ else:
210
+ # Field exists - evaluate normally
211
+ return not self._evaluate_node(operand, record, field_mappings)
212
+ elif self._operand_has_missing_fields(operand, record, field_mappings):
213
+ # For operations on missing fields (except exists/null checks), NOT returns True
214
+ # This matches OpenSearch behavior where must_not includes docs with missing fields
215
+ return True
216
+ else:
217
+ # Standard NOT operation
218
+ return not self._evaluate_node(operand, record, field_mappings)
219
+ else:
220
+ raise ValueError(f"Unknown unary operator: {operator}")
221
+
222
+ def _evaluate_collection_op( # noqa: C901
223
+ self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
224
+ ) -> bool:
225
+ """Evaluate a collection operation (ANY/ALL).
226
+
227
+ Args:
228
+ node: Collection operation node
229
+ record: Record to evaluate against
230
+ field_mappings: Field name mappings
231
+
232
+ Returns:
233
+ Boolean result of collection operation
234
+ """
235
+ operator = node["operator"]
236
+ field_name = node["field"]
237
+ comparison_operator = node["comparison_operator"]
238
+ expected_value = node["value"]
239
+ field_mutators = node.get("field_mutators", [])
240
+
241
+ # Apply field mapping
242
+ actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
243
+
244
+ # Get the field value
245
+ field_value = self._get_field_value(record, actual_field)
246
+
247
+ # If field is missing, return False
248
+ if field_value is self._MISSING_FIELD:
249
+ return False
250
+
251
+ # Apply mutators if any
252
+ if field_mutators:
253
+ field_value = self._apply_collection_mutators(field_value, field_mutators, field_name, record)
254
+
255
+ # For non-list values, convert to single-element list
256
+ if not isinstance(field_value, (list, tuple, set)):
257
+ field_value = [field_value]
258
+
259
+ # Evaluate the collection operation
260
+ if operator == "any":
261
+ # ANY: at least one element must match
262
+ for element in field_value:
263
+ if self.value_comparator.compare_values(element, comparison_operator, expected_value):
264
+ return True
265
+ return False
266
+ elif operator == "all":
267
+ # ALL: all elements must match
268
+ if not field_value: # Empty collection
269
+ return False
270
+ for element in field_value:
271
+ if not self.value_comparator.compare_values(element, comparison_operator, expected_value):
272
+ return False
273
+ return True
274
+ else:
275
+ raise ValueError(f"Unknown collection operator: {operator}")
276
+
277
+ def _get_field_value(self, record: Dict[str, Any], field_path: str) -> Any:
278
+ """Get a field value from a record, supporting nested field access.
279
+
280
+ Args:
281
+ record: The record dictionary
282
+ field_path: Dot-separated field path (e.g., "user.name")
283
+
284
+ Returns:
285
+ The field value or _MISSING_FIELD if not found
286
+ """
287
+ return self.field_accessor.get_field_value(record, field_path)
288
+
289
+ def _operand_has_missing_fields(self, node: Any, record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
290
+ """Check if an operand references missing fields.
291
+
292
+ This is used for NOT operations to handle missing field cases properly.
293
+
294
+ Args:
295
+ node: AST node to check
296
+ record: Record to check against
297
+ field_mappings: Field name mappings
298
+
299
+ Returns:
300
+ True if the operand references any missing fields
301
+ """
302
+ if isinstance(node, dict):
303
+ node_type = node.get("type")
304
+
305
+ if node_type == "comparison":
306
+ field_name = node["field"]
307
+ # Apply field mapping
308
+ actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
309
+ # Check if the field exists
310
+ field_value = self._get_field_value(record, actual_field)
311
+ return field_value is self._MISSING_FIELD
312
+ elif node_type == "logical_op":
313
+ # For logical operations, check both sides
314
+ left_missing = self._operand_has_missing_fields(node["left"], record, field_mappings)
315
+ right_missing = self._operand_has_missing_fields(node["right"], record, field_mappings)
316
+ return left_missing or right_missing
317
+ elif node_type == "unary_op":
318
+ # Don't recurse through NOT operators - they handle missing fields themselves
319
+ return False
320
+ elif node_type == "collection_op":
321
+ field_name = node["field"]
322
+ # Apply field mapping
323
+ actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
324
+ # Check if the field exists
325
+ field_value = self._get_field_value(record, actual_field)
326
+ return field_value is self._MISSING_FIELD
327
+
328
+ return False
329
+
330
+ def _is_exists_operation(self, node: Any) -> bool:
331
+ """Check if a node is an exists operation."""
332
+ if isinstance(node, dict) and node.get("type") == "comparison":
333
+ return node.get("operator") in ["exists", "not_exists"]
334
+ return False
335
+
336
+ def _is_null_operation(self, node: Any) -> bool:
337
+ """Check if a node is checking for null (field IS NULL)."""
338
+ if isinstance(node, dict) and node.get("type") == "comparison":
339
+ if node.get("operator") == "is":
340
+ value = node.get("value")
341
+ return value is None or (isinstance(value, str) and value.lower() == "null")
342
+ return False
343
+
344
+ def _is_not_null_operation(self, node: Any) -> bool:
345
+ """Check if a node is checking for not null (field IS NOT NULL)."""
346
+ if isinstance(node, dict) and node.get("type") == "comparison":
347
+ if node.get("operator") == "is_not":
348
+ value = node.get("value")
349
+ return value is None or (isinstance(value, str) and value.lower() == "null")
350
+ return False
351
+
352
+ def _apply_collection_mutators(
353
+ self, field_value: Any, mutators: List[Dict[str, Any]], field_name: str, record: Dict[str, Any]
354
+ ) -> Any:
355
+ """Apply mutators that work on collections.
356
+
357
+ Some mutators like split() can convert single values to arrays.
358
+
359
+ Args:
360
+ field_value: Original field value
361
+ mutators: List of mutators to apply
362
+ field_name: The field name
363
+ record: The record being processed
364
+
365
+ Returns:
366
+ Mutated value
367
+ """
368
+ # Apply mutators
369
+ result = apply_mutators(field_value, mutators, field_name, record)
370
+
371
+ # Check if any mutator converted to array
372
+ for mutator in mutators:
373
+ if mutator.get("name") == "split":
374
+ # Split always returns a list
375
+ if not isinstance(result, (list, tuple)):
376
+ result = [result]
377
+ break
378
+
379
+ return result
@@ -0,0 +1,131 @@
1
+ # Evaluator Components
2
+
3
+ This package contains the modular components for in-memory query evaluation.
4
+
5
+ ## Overview
6
+
7
+ The evaluator components package provides the building blocks for evaluating TQL queries against Python dictionaries (records).
8
+
9
+ ### Components
10
+
11
+ #### `field_access.py` - Field Access Utilities
12
+ Handles accessing values from nested dictionary structures:
13
+ - Dot-notation field paths (e.g., `user.profile.name`)
14
+ - Array indexing support (e.g., `items.0.price`)
15
+ - Field mapping resolution
16
+ - Type hint application
17
+ - Missing field handling
18
+
19
+ **Key Classes:**
20
+ - `FieldAccessor` - Field value extraction
21
+
22
+ **Key Methods:**
23
+ - `get_field_value()` - Extract value from nested path
24
+ - `apply_field_mapping()` - Resolve field name mappings
25
+ - `apply_type_hint()` - Convert values based on type hints
26
+
27
+ #### `value_comparison.py` - Value Comparison Operations
28
+ Implements all TQL comparison operators:
29
+ - Equality and inequality operators
30
+ - Range comparisons (>, <, >=, <=, between)
31
+ - String operations (contains, startswith, endswith)
32
+ - Pattern matching (regexp)
33
+ - List operations (in, not_in)
34
+ - Array operators (any, all, not_any, not_all)
35
+ - Network operations (cidr)
36
+ - Null handling (is, is_not)
37
+
38
+ **Key Classes:**
39
+ - `ValueComparator` - Comparison operations
40
+
41
+ **Key Methods:**
42
+ - `compare_values()` - Main comparison entry point
43
+ - `_convert_numeric()` - Smart type conversion
44
+ - `_check_cidr()` - CIDR range matching
45
+
46
+ #### `special_expressions.py` - Special Expression Evaluators
47
+ Handles geo() and nslookup() expressions:
48
+ - GeoIP lookups with enrichment
49
+ - DNS lookups and resolution
50
+ - Conditional evaluation on enriched data
51
+ - Caching of enrichment results
52
+
53
+ **Key Classes:**
54
+ - `SpecialExpressionEvaluator` - Special expression handler
55
+
56
+ **Key Methods:**
57
+ - `evaluate_geo_expr()` - Evaluate geo() expressions
58
+ - `evaluate_nslookup_expr()` - Evaluate nslookup() expressions
59
+
60
+ ## Value Comparison Behavior
61
+
62
+ ### Missing Fields
63
+ - Most operators return `False` for missing fields
64
+ - `not_exists` returns `True` for missing fields
65
+ - Negated string operators return `True` for missing fields
66
+
67
+ ### Null Values
68
+ - `exists` returns `True` (field exists even if null)
69
+ - `is null` returns `True`
70
+ - Other operators return `False`
71
+
72
+ ### Type Conversion
73
+ - Numeric strings are converted for comparison
74
+ - Boolean strings ("true"/"false") are converted
75
+ - CIDR operations validate IP addresses
76
+
77
+ ### Array Handling
78
+ - `contains` checks if value is in array
79
+ - `any` checks if any element matches
80
+ - `all` checks if all elements match
81
+ - Single values are treated as one-element arrays for collection operators
82
+
83
+ ## Architecture
84
+
85
+ ```
86
+ TQLEvaluator (main class)
87
+ ├── FieldAccessor (field extraction)
88
+ ├── ValueComparator (comparisons)
89
+ └── SpecialExpressionEvaluator (geo/nslookup)
90
+ ```
91
+
92
+ ## Special Features
93
+
94
+ ### Sentinel Value
95
+ The evaluator uses a sentinel value `_MISSING_FIELD` to distinguish between:
96
+ - Fields that don't exist in the record
97
+ - Fields that exist but have `None` value
98
+
99
+ This distinction is important for operators like `exists` and `is null`.
100
+
101
+ ### Type Hints
102
+ Support for explicit type conversion:
103
+ ```
104
+ ip_field:ip cidr "10.0.0.0/8"
105
+ count:integer > 100
106
+ active:boolean = true
107
+ ```
108
+
109
+ ### Mutator Support
110
+ Field and value mutators are applied during evaluation:
111
+ - Field mutators transform the field value before comparison
112
+ - Value mutators transform the expected value
113
+ - Special mutators (geo, nslookup) enrich data
114
+
115
+ ## Usage
116
+
117
+ Used internally by `TQLEvaluator`:
118
+
119
+ ```python
120
+ from tql import TQL
121
+
122
+ tql = TQL()
123
+ data = [
124
+ {"name": "Alice", "age": 30, "city": "NYC"},
125
+ {"name": "Bob", "age": 25, "city": "LA"}
126
+ ]
127
+
128
+ # Evaluation happens internally
129
+ results = tql.query(data, "age > 27 AND city = 'NYC'")
130
+ # Returns: [{"name": "Alice", "age": 30, "city": "NYC"}]
131
+ ```
@@ -0,0 +1,17 @@
1
+ """Evaluator components for TQL.
2
+
3
+ This package contains modular components for TQL evaluation:
4
+ - field_access: Field value extraction and type handling
5
+ - value_comparison: Value comparison operations and operator implementations
6
+ - special_expressions: Geo and NSLookup expression evaluators
7
+ """
8
+
9
+ from .field_access import FieldAccessor
10
+ from .special_expressions import SpecialExpressionEvaluator
11
+ from .value_comparison import ValueComparator
12
+
13
+ __all__ = [
14
+ "FieldAccessor",
15
+ "ValueComparator",
16
+ "SpecialExpressionEvaluator",
17
+ ]