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
tql/stats_transformer.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Clean, user-friendly stats result transformation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatsResultTransformer:
|
|
7
|
+
"""Transform OpenSearch aggregation results into clean, user-friendly format."""
|
|
8
|
+
|
|
9
|
+
def transform(self, response: Dict[str, Any], stats_ast: Dict[str, Any]) -> Dict[str, Any]:
|
|
10
|
+
"""Transform OpenSearch response to clean format.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
response: Raw OpenSearch response
|
|
14
|
+
stats_ast: Stats AST for context
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Structured results with metadata
|
|
18
|
+
"""
|
|
19
|
+
aggregations = stats_ast.get("aggregations", [])
|
|
20
|
+
group_by_fields = stats_ast.get("group_by", [])
|
|
21
|
+
|
|
22
|
+
# Build result structure
|
|
23
|
+
result: Dict[str, Any] = {"type": "stats"}
|
|
24
|
+
|
|
25
|
+
# Add operation info
|
|
26
|
+
if len(aggregations) == 1:
|
|
27
|
+
result["operation"] = aggregations[0]["function"]
|
|
28
|
+
result["field"] = aggregations[0]["field"]
|
|
29
|
+
else:
|
|
30
|
+
result["operations"] = [{"function": agg["function"], "field": agg["field"]} for agg in aggregations]
|
|
31
|
+
|
|
32
|
+
# Add group_by info if present
|
|
33
|
+
if group_by_fields:
|
|
34
|
+
result["group_by"] = group_by_fields
|
|
35
|
+
|
|
36
|
+
# Transform the actual data
|
|
37
|
+
if not group_by_fields:
|
|
38
|
+
# Simple aggregation (no grouping)
|
|
39
|
+
result["values"] = self._transform_simple(response, aggregations)
|
|
40
|
+
else:
|
|
41
|
+
# Grouped aggregation
|
|
42
|
+
result["values"] = self._transform_grouped(response, aggregations, group_by_fields)
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
def _transform_simple(
|
|
47
|
+
self, response: Dict[str, Any], aggregations: List[Dict[str, Any]]
|
|
48
|
+
) -> Union[int, float, Dict[str, Union[int, float, None]], None]:
|
|
49
|
+
"""Transform simple aggregation (no grouping).
|
|
50
|
+
|
|
51
|
+
For single aggregation: returns the value directly
|
|
52
|
+
For multiple aggregations: returns a dict of function->value
|
|
53
|
+
"""
|
|
54
|
+
aggs_data = response.get("aggregations", {})
|
|
55
|
+
|
|
56
|
+
if len(aggregations) == 1:
|
|
57
|
+
# Single aggregation - return just the value
|
|
58
|
+
agg = aggregations[0]
|
|
59
|
+
agg_key = self._get_agg_key(agg)
|
|
60
|
+
return self._extract_value(aggs_data.get(agg_key, {}), agg["function"])
|
|
61
|
+
else:
|
|
62
|
+
# Multiple aggregations - return dict
|
|
63
|
+
result: Dict[str, Union[int, float, None]] = {}
|
|
64
|
+
for i, agg in enumerate(aggregations):
|
|
65
|
+
agg_key = self._get_agg_key(agg, i)
|
|
66
|
+
value = self._extract_value(aggs_data.get(agg_key, {}), agg["function"])
|
|
67
|
+
# Use clean key: just function name or alias
|
|
68
|
+
clean_key = agg.get("alias") or agg["function"]
|
|
69
|
+
result[clean_key] = value
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def _transform_grouped(
|
|
73
|
+
self, response: Dict[str, Any], aggregations: List[Dict[str, Any]], group_by_fields: List[str]
|
|
74
|
+
) -> List[Dict[str, Any]]:
|
|
75
|
+
"""Transform grouped aggregation.
|
|
76
|
+
|
|
77
|
+
Returns a list of dictionaries, each containing:
|
|
78
|
+
- The grouping field value(s)
|
|
79
|
+
- The aggregation result(s)
|
|
80
|
+
"""
|
|
81
|
+
aggs_data = response.get("aggregations", {})
|
|
82
|
+
|
|
83
|
+
# Get the first grouping key
|
|
84
|
+
first_group_key = f"group_by_{group_by_fields[0]}"
|
|
85
|
+
grouped_data = aggs_data.get(first_group_key, {})
|
|
86
|
+
buckets = grouped_data.get("buckets", [])
|
|
87
|
+
|
|
88
|
+
results = []
|
|
89
|
+
for bucket in buckets:
|
|
90
|
+
if len(group_by_fields) == 1:
|
|
91
|
+
# Single group by - simple case
|
|
92
|
+
entry = {group_by_fields[0]: bucket.get("key")}
|
|
93
|
+
|
|
94
|
+
# Add aggregation values
|
|
95
|
+
self._add_aggregation_values(entry, bucket, aggregations)
|
|
96
|
+
results.append(entry)
|
|
97
|
+
else:
|
|
98
|
+
# Multiple group by - process nested buckets
|
|
99
|
+
nested_results = self._process_nested_buckets(bucket, group_by_fields, aggregations)
|
|
100
|
+
results.extend(nested_results)
|
|
101
|
+
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
def _process_nested_buckets(
|
|
105
|
+
self, bucket: Dict[str, Any], group_by_fields: List[str], aggregations: List[Dict[str, Any]], level: int = 0
|
|
106
|
+
) -> List[Dict[str, Any]]:
|
|
107
|
+
"""Process nested buckets for multi-field grouping."""
|
|
108
|
+
results = []
|
|
109
|
+
|
|
110
|
+
if level >= len(group_by_fields) - 1:
|
|
111
|
+
# We're at the last level, return single entry
|
|
112
|
+
entry = {}
|
|
113
|
+
# Add all group keys up to this level
|
|
114
|
+
entry[group_by_fields[level]] = bucket.get("key")
|
|
115
|
+
self._add_aggregation_values(entry, bucket, aggregations)
|
|
116
|
+
return [entry]
|
|
117
|
+
|
|
118
|
+
# Get current field key
|
|
119
|
+
current_field = group_by_fields[level]
|
|
120
|
+
current_key = bucket.get("key")
|
|
121
|
+
|
|
122
|
+
# Look for next level
|
|
123
|
+
next_field = group_by_fields[level + 1]
|
|
124
|
+
next_group_key = f"group_by_{next_field}"
|
|
125
|
+
|
|
126
|
+
if next_group_key in bucket:
|
|
127
|
+
sub_buckets = bucket[next_group_key].get("buckets", [])
|
|
128
|
+
for sub_bucket in sub_buckets:
|
|
129
|
+
# Process each sub-bucket recursively
|
|
130
|
+
sub_results = self._process_nested_buckets(sub_bucket, group_by_fields, aggregations, level + 1)
|
|
131
|
+
# Add current field to each result
|
|
132
|
+
for result in sub_results:
|
|
133
|
+
result[current_field] = current_key
|
|
134
|
+
results.extend(sub_results)
|
|
135
|
+
|
|
136
|
+
return results
|
|
137
|
+
|
|
138
|
+
def _add_aggregation_values(
|
|
139
|
+
self, entry: Dict[str, Any], bucket: Dict[str, Any], aggregations: List[Dict[str, Any]]
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Add aggregation values to an entry."""
|
|
142
|
+
if len(aggregations) == 1:
|
|
143
|
+
# Single aggregation
|
|
144
|
+
agg = aggregations[0]
|
|
145
|
+
agg_key = self._get_agg_key(agg)
|
|
146
|
+
value = self._extract_value(bucket.get(agg_key, {}), agg["function"])
|
|
147
|
+
value_key = agg.get("alias") or agg["function"]
|
|
148
|
+
entry[value_key] = value
|
|
149
|
+
else:
|
|
150
|
+
# Multiple aggregations
|
|
151
|
+
for i, agg in enumerate(aggregations):
|
|
152
|
+
agg_key = self._get_agg_key(agg, i)
|
|
153
|
+
value = self._extract_value(bucket.get(agg_key, {}), agg["function"])
|
|
154
|
+
value_key = agg.get("alias") or agg["function"]
|
|
155
|
+
entry[value_key] = value
|
|
156
|
+
|
|
157
|
+
def _get_agg_key(self, agg: Dict[str, Any], index: int = 0) -> str:
|
|
158
|
+
"""Get the OpenSearch aggregation key."""
|
|
159
|
+
return agg.get("alias") or f"{agg['function']}_{agg['field']}_{index}"
|
|
160
|
+
|
|
161
|
+
def _extract_value(self, agg_result: Dict[str, Any], function: str) -> Union[int, float, None]:
|
|
162
|
+
"""Extract clean value from OpenSearch aggregation result."""
|
|
163
|
+
if function in ["count", "unique_count"]:
|
|
164
|
+
# Count functions default to 0 if missing
|
|
165
|
+
return agg_result.get("value", 0)
|
|
166
|
+
elif function in ["sum", "min", "max", "average", "avg"]:
|
|
167
|
+
# Numeric functions return None if missing (no default)
|
|
168
|
+
return agg_result.get("value")
|
|
169
|
+
elif function == "median":
|
|
170
|
+
values = agg_result.get("values", {})
|
|
171
|
+
return values.get("50.0") or values.get("50")
|
|
172
|
+
elif function == "percentile":
|
|
173
|
+
# Percentiles return a values dict with keys as percentile strings
|
|
174
|
+
values = agg_result.get("values", {})
|
|
175
|
+
# Get the first (and usually only) percentile value
|
|
176
|
+
if values:
|
|
177
|
+
# Keys are like "50.0", "95.0", etc.
|
|
178
|
+
first_key = list(values.keys())[0]
|
|
179
|
+
return values.get(first_key)
|
|
180
|
+
return None
|
|
181
|
+
elif function == "std":
|
|
182
|
+
return agg_result.get("std_deviation")
|
|
183
|
+
else:
|
|
184
|
+
return None
|
tql/validators.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Validator functions for the TQL query language.
|
|
2
|
+
|
|
3
|
+
This module provides functions to validate field values against provided validators.
|
|
4
|
+
Validators can be provided as:
|
|
5
|
+
- A Python type (e.g. int, float, str, bool, list, dict).
|
|
6
|
+
- A tuple of the form (container_type, element_type) to check container element types.
|
|
7
|
+
- A callable that accepts the field value and returns a boolean.
|
|
8
|
+
- A string representing a type (for backward compatibility) like "int", "string", etc.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ipaddress
|
|
12
|
+
from typing import Any, Callable, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_field_with_validator( # noqa: C901
|
|
16
|
+
field: Any, validator: Union[type, tuple, Callable[[Any], bool], str]
|
|
17
|
+
) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Validate a field value against a single validator.
|
|
20
|
+
|
|
21
|
+
The validator may be:
|
|
22
|
+
- A Python type, e.g. int, float, str, bool, list, dict.
|
|
23
|
+
- A tuple (container_type, element_type) to ensure that the field is a container of the element type.
|
|
24
|
+
- A callable that accepts the field and returns a boolean.
|
|
25
|
+
- A string shorthand for a type check (e.g. "int", "string", "list", "ip", "ipv4", "ipv6").
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
field: The field value to validate.
|
|
29
|
+
validator: The validator to apply.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if the field passes the validator, False otherwise.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If the validator is not of a supported form.
|
|
36
|
+
"""
|
|
37
|
+
# If validator is a string, map it to a type check.
|
|
38
|
+
if isinstance(validator, str):
|
|
39
|
+
val = validator.lower()
|
|
40
|
+
if val == "int":
|
|
41
|
+
return isinstance(field, int)
|
|
42
|
+
if val == "float":
|
|
43
|
+
return isinstance(field, float)
|
|
44
|
+
if val == "string":
|
|
45
|
+
return isinstance(field, str)
|
|
46
|
+
if val == "bool":
|
|
47
|
+
return isinstance(field, bool)
|
|
48
|
+
if val == "list":
|
|
49
|
+
return isinstance(field, list)
|
|
50
|
+
if val == "dict":
|
|
51
|
+
return isinstance(field, dict)
|
|
52
|
+
if val == "none":
|
|
53
|
+
return field is None
|
|
54
|
+
if val == "ip":
|
|
55
|
+
try:
|
|
56
|
+
ipaddress.ip_address(field)
|
|
57
|
+
return True
|
|
58
|
+
except ValueError:
|
|
59
|
+
return False
|
|
60
|
+
if val == "ipv4":
|
|
61
|
+
try:
|
|
62
|
+
ipaddress.IPv4Address(field)
|
|
63
|
+
return True
|
|
64
|
+
except ValueError:
|
|
65
|
+
return False
|
|
66
|
+
if val == "ipv6":
|
|
67
|
+
try:
|
|
68
|
+
ipaddress.IPv6Address(field)
|
|
69
|
+
return True
|
|
70
|
+
except ValueError:
|
|
71
|
+
return False
|
|
72
|
+
raise ValueError(f"Unknown string validator: {validator}")
|
|
73
|
+
|
|
74
|
+
# If validator is a type, use isinstance.
|
|
75
|
+
if isinstance(validator, type):
|
|
76
|
+
return isinstance(field, validator)
|
|
77
|
+
|
|
78
|
+
# If validator is a tuple of (container_type, element_type)
|
|
79
|
+
if isinstance(validator, tuple) and len(validator) == 2:
|
|
80
|
+
container_type, element_type = validator
|
|
81
|
+
if not isinstance(field, container_type):
|
|
82
|
+
return False
|
|
83
|
+
return all(isinstance(item, element_type) for item in field)
|
|
84
|
+
|
|
85
|
+
# If validator is callable, use it.
|
|
86
|
+
if callable(validator):
|
|
87
|
+
return bool(validator(field))
|
|
88
|
+
|
|
89
|
+
raise ValueError("Validator must be a type, a tuple, a callable, or a recognized string.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_field(
|
|
93
|
+
field: Any, validators: Optional[List[Union[type, tuple, Callable[[Any], bool], str]]] = None
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Validate a field value against a list of validators.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
field: The field value to validate.
|
|
100
|
+
validators: A list of validators to apply.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if the field passes all validators, False otherwise.
|
|
104
|
+
"""
|
|
105
|
+
if validators is None:
|
|
106
|
+
return True
|
|
107
|
+
for v in validators:
|
|
108
|
+
if not validate_field_with_validator(field, v):
|
|
109
|
+
return False
|
|
110
|
+
return True
|