ransacklib 0.1.10__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.
ransack/transformer.py ADDED
@@ -0,0 +1,846 @@
1
+ """
2
+ transformer.py - Provides transformers for converting Lark's AST into more
3
+ usable Python objects.
4
+
5
+ This module contains the `ExpressionTransformer` class,
6
+ which extend Lark's `Transformer` to convert the parse tree
7
+ generated by the Lark parser into more useful Python objects.
8
+ `ExpressionTransformer` handles the interpretation of parsed structures
9
+ like IP addresses, ranges, datetime objects, and arithmetic expressions,
10
+ transforming them into Python data structures.
11
+
12
+ Classes:
13
+ - TokenWrapper: A wrapper for Lark Token to provide a `real_value`
14
+ attribute.
15
+ - ExpressionTransformer: Converts nodes from the Lark parse tree
16
+ into Python objects (e.g., IP addresses,
17
+ datetime objects, timedelta, lists,
18
+ strings).
19
+ """
20
+
21
+ import re
22
+ from collections.abc import Mapping, MutableSequence
23
+ from datetime import datetime, timedelta, timezone
24
+ from typing import Any, Optional, Tuple
25
+
26
+ from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
27
+ from lark import Token, Transformer, Tree, v_args
28
+ from lark.tree import Meta
29
+ from lark.visitors import Interpreter
30
+
31
+ from .exceptions import EvaluationError
32
+ from .function import predefined_functions
33
+ from .operator import binary_operation
34
+
35
+
36
+ class TokenWrapper:
37
+ """A wrapper of Lark Token to provide a real_value attribute."""
38
+
39
+ def __init__(self, token: Token, value: Any):
40
+ """
41
+ Initializes the TokenWrapper with a token and a value.
42
+
43
+ Parameters:
44
+ token: The Lark Token to be wrapped.
45
+ value: The value to associate with the token.
46
+ """
47
+ self.token = token
48
+ self._real_value = value
49
+
50
+ @property
51
+ def real_value(self) -> Any:
52
+ """Returns the stored value."""
53
+ return self._real_value
54
+
55
+ def __repr__(self) -> str:
56
+ return f"TokenWrapper({self._real_value})"
57
+
58
+ def __getattr__(self, name: str) -> Any:
59
+ """
60
+ Delegates attribute access to the wrapped token.
61
+
62
+ Parameters:
63
+ name: The attribute name.
64
+
65
+ Returns:
66
+ The value of the attribute from the wrapped token.
67
+ """
68
+ return getattr(self.token, name)
69
+
70
+
71
+ def _add_tokens(token1: Token, token2: Token) -> Token:
72
+ """
73
+ Concatenates two tokens, creating a new token with combined
74
+ value and updated positions.
75
+
76
+ Parameters:
77
+ token1: The first token.
78
+ token2: The second token.
79
+
80
+ Returns:
81
+ A new token with concatenated values and correctly calculated positions.
82
+ """
83
+ concatenated_value = token1.value + " " + token2.value
84
+ return Token(
85
+ type="CONCAT",
86
+ value=concatenated_value,
87
+ start_pos=token1.start_pos,
88
+ end_pos=token2.end_pos,
89
+ line=token1.line,
90
+ column=token1.column,
91
+ end_line=token2.end_line,
92
+ end_column=token2.end_column,
93
+ )
94
+
95
+
96
+ def _create_meta(token: Token | TokenWrapper) -> Meta:
97
+ meta = Meta()
98
+ meta.empty = False
99
+ meta.line = token.line if token.line else 1
100
+ meta.column = token.column if token.column else 1
101
+ meta.start_pos = token.start_pos if token.start_pos else 0
102
+ meta.end_line = token.end_line if token.end_line else -1
103
+ meta.end_column = token.end_column if token.end_column else -1
104
+ meta.end_pos = token.end_pos if token.end_pos else -1
105
+ return meta
106
+
107
+
108
+ def _get_data_value(
109
+ path: str, data: Optional[Mapping | MutableSequence]
110
+ ) -> Tuple[Any, bool]:
111
+ """
112
+ Retrieves a value from the data structure (dictionary-like or list-like)
113
+ based on a dotted path.
114
+
115
+ This method can handle nested dictionaries and lists recursively. If the
116
+ path leads to a list, it aggregates values from matching keys across all
117
+ items in the list.
118
+
119
+ Parameters:
120
+ path: A string representing a path of keys, separated by dots.
121
+ data: The current level of the data structure being processed.
122
+
123
+ Returns:
124
+ A tuple containing:
125
+ - The value found at the specified path within the data structure,
126
+ aggregated if the path includes lists.
127
+ - A boolean indicating whether the variable name was found in the
128
+ data structure.
129
+
130
+ Example:
131
+ Given the data:
132
+ {
133
+ "Source": [
134
+ {"IP4": [1.1.1.1]},
135
+ {"IP4": [2.2.2.2]}
136
+ ]
137
+ }
138
+ Querying "Source.IP4" will return ([1.1.1.1, 2.2.2.2], True).
139
+ """
140
+ if not path:
141
+ return data, True # Return the current data when path is empty
142
+
143
+ key, *remaining_list = path.split(".", 1)
144
+ remaining_path = remaining_list[0] if remaining_list else ""
145
+
146
+ if isinstance(data, Mapping):
147
+ # Navigate dictionary
148
+ if key not in data:
149
+ return None, False
150
+ return _get_data_value(remaining_path, data[key])
151
+
152
+ elif isinstance(data, MutableSequence):
153
+ # Aggregate results from all list elements
154
+ aggregated: Any = []
155
+ for item in data:
156
+ if isinstance(item, (Mapping, MutableSequence)):
157
+ result, _ = _get_data_value(path, item)
158
+ if result is not None:
159
+ if isinstance(result, MutableSequence):
160
+ aggregated.extend(result)
161
+ else:
162
+ aggregated.append(result)
163
+ if aggregated:
164
+ return aggregated, True
165
+ return None, False
166
+ return None, False
167
+
168
+
169
+ def get_values(data: Optional[Mapping | MutableSequence], path: str) -> list:
170
+ """
171
+ Public API method to retrieve values from a nested data structure
172
+ using a dotted path. Returns a flat list of values or an empty list
173
+ if nothing is found.
174
+
175
+ Parameters:
176
+ path: A string representing a path of keys, separated by dots.
177
+ data: The data to search within.
178
+
179
+ Returns:
180
+ A list of values found at the specified path.
181
+ """
182
+ result, found = _get_data_value(path, data)
183
+ if not found or result is None:
184
+ return []
185
+ if isinstance(result, MutableSequence):
186
+ return list(result)
187
+ return [result]
188
+
189
+
190
+ @v_args(inline=True)
191
+ class ExpressionTransformer(Transformer):
192
+ """
193
+ A transformer that converts various nodes from the Lark parse tree
194
+ into appropriate objects (ipranges, datetime, list, ...).
195
+ """
196
+
197
+ def __init__(self, context: dict[str, Any] | None = None) -> None:
198
+ """
199
+ Initializes the ExpressionTransformer with a context dictionary
200
+ for constants.
201
+
202
+ Parameters:
203
+ context: A dictionary containing constant values that can be
204
+ accessed by variable names within expressions.
205
+ Defaults to an empty dictionary if none is provided.
206
+ """
207
+ self.context = context if context is not None else {}
208
+
209
+ def number(self, data: Token) -> Tree[TokenWrapper]:
210
+ """
211
+ Transforms a token representing a number into an integer or float.
212
+
213
+ Parameters:
214
+ data: A token representing a number (can be an integer or
215
+ float in string form).
216
+
217
+ Returns:
218
+ A tree node wrapping the number as int or float.
219
+ """
220
+ value = int(data) if data.isdigit() else float(data)
221
+ return Tree("number", [TokenWrapper(data, value)], _create_meta(data))
222
+
223
+ def ipv4_single(self, data: Token) -> Tree[TokenWrapper]:
224
+ """
225
+ Transforms a single IPv4 address into an IP4 object.
226
+
227
+ Parameters:
228
+ data: A token representing a single IPv4 address.
229
+ Returns:
230
+ A tree node wrapping an IP4 object.
231
+ """
232
+ try:
233
+ return Tree("ip", [TokenWrapper(data, IP4(data))], _create_meta(data))
234
+ except ValueError as e:
235
+ raise ValueError(f"Invalid IPv4 address '{data}'") from e
236
+
237
+ def ipv4_cidr(self, net: Token) -> Tree[TokenWrapper]:
238
+ """
239
+ Transforms an IPv4 CIDR (network address and prefix) into
240
+ an IP4Net object.
241
+
242
+ Parameters:
243
+ net: The CIDR network.
244
+ Returns:
245
+ A tree node wrapping an IP4Net object.
246
+ """
247
+ try:
248
+ return Tree("ip", [TokenWrapper(net, IP4Net(net))], _create_meta(net))
249
+ except ValueError as e:
250
+ raise ValueError(f"Invalid IPv4 CIDR '{net}'") from e
251
+
252
+ def ipv4_range(self, range_: Token) -> Tree[TokenWrapper]:
253
+ """
254
+ Transforms an IPv4 range (start IP to end IP) into an IP4Range object.
255
+
256
+ Parameters:
257
+ range_: The IPv4 range in a format start-end.
258
+ Returns:
259
+ A tree node wrapping an IP4Range object
260
+ """
261
+ return Tree(
262
+ "ip", [TokenWrapper(range_, IP4Range(range_))], _create_meta(range_)
263
+ )
264
+
265
+ def ipv6_single(self, data: Token) -> Tree[TokenWrapper]:
266
+ """
267
+ Transforms a single IPv6 address into an IP6 object.
268
+
269
+ Parameters:
270
+ data: A token representing a single IPv6 address.
271
+ Returns:
272
+ A tree node wrapping an IP6 object.
273
+ """
274
+ try:
275
+ return Tree("ip", [TokenWrapper(data, IP6(data))], _create_meta(data))
276
+ except ValueError as e:
277
+ raise ValueError(f"Invalid IPv6 address '{data}'") from e
278
+
279
+ def ipv6_cidr(self, net: Token) -> Tree[TokenWrapper]:
280
+ """
281
+ Transforms an IPv6 CIDR (network address and prefix) into
282
+ an IP6Net object.
283
+
284
+ Parameters:
285
+ net: The CIDR network.
286
+ Returns:
287
+ A tree node wrapping an IP6Net object.
288
+ """
289
+ try:
290
+ return Tree("ip", [TokenWrapper(net, IP6Net(net))], _create_meta(net))
291
+ except ValueError as e:
292
+ raise ValueError(f"Invalid IPv6 CIDR '{net}'") from e
293
+
294
+ def ipv6_range(self, range_: Token) -> Tree[TokenWrapper]:
295
+ """
296
+ Transforms an IPv6 range (start IP to end IP) into an IP6Range object.
297
+
298
+ Parameters:
299
+ range_: The IPv6 range in a format start-end.
300
+ Returns:
301
+ A tree node wrapping an IP6Range object.
302
+ """
303
+ return Tree(
304
+ "ip", [TokenWrapper(range_, IP6Range(range_))], _create_meta(range_)
305
+ )
306
+
307
+ def datetime_full(self, date: Token, time: Token) -> Tree[TokenWrapper]:
308
+ """
309
+ Transforms a full datetime string into a datetime object.
310
+
311
+ Parameters:
312
+ date: A token representing the date.
313
+ time: A token representing the time (can include timezone).
314
+ Returns:
315
+ A tree node wrapping a datetime object.
316
+ """
317
+ datetime_str = f"{date} {time}"
318
+
319
+ # Convert z/Z to '+00:00', as it works with both 'T' and ' '
320
+ if datetime_str.upper().endswith("Z"):
321
+ datetime_str = datetime_str.upper().replace("Z", "+00:00")
322
+
323
+ # Convert to datetime object
324
+ dtime = datetime.fromisoformat(datetime_str)
325
+
326
+ # If no time zone was specified, then set to UTC
327
+ if dtime.tzinfo is None:
328
+ dtime = dtime.replace(tzinfo=timezone.utc)
329
+
330
+ tw = TokenWrapper(_add_tokens(date, time), dtime)
331
+
332
+ return Tree("datetime", [tw], _create_meta(tw))
333
+
334
+ def datetime_only_date(self, date: Token) -> Tree[TokenWrapper]:
335
+ """
336
+ Transforms a date string into a datetime object set to midnight UTC.
337
+
338
+ Parameters:
339
+ date: A token representing the date.
340
+ Returns:
341
+ A tree node wrapping a datetime object set to midnight UTC.
342
+ """
343
+ return Tree(
344
+ "datetime",
345
+ [TokenWrapper(date, datetime.fromisoformat(date + "T00:00:00+00:00"))],
346
+ _create_meta(date),
347
+ )
348
+
349
+ def timedelta(self, duration: Token) -> Tree[TokenWrapper]:
350
+ """
351
+ Transforms a token representing a duration into a timedelta object.
352
+
353
+ Parameters:
354
+ duration: A token representing the duration in the format
355
+ [days]D HH:MM:SS, e.g., '2D12:34:56'.
356
+ Returns:
357
+ A tree node wrapping a timedelta object.
358
+ """
359
+ # Match against the regular expression to parse the duration
360
+ DURATION_RE = re.compile(
361
+ r"^((?P<days>[0-9]+)[Dd])?" # Optional days part with 'D' or 'd'
362
+ r"(?P<hrs>[0-9]{2}):" # Hours part (exactly 2 digits)
363
+ r"(?P<mins>[0-9]{2}):" # Minutes part (exactly 2 digits)
364
+ r"(?P<secs>[0-9]{2})$" # Seconds part (exactly 2 digits)
365
+ )
366
+ match = DURATION_RE.match(duration)
367
+
368
+ # Raise an exception if the duration format is invalid
369
+ if not match:
370
+ raise ValueError(f"Invalid duration format: {duration}")
371
+
372
+ # Extract days, hours, minutes, and seconds, defaulting to 0 if missing
373
+ days = int(match.group("days") or 0)
374
+ hours = int(match.group("hrs") or 0)
375
+ minutes = int(match.group("mins") or 0)
376
+ seconds = int(match.group("secs") or 0)
377
+
378
+ # Return a timedelta object
379
+ delta = timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
380
+ return Tree(
381
+ "timedelta_", [TokenWrapper(duration, delta)], _create_meta(duration)
382
+ )
383
+
384
+ def string(self, data: Token) -> Tree[TokenWrapper]:
385
+ """
386
+ Transforms a string (enclosed in quotes) into a regular string.
387
+
388
+ Parameters:
389
+ data: A token representing a string (enclosed in single
390
+ or double quotes).
391
+
392
+ Returns:
393
+ A tree node wrapping the string with quotes stripped.
394
+ """
395
+ stripped_string = data[1:-1] # Strip quotes
396
+ return Tree(
397
+ "string_", [TokenWrapper(data, stripped_string)], _create_meta(data)
398
+ )
399
+
400
+ def function(self, name: Token, args: Tree[TokenWrapper]) -> Tree[TokenWrapper]:
401
+ """
402
+ Transforms a function token and its arguments into a function tree node.
403
+
404
+ Parameters:
405
+ name: A token representing the function name, including an opening bracket.
406
+ args: A Tree object containing function arguments.
407
+
408
+ Returns:
409
+ A tree node representing the function call.
410
+ """
411
+ func = name[:-1] # Remove opening bracket
412
+ return Tree("function_", [TokenWrapper(name, func), args], _create_meta(name))
413
+
414
+ def variable(self, var: Token) -> Tree:
415
+ """
416
+ Resolves a variable token to either a context value or data-driven variable.
417
+
418
+ Parameters:
419
+ var: The variable token to resolve. If prefixed by '.', it is
420
+ interpreted as coming directly from data; otherwise, it
421
+ checks the context for predefined constants.
422
+
423
+ Returns:
424
+ A tree node representing the variable's resolved value, either
425
+ from `context` or as a raw variable name from data.
426
+ """
427
+ is_from_data = var.startswith(".")
428
+ if is_from_data:
429
+ # Strip the '.' prefix for data-driven vars
430
+ return Tree(
431
+ "var_from_data", [TokenWrapper(var, var[1:])], _create_meta(var)
432
+ )
433
+ if var in self.context:
434
+ # Resolve from context constants
435
+ return Tree("var_from_context", [self.context[var]], _create_meta(var))
436
+ return Tree("var_from_data", [TokenWrapper(var, var)], _create_meta(var))
437
+
438
+ def range_op(self, start: TokenWrapper, end: TokenWrapper):
439
+ """
440
+ Transforms a range operation (start to end) into an appropriate range object.
441
+
442
+ Parameters:
443
+ start: The start token of the range.
444
+ end: The end token of the range.
445
+
446
+ Returns:
447
+ A tree node wrapping a range object. If the tokens represent
448
+ IP addresses, the range will be transformed into an IP4Range
449
+ or IP6Range object, depending on the IP version; otherwise,
450
+ it will return a generic range operation node.
451
+ """
452
+
453
+ def _create_ip_node(range_class) -> Tree[TokenWrapper]:
454
+ """
455
+ Helper function to create a tree node for IP ranges.
456
+
457
+ Parameters:
458
+ range_class: The class (IP4Range or IP6Range) to instantiate.
459
+
460
+ Returns:
461
+ A tree node wrapping the created IP range object.
462
+ """
463
+ return Tree(
464
+ "ip",
465
+ [
466
+ TokenWrapper(
467
+ _add_tokens(_start.token, _end.token),
468
+ range_class(f"{_start.token}-{_end.token}"),
469
+ )
470
+ ],
471
+ )
472
+
473
+ _start, _end = start.children[0], end.children[0]
474
+ if isinstance(_start.real_value, IP4) and isinstance(_end.real_value, IP4):
475
+ return _create_ip_node(IP4Range)
476
+ elif isinstance(_start.real_value, IP6) and isinstance(_end.real_value, IP6):
477
+ return _create_ip_node(IP6Range)
478
+ return Tree("range_op", [start, end])
479
+
480
+
481
+ @v_args(inline=True)
482
+ class Filter(Interpreter):
483
+ """
484
+ A class that evaluates parsed expressions using provided data.
485
+
486
+ It provides methods to handle various operations such as arithmetic,
487
+ logical conditions, and data extraction.
488
+ """
489
+
490
+ data = None
491
+
492
+ def eval(self, tree: Tree, data: Optional[dict] = None):
493
+ """
494
+ Evaluates the given tree using the provided data.
495
+
496
+ Parameters:
497
+ tree: The parse tree to evaluate.
498
+ data: Optional dictionary containing the data being searched.
499
+
500
+ Returns:
501
+ The result of evaluating the tree.
502
+ """
503
+ self.data = data if data is not None else {}
504
+
505
+ res = self.visit(tree)
506
+
507
+ self.data = None
508
+ return res
509
+
510
+ def number(self, token: TokenWrapper) -> int | float:
511
+ """
512
+ Extracts a numeric value from a token.
513
+
514
+ Parameters:
515
+ token: A TokenWrapper object.
516
+
517
+ Returns:
518
+ The numeric value associated with the token.
519
+ """
520
+ return token.real_value
521
+
522
+ def ip(self, token: TokenWrapper) -> IP4:
523
+ """
524
+ Extracts an IP address from a token.
525
+
526
+ Parameters:
527
+ token: A TokenWrapper object.
528
+
529
+ Returns:
530
+ The IP4 address associated with the token.
531
+ """
532
+ return token.real_value
533
+
534
+ def datetime(self, token: TokenWrapper) -> datetime:
535
+ """
536
+ Extracts a datetime value from a token.
537
+
538
+ Parameters:
539
+ token: A TokenWrapper object.
540
+
541
+ Returns:
542
+ The datetime value associated with the token.
543
+ """
544
+ return token.real_value
545
+
546
+ def timedelta_(self, token: TokenWrapper) -> timedelta:
547
+ """
548
+ Extracts a timedelta value from a token.
549
+
550
+ Parameters:
551
+ token: A TokenWrapper object.
552
+
553
+ Returns:
554
+ The timedelta value associated with the token.
555
+ """
556
+ return token.real_value
557
+
558
+ def string_(self, token: TokenWrapper) -> str:
559
+ """
560
+ Extracts a string value from a token.
561
+
562
+ Parameters:
563
+ token: A TokenWrapper object.
564
+
565
+ Returns:
566
+ The string value associated with the token.
567
+ """
568
+ return token.real_value
569
+
570
+ def _binary_operation(self, op: str, l_tree: Tree, r_tree: Tree) -> Any:
571
+ try:
572
+ return binary_operation(op, self.visit(l_tree), self.visit(r_tree))
573
+ except Exception as e:
574
+ raise EvaluationError(
575
+ str(e),
576
+ line=l_tree.meta.line,
577
+ column=l_tree.meta.column,
578
+ start_pos=l_tree.meta.start_pos,
579
+ end_line=r_tree.meta.end_line,
580
+ end_column=r_tree.meta.end_column,
581
+ end_pos=r_tree.meta.end_pos,
582
+ ) from None
583
+
584
+ def add(self, l_tree: Tree, r_tree: Tree) -> Any:
585
+ return self._binary_operation("+", l_tree, r_tree)
586
+
587
+ def sub(self, l_tree: Tree, r_tree: Tree) -> Any:
588
+ return self._binary_operation("-", l_tree, r_tree)
589
+
590
+ def neg(self, tree: Tree) -> Any:
591
+ return -(self.visit(tree))
592
+
593
+ def mul(self, l_tree: Tree, r_tree: Tree) -> Any:
594
+ return self._binary_operation("*", l_tree, r_tree)
595
+
596
+ def div(self, l_tree: Tree, r_tree: Tree) -> Any:
597
+ return self._binary_operation("/", l_tree, r_tree)
598
+
599
+ def mod(self, l_tree: Tree, r_tree: Tree) -> Any:
600
+ return self._binary_operation("%", l_tree, r_tree)
601
+
602
+ def eq(self, l_tree: Tree, r_tree: Tree) -> bool:
603
+ return self.visit(l_tree) == self.visit(r_tree)
604
+
605
+ def gt(self, l_tree: Tree, r_tree: Tree) -> bool:
606
+ return self._binary_operation(">", l_tree, r_tree)
607
+
608
+ def gte(self, l_tree: Tree, r_tree: Tree) -> bool:
609
+ return self._binary_operation(">=", l_tree, r_tree)
610
+
611
+ def lt(self, l_tree: Tree, r_tree: Tree) -> bool:
612
+ return self._binary_operation("<", l_tree, r_tree)
613
+
614
+ def lte(self, l_tree: Tree, r_tree: Tree) -> bool:
615
+ return self._binary_operation("<=", l_tree, r_tree)
616
+
617
+ def any_eq(self, l_tree: Tree, r_tree: Tree) -> bool:
618
+ return self._binary_operation("=", l_tree, r_tree)
619
+
620
+ def or_op(self, l_tree: Tree, r_tree: Tree) -> bool:
621
+ """
622
+ Performs a logical OR operation.
623
+
624
+ If the left tree is evaluated to True, the right tree is not traversed.
625
+
626
+ Parameters:
627
+ l_tree: The left subtree.
628
+ r_tree: The right subtree.
629
+
630
+ Returns:
631
+ True if either subtree evaluates to True, otherwise False.
632
+ """
633
+ return self.visit(l_tree) or self.visit(r_tree)
634
+
635
+ def and_op(self, l_tree: Tree, r_tree: Tree) -> bool:
636
+ """
637
+ Performs a logical AND operation.
638
+
639
+ If the left tree is evaluated to False, the right tree is not traversed.
640
+
641
+ Parameters:
642
+ l_tree: The left subtree.
643
+ r_tree: The right subtree.
644
+
645
+ Returns:
646
+ True if both subtrees evaluate to True, otherwise False.
647
+ """
648
+ return self.visit(l_tree) and self.visit(r_tree)
649
+
650
+ def not_op(self, tree: Tree) -> bool:
651
+ """
652
+ Performs a logical NOT operation.
653
+
654
+ Parameters:
655
+ tree: The subtree.
656
+
657
+ Returns:
658
+ True if the subtree evaluates to False, otherwise False.
659
+ """
660
+ return not self.visit(tree)
661
+
662
+ def like_op(self, l_tree: Tree, r_tree: Tree) -> bool:
663
+ """
664
+ Checks if a string matches a regular expression pattern.
665
+
666
+ Parameters:
667
+ l_tree: Subtree representing the string to check.
668
+ r_tree: Subtree representing the regex pattern.
669
+
670
+ Returns:
671
+ True if the string matches the pattern, otherwise False.
672
+ """
673
+ return self._binary_operation("like", l_tree, r_tree)
674
+
675
+ def in_op(self, l_tree: Tree, r_tree: Tree) -> bool:
676
+ """
677
+ Checks if a value exists within a data structure.
678
+
679
+ Parameters:
680
+ l_tree: Subtree representing the value to look for.
681
+ r_tree: Subtree representing the data structure.
682
+
683
+ Returns:
684
+ True if the value exists in the data structure, otherwise False.
685
+ """
686
+ return self._binary_operation("in", l_tree, r_tree)
687
+
688
+ def contains_op(self, l_tree: Tree, r_tree: Tree) -> bool:
689
+ """
690
+ Checks if a string data contains string value.
691
+
692
+ Parameters:
693
+ l_tree: Subtree representing the string being searched.
694
+ r_tree: Subtree representing the value to check.
695
+
696
+ Returns:
697
+ True if the string contains the value, otherwise False.
698
+ """
699
+ return self._binary_operation("contains", l_tree, r_tree)
700
+
701
+ def concat_op(self, l_tree: Tree, r_tree: Tree) -> list | str:
702
+ """
703
+ Concatenates two sequences or strings.
704
+
705
+ Parameters:
706
+ l_tree: Subtree representing the first sequence or string.
707
+ r_tree: Subtree representing the second sequence or string.
708
+
709
+ Returns:
710
+ The concatenated result.
711
+ """
712
+ return self._binary_operation(".", l_tree, r_tree)
713
+
714
+ def var_from_context(self, var) -> Any:
715
+ """
716
+ Retrieves a variable directly from the context.
717
+
718
+ Parameters:
719
+ var: The variable name.
720
+
721
+ Returns:
722
+ The value of the variable in the context.
723
+ """
724
+ return var
725
+
726
+ def var_from_data(self, var: TokenWrapper):
727
+ """
728
+ Retrieves a variable's value from the data dictionary.
729
+
730
+ Parameters:
731
+ var: The variable name as a string.
732
+
733
+ Returns:
734
+ The value associated with the variable in the data dictionary.
735
+
736
+ Raises:
737
+ EvaluationError: If var is not found in the provided data.
738
+ """
739
+ res, exists = _get_data_value(var.real_value, self.data)
740
+ if not exists:
741
+ raise EvaluationError(
742
+ f"Variable '{var.real_value}' not found in the provided data!",
743
+ line=var.line,
744
+ column=var.column,
745
+ start_pos=var.start_pos,
746
+ end_line=var.end_line,
747
+ end_column=var.end_column,
748
+ end_pos=var.end_pos,
749
+ )
750
+ return res
751
+
752
+ @v_args(tree=True)
753
+ def list(self, data: Tree) -> list:
754
+ """
755
+ Extracts non-None children from a tree structure.
756
+
757
+ Parameters:
758
+ data: A Tree object.
759
+
760
+ Returns:
761
+ A list of non-None children from the tree.
762
+ """
763
+ return [self.visit(x) for x in data.children if x is not None]
764
+
765
+ def range_op(self, l_tree: Tree, r_tree: Tree) -> tuple:
766
+ """
767
+ Creates a tuple representing a range.
768
+
769
+ Parameters:
770
+ l_tree: Subtree representing the start of the range.
771
+ r_tree: Subtree representing the end of the range.
772
+
773
+ Returns:
774
+ A tuple containing the start and end values.
775
+ """
776
+ return (self.visit(l_tree), self.visit(r_tree))
777
+
778
+ def exists_op(self, path: Token) -> bool:
779
+ """
780
+ Tests whether a variable on a given path exists in data.
781
+
782
+ Parameters:
783
+ path: The variable name as a string.
784
+
785
+ Returns:
786
+ True if the variable exists, otherwise False.
787
+ """
788
+ _, exists = _get_data_value(path, self.data)
789
+ return exists
790
+
791
+ def exists_with_default(self, path: Token, default: Any) -> Any:
792
+ """
793
+ Tests whether a variable on a given path exists in data and returns its
794
+ value or a default if not found.
795
+
796
+ Parameters:
797
+ path: The variable name as a string.
798
+ default: The value to return if the variable does not exist.
799
+
800
+ Returns:
801
+ The value of the variable if it exists, otherwise the default value.
802
+ """
803
+ res, exists = _get_data_value(path, self.data)
804
+ if exists:
805
+ return res
806
+ return self.visit(default)
807
+
808
+ def function_(self, name: TokenWrapper, args: Tree | None):
809
+ """
810
+ Calls a predefined function with the given arguments.
811
+
812
+ Parameters:
813
+ name: The name of the function as a TokenWrapper.
814
+ args: A Tree object containing function arguments or None.
815
+
816
+ Returns:
817
+ The result of the function call.
818
+
819
+ Raises:
820
+ ValueError: If the function name is not found in predefined functions.
821
+ """
822
+ function_name = name.real_value
823
+ if function_name in predefined_functions:
824
+ try:
825
+ return predefined_functions[function_name](
826
+ *(self.visit(x) for x in (args.children if args else [])) # type: ignore
827
+ )
828
+ except TypeError as e:
829
+ raise EvaluationError(
830
+ str(e),
831
+ line=args.meta.line if args else name.line,
832
+ column=args.meta.column if args else name.column,
833
+ start_pos=args.meta.start_pos if args else name.start_pos,
834
+ end_line=args.meta.end_line if args else name.end_line,
835
+ end_column=args.meta.end_column if args else name.end_column,
836
+ end_pos=args.meta.end_pos if args else name.end_pos,
837
+ )
838
+ raise EvaluationError(
839
+ f"Function '{name.real_value}' was not found.",
840
+ line=name.line,
841
+ column=name.column,
842
+ start_pos=name.start_pos,
843
+ end_line=name.end_line,
844
+ end_column=name.end_column,
845
+ end_pos=name.end_pos,
846
+ )