exdrf 0.0.1.dev0__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 (57) hide show
  1. exdrf/__init__.py +0 -0
  2. exdrf/__version__.py +24 -0
  3. exdrf/api.py +51 -0
  4. exdrf/constants.py +30 -0
  5. exdrf/dataset.py +197 -0
  6. exdrf/field.py +554 -0
  7. exdrf/field_types/__init__.py +0 -0
  8. exdrf/field_types/api.py +78 -0
  9. exdrf/field_types/blob_field.py +44 -0
  10. exdrf/field_types/bool_field.py +47 -0
  11. exdrf/field_types/date_field.py +49 -0
  12. exdrf/field_types/date_time.py +52 -0
  13. exdrf/field_types/dur_field.py +44 -0
  14. exdrf/field_types/enum_field.py +41 -0
  15. exdrf/field_types/filter_field.py +11 -0
  16. exdrf/field_types/float_field.py +85 -0
  17. exdrf/field_types/float_list.py +18 -0
  18. exdrf/field_types/formatted.py +39 -0
  19. exdrf/field_types/int_field.py +70 -0
  20. exdrf/field_types/int_list.py +18 -0
  21. exdrf/field_types/ref_base.py +105 -0
  22. exdrf/field_types/ref_m2m.py +39 -0
  23. exdrf/field_types/ref_m2o.py +23 -0
  24. exdrf/field_types/ref_o2m.py +36 -0
  25. exdrf/field_types/ref_o2o.py +32 -0
  26. exdrf/field_types/sort_field.py +18 -0
  27. exdrf/field_types/str_field.py +77 -0
  28. exdrf/field_types/str_list.py +18 -0
  29. exdrf/field_types/time_field.py +49 -0
  30. exdrf/filter.py +653 -0
  31. exdrf/filter_dsl.py +950 -0
  32. exdrf/filter_op_catalog.py +222 -0
  33. exdrf/label_dsl.py +691 -0
  34. exdrf/moment.py +496 -0
  35. exdrf/py.typed +0 -0
  36. exdrf/py_support.py +21 -0
  37. exdrf/resource.py +901 -0
  38. exdrf/sa_fi_item.py +69 -0
  39. exdrf/sa_filter_op.py +324 -0
  40. exdrf/utils.py +17 -0
  41. exdrf/validator.py +45 -0
  42. exdrf/var_bag.py +328 -0
  43. exdrf/visitor.py +58 -0
  44. exdrf-0.0.1.dev0.dist-info/METADATA +42 -0
  45. exdrf-0.0.1.dev0.dist-info/RECORD +57 -0
  46. exdrf-0.0.1.dev0.dist-info/WHEEL +5 -0
  47. exdrf-0.0.1.dev0.dist-info/top_level.txt +3 -0
  48. exdrf_tests/__init__.py +0 -0
  49. exdrf_tests/test_dataset.py +422 -0
  50. exdrf_tests/test_field.py +109 -0
  51. exdrf_tests/test_filter.py +425 -0
  52. exdrf_tests/test_filter_dsl.py +556 -0
  53. exdrf_tests/test_label_dsl.py +234 -0
  54. exdrf_tests/test_resource.py +107 -0
  55. exdrf_tests/test_utils.py +43 -0
  56. exdrf_tests/test_visitor.py +31 -0
  57. exdrf_tests/var_bag_test.py +502 -0
exdrf/label_dsl.py ADDED
@@ -0,0 +1,691 @@
1
+ """Domain-specific language for parsing label expressions.
2
+
3
+ The expression always starts with an open parenthesis and ends with a close
4
+ parenthesis. Nested statements are also surrounded by parentheses. The first
5
+ element in the expression is the operator, and the rest are its arguments.
6
+ Strings are surrounded by double quotes. Other identifiers are treated as
7
+ properties of the SQLAlchemy model instance.
8
+
9
+ You can use this DSL to associate labels with Resources.
10
+
11
+ Examples:
12
+
13
+ ```
14
+ (concat first_name " " last_name)
15
+ (if (upper name) "Yes" "No")
16
+ (concat (upper first_name) (lower last_name))
17
+ (concat (upper name.first) (lower name.last))
18
+ (is_none attrib "Is none" "Is not none")
19
+ ```
20
+ """
21
+
22
+ import re
23
+ from typing import Any, Dict, List, Literal, Union, cast
24
+
25
+ from attrs import define, field
26
+
27
+ ASTNode = Union["ParsedOp", "ParsedLiteral", "ParsedIdentifier", List["ASTNode"]]
28
+
29
+ # \s*: This part matches zero or more whitespace characters.
30
+ # (\(|\)|\"[^\"]*\"|[^\s()]+): This is the main capturing group, enclosed
31
+ # in parentheses (). It uses the | operator (logical OR) to match one of
32
+ # four possible patterns:
33
+ # \(: Matches an opening parenthesis (.
34
+ # \): Matches a closing parenthesis ).
35
+ # \"[^\"]*\": Matches a double-quoted string.
36
+ # [^\s()]+: Matches one or more characters that are not whitespace (\s),
37
+ # an opening parenthesis (, or a closing parenthesis ).
38
+ # The + quantifier ensures that at least one such character is matched.
39
+ # This is useful for capturing standalone tokens or words.
40
+ token_pattern = re.compile(r"\s*(" r"\(|\)|\"[^\"]*\"|" r"[^\s()]+" ")")
41
+
42
+ # Simple regex patterns to match integers.
43
+ int_pattern = re.compile(r"^\d+$")
44
+
45
+ # Simple regex patterns to match floats.
46
+ float_pattern = re.compile(r"^\d+\.\d+$")
47
+
48
+
49
+ class Null:
50
+ """A class to represent a null value."""
51
+
52
+
53
+ @define(eq=False)
54
+ class Parsed:
55
+ """Base class for parsed elements in the AST.
56
+
57
+ Attributes:
58
+ value: The string value of the parsed element.
59
+ """
60
+
61
+ value: str
62
+
63
+ def __str__(self) -> str:
64
+ return self.value
65
+
66
+ def __eq__(self, other: object) -> bool:
67
+ if isinstance(other, Parsed):
68
+ return self.value == other.value
69
+ elif isinstance(other, str):
70
+ return self.value == other
71
+ return False
72
+
73
+
74
+ @define(eq=False)
75
+ class ParsedOp(Parsed):
76
+ """Parsed operator in the AST.
77
+
78
+ This is the first element of an expression and indicates the operation to
79
+ be performed on the subsequent elements.
80
+ """
81
+
82
+
83
+ @define(eq=False)
84
+ class ParsedLiteral(Parsed):
85
+ """Parsed literal in the AST.
86
+
87
+ Arguments can be either literals (this class) or identifiers. The literals
88
+ can be strings (denoted by double quotes), integers, or floats.
89
+
90
+ Attributes:
91
+ type: The type of the literal (string, int, float).
92
+ """
93
+
94
+ type: Literal["string", "int", "float"] = field(default="string")
95
+
96
+ @property
97
+ def as_string(self) -> str:
98
+ """The value ass it can be used in a string representation.
99
+
100
+ Strings are double-quoted, numbers are inserted as they are.
101
+ """
102
+ if self.type == "string":
103
+ return f'"{self.value}"'
104
+ else:
105
+ return self.value
106
+
107
+ @property
108
+ def raw_value(self) -> Any:
109
+ """Python value representation.
110
+
111
+ Strings are returned as they are (we store strings), numbers
112
+ are converted to integers or reals.
113
+ """
114
+ if self.type == "string":
115
+ return self.value
116
+ elif self.type == "int":
117
+ return int(self.value)
118
+ elif self.type == "float":
119
+ return float(self.value)
120
+ else:
121
+ raise ValueError(f"Unknown type: {self.type}")
122
+
123
+ @property
124
+ def ensure_str(self) -> Any:
125
+ """Python value representation."""
126
+ if self.type == "string":
127
+ return self.value
128
+ elif self.type == "int":
129
+ return int(self.value)
130
+ elif self.type == "float":
131
+ return float(self.value)
132
+ else:
133
+ raise ValueError(f"Unknown type: {self.type}")
134
+
135
+
136
+ @define(eq=False)
137
+ class ParsedIdentifier(Parsed):
138
+ """Parsed identifier in the AST.
139
+
140
+ The identifier is a type of argument that can retrieve the actual value
141
+ from the resource.
142
+ """
143
+
144
+ def retrieve(self, context: Any) -> Any:
145
+ """Retrieve the value of the identifier from the context.
146
+
147
+ Args:
148
+ context: The context in which to evaluate the expression.
149
+ """
150
+ parts = self.value.split(".")
151
+ attr = context
152
+
153
+ if isinstance(attr, dict):
154
+ for part in parts:
155
+ attr = attr.get(part, Null)
156
+ if attr is Null:
157
+ raise AttributeError(f"Value `{part}` not found in {attr}")
158
+ else:
159
+ for part in parts:
160
+ attr = getattr(attr, part, Null)
161
+ if attr is Null:
162
+ raise AttributeError(f"Attribute `{part}` not found in {attr}")
163
+ return attr
164
+
165
+
166
+ @define
167
+ class Operation:
168
+ """Base class for operations in the DSL.
169
+
170
+ The ParsedOp will end up an instance of this class, which will be used to
171
+ evaluate the expression.
172
+
173
+ Attributes:
174
+ key: The key that identifies the operation.
175
+ """
176
+
177
+ key: str
178
+
179
+ def evaluate(self, *args) -> str:
180
+ """Evaluate the operation online with the given arguments."""
181
+ raise NotImplementedError("Subclasses should implement this!")
182
+
183
+ def to_python(self, *args) -> str:
184
+ """Generate Python code for the operation."""
185
+ raise NotImplementedError("Subclasses should implement this!")
186
+
187
+ def to_typescript(self, *args) -> str:
188
+ """Generate TypeScript code for the operation."""
189
+ raise NotImplementedError("Subclasses should implement this!")
190
+
191
+
192
+ @define
193
+ class Concat(Operation):
194
+ """Concatenate strings together.
195
+
196
+ Example:
197
+ (concat first_name " " last_name) -> "John Doe"
198
+ """
199
+
200
+ key: str = field(default="concat", init=False)
201
+
202
+ def evaluate(self, *args) -> Any:
203
+ return "".join(map(str, args))
204
+
205
+ def to_python(self, *args) -> str:
206
+ return "(" + " + ".join([f"str({a})" for a in args]) + ")"
207
+
208
+ def to_typescript(self, *args) -> str:
209
+ return "(" + " + ".join(args) + ")"
210
+
211
+
212
+ @define
213
+ class If(Operation):
214
+ """If statement.
215
+
216
+ Example:
217
+ (if (upper name) "Yes" "No") -> "Yes" if name is upper, else "No"
218
+ """
219
+
220
+ key: str = field(default="if", init=False)
221
+
222
+ def evaluate(self, *args) -> Any:
223
+ assert len(args) == 3, (
224
+ f"If operation takes three arguments, got {len(args)}: "
225
+ f"{'\n'.join(map(str, args))}"
226
+ )
227
+ cond, a, b = args
228
+ return a if cond else b
229
+
230
+ def to_python(self, *args) -> str:
231
+ assert len(args) == 3, (
232
+ f"If operation takes three arguments, got {len(args)}: "
233
+ f"{'\n'.join(map(str, args))}"
234
+ )
235
+ cond, a, b = args
236
+ return f"({a} if {cond} else {b})"
237
+
238
+ def to_typescript(self, *args) -> str:
239
+ assert len(args) == 3, (
240
+ f"If operation takes three arguments, got {len(args)}: "
241
+ f"{'\n'.join(map(str, args))}"
242
+ )
243
+ cond, a, b = args
244
+ return f"({cond} ? {a} : {b})"
245
+
246
+
247
+ @define
248
+ class Upper(Operation):
249
+ """Convert string to upper case.
250
+
251
+ Example:
252
+ (upper name) -> "JOHN DOE"
253
+ """
254
+
255
+ key: str = field(default="upper", init=False)
256
+
257
+ def evaluate(self, *args) -> Any:
258
+ assert len(args) == 1, (
259
+ f"Upper operation takes one argument, got {len(args)}: "
260
+ f"{'\n'.join(map(str, args))}"
261
+ )
262
+ (s,) = args
263
+ return str(s).upper()
264
+
265
+ def to_python(self, *args) -> str:
266
+ assert len(args) == 1, (
267
+ f"Upper operation takes one argument, got {len(args)}: "
268
+ f"{'\n'.join(map(str, args))}"
269
+ )
270
+ (s,) = args
271
+ return f"str({s}).upper()"
272
+
273
+ def to_typescript(self, *args) -> str:
274
+ assert len(args) == 1, (
275
+ f"Upper operation takes one argument, got {len(args)}: "
276
+ f"{'\n'.join(map(str, args))}"
277
+ )
278
+ (s,) = args
279
+ return f"String({s}).toUpperCase()"
280
+
281
+
282
+ @define
283
+ class Lower(Operation):
284
+ """Convert string to lower case.
285
+
286
+ Example:
287
+ (lower name) -> "john doe"
288
+ """
289
+
290
+ key: str = field(default="lower", init=False)
291
+
292
+ def evaluate(self, *args) -> Any:
293
+ assert len(args) == 1, (
294
+ f"Lower operation takes one argument, got {len(args)}: "
295
+ f"{'\n'.join(map(str, args))}"
296
+ )
297
+ (s,) = args
298
+ return str(s).lower()
299
+
300
+ def to_python(self, *args) -> str:
301
+ assert len(args) == 1, (
302
+ f"Lower operation takes one argument, got {len(args)}: "
303
+ f"{'\n'.join(map(str, args))}"
304
+ )
305
+ (s,) = args
306
+ return f"str({s}).lower()"
307
+
308
+ def to_typescript(self, *args) -> str:
309
+ assert len(args) == 1, (
310
+ f"Lower operation takes one argument, got {len(args)}: "
311
+ f"{'\n'.join(map(str, args))}"
312
+ )
313
+ (s,) = args
314
+ return f"String({s}).toLowerCase()"
315
+
316
+
317
+ @define
318
+ class IsNone(Operation):
319
+ """Check if the first argument is None.
320
+
321
+ Example:
322
+ (is_none attrib "Is none" "Is not none") -> "Is none"
323
+ """
324
+
325
+ key: str = field(default="is_none", init=False)
326
+
327
+ def evaluate(self, *args) -> Any:
328
+ assert len(args) == 3, (
329
+ f"IsNone operation takes three arguments, got {len(args)}: "
330
+ f"{'\n'.join(map(str, args))}"
331
+ )
332
+ cond, a, b = args
333
+ return a if cond is None else b
334
+
335
+ def to_python(self, *args) -> str:
336
+ assert len(args) == 3, (
337
+ f"IsNone operation takes three arguments, got {len(args)}: "
338
+ f"{'\n'.join(map(str, args))}"
339
+ )
340
+ cond, a, b = args
341
+ return f"({a} if {cond} is None else {b})"
342
+
343
+ def to_typescript(self, *args) -> str:
344
+ assert len(args) == 3, (
345
+ f"IsNone operation takes three arguments, got {len(args)}: "
346
+ f"{'\n'.join(map(str, args))}"
347
+ )
348
+ cond, a, b = args
349
+ return f"(({cond} == null || {cond} == undefined) ? {a} : {b})"
350
+
351
+
352
+ @define
353
+ class Equals(Operation):
354
+ """Check if the first argument is equal to the second.
355
+
356
+ Example:
357
+ (= name "John Doe" "Yes" "No") -> "Yes"
358
+ """
359
+
360
+ key: str = field(default="=", init=False)
361
+
362
+ def evaluate(self, *args) -> Any:
363
+ assert len(args) == 4, (
364
+ f"Equals operation takes four arguments, got {len(args)}: "
365
+ f"{'\n'.join(map(str, args))}"
366
+ )
367
+ cond1, cond2, a, b = args
368
+ return a if cond1 == cond2 else b
369
+
370
+ def to_python(self, *args) -> str:
371
+ assert len(args) == 4, (
372
+ f"Equals operation takes four arguments, got {len(args)}: "
373
+ f"{'\n'.join(map(str, args))}"
374
+ )
375
+ cond1, cond2, a, b = args
376
+ return f"({a} if {cond1} == {cond2} else {b})"
377
+
378
+ def to_typescript(self, *args) -> str:
379
+ assert len(args) == 4, (
380
+ f"Equals operation takes four arguments, got {len(args)}: "
381
+ f"{'\n'.join(map(str, args))}"
382
+ )
383
+ cond1, cond2, a, b = args
384
+ return f"(({cond1} == {cond2}) ? {a} : {b})"
385
+
386
+
387
+ @define
388
+ class DateStr(Operation):
389
+ """Convert date to string using strftime.
390
+
391
+ Note that this relies on the existence of a strftime method
392
+ on the date class in javascript, which is not standard.
393
+
394
+ Example:
395
+ (date_str date "%Y-%m-%d") -> "2023-10-01"
396
+ """
397
+
398
+ key: str = field(default="date_str", init=False)
399
+
400
+ def evaluate(self, *args) -> Any:
401
+ assert len(args) == 2, (
402
+ f"DateStr operation takes two arguments, got {len(args)}: "
403
+ f"{'\n'.join(map(str, args))}"
404
+ )
405
+ date, format = args
406
+ return date.strftime(format)
407
+
408
+ def to_python(self, *args) -> str:
409
+ assert len(args) == 2, (
410
+ f"DateStr operation takes two arguments, got {len(args)}: "
411
+ f"{'\n'.join(map(str, args))}"
412
+ )
413
+ date, format = args
414
+ return f"({date}.strftime({format}))"
415
+
416
+ def to_typescript(self, *args) -> str:
417
+ assert len(args) == 2, (
418
+ f"DateStr operation takes two arguments, got {len(args)}: "
419
+ f"{'\n'.join(map(str, args))}"
420
+ )
421
+ date, format = args
422
+ return f"({date}.strftime({format}))"
423
+
424
+
425
+ @define
426
+ class FloatStr(Operation):
427
+ """Convert float to string with specified number of digits.
428
+
429
+ Example:
430
+ (float_str number digits) -> "123.45"
431
+ """
432
+
433
+ key: str = field(default="float_str", init=False)
434
+
435
+ def evaluate(self, *args) -> Any:
436
+ assert len(args) == 2, (
437
+ f"FloatStr operation takes two arguments, got {len(args)}: "
438
+ f"{'\n'.join(map(str, args))}"
439
+ )
440
+ number, digits = args
441
+ return ("{:." + str(digits) + "f}").format(number)
442
+
443
+ def to_python(self, *args) -> str:
444
+ assert len(args) == 2, (
445
+ f"FloatStr operation takes two arguments, got {len(args)}: "
446
+ f"{'\n'.join(map(str, args))}"
447
+ )
448
+ number, digits = args
449
+ return '("{:." + str(' + str(digits) + ') + "f}").format(' + str(number) + ")"
450
+
451
+ def to_typescript(self, *args) -> str:
452
+ assert len(args) == 2, (
453
+ f"FloatStr operation takes two arguments, got {len(args)}: "
454
+ f"{'\n'.join(map(str, args))}"
455
+ )
456
+ number, digits = args
457
+ return (
458
+ f"({number})."
459
+ "toLocaleString('en-US', { "
460
+ f"minimumFractionDigits: {digits}, "
461
+ f"maximumFractionDigits: {digits}"
462
+ "});"
463
+ )
464
+
465
+
466
+ @define
467
+ class IntStr(Operation):
468
+ """Convert int to string with thousands separator.
469
+
470
+ Example:
471
+ (int_str number) -> "1,234,567"
472
+ """
473
+
474
+ key: str = field(default="int_str", init=False)
475
+
476
+ def evaluate(self, *args) -> Any:
477
+ assert len(args) == 1, (
478
+ f"IntStr operation takes one argument, got {len(args)}: "
479
+ f"{'\n'.join(map(str, args))}"
480
+ )
481
+ (number,) = args
482
+ return f"{number:,}"
483
+
484
+ def to_python(self, *args) -> str:
485
+ assert len(args) == 1, (
486
+ f"IntStr operation takes one argument, got {len(args)}: "
487
+ f"{'\n'.join(map(str, args))}"
488
+ )
489
+ (number,) = args
490
+ return 'f"{' + str(number) + ':,}"'
491
+
492
+ def to_typescript(self, *args) -> str:
493
+ assert len(args) == 1, (
494
+ f"IntStr operation takes one argument, got {len(args)}: "
495
+ f"{'\n'.join(map(str, args))}"
496
+ )
497
+ (number,) = args
498
+ return (
499
+ f"({number})."
500
+ "toLocaleString('en-US', { "
501
+ "minimumFractionDigits: 0, "
502
+ "maximumFractionDigits: 0"
503
+ "});"
504
+ )
505
+
506
+
507
+ ops: Dict[str, Operation] = {
508
+ "concat": Concat(),
509
+ "if": If(),
510
+ "upper": Upper(),
511
+ "lower": Lower(),
512
+ "is_none": IsNone(),
513
+ "=": Equals(),
514
+ "date_str": DateStr(),
515
+ "float_str": FloatStr(),
516
+ "int_str": IntStr(),
517
+ }
518
+
519
+
520
+ def parse_expr(expr: str) -> ASTNode:
521
+ """Parses the label expression string into an AST.
522
+
523
+ Supports nested expressions like: (if (upper name) "Yes" "No")
524
+ """
525
+
526
+ # Tokenize: parentheses, quoted strings, or other tokens
527
+ tokens = re.findall(token_pattern, expr)
528
+
529
+ def parse_tokens(tokens):
530
+ if not tokens:
531
+ raise SyntaxError("Unexpected EOF while reading")
532
+
533
+ token = tokens.pop(0)
534
+
535
+ if token == "(":
536
+ lst = []
537
+ try:
538
+ op_token = tokens.pop(0)
539
+ if not isinstance(op_token, str):
540
+ raise SyntaxError("Expected operator after `(`")
541
+ lst.append(ParsedOp(op_token))
542
+
543
+ while tokens[0] != ")":
544
+ lst.append(parse_tokens(tokens))
545
+ except IndexError:
546
+ raise SyntaxError("Unexpected EOF while expecting `)`")
547
+ tokens.pop(0) # Remove ')'
548
+ return lst
549
+
550
+ elif token == ")":
551
+ raise SyntaxError("Unexpected )")
552
+
553
+ elif token.startswith('"') and token.endswith('"'):
554
+ return ParsedLiteral(token[1:-1]) # Remove surrounding quotes
555
+
556
+ elif int_pattern.match(token):
557
+ return ParsedLiteral(token, type="int")
558
+
559
+ elif float_pattern.match(token):
560
+ return ParsedLiteral(token, type="float")
561
+
562
+ else:
563
+ return ParsedIdentifier(token)
564
+
565
+ ast = parse_tokens(tokens)
566
+ if tokens:
567
+ raise SyntaxError("Unexpected tokens after parsing")
568
+ return ast
569
+
570
+
571
+ def _eval_op(op: ParsedOp, args: List[Any]) -> Any:
572
+ """Evaluate the operator with the given arguments.
573
+
574
+ Args:
575
+ op: The operator to evaluate.
576
+ args: The arguments to the operator.
577
+ Returns:
578
+ The result of the evaluation.
579
+ """
580
+ if op.value not in ops:
581
+ raise ValueError(f"Unknown operator: {op}")
582
+ return ops[op.value].evaluate(*args)
583
+
584
+
585
+ def evaluate(ast_node: ASTNode, context: Any) -> Any:
586
+ """Evaluate the parsed AST in the context of a Resource instance.
587
+
588
+ The context is usually a SQLAlchemy model instance. The function will
589
+ replace identifiers with their values from the context and evaluate the
590
+ expression.
591
+
592
+ For example, if the context has a field `name`, and the AST is `["upper",
593
+ "name"]`, the function will return `context.name.upper()`.
594
+
595
+ Args:
596
+ ast_node: The parsed AST node.
597
+ context: The context in which to evaluate the expression.
598
+
599
+ Returns:
600
+ The result of the evaluation.
601
+ """
602
+ if isinstance(ast_node, ParsedIdentifier):
603
+ return ast_node.retrieve(context)
604
+
605
+ elif isinstance(ast_node, ParsedLiteral):
606
+ return ast_node.raw_value
607
+
608
+ elif isinstance(ast_node, list):
609
+ op = cast(ParsedOp, ast_node[0])
610
+ assert isinstance(op, ParsedOp), "First element must be an operator"
611
+ args = [evaluate(arg, context) for arg in ast_node[1:]]
612
+ return _eval_op(op, args)
613
+
614
+
615
+ def generate_python_code(ast_node: ASTNode) -> Any:
616
+ """Generate Python code from the AST.
617
+
618
+ This function traverses the AST and generates Python code that can be
619
+ used to evaluate the expression.
620
+
621
+ Args:
622
+ ast_node: The parsed AST node.
623
+
624
+ Returns:
625
+ The generated Python code as a string.
626
+ """
627
+ if isinstance(ast_node, ParsedLiteral):
628
+ return ast_node.as_string
629
+
630
+ elif isinstance(ast_node, ParsedIdentifier):
631
+ return f"record.{ast_node}"
632
+
633
+ elif isinstance(ast_node, list):
634
+ op = cast(ParsedOp, ast_node[0])
635
+ args = [generate_python_code(arg) for arg in ast_node[1:]]
636
+ op_class = ops.get(op.value)
637
+ if op_class:
638
+ return op_class.to_python(*args)
639
+ else:
640
+ raise ValueError(f"Unsupported operator: {op}")
641
+
642
+
643
+ def generate_typescript_code(ast_node: ASTNode) -> Any:
644
+ """Generate TypeScript code from the AST.
645
+
646
+ Args:
647
+ ast_node: The parsed AST node.
648
+
649
+ Returns:
650
+ The generated TypeScript code as a string.
651
+ """
652
+ if isinstance(ast_node, ParsedIdentifier):
653
+ return f"record.{ast_node}"
654
+
655
+ elif isinstance(ast_node, ParsedLiteral):
656
+ return ast_node.as_string
657
+
658
+ elif isinstance(ast_node, list):
659
+ op = cast(ParsedOp, ast_node[0])
660
+ args = [generate_typescript_code(arg) for arg in ast_node[1:]]
661
+ op_class = ops.get(op.value)
662
+ if op_class:
663
+ return op_class.to_typescript(*args)
664
+ else:
665
+ raise ValueError(f"Unsupported operator: {op}")
666
+
667
+
668
+ def get_used_fields(ast: ASTNode) -> List[str]:
669
+ """Get the list of fields used in the AST.
670
+
671
+ This function traverses the AST and collects all identifiers that are
672
+ valid Python identifiers. It returns a sorted list of these identifiers.
673
+
674
+ Args:
675
+ ast: The parsed AST node.
676
+
677
+ Returns:
678
+ A sorted list of field names used in the AST.
679
+ """
680
+ fields = set()
681
+
682
+ def walk(node):
683
+ if isinstance(node, ParsedIdentifier):
684
+ fields.add(node.value)
685
+ elif isinstance(node, list):
686
+ # skip operator
687
+ for sub in node[1:]:
688
+ walk(sub)
689
+
690
+ walk(ast)
691
+ return sorted(fields)