tellaro-query-language 0.2.0__py3-none-any.whl → 0.2.2__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.2.0.dist-info → tellaro_query_language-0.2.2.dist-info}/METADATA +24 -1
- {tellaro_query_language-0.2.0.dist-info → tellaro_query_language-0.2.2.dist-info}/RECORD +27 -27
- tql/core.py +225 -54
- tql/core_components/opensearch_operations.py +415 -99
- tql/core_components/stats_operations.py +11 -1
- tql/evaluator.py +39 -2
- tql/evaluator_components/special_expressions.py +25 -6
- tql/evaluator_components/value_comparison.py +31 -3
- tql/mutator_analyzer.py +640 -242
- tql/mutators/__init__.py +5 -1
- tql/mutators/dns.py +76 -53
- tql/mutators/security.py +101 -100
- tql/mutators/string.py +74 -0
- tql/opensearch_components/field_mapping.py +9 -3
- tql/opensearch_components/lucene_converter.py +12 -0
- tql/opensearch_components/query_converter.py +134 -25
- tql/opensearch_mappings.py +2 -2
- tql/opensearch_stats.py +170 -39
- tql/parser.py +92 -37
- tql/parser_components/ast_builder.py +37 -1
- tql/parser_components/field_extractor.py +9 -1
- tql/parser_components/grammar.py +32 -8
- tql/post_processor.py +489 -31
- tql/stats_evaluator.py +170 -12
- {tellaro_query_language-0.2.0.dist-info → tellaro_query_language-0.2.2.dist-info}/LICENSE +0 -0
- {tellaro_query_language-0.2.0.dist-info → tellaro_query_language-0.2.2.dist-info}/WHEEL +0 -0
- {tellaro_query_language-0.2.0.dist-info → tellaro_query_language-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tellaro-query-language
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A flexible, human-friendly query language for searching and filtering structured data
|
|
5
5
|
Home-page: https://github.com/tellaro/tellaro-query-language
|
|
6
6
|
License: MIT
|
|
@@ -24,6 +24,7 @@ Requires-Dist: opensearch-dsl (>=2.1.0,<3.0.0) ; extra == "opensearch"
|
|
|
24
24
|
Requires-Dist: opensearch-py (>=2.4.2,<3.0.0) ; extra == "opensearch"
|
|
25
25
|
Requires-Dist: pyparsing (>=3.2.1,<4.0.0)
|
|
26
26
|
Requires-Dist: setuptools (>=80.0.0,<81.0.0)
|
|
27
|
+
Requires-Dist: urllib3 (>=2.5.0,<3.0.0)
|
|
27
28
|
Project-URL: Documentation, https://github.com/tellaro/tellaro-query-language/tree/main/docs
|
|
28
29
|
Project-URL: Repository, https://github.com/tellaro/tellaro-query-language
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
@@ -217,6 +218,28 @@ poetry run tests
|
|
|
217
218
|
|
|
218
219
|
**Note**: The development setup uses `python-dotenv` to load OpenSearch credentials from `.env` files for integration testing. This is NOT required when using TQL as a package - see the [Package Usage Guide](docs/package-usage-guide.md) for production configuration patterns.
|
|
219
220
|
|
|
221
|
+
### TQL Playground
|
|
222
|
+
|
|
223
|
+
The repository includes an interactive web playground for testing TQL queries:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Navigate to the playground directory
|
|
227
|
+
cd playground
|
|
228
|
+
|
|
229
|
+
# Start with Docker (recommended)
|
|
230
|
+
docker-compose up
|
|
231
|
+
|
|
232
|
+
# Or start with OpenSearch included
|
|
233
|
+
docker-compose --profile opensearch up
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Access the playground at:
|
|
237
|
+
- Frontend: http://localhost:5173
|
|
238
|
+
- API: http://localhost:8000
|
|
239
|
+
- API Docs: http://localhost:8000/docs
|
|
240
|
+
|
|
241
|
+
The playground uses your local TQL source code, so any changes you make are immediately reflected. See [playground/README.md](playground/README.md) for more details.
|
|
242
|
+
|
|
220
243
|
### File Operations
|
|
221
244
|
|
|
222
245
|
```python
|
|
@@ -4,53 +4,53 @@ tql/cache/__init__.py,sha256=GIzIEMZUZEYJj72sAhuVLEG-OJEKUG2srUWNM3Ix-T8,213
|
|
|
4
4
|
tql/cache/base.py,sha256=0b-8uyh3JltayGmXQI45snTqsM5sQu9u0KcNvZIRa-I,687
|
|
5
5
|
tql/cache/memory.py,sha256=ibcmQSAxNvqCy6DksbU7gLu6UArYp1u3fW-oLubxtV0,2056
|
|
6
6
|
tql/cache/redis.py,sha256=ZU_IsVDvpSYpNvPfnZ4iulJDODpEGx3c4dkXLzPzPVc,2309
|
|
7
|
-
tql/core.py,sha256=
|
|
7
|
+
tql/core.py,sha256=bMPrcuutY-1yvC-4M7w2y1JxNitMyBSpxPfg8ohjO60,48406
|
|
8
8
|
tql/core_components/README.md,sha256=Rm7w4UHdQ0vPBEFybE5b62IOvSA5Nzq2GRvtBHOapmc,3068
|
|
9
9
|
tql/core_components/__init__.py,sha256=v8BBybPlqV7dkVY9mw1mblvqyAFJZ7Pf_bEc-jAL7FI,643
|
|
10
10
|
tql/core_components/file_operations.py,sha256=Jr0kkxz_OP2KHOAsIr7KMtYe_lbu8LuBUySt2LQbjJw,3925
|
|
11
|
-
tql/core_components/opensearch_operations.py,sha256=
|
|
12
|
-
tql/core_components/stats_operations.py,sha256=
|
|
11
|
+
tql/core_components/opensearch_operations.py,sha256=KvmK1FnkGZFBjBysH_sDjzIRnyUcNn7wzLzuRr1rBlg,54264
|
|
12
|
+
tql/core_components/stats_operations.py,sha256=aqTGAqIFvR6EkSbJEd0qft8Ldy8uiTrK2XI9o5bZUOs,8014
|
|
13
13
|
tql/core_components/validation_operations.py,sha256=_VPXh0HABBjsXF99jFT7B6-5QAPsADOCy6poinGrxeE,22454
|
|
14
|
-
tql/evaluator.py,sha256=
|
|
14
|
+
tql/evaluator.py,sha256=_JYr-wK3F1wvBoNGIBiAEaP6Ot1g2qxZ4lOjPdOqvDk,17698
|
|
15
15
|
tql/evaluator_components/README.md,sha256=c59yf2au34yPhrru7JWgGop_ORteB6w5vfMhsac8j3k,3882
|
|
16
16
|
tql/evaluator_components/__init__.py,sha256=DourRUSYXWPnCghBFj7W0YfMeymT3X8YTDCwnLIyP1c,535
|
|
17
17
|
tql/evaluator_components/field_access.py,sha256=BuXvL9jlv4H77neT70Vh7_qokmzs-d4EbSDA2FB1IT0,6435
|
|
18
|
-
tql/evaluator_components/special_expressions.py,sha256=
|
|
19
|
-
tql/evaluator_components/value_comparison.py,sha256=
|
|
18
|
+
tql/evaluator_components/special_expressions.py,sha256=K6M5pW4Re2kEqxfxj9sc7I_M1tU3pn6LKJ2AfjHeciA,12917
|
|
19
|
+
tql/evaluator_components/value_comparison.py,sha256=pL7-hxdNbzJ53DrTSiDdd7KYbVLChuNwFRLjG7P_1KM,17939
|
|
20
20
|
tql/exceptions.py,sha256=hatIixXci6p57J9RrkfdvmKM_2i-JKb8ViL2kU4z7a8,5550
|
|
21
21
|
tql/geoip_normalizer.py,sha256=tvie-5xevJEeLp2KmjoXDjYdND8AvyVE7lCO8qgUzGY,10486
|
|
22
|
-
tql/mutator_analyzer.py,sha256=
|
|
23
|
-
tql/mutators/__init__.py,sha256=
|
|
22
|
+
tql/mutator_analyzer.py,sha256=OzI7t3C4H0IJOonpywE5LWz2cm5Dco5xnp2RTQOiSWg,55638
|
|
23
|
+
tql/mutators/__init__.py,sha256=eTK8sRw4KXXnTZTn5ETIqwcaIek5rSUIVyZsxTwNNHA,6966
|
|
24
24
|
tql/mutators/base.py,sha256=4Ze_x1sTO11OILXfcF2XN7ttyHcZ4gwn96UXFMMaC6M,2523
|
|
25
|
-
tql/mutators/dns.py,sha256=
|
|
25
|
+
tql/mutators/dns.py,sha256=1IKgHolFLRMR4TOgK0AiLjz5vDtFiqO328mVF4Vzk3s,14428
|
|
26
26
|
tql/mutators/encoding.py,sha256=yt12BJrHAIJfBesP8VOSfVlvJqB1yOmEeT_8QDPvNN8,7985
|
|
27
27
|
tql/mutators/geo.py,sha256=fFQSg_Li3KjFKS3TI26yDzrDpWsmC3MfmgcsxYoQMgM,14507
|
|
28
28
|
tql/mutators/list.py,sha256=949ZrKKhL4INkH2Od8bq7Ey80kFX_23PEfRKueG82cU,7084
|
|
29
29
|
tql/mutators/network.py,sha256=1lZpmKt1GoTfNxiXUmSXkTwJIzPQZnQEgU7ojpBSm3A,5458
|
|
30
|
-
tql/mutators/security.py,sha256=
|
|
31
|
-
tql/mutators/string.py,sha256=
|
|
30
|
+
tql/mutators/security.py,sha256=XyWuPxgpCi-igHKmkbP0_0V-evOc3FG2a1igY8rQRX4,9256
|
|
31
|
+
tql/mutators/string.py,sha256=0pIqIcpbRjUvGexsVV2lREPrJAAlIA2INhjvssLEKxw,9180
|
|
32
32
|
tql/opensearch.py,sha256=J7LhfVJfaXEWtyZqVDqNZaeIbIcUYJr2cQtfKzdyIhM,3362
|
|
33
33
|
tql/opensearch_components/README.md,sha256=gt-qLmmach8Kh7-QwLZmoAxxIL79XIG1EDqJum8PMZE,3756
|
|
34
34
|
tql/opensearch_components/__init__.py,sha256=_zIZY8Fns7mkEcY6w2p9FNRBXtEmmPFFJEcFRfrVyXA,514
|
|
35
|
-
tql/opensearch_components/field_mapping.py,sha256=
|
|
36
|
-
tql/opensearch_components/lucene_converter.py,sha256=
|
|
37
|
-
tql/opensearch_components/query_converter.py,sha256=
|
|
38
|
-
tql/opensearch_mappings.py,sha256=
|
|
39
|
-
tql/opensearch_stats.py,sha256=
|
|
40
|
-
tql/parser.py,sha256=
|
|
35
|
+
tql/opensearch_components/field_mapping.py,sha256=fj388cKVyDXLJKi8giSiGHL9zg4cFRzy0VJ6nIsppSo,18102
|
|
36
|
+
tql/opensearch_components/lucene_converter.py,sha256=OvYTZHNBktPGow1fsVm4TMlvxHSmWrnqo42lFZNxXTo,13175
|
|
37
|
+
tql/opensearch_components/query_converter.py,sha256=vLoBqv7W3ntqUH6hcuT4PDJkGkAGSQCxMvAWC482c0g,41971
|
|
38
|
+
tql/opensearch_mappings.py,sha256=sVLlQlE3eGD7iNNZ_m4F4j5GVzQAJhZyCqDKYRhLRh8,11531
|
|
39
|
+
tql/opensearch_stats.py,sha256=aMV__jtlfogGBnFucsNPazORro2mYTz_C_w9uxOqsMI,24384
|
|
40
|
+
tql/parser.py,sha256=9kewX4IbBL3W5hbq9Xhi4BGrQ4QaoWqz9AJV0Yuf9YA,78665
|
|
41
41
|
tql/parser_components/README.md,sha256=lvQX72ckq2zyotGs8QIHHCIFqaA7bOHwkP44wU8Zoiw,2322
|
|
42
42
|
tql/parser_components/__init__.py,sha256=zBwHBMPJyHSBbaOojf6qTrJYjJg5A6tPUE8nHFdRiQs,521
|
|
43
|
-
tql/parser_components/ast_builder.py,sha256
|
|
43
|
+
tql/parser_components/ast_builder.py,sha256=erHoeKAMzobswoRIXB9xcsZbzQ5-2ZwaYfQgRWoUAa8,9653
|
|
44
44
|
tql/parser_components/error_analyzer.py,sha256=qlCD9vKyW73aeKQYI33P1OjIWSJ3LPd08wuN9cis2fU,4012
|
|
45
|
-
tql/parser_components/field_extractor.py,sha256=
|
|
46
|
-
tql/parser_components/grammar.py,sha256=
|
|
47
|
-
tql/post_processor.py,sha256
|
|
45
|
+
tql/parser_components/field_extractor.py,sha256=eUEkmiYWX2OexanFqhHeX8hcIkRlfIcgMB667e0HRYs,4629
|
|
46
|
+
tql/parser_components/grammar.py,sha256=h58RBshZHXgbP1EmNwmf7dny-fgVloNg-qN4Rivross,20599
|
|
47
|
+
tql/post_processor.py,sha256=MZOJzuWTL2qdvu-AUNMryYF2D-piv8rYH5vCcrLt5-A,50069
|
|
48
48
|
tql/scripts.py,sha256=VOr5vCjIvKlW36kwvJx7JGFIRM16IJZlbJcWlBexBtk,3728
|
|
49
|
-
tql/stats_evaluator.py,sha256=
|
|
49
|
+
tql/stats_evaluator.py,sha256=OQZuNLwLHAtWrwAh3utdtr1fQt3tftCs6L-1G1NQCGQ,22318
|
|
50
50
|
tql/stats_transformer.py,sha256=MT-4rDWZSySgn4Fuq9H0c-mvwFYLM6FqWpPv2rHX-rE,7588
|
|
51
51
|
tql/validators.py,sha256=e9MlX-zQ_O3M8YP8vXyMjKU8iiJMTh6mMK0iv0_4gTY,3771
|
|
52
|
-
tellaro_query_language-0.2.
|
|
53
|
-
tellaro_query_language-0.2.
|
|
54
|
-
tellaro_query_language-0.2.
|
|
55
|
-
tellaro_query_language-0.2.
|
|
56
|
-
tellaro_query_language-0.2.
|
|
52
|
+
tellaro_query_language-0.2.2.dist-info/LICENSE,sha256=zRhQ85LnW55fWgAjQctckwQ67DX5Jmt64lq343ThZFU,1063
|
|
53
|
+
tellaro_query_language-0.2.2.dist-info/METADATA,sha256=QsCXKY_0aHeMorc4PepJ84ViZbTK53suxILmr868Lkk,15740
|
|
54
|
+
tellaro_query_language-0.2.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
55
|
+
tellaro_query_language-0.2.2.dist-info/entry_points.txt,sha256=H43APfGBMsZkKsUCnFTaqprQPW-Kce2yz2qsBL3dZrw,164
|
|
56
|
+
tellaro_query_language-0.2.2.dist-info/RECORD,,
|
tql/core.py
CHANGED
|
@@ -212,8 +212,10 @@ class TQL:
|
|
|
212
212
|
if query_type == "stats_expr":
|
|
213
213
|
# This is a pure stats query like "| stats count()"
|
|
214
214
|
stats_result = self.stats(data, query)
|
|
215
|
+
# Get viz_hint from the parsed AST
|
|
216
|
+
viz_hint = ast.get("viz_hint")
|
|
215
217
|
# Convert to execute_opensearch format
|
|
216
|
-
result["stats"] = self._convert_stats_result(stats_result)
|
|
218
|
+
result["stats"] = self._convert_stats_result(stats_result, viz_hint)
|
|
217
219
|
result["total"] = len(records)
|
|
218
220
|
return result
|
|
219
221
|
|
|
@@ -232,7 +234,9 @@ class TQL:
|
|
|
232
234
|
|
|
233
235
|
# Apply stats to filtered data
|
|
234
236
|
stats_result = self.stats_evaluator.evaluate_stats(filtered_records, stats_ast)
|
|
235
|
-
|
|
237
|
+
# Get viz_hint from the stats AST
|
|
238
|
+
viz_hint = stats_ast.get("viz_hint")
|
|
239
|
+
result["stats"] = self._convert_stats_result(stats_result, viz_hint)
|
|
236
240
|
result["total"] = len(filtered_records)
|
|
237
241
|
|
|
238
242
|
# Include filtered documents if size > 0
|
|
@@ -241,40 +245,63 @@ class TQL:
|
|
|
241
245
|
|
|
242
246
|
return result
|
|
243
247
|
|
|
244
|
-
# Handle regular filter queries
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
# Handle regular filter queries with mutator analysis
|
|
249
|
+
# Analyze the query for mutators
|
|
250
|
+
from .mutator_analyzer import MutatorAnalyzer
|
|
251
|
+
from .post_processor import QueryPostProcessor
|
|
252
|
+
|
|
253
|
+
analyzer = MutatorAnalyzer(field_mappings=self.field_mappings)
|
|
254
|
+
analysis_result = analyzer.analyze_ast(ast, context="in_memory")
|
|
247
255
|
|
|
256
|
+
# Use the optimized AST for evaluation
|
|
257
|
+
optimized_ast = analysis_result.optimized_ast
|
|
258
|
+
|
|
259
|
+
# First pass: collect all matching records using optimized AST
|
|
260
|
+
matched_records = []
|
|
248
261
|
for record in records:
|
|
249
|
-
# Check if record matches
|
|
250
|
-
if self.evaluator._evaluate_node(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
262
|
+
# Check if record matches using the optimized AST (without array operators)
|
|
263
|
+
if self.evaluator._evaluate_node(optimized_ast, record, self._simple_mappings):
|
|
264
|
+
matched_records.append(record)
|
|
265
|
+
|
|
266
|
+
# Apply post-processing if needed
|
|
267
|
+
if analysis_result.post_processing_requirements:
|
|
268
|
+
processor = QueryPostProcessor()
|
|
269
|
+
|
|
270
|
+
# Apply mutators/enrichments
|
|
271
|
+
processed_records = processor.process_results(
|
|
272
|
+
matched_records, analysis_result.post_processing_requirements, track_enrichments=save_enrichment
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Apply filters (for array operators like any/all/none)
|
|
276
|
+
filtered_records = processor.filter_results(processed_records, analysis_result.post_processing_requirements)
|
|
277
|
+
|
|
278
|
+
matched_records = filtered_records
|
|
279
|
+
result["post_processing_applied"] = True
|
|
280
|
+
|
|
281
|
+
# Add post-processing stats
|
|
282
|
+
result["post_processing_stats"] = {
|
|
283
|
+
"documents_retrieved": len(processed_records),
|
|
284
|
+
"documents_returned": len(filtered_records),
|
|
285
|
+
"documents_filtered": len(processed_records) - len(filtered_records),
|
|
286
|
+
}
|
|
257
287
|
|
|
258
288
|
# Set result data
|
|
259
289
|
result["total"] = len(matched_records)
|
|
260
290
|
if size > 0:
|
|
261
291
|
result["results"] = matched_records[:size]
|
|
262
292
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
293
|
+
# Update health status based on analysis
|
|
294
|
+
result["health_status"] = analysis_result.health_status
|
|
295
|
+
result["health_reasons"] = [
|
|
296
|
+
reason.get("reason", reason.get("description", "")) for reason in analysis_result.health_reasons
|
|
297
|
+
]
|
|
266
298
|
|
|
267
299
|
# Save enrichments if requested
|
|
268
|
-
if save_enrichment and
|
|
269
|
-
# For file sources,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
all_enriched.append(enriched_record)
|
|
274
|
-
|
|
275
|
-
# Save based on file type
|
|
276
|
-
if source_file.lower().endswith(".json"):
|
|
277
|
-
self.file_ops.save_enrichments_to_json(source_file, all_enriched)
|
|
300
|
+
if save_enrichment and result.get("post_processing_applied") and source_file:
|
|
301
|
+
# For file sources, we would need to re-process all records with enrichments
|
|
302
|
+
# This is a complex operation that would require applying mutators to all records
|
|
303
|
+
# For now, we'll skip this functionality in the in-memory implementation
|
|
304
|
+
pass
|
|
278
305
|
|
|
279
306
|
return result
|
|
280
307
|
|
|
@@ -370,6 +397,38 @@ class TQL:
|
|
|
370
397
|
elif node_type == "unary_op":
|
|
371
398
|
stats["logical_operators"].add("not")
|
|
372
399
|
traverse_ast(node.get("operand"), depth + 1)
|
|
400
|
+
elif node_type == "query_with_stats":
|
|
401
|
+
# Traverse into the filter part to find mutators and fields
|
|
402
|
+
filter_node = node.get("filter")
|
|
403
|
+
if filter_node:
|
|
404
|
+
traverse_ast(filter_node, depth + 1)
|
|
405
|
+
# Also traverse the stats part
|
|
406
|
+
stats_node = node.get("stats")
|
|
407
|
+
if stats_node:
|
|
408
|
+
traverse_ast(stats_node, depth + 1)
|
|
409
|
+
elif node_type == "stats_expr":
|
|
410
|
+
# Check aggregations for any fields or mutators
|
|
411
|
+
aggregations = node.get("aggregations", [])
|
|
412
|
+
for agg in aggregations:
|
|
413
|
+
if isinstance(agg, dict):
|
|
414
|
+
field = agg.get("field")
|
|
415
|
+
if field and field != "*":
|
|
416
|
+
stats["fields"].add(field)
|
|
417
|
+
# Check for field mutators in aggregations
|
|
418
|
+
if agg.get("field_mutators"):
|
|
419
|
+
stats["has_mutators"] = True
|
|
420
|
+
elif node_type == "geo_expr":
|
|
421
|
+
# Geo expressions always have mutators
|
|
422
|
+
field = node.get("field")
|
|
423
|
+
if field:
|
|
424
|
+
stats["fields"].add(field)
|
|
425
|
+
stats["has_mutators"] = True
|
|
426
|
+
elif node_type == "nslookup_expr":
|
|
427
|
+
# NSLookup expressions always have mutators
|
|
428
|
+
field = node.get("field")
|
|
429
|
+
if field:
|
|
430
|
+
stats["fields"].add(field)
|
|
431
|
+
stats["has_mutators"] = True
|
|
373
432
|
|
|
374
433
|
traverse_ast(ast)
|
|
375
434
|
|
|
@@ -450,7 +509,7 @@ class TQL:
|
|
|
450
509
|
index: Optional[str] = None,
|
|
451
510
|
query: Optional[str] = None,
|
|
452
511
|
size: int = 500,
|
|
453
|
-
|
|
512
|
+
from_: int = 0,
|
|
454
513
|
sort: Optional[List[Dict[str, Any]]] = None,
|
|
455
514
|
timestamp_field: str = "@timestamp",
|
|
456
515
|
time_range: Optional[Dict[str, str]] = None,
|
|
@@ -506,24 +565,6 @@ class TQL:
|
|
|
506
565
|
# Remove parameters that the new implementation doesn't understand
|
|
507
566
|
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ["save_enrichment", "opensearch_client"]}
|
|
508
567
|
|
|
509
|
-
# Check for backward compatibility - if from_ is in kwargs, it's old usage
|
|
510
|
-
if "from_" in kwargs:
|
|
511
|
-
# Old style pagination - convert to search_after with default sort
|
|
512
|
-
from_ = kwargs.pop("from_")
|
|
513
|
-
if from_ > 0 and not sort:
|
|
514
|
-
# Need a sort for proper pagination
|
|
515
|
-
sort = [{"_id": "asc"}]
|
|
516
|
-
# Note: We can't directly convert from_ to search_after without previous results
|
|
517
|
-
# This is a limitation of moving to search_after
|
|
518
|
-
if from_ > 0:
|
|
519
|
-
import warnings
|
|
520
|
-
|
|
521
|
-
warnings.warn(
|
|
522
|
-
"from_ parameter is deprecated. Use search_after with sort for efficient pagination.",
|
|
523
|
-
DeprecationWarning,
|
|
524
|
-
stacklevel=2,
|
|
525
|
-
)
|
|
526
|
-
|
|
527
568
|
# Add the supported parameters
|
|
528
569
|
filtered_kwargs.update(
|
|
529
570
|
{
|
|
@@ -532,7 +573,7 @@ class TQL:
|
|
|
532
573
|
"scan_all": scan_all,
|
|
533
574
|
"scroll_size": scroll_size,
|
|
534
575
|
"scroll_timeout": scroll_timeout,
|
|
535
|
-
"
|
|
576
|
+
"from_": from_,
|
|
536
577
|
"sort": sort,
|
|
537
578
|
}
|
|
538
579
|
)
|
|
@@ -1001,22 +1042,140 @@ class TQL:
|
|
|
1001
1042
|
Returns:
|
|
1002
1043
|
Enriched record (may be same as input if no enrichments)
|
|
1003
1044
|
"""
|
|
1004
|
-
#
|
|
1005
|
-
|
|
1006
|
-
|
|
1045
|
+
# Check if we need to apply mutators
|
|
1046
|
+
if not self._has_output_mutators(ast):
|
|
1047
|
+
return record
|
|
1048
|
+
|
|
1049
|
+
# Deep copy to avoid modifying original
|
|
1050
|
+
import copy
|
|
1051
|
+
|
|
1052
|
+
enriched_record = copy.deepcopy(record)
|
|
1053
|
+
|
|
1054
|
+
# Apply mutators from AST nodes
|
|
1055
|
+
self._apply_node_mutators(ast, enriched_record)
|
|
1056
|
+
|
|
1057
|
+
return enriched_record
|
|
1058
|
+
|
|
1059
|
+
def _has_output_mutators(self, ast: Dict[str, Any]) -> bool:
|
|
1060
|
+
"""Check if AST contains mutators that should transform output.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
ast: Query AST
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
True if output mutators are present
|
|
1067
|
+
"""
|
|
1068
|
+
if isinstance(ast, dict):
|
|
1069
|
+
node_type = ast.get("type")
|
|
1070
|
+
|
|
1071
|
+
# Check for field mutators with exists operator (output transformation)
|
|
1072
|
+
if node_type == "comparison" and ast.get("operator") == "exists" and ast.get("field_mutators"):
|
|
1073
|
+
return True
|
|
1074
|
+
|
|
1075
|
+
# Recursively check child nodes
|
|
1076
|
+
if node_type == "logical_op":
|
|
1077
|
+
left = ast.get("left", {})
|
|
1078
|
+
right = ast.get("right", {})
|
|
1079
|
+
return self._has_output_mutators(left) or self._has_output_mutators(right)
|
|
1080
|
+
elif node_type == "unary_op":
|
|
1081
|
+
operand = ast.get("operand", {})
|
|
1082
|
+
return self._has_output_mutators(operand)
|
|
1083
|
+
|
|
1084
|
+
return False
|
|
1085
|
+
|
|
1086
|
+
def _apply_node_mutators(self, ast: Dict[str, Any], record: Dict[str, Any]) -> None:
|
|
1087
|
+
"""Apply mutators from AST nodes to the record.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
ast: Query AST
|
|
1091
|
+
record: Record to modify (in-place)
|
|
1092
|
+
"""
|
|
1093
|
+
if not isinstance(ast, dict):
|
|
1094
|
+
return
|
|
1095
|
+
|
|
1096
|
+
node_type = ast.get("type")
|
|
1097
|
+
|
|
1098
|
+
# Apply mutators for exists operator (output transformation)
|
|
1099
|
+
if node_type == "comparison" and ast.get("operator") == "exists" and ast.get("field_mutators"):
|
|
1100
|
+
field_name = ast["field"]
|
|
1101
|
+
field_mutators = ast["field_mutators"]
|
|
1102
|
+
|
|
1103
|
+
# Get field value
|
|
1104
|
+
field_value = self._get_nested_field(record, field_name)
|
|
1105
|
+
|
|
1106
|
+
if field_value is not None:
|
|
1107
|
+
# Apply mutators
|
|
1108
|
+
from .mutators import apply_mutators
|
|
1109
|
+
|
|
1110
|
+
mutated_value = apply_mutators(field_value, field_mutators, field_name, record)
|
|
1111
|
+
|
|
1112
|
+
# Update record with mutated value
|
|
1113
|
+
self._set_nested_field(record, field_name, mutated_value)
|
|
1114
|
+
|
|
1115
|
+
# Recursively process child nodes
|
|
1116
|
+
elif node_type == "logical_op":
|
|
1117
|
+
self._apply_node_mutators(ast.get("left", {}), record)
|
|
1118
|
+
self._apply_node_mutators(ast.get("right", {}), record)
|
|
1119
|
+
elif node_type == "unary_op":
|
|
1120
|
+
self._apply_node_mutators(ast.get("operand", {}), record)
|
|
1121
|
+
|
|
1122
|
+
def _get_nested_field(self, record: Dict[str, Any], field_path: str) -> Any:
|
|
1123
|
+
"""Get value from nested field path.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
record: Record dictionary
|
|
1127
|
+
field_path: Dot-separated field path
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
Field value or None if not found
|
|
1131
|
+
"""
|
|
1132
|
+
parts = field_path.split(".")
|
|
1133
|
+
current = record
|
|
1134
|
+
|
|
1135
|
+
for part in parts:
|
|
1136
|
+
if isinstance(current, dict) and part in current:
|
|
1137
|
+
current = current[part]
|
|
1138
|
+
else:
|
|
1139
|
+
return None
|
|
1140
|
+
|
|
1141
|
+
return current
|
|
1142
|
+
|
|
1143
|
+
def _set_nested_field(self, record: Dict[str, Any], field_path: str, value: Any) -> None:
|
|
1144
|
+
"""Set value in nested field path.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
record: Record dictionary to modify
|
|
1148
|
+
field_path: Dot-separated field path
|
|
1149
|
+
value: Value to set
|
|
1150
|
+
"""
|
|
1151
|
+
parts = field_path.split(".")
|
|
1152
|
+
current = record
|
|
1007
1153
|
|
|
1008
|
-
|
|
1154
|
+
# Navigate to parent of target field
|
|
1155
|
+
for part in parts[:-1]:
|
|
1156
|
+
if part not in current:
|
|
1157
|
+
current[part] = {}
|
|
1158
|
+
current = current[part]
|
|
1159
|
+
|
|
1160
|
+
# Set the value
|
|
1161
|
+
if len(parts) > 0:
|
|
1162
|
+
current[parts[-1]] = value
|
|
1163
|
+
|
|
1164
|
+
def _convert_stats_result(self, stats_result: Dict[str, Any], viz_hint: Optional[str] = None) -> Dict[str, Any]:
|
|
1009
1165
|
"""Convert stats result from query() format to execute_opensearch format.
|
|
1010
1166
|
|
|
1011
1167
|
Args:
|
|
1012
1168
|
stats_result: Result from stats() or query_stats() method
|
|
1169
|
+
viz_hint: Optional visualization hint from the query
|
|
1013
1170
|
|
|
1014
1171
|
Returns:
|
|
1015
1172
|
Stats result in execute_opensearch format
|
|
1016
1173
|
"""
|
|
1017
1174
|
# Map the stats evaluator format to execute_opensearch format
|
|
1175
|
+
result = {}
|
|
1176
|
+
|
|
1018
1177
|
if stats_result.get("type") == "simple_aggregation":
|
|
1019
|
-
|
|
1178
|
+
result = {
|
|
1020
1179
|
"type": "stats",
|
|
1021
1180
|
"operation": stats_result["function"],
|
|
1022
1181
|
"field": stats_result["field"],
|
|
@@ -1024,10 +1183,22 @@ class TQL:
|
|
|
1024
1183
|
}
|
|
1025
1184
|
elif stats_result.get("type") == "multiple_aggregations":
|
|
1026
1185
|
# For multiple aggregations, return the results dict
|
|
1027
|
-
|
|
1186
|
+
result = {"type": "stats_multiple", "results": stats_result["results"]}
|
|
1028
1187
|
elif stats_result.get("type") == "grouped_aggregation":
|
|
1029
1188
|
# For grouped aggregations
|
|
1030
|
-
|
|
1189
|
+
result = {
|
|
1190
|
+
"type": "stats_grouped",
|
|
1191
|
+
"group_by": stats_result["group_by"],
|
|
1192
|
+
"results": stats_result["results"],
|
|
1193
|
+
"operation": stats_result.get("function", "count"),
|
|
1194
|
+
"field": stats_result.get("field", "*"),
|
|
1195
|
+
}
|
|
1031
1196
|
else:
|
|
1032
1197
|
# Return as-is if format is unknown
|
|
1033
|
-
|
|
1198
|
+
result = stats_result
|
|
1199
|
+
|
|
1200
|
+
# Add visualization hint if provided
|
|
1201
|
+
if viz_hint:
|
|
1202
|
+
result["viz_hint"] = viz_hint
|
|
1203
|
+
|
|
1204
|
+
return result
|