tellaro-query-language 0.2.6__py3-none-any.whl → 0.2.8__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.
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: tellaro-query-language
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: A flexible, human-friendly query language for searching and filtering structured data
5
- Home-page: https://github.com/tellaro/tellaro-query-language
6
5
  License: Proprietary
6
+ License-File: LICENSE
7
7
  Keywords: query,language,opensearch,elasticsearch,search,filter,tql
8
8
  Author: Justin Henderson
9
9
  Author-email: justin@tellaro.io
@@ -18,14 +18,15 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Classifier: Topic :: Text Processing :: Linguistic
20
20
  Provides-Extra: opensearch
21
- Requires-Dist: dnspython (>=2.7.0,<3.0.0)
22
- Requires-Dist: maxminddb (>=2.7.0,<3.0.0)
21
+ Requires-Dist: dnspython (>=2.8.0,<3.0.0)
22
+ Requires-Dist: maxminddb (>=3.0.0,<4.0.0)
23
23
  Requires-Dist: opensearch-dsl (>=2.1.0,<3.0.0) ; extra == "opensearch"
24
- Requires-Dist: opensearch-py (>=2.4.2,<3.0.0) ; extra == "opensearch"
25
- Requires-Dist: pyparsing (>=3.2.1,<4.0.0)
26
- Requires-Dist: setuptools (>=80.0.0,<81.0.0)
27
- Requires-Dist: urllib3 (>=2.5.0,<3.0.0)
24
+ Requires-Dist: opensearch-py (>=2.8.0,<3.0.0) ; extra == "opensearch"
25
+ Requires-Dist: pyparsing (>=3.3.0,<4.0.0)
26
+ Requires-Dist: setuptools (>=80.9.0,<81.0.0)
27
+ Requires-Dist: urllib3 (>=2.6.0,<3.0.0)
28
28
  Project-URL: Documentation, https://github.com/tellaro/tellaro-query-language/tree/main/docs
29
+ Project-URL: Homepage, https://github.com/tellaro/tellaro-query-language
29
30
  Project-URL: Repository, https://github.com/tellaro/tellaro-query-language
30
31
  Description-Content-Type: text/markdown
31
32
 
@@ -17,7 +17,7 @@ tql/evaluator_components/README.md,sha256=c59yf2au34yPhrru7JWgGop_ORteB6w5vfMhsa
17
17
  tql/evaluator_components/__init__.py,sha256=DourRUSYXWPnCghBFj7W0YfMeymT3X8YTDCwnLIyP1c,535
18
18
  tql/evaluator_components/field_access.py,sha256=BuXvL9jlv4H77neT70Vh7_qokmzs-d4EbSDA2FB1IT0,6435
19
19
  tql/evaluator_components/special_expressions.py,sha256=prhXnVRnFfmecgmuTz0fsefTA1clb2eyyYf-zPtzXGs,15703
20
- tql/evaluator_components/value_comparison.py,sha256=a0Vo3BWyPxMtEgAKtUuaN0HN9Arc0A7BUVrxgZyH6_o,21200
20
+ tql/evaluator_components/value_comparison.py,sha256=k4-4AnNSvVXkCF-ZdmhBpwLaMC_vajAv5m8BjIinni8,21276
21
21
  tql/exceptions.py,sha256=GVssdBp9134Wyk1bWbcLQFU9U8yQKl5zzquTTrM22A0,5620
22
22
  tql/field_type_inference.py,sha256=KOazjp8CK6s9vwOcFkQrdOwcazOwHqSIhYgX8hWCiDo,9700
23
23
  tql/geoip_normalizer.py,sha256=tvie-5xevJEeLp2KmjoXDjYdND8AvyVE7lCO8qgUzGY,10486
@@ -39,21 +39,21 @@ tql/opensearch_components/lucene_converter.py,sha256=OvYTZHNBktPGow1fsVm4TMlvxHS
39
39
  tql/opensearch_components/query_converter.py,sha256=INjX6hd-1dlCdCn_dGSBnub2mpNyUBpHXhHA5WoBQX4,41985
40
40
  tql/opensearch_mappings.py,sha256=sVLlQlE3eGD7iNNZ_m4F4j5GVzQAJhZyCqDKYRhLRh8,11531
41
41
  tql/opensearch_stats.py,sha256=sxJ4KziV-Yv1kvjo22souxjBQH3chmlm4lAgEhRBtyA,24530
42
- tql/parser.py,sha256=y4LKYuNqE74Xx5UpHZBZE6PlVaRBiwr5i49NeMJW-jU,80383
42
+ tql/parser.py,sha256=wQHoy9OPCNw80hlqOkFZOaNW8_RbuAUen41J0c6t6ms,83696
43
43
  tql/parser_components/README.md,sha256=lvQX72ckq2zyotGs8QIHHCIFqaA7bOHwkP44wU8Zoiw,2322
44
44
  tql/parser_components/__init__.py,sha256=zBwHBMPJyHSBbaOojf6qTrJYjJg5A6tPUE8nHFdRiQs,521
45
45
  tql/parser_components/ast_builder.py,sha256=erHoeKAMzobswoRIXB9xcsZbzQ5-2ZwaYfQgRWoUAa8,9653
46
46
  tql/parser_components/error_analyzer.py,sha256=qlCD9vKyW73aeKQYI33P1OjIWSJ3LPd08wuN9cis2fU,4012
47
47
  tql/parser_components/field_extractor.py,sha256=eUEkmiYWX2OexanFqhHeX8hcIkRlfIcgMB667e0HRYs,4629
48
- tql/parser_components/grammar.py,sha256=h58RBshZHXgbP1EmNwmf7dny-fgVloNg-qN4Rivross,20599
48
+ tql/parser_components/grammar.py,sha256=vEPiYOMMk-a6Xzz0y6kjRzyq4slwBG5T9YDeBg1txAk,21460
49
49
  tql/post_processor.py,sha256=5w_rP0V-t3AC7iAFLnlDxUtawcDFovbhbRsoZxt9yP4,51787
50
50
  tql/scripts.py,sha256=DUY0H5IoGs_CNdG_oxITvLiCNVsogb2RJqUs2xXOs24,4319
51
51
  tql/stats_evaluator.py,sha256=2qnjeH5Qx14qpHDS_YJn9jRPeoPUfkeiYJabBagdfRs,36126
52
52
  tql/stats_transformer.py,sha256=MT-4rDWZSySgn4Fuq9H0c-mvwFYLM6FqWpPv2rHX-rE,7588
53
53
  tql/streaming_file_processor.py,sha256=cftWhYcvUo984P3ALf2CO3FoCQPJPe_2s2HLcXTp5UQ,12437
54
54
  tql/validators.py,sha256=e9MlX-zQ_O3M8YP8vXyMjKU8iiJMTh6mMK0iv0_4gTY,3771
55
- tellaro_query_language-0.2.6.dist-info/LICENSE,sha256=eWf8lkuXlVX_8WiDpUgQvzxc1cxCeVne_e6P-pVJpwM,3038
56
- tellaro_query_language-0.2.6.dist-info/METADATA,sha256=FJV1TPy9q1SQt0eReXeVJw8XcMr2-OkGMEMF9Y99J5Q,21857
57
- tellaro_query_language-0.2.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
- tellaro_query_language-0.2.6.dist-info/entry_points.txt,sha256=D0lbIGUYuDyfcYeqju1rWcMBFzft4sZtfIlw5uPNx5g,181
59
- tellaro_query_language-0.2.6.dist-info/RECORD,,
55
+ tellaro_query_language-0.2.8.dist-info/METADATA,sha256=NwgXSoSzBB6oDL-aBxAnjwB46BWYiwbi_D16TMoBjE8,21891
56
+ tellaro_query_language-0.2.8.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
57
+ tellaro_query_language-0.2.8.dist-info/entry_points.txt,sha256=D0lbIGUYuDyfcYeqju1rWcMBFzft4sZtfIlw5uPNx5g,181
58
+ tellaro_query_language-0.2.8.dist-info/licenses/LICENSE,sha256=eWf8lkuXlVX_8WiDpUgQvzxc1cxCeVne_e6P-pVJpwM,3038
59
+ tellaro_query_language-0.2.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 2.3.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -170,15 +170,17 @@ class ValueComparator:
170
170
  return str(field_value).lower().endswith(str(expected_value).lower())
171
171
  elif operator == "in":
172
172
  if isinstance(expected_value, list):
173
- if len(expected_value) == 1 and isinstance(field_value, list):
174
- # This is likely a reversed 'in' case: 'value' in field_list
175
- # Check if the single expected value is in the field list
176
- converted_expected = self._convert_numeric(expected_value[0])
177
- return converted_expected in field_value
173
+ # Convert list elements to appropriate types for comparison
174
+ converted_list = [self._convert_numeric(val) for val in expected_value]
175
+
176
+ if isinstance(field_value, (list, tuple)):
177
+ # Field is an array - check if ANY element of field equals ANY value in list
178
+ # This handles both:
179
+ # - "value" in field (single expected value, array field)
180
+ # - field in ["val1", "val2"] (multiple expected values, array field)
181
+ return any(elem in converted_list for elem in field_value)
178
182
  else:
179
- # Standard case: field_value in list
180
- # Convert list elements to appropriate types for comparison
181
- converted_list = [self._convert_numeric(val) for val in expected_value]
183
+ # Scalar field - check if field_value is in the list
182
184
  return field_value in converted_list
183
185
  else:
184
186
  return field_value == expected_value
tql/parser.py CHANGED
@@ -332,6 +332,29 @@ class TQLParser:
332
332
  return result
333
333
 
334
334
  if len(parsed) == 4:
335
+ # Check for field_in_values marker: field in [values] __field_in_values__
336
+ if isinstance(parsed[3], str) and parsed[3] == "__field_in_values__":
337
+ # This is field in [values] syntax
338
+ field_part, op, values_list, marker = parsed
339
+ field_name, type_hint, field_mutators = self.ast_builder.extract_field_info(field_part)
340
+ # Extract values from the list
341
+ values = []
342
+ for item in values_list:
343
+ if isinstance(item, list) and len(item) >= 1:
344
+ values.append(item[0] if len(item) == 1 else item)
345
+ else:
346
+ values.append(item)
347
+ result = {
348
+ "type": "comparison",
349
+ "field": field_name,
350
+ "type_hint": type_hint,
351
+ "operator": "in",
352
+ "value": values,
353
+ }
354
+ if field_mutators:
355
+ result["field_mutators"] = field_mutators
356
+ return result
357
+
335
358
  # Check for ANY/ALL operators: ANY field op value
336
359
  first, field, operator, value = parsed
337
360
 
@@ -368,12 +391,25 @@ class TQLParser:
368
391
  normalized_operator = "any"
369
392
  else:
370
393
  normalized_operator = f"not_{third.lower()}"
394
+
395
+ # Extract value properly - unwrap if it's a double-wrapped list
396
+ # This happens for "field not in [values]" where the list is wrapped twice
397
+ value = fourth
398
+ if (
399
+ third.lower() == "in"
400
+ and isinstance(fourth, list)
401
+ and len(fourth) == 1
402
+ and isinstance(fourth[0], list)
403
+ ):
404
+ # Unwrap the double-wrapped list for "not in" with values
405
+ value = fourth[0]
406
+
371
407
  result = {
372
408
  "type": "comparison",
373
409
  "field": field_name,
374
410
  "type_hint": type_hint,
375
411
  "operator": normalized_operator,
376
- "value": fourth,
412
+ "value": value,
377
413
  }
378
414
  if field_mutators:
379
415
  result["field_mutators"] = field_mutators
@@ -577,6 +613,29 @@ class TQLParser:
577
613
  result["value_mutators"] = value_mutators
578
614
  return result
579
615
  elif len(parsed) == 5:
616
+ # Check for field_not_in_values marker: field not in [values] __field_not_in_values__
617
+ if isinstance(parsed[4], str) and parsed[4] == "__field_not_in_values__":
618
+ # This is field not in [values] syntax
619
+ field_part, not_kw, in_kw, values_list, marker = parsed
620
+ field_name, type_hint, field_mutators = self.ast_builder.extract_field_info(field_part)
621
+ # Extract values from the list
622
+ values = []
623
+ for item in values_list:
624
+ if isinstance(item, list) and len(item) >= 1:
625
+ values.append(item[0] if len(item) == 1 else item)
626
+ else:
627
+ values.append(item)
628
+ result = {
629
+ "type": "comparison",
630
+ "field": field_name,
631
+ "type_hint": type_hint,
632
+ "operator": "not_in",
633
+ "value": values,
634
+ }
635
+ if field_mutators:
636
+ result["field_mutators"] = field_mutators
637
+ return result
638
+
580
639
  # Check for natural between syntax: field between value1 and value2
581
640
  # Only process as between if the second element is "between"
582
641
  if (
@@ -996,7 +1055,7 @@ class TQLParser:
996
1055
  return {"type": "unknown", "value": parsed}
997
1056
  else:
998
1057
  # Comparison operation
999
- # Handle 'in' operator - always value in field(s)
1058
+ # Handle 'in' operator - can be "value in field(s)" or "field in [values]"
1000
1059
  if isinstance(operator, str) and operator.lower() == "in":
1001
1060
  # Check for old syntax: [field1, field2] in value
1002
1061
  # The parser wraps list literals, so check for wrapped lists too
@@ -1031,26 +1090,21 @@ class TQLParser:
1031
1090
  position=0,
1032
1091
  )
1033
1092
 
1034
- # For 'in' operator, left is always the value, right is field(s)
1035
- # Extract the value from left
1036
- value_extracted, value_mutators = self.ast_builder.extract_value_info(left)
1037
-
1038
- # Check if right is a list of fields
1093
+ # Check if right is a list of fields (value in [fields] syntax)
1094
+ # Note: field in [values] is now handled by field_in_values grammar rule
1095
+ # with __field_in_values__ marker, so this is only for value in [fields]
1039
1096
  if isinstance(right, list) and len(right) > 0:
1040
- # Check if all elements are fields
1041
- all_fields = True
1042
- for item in right:
1043
- if isinstance(item, list):
1044
- # This is a typed_field group
1045
- if not (len(item) >= 1 and isinstance(item[0], str)):
1046
- all_fields = False
1047
- break
1048
- elif not isinstance(item, str):
1049
- all_fields = False
1050
- break
1097
+ # Check if all elements are fields (typed_fields wrapped in lists)
1098
+ # value in [field1, field2] produces right = [['field1'], ['field2']]
1099
+ # field in ["val1", "val2"] produces right = ['val1', 'val2']
1100
+ # (but field in [values] is now handled by __field_in_values__ marker)
1101
+ is_field_list = all(
1102
+ isinstance(item, list) and len(item) >= 1 and isinstance(item[0], str) for item in right
1103
+ )
1051
1104
 
1052
- if all_fields:
1105
+ if is_field_list:
1053
1106
  # This is "value in [field1, field2, ...]" format
1107
+ value_extracted, value_mutators = self.ast_builder.extract_value_info(left)
1054
1108
  # Create an OR expression for all fields
1055
1109
  field_comparisons = []
1056
1110
  for field in right:
@@ -1083,7 +1137,11 @@ class TQLParser:
1083
1137
  }
1084
1138
  return result
1085
1139
 
1086
- # Otherwise, treat as standard "value in field" (single field)
1140
+ # For 'in' operator with single field, left is the value, right is field
1141
+ # Extract the value from left
1142
+ value_extracted, value_mutators = self.ast_builder.extract_value_info(left)
1143
+
1144
+ # Treat as standard "value in field" (single field)
1087
1145
  field_name, type_hint, field_mutators = self.ast_builder.extract_field_info(right)
1088
1146
  result = {
1089
1147
  "type": "comparison",
@@ -225,10 +225,26 @@ class TQLGrammar:
225
225
  self.field_list_item = self.typed_field
226
226
  self.field_list = Group(Suppress("[") + delimitedList(self.field_list_item) + Suppress("]"))
227
227
 
228
- # Special case for 'in' operator - always value in field(s)
228
+ # Special case for 'in' operator - value in field(s)
229
229
  self.value_in_field = Group(self.value + CaselessKeyword("in") + self.typed_field)
230
230
  self.value_in_field_list = Group(self.value + CaselessKeyword("in") + self.field_list)
231
231
 
232
+ # Field-first 'in' operator: field in [value1, value2, ...] (checks if field equals any value)
233
+ # Add a marker "__field_in_values__" to distinguish from value_in_field
234
+ self.field_in_values = Group(
235
+ self.typed_field
236
+ + CaselessKeyword("in")
237
+ + self.list_literal
238
+ + Literal("").setParseAction(lambda: "__field_in_values__")
239
+ )
240
+ self.field_not_in_values = Group(
241
+ self.typed_field
242
+ + (CaselessKeyword("not") | Literal("!"))
243
+ + CaselessKeyword("in")
244
+ + self.list_literal
245
+ + Literal("").setParseAction(lambda: "__field_not_in_values__")
246
+ )
247
+
232
248
  def _setup_special_expressions(self):
233
249
  """Set up special expressions like geo() and nslookup()."""
234
250
  # Forward declare for recursive use
@@ -455,6 +471,8 @@ class TQLGrammar:
455
471
  | self.is_not_comparison
456
472
  | self.not_between_comparison_natural
457
473
  | self.not_between_comparison_list
474
+ | self.field_not_in_values # field not in [values] - must come before std_comparison
475
+ | self.field_in_values # field in [values] - must come before std_comparison
458
476
  | self.std_comparison
459
477
  | self.between_comparison_natural
460
478
  | self.between_comparison_list