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,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