pylegend 0.11.0__py3-none-any.whl → 0.13.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 (40) hide show
  1. pylegend/core/database/sql_to_string/db_extension.py +244 -6
  2. pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +190 -5
  3. pylegend/core/language/pandas_api/pandas_api_series.py +3 -0
  4. pylegend/core/language/shared/expression.py +5 -0
  5. pylegend/core/language/shared/literal_expressions.py +22 -1
  6. pylegend/core/language/shared/operations/boolean_operation_expressions.py +144 -0
  7. pylegend/core/language/shared/operations/date_operation_expressions.py +91 -0
  8. pylegend/core/language/shared/operations/integer_operation_expressions.py +183 -1
  9. pylegend/core/language/shared/operations/string_operation_expressions.py +31 -1
  10. pylegend/core/language/shared/primitives/boolean.py +40 -0
  11. pylegend/core/language/shared/primitives/date.py +39 -0
  12. pylegend/core/language/shared/primitives/datetime.py +18 -0
  13. pylegend/core/language/shared/primitives/integer.py +54 -1
  14. pylegend/core/language/shared/primitives/strictdate.py +25 -1
  15. pylegend/core/language/shared/primitives/string.py +16 -2
  16. pylegend/core/sql/metamodel.py +54 -2
  17. pylegend/core/sql/metamodel_extension.py +77 -1
  18. pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +53 -7
  19. pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
  20. pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
  21. pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
  22. pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
  23. pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
  24. pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
  25. pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
  26. pylegend/core/tds/pandas_api/frames/functions/iloc.py +99 -0
  27. pylegend/core/tds/pandas_api/frames/functions/loc.py +136 -0
  28. pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
  29. pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
  30. pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +340 -34
  31. pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +90 -9
  32. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
  33. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
  34. pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
  35. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/METADATA +1 -1
  36. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/RECORD +40 -36
  37. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/WHEEL +1 -1
  38. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE +0 -0
  39. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE.spdx +0 -0
  40. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -33,7 +33,13 @@ from pylegend.core.language.shared.operations.integer_operation_expressions impo
33
33
  PyLegendIntegerSubtractExpression,
34
34
  PyLegendIntegerMultiplyExpression,
35
35
  PyLegendIntegerModuloExpression,
36
- PyLegendIntegerCharExpression
36
+ PyLegendIntegerCharExpression,
37
+ PyLegendIntegerBitAndExpression,
38
+ PyLegendIntegerBitOrExpression,
39
+ PyLegendIntegerBitXorExpression,
40
+ PyLegendIntegerBitShiftLeftExpression,
41
+ PyLegendIntegerBitShiftRightExpression,
42
+ PyLegendIntegerBitNotExpression
37
43
  )
38
44
  if TYPE_CHECKING:
39
45
  from pylegend.core.language.shared.primitives import PyLegendFloat
@@ -159,6 +165,53 @@ class PyLegendInteger(PyLegendNumber):
159
165
  def __pos__(self) -> "PyLegendInteger":
160
166
  return self
161
167
 
168
+ def __invert__(self) -> "PyLegendInteger":
169
+ return PyLegendInteger(PyLegendIntegerBitNotExpression(self.__value_copy))
170
+
171
+ def __and__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
172
+ return self._create_binary_expression(other, PyLegendIntegerBitAndExpression, "and (&)")
173
+
174
+ def __rand__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
175
+ return self._create_binary_expression(other, PyLegendIntegerBitAndExpression, "and (&)", reverse=True)
176
+
177
+ def __or__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
178
+ return self._create_binary_expression(other, PyLegendIntegerBitOrExpression, "or (|)")
179
+
180
+ def __ror__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
181
+ return self._create_binary_expression(other, PyLegendIntegerBitOrExpression, "or (|)", reverse=True)
182
+
183
+ def __xor__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
184
+ return self._create_binary_expression(other, PyLegendIntegerBitXorExpression, "xor (^)")
185
+
186
+ def __rxor__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
187
+ return self._create_binary_expression(other, PyLegendIntegerBitXorExpression, "xor (^)", reverse=True)
188
+
189
+ def __lshift__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
190
+ return self._create_binary_expression(other, PyLegendIntegerBitShiftLeftExpression, "left shift (<<)")
191
+
192
+ def __rlshift__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
193
+ return self._create_binary_expression(other, PyLegendIntegerBitShiftLeftExpression, "left shift (<<)", reverse=True)
194
+
195
+ def __rshift__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
196
+ return self._create_binary_expression(other, PyLegendIntegerBitShiftRightExpression, "right shift (>>)")
197
+
198
+ def __rrshift__(self, other: PyLegendUnion[int, "PyLegendInteger"]) -> "PyLegendInteger":
199
+ return self._create_binary_expression(other, PyLegendIntegerBitShiftRightExpression, "right shift (>>)", reverse=True)
200
+
201
+ def _create_binary_expression(
202
+ self,
203
+ other: PyLegendUnion[int, "PyLegendInteger"],
204
+ expression_class: type,
205
+ operation_name: str,
206
+ reverse: bool = False
207
+ ) -> "PyLegendInteger":
208
+ PyLegendInteger.__validate__param_to_be_integer(other, f"Integer {operation_name} parameter")
209
+ other_op = PyLegendInteger.__convert_to_integer_expr(other)
210
+
211
+ if reverse:
212
+ return PyLegendInteger(expression_class(other_op, self.__value_copy))
213
+ return PyLegendInteger(expression_class(self.__value_copy, other_op))
214
+
162
215
  @staticmethod
163
216
  def __convert_to_integer_expr(
164
217
  val: PyLegendUnion[int, "PyLegendInteger"]
@@ -24,13 +24,16 @@ from pylegend.core.language.shared.expression import (
24
24
  )
25
25
  from pylegend.core.language.shared.literal_expressions import (
26
26
  PyLegendStrictDateLiteralExpression,
27
+ PyLegendIntegerLiteralExpression,
28
+ PyLegendStringLiteralExpression
27
29
  )
28
30
  from pylegend.core.sql.metamodel import (
29
31
  Expression,
30
32
  QuerySpecification
31
33
  )
32
34
  from pylegend.core.tds.tds_frame import FrameToSqlConfig
33
-
35
+ from pylegend.core.language.shared.operations.date_operation_expressions import PyLegendDateTimeBucketExpression
36
+ from pylegend.core.language.shared.primitives.integer import PyLegendInteger
34
37
 
35
38
  __all__: PyLegendSequence[str] = [
36
39
  "PyLegendStrictDate"
@@ -57,6 +60,20 @@ class PyLegendStrictDate(PyLegendDate):
57
60
  def value(self) -> PyLegendExpressionStrictDateReturn:
58
61
  return self.__value
59
62
 
63
+ def time_bucket(
64
+ self,
65
+ quantity: PyLegendUnion[int, "PyLegendInteger"],
66
+ duration_unit: str) -> "PyLegendDate":
67
+ self.validate_param_to_be_int_or_int_expr(quantity, "time bucket quantity parameter")
68
+ quantity_op = PyLegendIntegerLiteralExpression(quantity) if isinstance(quantity, int) else quantity.value()
69
+ self.validate_duration_unit_param(duration_unit)
70
+ duration_unit_op = PyLegendStringLiteralExpression(duration_unit.upper())
71
+ return PyLegendDate(PyLegendDateTimeBucketExpression([
72
+ self.__value,
73
+ quantity_op,
74
+ duration_unit_op,
75
+ PyLegendStringLiteralExpression("STRICTDATE")]))
76
+
60
77
  @staticmethod
61
78
  def __convert_to_strictdate_expr(
62
79
  val: PyLegendUnion[date, "PyLegendStrictDate"]
@@ -73,3 +90,10 @@ class PyLegendStrictDate(PyLegendDate):
73
90
  if not isinstance(param, (date, PyLegendStrictDate)):
74
91
  raise TypeError(desc + " should be a datetime.date or a StrictDate expression (PyLegendStrictDate)."
75
92
  " Got value " + str(param) + " of type: " + str(type(param)))
93
+
94
+ @staticmethod
95
+ def validate_duration_unit_param(duration_unit: str) -> None:
96
+ if duration_unit.lower() not in ('years', 'months', 'weeks', 'days'):
97
+ raise ValueError(
98
+ f"Unknown duration unit - {duration_unit}. Supported values are - YEARS, MONTHS, WEEKS, DAYS"
99
+ )
@@ -18,7 +18,10 @@ from pylegend._typing import (
18
18
  PyLegendUnion,
19
19
  PyLegendOptional
20
20
  )
21
- from pylegend.core.language.shared.literal_expressions import PyLegendIntegerLiteralExpression
21
+ from pylegend.core.language.shared.literal_expressions import (
22
+ PyLegendIntegerLiteralExpression,
23
+ convert_literal_to_literal_expression
24
+ )
22
25
  from pylegend.core.language.shared.primitives.primitive import PyLegendPrimitive
23
26
  from pylegend.core.language.shared.primitives.integer import PyLegendInteger
24
27
  from pylegend.core.language.shared.primitives.float import PyLegendFloat
@@ -67,7 +70,8 @@ from pylegend.core.language.shared.operations.string_operation_expressions impor
67
70
  PyLegendStringSplitPartExpression,
68
71
  PyLegendStringFullMatchExpression,
69
72
  PyLegendStringRepeatStringExpression,
70
- PyLegendStringMatchExpression
73
+ PyLegendStringMatchExpression,
74
+ PyLegendStringCoalesceExpression
71
75
  )
72
76
 
73
77
  __all__: PyLegendSequence[str] = [
@@ -242,6 +246,16 @@ class PyLegendString(PyLegendPrimitive):
242
246
  times_op = PyLegendIntegerLiteralExpression(times) if isinstance(times, int) else times.value()
243
247
  return PyLegendString(PyLegendStringRepeatStringExpression(self.__value, times_op))
244
248
 
249
+ def coalesce(self, *other: PyLegendOptional[PyLegendUnion[str, "PyLegendString"]]) -> "PyLegendString":
250
+ other_op = []
251
+ for op in other:
252
+ if op is not None:
253
+ PyLegendString.__validate_param_to_be_str_or_str_expr(op, "coalesce parameter")
254
+ other_op.append(
255
+ convert_literal_to_literal_expression(op) if not isinstance(op, PyLegendString) else op.__value)
256
+
257
+ return PyLegendString(PyLegendStringCoalesceExpression([self.__value, *other_op]))
258
+
245
259
  def __add__(self, other: PyLegendUnion[str, "PyLegendString"]) -> "PyLegendString":
246
260
  PyLegendString.__validate_param_to_be_str_or_str_expr(other, "String plus (+) parameter")
247
261
  other_op = PyLegendStringLiteralExpression(other) if isinstance(other, str) else other.__value
@@ -87,7 +87,11 @@ __all__: PyLegendSequence[str] = [
87
87
  'InPredicate',
88
88
  'Window',
89
89
  'WindowFrame',
90
- 'FrameBound'
90
+ 'FrameBound',
91
+ 'BitwiseShiftDirection',
92
+ 'BitwiseBinaryOperator',
93
+ 'BitwiseBinaryExpression',
94
+ 'BitwiseShiftExpression',
91
95
  ]
92
96
 
93
97
 
@@ -179,6 +183,17 @@ class ExtractField(Enum):
179
183
  EPOCH = 17
180
184
 
181
185
 
186
+ class BitwiseShiftDirection(Enum):
187
+ LEFT = 1
188
+ RIGHT = 2
189
+
190
+
191
+ class BitwiseBinaryOperator(Enum):
192
+ AND = 1
193
+ OR = 2
194
+ XOR = 3
195
+
196
+
182
197
  class Node:
183
198
  _type: str
184
199
 
@@ -918,12 +933,49 @@ class WindowFrame(Node):
918
933
  class FrameBound(Node):
919
934
  type_: "FrameBoundType"
920
935
  value: "PyLegendOptional[Expression]"
936
+ duration_unit: "PyLegendOptional[StringLiteral]"
921
937
 
922
938
  def __init__(
923
939
  self,
924
940
  type_: "FrameBoundType",
925
- value: "PyLegendOptional[Expression]"
941
+ value: "PyLegendOptional[Expression]",
942
+ duration_unit: "PyLegendOptional[StringLiteral]" = None
926
943
  ) -> None:
927
944
  super().__init__(_type="frameBound")
928
945
  self.type_ = type_
929
946
  self.value = value
947
+ self.duration_unit = duration_unit
948
+
949
+
950
+ class BitwiseBinaryExpression(Expression):
951
+ left: "Expression"
952
+ right: "Expression"
953
+ operator: "BitwiseBinaryOperator"
954
+
955
+ def __init__(
956
+ self,
957
+ left: "Expression",
958
+ right: "Expression",
959
+ operator: "BitwiseBinaryOperator"
960
+ ) -> None:
961
+ super().__init__(_type="bitwiseBinaryExpression")
962
+ self.left = left
963
+ self.right = right
964
+ self.operator = operator
965
+
966
+
967
+ class BitwiseShiftExpression(Expression):
968
+ value: "Expression"
969
+ shift: "Expression"
970
+ direction: "BitwiseShiftDirection"
971
+
972
+ def __init__(
973
+ self,
974
+ value: "Expression",
975
+ shift: "Expression",
976
+ direction: "BitwiseShiftDirection"
977
+ ) -> None:
978
+ super().__init__(_type="bitwiseShiftExpression")
979
+ self.value = value
980
+ self.shift = shift
981
+ self.direction = direction
@@ -20,6 +20,7 @@ from pylegend._typing import (
20
20
  from pylegend.core.sql.metamodel import (
21
21
  Expression,
22
22
  Window,
23
+ StringLiteral,
23
24
  )
24
25
 
25
26
  __all__: PyLegendSequence[str] = [
@@ -81,7 +82,12 @@ __all__: PyLegendSequence[str] = [
81
82
  "EpochExpression",
82
83
  "WindowExpression",
83
84
  "ConstantExpression",
84
- "StringSubStringExpression"
85
+ "StringSubStringExpression",
86
+ "DateAdjustExpression",
87
+ "BitwiseNotExpression",
88
+ "DateDiffExpression",
89
+ "DateTimeBucketExpression",
90
+ "DateType"
85
91
  ]
86
92
 
87
93
 
@@ -763,3 +769,73 @@ class StringSubStringExpression(Expression):
763
769
  self.value = value
764
770
  self.start = start
765
771
  self.end = end
772
+
773
+
774
+ class DateAdjustExpression(Expression):
775
+ date: "Expression"
776
+ number: "Expression"
777
+ duration_unit: "StringLiteral"
778
+
779
+ def __init__(
780
+ self,
781
+ date: "Expression",
782
+ number: "Expression",
783
+ duration_unit: "StringLiteral",
784
+ ) -> None:
785
+ super().__init__(_type="dateAdjustExpression")
786
+ self.date = date
787
+ self.number = number
788
+ self.duration_unit = duration_unit
789
+
790
+
791
+ class DateDiffExpression(Expression):
792
+ start_date: "Expression"
793
+ end_date: "Expression"
794
+ duration_unit: "StringLiteral"
795
+
796
+ def __init__(
797
+ self,
798
+ start_date: "Expression",
799
+ end_date: "Expression",
800
+ duration_unit: "StringLiteral",
801
+ ) -> None:
802
+ super().__init__(_type="dateDiffExpression")
803
+ self.start_date = start_date
804
+ self.end_date = end_date
805
+ self.duration_unit = duration_unit
806
+
807
+
808
+ class DateType(Enum):
809
+ DateTime = 1
810
+ StrictDate = 2
811
+
812
+
813
+ class DateTimeBucketExpression(Expression):
814
+ date: "Expression"
815
+ quantity: "Expression"
816
+ duration_unit: "StringLiteral"
817
+ date_type: DateType
818
+
819
+ def __init__(
820
+ self,
821
+ date: "Expression",
822
+ quantity: "Expression",
823
+ duration_unit: "StringLiteral",
824
+ date_type: DateType = DateType.DateTime,
825
+ ) -> None:
826
+ super().__init__(_type="dateTimeBucketExpression")
827
+ self.date = date
828
+ self.quantity = quantity
829
+ self.duration_unit = duration_unit
830
+ self.date_type = date_type
831
+
832
+
833
+ class BitwiseNotExpression(Expression):
834
+ value: "Expression"
835
+
836
+ def __init__(
837
+ self,
838
+ value: "Expression",
839
+ ) -> None:
840
+ super().__init__(_type="bitwiseNotExpression")
841
+ self.value = value
@@ -14,7 +14,10 @@
14
14
 
15
15
  from pylegend._typing import (
16
16
  PyLegendList,
17
- PyLegendSequence
17
+ PyLegendSequence,
18
+ PyLegendUnion,
19
+ PyLegendOptional,
20
+ PyLegendCallable
18
21
  )
19
22
  from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
20
23
  from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
@@ -25,7 +28,10 @@ from pylegend.core.tds.tds_column import TdsColumn
25
28
  from pylegend.core.tds.tds_frame import FrameToSqlConfig
26
29
  from pylegend.core.tds.tds_frame import FrameToPureConfig
27
30
  from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
28
-
31
+ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import LegendQLApiPrimitive
32
+ from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
33
+ from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import infer_columns_from_frame
34
+ from pylegend.core.language.shared.helpers import escape_column_name
29
35
 
30
36
  __all__: PyLegendSequence[str] = [
31
37
  "LegendQLApiDistinctFunction"
@@ -34,27 +40,56 @@ __all__: PyLegendSequence[str] = [
34
40
 
35
41
  class LegendQLApiDistinctFunction(LegendQLApiAppliedFunction):
36
42
  __base_frame: LegendQLApiBaseTdsFrame
43
+ __column_name_list: PyLegendOptional[PyLegendList[str]]
37
44
 
38
45
  @classmethod
39
46
  def name(cls) -> str:
40
47
  return "distinct"
41
48
 
42
- def __init__(self, base_frame: LegendQLApiBaseTdsFrame) -> None:
49
+ def __init__(
50
+ self,
51
+ base_frame: LegendQLApiBaseTdsFrame,
52
+ columns: PyLegendOptional[PyLegendUnion[
53
+ str,
54
+ PyLegendList[str],
55
+ PyLegendCallable[
56
+ [LegendQLApiTdsRow],
57
+ PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
58
+ ]
59
+ ]] = None
60
+ ) -> None:
43
61
  self.__base_frame = base_frame
62
+ self.__column_name_list = infer_columns_from_frame(base_frame, columns,
63
+ "'distinct' function 'columns'") if columns is not None else None
44
64
 
45
65
  def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
46
66
  base_query = self.__base_frame.to_sql_query_object(config)
47
- should_create_sub_query = (base_query.offset is not None) or (base_query.limit is not None)
67
+ should_create_sub_query = (base_query.offset is not None) or (base_query.limit is not None) or (
68
+ self.__column_name_list is not None)
69
+
70
+ quoted_columns = None
71
+ if self.__column_name_list is not None:
72
+ db_extension = config.sql_to_string_generator().get_db_extension()
73
+ quoted_columns = [
74
+ db_extension.quote_identifier(col)
75
+ for col in self.__column_name_list
76
+ ]
77
+
48
78
  new_query = (
49
- create_sub_query(base_query, config, "root") if should_create_sub_query else
79
+ create_sub_query(base_query, config, "root", quoted_columns) if should_create_sub_query else
50
80
  copy_query(base_query)
51
81
  )
52
82
  new_query.select.distinct = True
83
+
53
84
  return new_query
54
85
 
55
86
  def to_pure(self, config: FrameToPureConfig) -> str:
87
+ columns_expr = (
88
+ f"~[{', '.join(map(escape_column_name, self.__column_name_list))}]"
89
+ if self.__column_name_list else ""
90
+ )
56
91
  return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
57
- f"->distinct()")
92
+ f"->distinct({columns_expr})")
58
93
 
59
94
  def base_frame(self) -> LegendQLApiBaseTdsFrame:
60
95
  return self.__base_frame
@@ -63,7 +98,18 @@ class LegendQLApiDistinctFunction(LegendQLApiAppliedFunction):
63
98
  return []
64
99
 
65
100
  def calculate_columns(self) -> PyLegendSequence[TdsColumn]:
66
- return [c.copy() for c in self.__base_frame.columns()]
101
+ if not self.__column_name_list:
102
+ return [c.copy() for c in self.__base_frame.columns()]
103
+
104
+ base_columns = self.__base_frame.columns()
105
+ new_columns = []
106
+ for name in self.__column_name_list:
107
+ for col in base_columns:
108
+ if col.get_name() == name:
109
+ new_columns.append(col.copy())
110
+ break
111
+
112
+ return new_columns
67
113
 
68
114
  def validate(self) -> bool:
69
115
  return True
@@ -30,6 +30,11 @@ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import
30
30
  LegendQLApiWindow,
31
31
  LegendQLApiPartialFrame,
32
32
  LegendQLApiWindowReference,
33
+ LegendQLApiWindowFrame,
34
+ LegendQLApiWindowFrameMode,
35
+ LegendQLApiWindowFrameBound,
36
+ LegendQLApiWindowFrameBoundType,
37
+ LegendQLApiDurationUnit
33
38
  )
34
39
  from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
35
40
  from pylegend.core.tds.abstract.frames.base_tds_frame import BaseTdsFrame
@@ -59,14 +64,23 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
59
64
  def limit(self, row_count: int = 5) -> "LegendQLApiTdsFrame":
60
65
  return self.head(row_count=row_count)
61
66
 
62
- def distinct(self) -> "LegendQLApiTdsFrame":
67
+ def distinct(
68
+ self,
69
+ columns: PyLegendOptional[PyLegendUnion[
70
+ str,
71
+ PyLegendList[str],
72
+ PyLegendCallable[
73
+ [LegendQLApiTdsRow],
74
+ PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
75
+ ]
76
+ ]] = None) -> "LegendQLApiTdsFrame":
63
77
  from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import (
64
78
  LegendQLApiAppliedFunctionTdsFrame
65
79
  )
66
80
  from pylegend.core.tds.legendql_api.frames.functions.legendql_api_distinct_function import (
67
81
  LegendQLApiDistinctFunction
68
82
  )
69
- return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self))
83
+ return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self, columns))
70
84
 
71
85
  def select(
72
86
  self,
@@ -304,6 +318,80 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
304
318
  LegendQLApiGroupByFunction(self, grouping_columns, aggregate_specifications)
305
319
  )
306
320
 
321
+ def rows(
322
+ self,
323
+ start: PyLegendUnion[str, int],
324
+ end: PyLegendUnion[str, int]) -> LegendQLApiWindowFrame:
325
+ return LegendQLApiWindowFrame(
326
+ LegendQLApiWindowFrameMode.ROWS,
327
+ _infer_window_frame_bound(start, is_start_bound=True),
328
+ _infer_window_frame_bound(end)
329
+ )
330
+
331
+ def range(
332
+ self,
333
+ *,
334
+ number_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
335
+ number_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
336
+ duration_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
337
+ duration_start_unit: PyLegendOptional[str] = None,
338
+ duration_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
339
+ duration_end_unit: PyLegendOptional[str] = None) -> LegendQLApiWindowFrame:
340
+
341
+ has_number = number_start is not None or number_end is not None
342
+ has_duration = any([
343
+ duration_start is not None,
344
+ duration_end is not None,
345
+ duration_start_unit is not None,
346
+ duration_end_unit is not None,
347
+ ])
348
+
349
+ if not has_number and not has_duration:
350
+ raise ValueError(
351
+ "Either numeric range or duration range must be provided. "
352
+ "Specify number_start and number_end, or duration_start and duration_end "
353
+ "(with duration_start_unit and duration_end_unit as needed)."
354
+ )
355
+
356
+ if has_number and has_duration:
357
+ raise ValueError(
358
+ "Numeric range and duration range cannot be used together. "
359
+ "Use either (number_start, number_end) or (duration_start, duration_end)."
360
+ "(with duration_start_unit and duration_end_unit as needed)."
361
+ )
362
+
363
+ if has_number:
364
+ if number_start is None or number_end is None:
365
+ raise ValueError(
366
+ "Both number_start and number_end must be provided together."
367
+ )
368
+
369
+ return LegendQLApiWindowFrame(
370
+ LegendQLApiWindowFrameMode.RANGE,
371
+ _infer_window_frame_bound(number_start, is_start_bound=True),
372
+ _infer_window_frame_bound(number_end),
373
+ )
374
+
375
+ if duration_start is None or duration_end is None:
376
+ raise ValueError(
377
+ "Both duration_start and duration_end must be provided."
378
+ "(with duration_start_unit and duration_end_unit as needed).")
379
+
380
+ def is_unbounded(value: object) -> bool:
381
+ return isinstance(value, str) and value.lower() == "unbounded"
382
+
383
+ if not is_unbounded(duration_start) and duration_start_unit is None:
384
+ raise ValueError("duration_start_unit is required for bounded duration_start.")
385
+
386
+ if not is_unbounded(duration_end) and duration_end_unit is None:
387
+ raise ValueError("duration_end_unit is required for bounded duration_end.")
388
+
389
+ return LegendQLApiWindowFrame(
390
+ LegendQLApiWindowFrameMode.RANGE,
391
+ _infer_window_frame_bound(duration_start, is_start_bound=True, duration_unit=duration_start_unit),
392
+ _infer_window_frame_bound(duration_end, duration_unit=duration_end_unit)
393
+ )
394
+
307
395
  def window(
308
396
  self,
309
397
  partition_by: PyLegendOptional[
@@ -329,7 +417,8 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
329
417
  ]
330
418
  ]
331
419
  ]
332
- ] = None
420
+ ] = None,
421
+ frame: PyLegendOptional[LegendQLApiWindowFrame] = None
333
422
  ) -> "LegendQLApiWindow":
334
423
  from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import (
335
424
  infer_columns_from_frame,
@@ -344,7 +433,7 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
344
433
  None if order_by is None else
345
434
  infer_sorts_from_frame(self, order_by, "'window' function order_by")
346
435
  ),
347
- frame=None
436
+ frame=frame
348
437
  )
349
438
 
350
439
  def window_extend(
@@ -417,3 +506,56 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
417
506
  LegendQLApiProjectFunction
418
507
  )
419
508
  return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiProjectFunction(self, project_columns))
509
+
510
+
511
+ def _infer_window_frame_bound(
512
+ value: PyLegendOptional[
513
+ PyLegendUnion[str, int, float]
514
+ ] = None,
515
+ *,
516
+ is_start_bound: bool = False,
517
+ duration_unit: PyLegendOptional[str] = None,
518
+ ) -> LegendQLApiWindowFrameBound:
519
+ if isinstance(value, str):
520
+ if value.lower() != "unbounded":
521
+ raise ValueError(
522
+ f"Invalid window frame boundary '{value}'. "
523
+ "Only 'unbounded' is supported as a string. "
524
+ "Otherwise, provide a numeric offset where "
525
+ "positive = FOLLOWING, negative = PRECEDING, "
526
+ "and 0 = CURRENT ROW."
527
+ )
528
+
529
+ bound_type = (
530
+ LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING
531
+ if is_start_bound
532
+ else LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING
533
+ )
534
+
535
+ return LegendQLApiWindowFrameBound(bound_type)
536
+
537
+ if not isinstance(value, (int, float)):
538
+ raise TypeError(
539
+ f"Invalid type for window frame boundary: {type(value).__name__}. "
540
+ "Expected 'unbounded' (str) or numeric offset (int | float)."
541
+ )
542
+
543
+ duration_unit_enum = (
544
+ LegendQLApiDurationUnit.from_string(duration_unit)
545
+ if duration_unit
546
+ else None
547
+ )
548
+
549
+ if value == 0:
550
+ return LegendQLApiWindowFrameBound(
551
+ LegendQLApiWindowFrameBoundType.CURRENT_ROW,
552
+ row_offset=None,
553
+ duration_unit=duration_unit_enum
554
+ )
555
+
556
+ if value > 0:
557
+ bound_type = LegendQLApiWindowFrameBoundType.FOLLOWING
558
+ else:
559
+ bound_type = LegendQLApiWindowFrameBoundType.PRECEDING
560
+
561
+ return LegendQLApiWindowFrameBound(bound_type, value, duration_unit_enum)