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.
- pylegend/core/database/sql_to_string/db_extension.py +244 -6
- pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +190 -5
- pylegend/core/language/pandas_api/pandas_api_series.py +3 -0
- pylegend/core/language/shared/expression.py +5 -0
- pylegend/core/language/shared/literal_expressions.py +22 -1
- pylegend/core/language/shared/operations/boolean_operation_expressions.py +144 -0
- pylegend/core/language/shared/operations/date_operation_expressions.py +91 -0
- pylegend/core/language/shared/operations/integer_operation_expressions.py +183 -1
- pylegend/core/language/shared/operations/string_operation_expressions.py +31 -1
- pylegend/core/language/shared/primitives/boolean.py +40 -0
- pylegend/core/language/shared/primitives/date.py +39 -0
- pylegend/core/language/shared/primitives/datetime.py +18 -0
- pylegend/core/language/shared/primitives/integer.py +54 -1
- pylegend/core/language/shared/primitives/strictdate.py +25 -1
- pylegend/core/language/shared/primitives/string.py +16 -2
- pylegend/core/sql/metamodel.py +54 -2
- pylegend/core/sql/metamodel_extension.py +77 -1
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +53 -7
- pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
- pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
- pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
- pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
- pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
- pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
- pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
- pylegend/core/tds/pandas_api/frames/functions/iloc.py +99 -0
- pylegend/core/tds/pandas_api/frames/functions/loc.py +136 -0
- pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
- pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
- pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +340 -34
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +90 -9
- pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
- pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
- pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/METADATA +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/RECORD +40 -36
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/WHEEL +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE.spdx +0 -0
- {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
|
|
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
|
pylegend/core/sql/metamodel.py
CHANGED
|
@@ -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__(
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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)
|