pylegend 0.11.0__py3-none-any.whl → 0.12.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 +68 -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/sql/metamodel.py +4 -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/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 +300 -34
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +78 -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.12.0.dist-info}/METADATA +1 -1
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/RECORD +25 -23
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/WHEEL +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE.spdx +0 -0
- {pylegend-0.11.0.dist-info → pylegend-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -22,6 +22,7 @@ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import
|
|
|
22
22
|
LegendQLApiWindow,
|
|
23
23
|
LegendQLApiPartialFrame,
|
|
24
24
|
LegendQLApiWindowReference,
|
|
25
|
+
LegendQLApiWindowFrame,
|
|
25
26
|
)
|
|
26
27
|
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
27
28
|
from pylegend.core.tds.tds_frame import (
|
|
@@ -60,7 +61,17 @@ class LegendQLApiTdsFrame(PyLegendTdsFrame, metaclass=ABCMeta):
|
|
|
60
61
|
pass # pragma: no cover
|
|
61
62
|
|
|
62
63
|
@abstractmethod
|
|
63
|
-
def distinct(
|
|
64
|
+
def distinct(
|
|
65
|
+
self,
|
|
66
|
+
columns: PyLegendOptional[PyLegendUnion[
|
|
67
|
+
str,
|
|
68
|
+
PyLegendList[str],
|
|
69
|
+
PyLegendCallable[
|
|
70
|
+
[LegendQLApiTdsRow],
|
|
71
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
72
|
+
]
|
|
73
|
+
]] = None
|
|
74
|
+
) -> "LegendQLApiTdsFrame":
|
|
64
75
|
pass # pragma: no cover
|
|
65
76
|
|
|
66
77
|
@abstractmethod
|
|
@@ -261,7 +272,8 @@ class LegendQLApiTdsFrame(PyLegendTdsFrame, metaclass=ABCMeta):
|
|
|
261
272
|
]
|
|
262
273
|
]
|
|
263
274
|
]
|
|
264
|
-
] = None
|
|
275
|
+
] = None,
|
|
276
|
+
frame: PyLegendOptional[LegendQLApiWindowFrame] = None
|
|
265
277
|
) -> LegendQLApiWindow:
|
|
266
278
|
pass
|
|
267
279
|
|
|
@@ -325,3 +337,22 @@ class LegendQLApiTdsFrame(PyLegendTdsFrame, metaclass=ABCMeta):
|
|
|
325
337
|
]
|
|
326
338
|
) -> "LegendQLApiTdsFrame":
|
|
327
339
|
pass # pragma: no cover
|
|
340
|
+
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def rows(
|
|
343
|
+
self,
|
|
344
|
+
start: PyLegendUnion[str, int],
|
|
345
|
+
end: PyLegendUnion[str, int]) -> LegendQLApiWindowFrame:
|
|
346
|
+
pass # pragma: no cover
|
|
347
|
+
|
|
348
|
+
@abstractmethod
|
|
349
|
+
def range(
|
|
350
|
+
self,
|
|
351
|
+
*,
|
|
352
|
+
number_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
353
|
+
number_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
354
|
+
duration_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
355
|
+
duration_start_unit: PyLegendOptional[str] = None,
|
|
356
|
+
duration_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
|
|
357
|
+
duration_end_unit: PyLegendOptional[str] = None) -> LegendQLApiWindowFrame:
|
|
358
|
+
pass # pragma: no cover
|
|
@@ -28,8 +28,11 @@ from pylegend.core.language import (
|
|
|
28
28
|
PyLegendNumber,
|
|
29
29
|
PyLegendBoolean,
|
|
30
30
|
PyLegendString,
|
|
31
|
+
PyLegendDate,
|
|
32
|
+
PyLegendDateTime
|
|
31
33
|
)
|
|
32
34
|
from pylegend.core.language.pandas_api.pandas_api_tds_row import PandasApiTdsRow
|
|
35
|
+
from pylegend.core.language.shared.literal_expressions import convert_literal_to_literal_expression
|
|
33
36
|
from pylegend.core.sql.metamodel import (
|
|
34
37
|
QuerySpecification,
|
|
35
38
|
SingleColumn,
|
|
@@ -50,7 +53,7 @@ class AssignFunction(PandasApiAppliedFunction):
|
|
|
50
53
|
|
|
51
54
|
@classmethod
|
|
52
55
|
def name(cls) -> str:
|
|
53
|
-
return "assign"
|
|
56
|
+
return "assign" # pragma: no cover
|
|
54
57
|
|
|
55
58
|
def __init__(
|
|
56
59
|
self,
|
|
@@ -74,22 +77,53 @@ class AssignFunction(PandasApiAppliedFunction):
|
|
|
74
77
|
copy_query(base_query)
|
|
75
78
|
)
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
base_cols = {c.get_name() for c in self.__base_frame.columns()}
|
|
81
|
+
tds_row = PandasApiTdsRow.from_tds_frame("c", self.__base_frame)
|
|
78
82
|
for col, func in self.__col_definitions.items():
|
|
79
83
|
res = func(tds_row)
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{"frame": base_query},
|
|
84
|
+
res_expr = res if isinstance(res, PyLegendPrimitive) else convert_literal_to_literal_expression(res)
|
|
85
|
+
new_col_expr = res_expr.to_sql_expression(
|
|
86
|
+
{"c": base_query},
|
|
84
87
|
config
|
|
85
88
|
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
|
|
90
|
+
alias = db_extension.quote_identifier(col)
|
|
91
|
+
if col in base_cols:
|
|
92
|
+
for i, si in enumerate(new_query.select.selectItems):
|
|
93
|
+
if isinstance(si, SingleColumn) and si.alias == alias:
|
|
94
|
+
new_query.select.selectItems[i] = SingleColumn(alias=alias, expression=new_col_expr)
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
new_query.select.selectItems.append(SingleColumn(alias=alias, expression=new_col_expr))
|
|
89
98
|
return new_query
|
|
90
99
|
|
|
91
100
|
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
92
|
-
|
|
101
|
+
tds_row = PandasApiTdsRow.from_tds_frame("c", self.__base_frame)
|
|
102
|
+
base_cols = [c.get_name() for c in self.__base_frame.columns()]
|
|
103
|
+
|
|
104
|
+
assigned_exprs: PyLegendDict[str, str] = {}
|
|
105
|
+
for col, func in self.__col_definitions.items():
|
|
106
|
+
res = func(tds_row)
|
|
107
|
+
res_expr = res if isinstance(res, PyLegendPrimitive) else convert_literal_to_literal_expression(res)
|
|
108
|
+
assigned_exprs[col] = res_expr.to_pure_expression(config)
|
|
109
|
+
|
|
110
|
+
# build project clauses
|
|
111
|
+
clauses: PyLegendList[str] = []
|
|
112
|
+
|
|
113
|
+
for col in base_cols:
|
|
114
|
+
if col in assigned_exprs:
|
|
115
|
+
clauses.append(f"{col}:c|{assigned_exprs[col]}")
|
|
116
|
+
else:
|
|
117
|
+
clauses.append(f"{col}:c|$c.{col}")
|
|
118
|
+
|
|
119
|
+
for col, pure_expr in assigned_exprs.items():
|
|
120
|
+
if col not in base_cols:
|
|
121
|
+
clauses.append(f"{col}:c|{pure_expr}")
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
|
|
125
|
+
f"->project(~[{', '.join(clauses)}])"
|
|
126
|
+
)
|
|
93
127
|
|
|
94
128
|
def base_frame(self) -> PandasApiBaseTdsFrame:
|
|
95
129
|
return self.__base_frame
|
|
@@ -99,21 +133,29 @@ class AssignFunction(PandasApiAppliedFunction):
|
|
|
99
133
|
|
|
100
134
|
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
101
135
|
new_cols = [c.copy() for c in self.__base_frame.columns()]
|
|
136
|
+
base_cols = {c.get_name() for c in self.__base_frame.columns()}
|
|
102
137
|
tds_row = PandasApiTdsRow.from_tds_frame("frame", self.__base_frame)
|
|
103
138
|
for col, func in self.__col_definitions.items():
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
if col not in base_cols:
|
|
140
|
+
res = func(tds_row)
|
|
141
|
+
if isinstance(res, (int, PyLegendInteger)):
|
|
142
|
+
new_cols.append(PrimitiveTdsColumn.integer_column(col))
|
|
143
|
+
elif isinstance(res, (float, PyLegendFloat)):
|
|
144
|
+
new_cols.append(PrimitiveTdsColumn.float_column(col))
|
|
145
|
+
elif isinstance(res, PyLegendNumber):
|
|
146
|
+
new_cols.append(PrimitiveTdsColumn.number_column(col)) # pragma: no cover
|
|
147
|
+
elif isinstance(res, (bool, PyLegendBoolean)):
|
|
148
|
+
new_cols.append(
|
|
149
|
+
PrimitiveTdsColumn.boolean_column(col)
|
|
150
|
+
) # pragma: no cover (Boolean column not supported in PURE)
|
|
151
|
+
elif isinstance(res, (str, PyLegendString)):
|
|
152
|
+
new_cols.append(PrimitiveTdsColumn.string_column(col))
|
|
153
|
+
elif isinstance(res, (datetime, PyLegendDateTime)):
|
|
154
|
+
new_cols.append(PrimitiveTdsColumn.datetime_column(col))
|
|
155
|
+
elif isinstance(res, (date, PyLegendDate)):
|
|
156
|
+
new_cols.append(PrimitiveTdsColumn.date_column(col))
|
|
157
|
+
else:
|
|
158
|
+
raise RuntimeError("Type not supported")
|
|
117
159
|
return new_cols
|
|
118
160
|
|
|
119
161
|
def validate(self) -> bool:
|
|
@@ -160,10 +160,10 @@ class PandasApiDropFunction(PandasApiAppliedFunction):
|
|
|
160
160
|
self.__columns = _normalize_columns(self.__columns) # type: ignore
|
|
161
161
|
|
|
162
162
|
if isinstance(self.__inplace, (bool, PyLegendBoolean)):
|
|
163
|
-
if self.__inplace is
|
|
164
|
-
raise NotImplementedError(f"Only inplace=
|
|
163
|
+
if self.__inplace is True:
|
|
164
|
+
raise NotImplementedError(f"Only inplace=False is supported. Got inplace={self.__inplace!r}")
|
|
165
165
|
else:
|
|
166
|
-
raise TypeError(f"Inplace must be
|
|
166
|
+
raise TypeError(f"Inplace must be False. Got inplace={self.__inplace!r}") # pragma: no cover
|
|
167
167
|
|
|
168
168
|
if valid_paramters == 0:
|
|
169
169
|
raise ValueError("Need to specify at least one of 'labels' or 'columns'")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from functools import reduce
|
|
16
|
+
|
|
17
|
+
from pylegend._typing import (
|
|
18
|
+
PyLegendSequence,
|
|
19
|
+
PyLegendOptional,
|
|
20
|
+
PyLegendList,
|
|
21
|
+
PyLegendUnion,
|
|
22
|
+
)
|
|
23
|
+
from pylegend.core.language.pandas_api.pandas_api_tds_row import PandasApiTdsRow
|
|
24
|
+
from pylegend.core.sql.metamodel import (
|
|
25
|
+
QuerySpecification,
|
|
26
|
+
LogicalBinaryType,
|
|
27
|
+
LogicalBinaryExpression, BooleanLiteral,
|
|
28
|
+
)
|
|
29
|
+
from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import (
|
|
30
|
+
PandasApiAppliedFunction,
|
|
31
|
+
)
|
|
32
|
+
from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import (
|
|
33
|
+
PandasApiBaseTdsFrame,
|
|
34
|
+
)
|
|
35
|
+
from pylegend.core.tds.sql_query_helpers import copy_query
|
|
36
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
37
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
|
|
38
|
+
|
|
39
|
+
__all__: PyLegendSequence[str] = ["PandasApiDropnaFunction"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PandasApiDropnaFunction(PandasApiAppliedFunction):
|
|
43
|
+
__base_frame: PandasApiBaseTdsFrame
|
|
44
|
+
__axis: PyLegendUnion[int, str]
|
|
45
|
+
__how: str
|
|
46
|
+
__thresh: PyLegendOptional[int]
|
|
47
|
+
__subset: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str]]]
|
|
48
|
+
__inplace: bool
|
|
49
|
+
__ignore_index: bool
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def name(cls) -> str:
|
|
53
|
+
return "dropna" # pragma: no cover
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
base_frame: PandasApiBaseTdsFrame,
|
|
58
|
+
axis: PyLegendUnion[int, str],
|
|
59
|
+
how: str,
|
|
60
|
+
thresh: PyLegendOptional[int],
|
|
61
|
+
subset: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str]]],
|
|
62
|
+
inplace: bool,
|
|
63
|
+
ignore_index: bool
|
|
64
|
+
) -> None:
|
|
65
|
+
self.__base_frame = base_frame
|
|
66
|
+
self.__axis = axis
|
|
67
|
+
self.__how = how
|
|
68
|
+
self.__thresh = thresh
|
|
69
|
+
self.__subset = subset
|
|
70
|
+
self.__inplace = inplace
|
|
71
|
+
self.__ignore_index = ignore_index
|
|
72
|
+
|
|
73
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
74
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
75
|
+
new_query = copy_query(base_query)
|
|
76
|
+
|
|
77
|
+
if self.__subset is not None:
|
|
78
|
+
cols_to_check = self.__subset
|
|
79
|
+
else:
|
|
80
|
+
cols_to_check = [c.get_name() for c in self.__base_frame.columns()]
|
|
81
|
+
|
|
82
|
+
if not cols_to_check:
|
|
83
|
+
if self.__how == 'all':
|
|
84
|
+
new_query.where = BooleanLiteral(value=False)
|
|
85
|
+
return new_query
|
|
86
|
+
|
|
87
|
+
tds_row = PandasApiTdsRow.from_tds_frame("c", self.__base_frame)
|
|
88
|
+
|
|
89
|
+
filter_expr = None
|
|
90
|
+
|
|
91
|
+
conditions = [tds_row[col].is_not_null() for col in cols_to_check]
|
|
92
|
+
if self.__how == "any":
|
|
93
|
+
filter_expr = reduce(lambda x, y: x & y, conditions)
|
|
94
|
+
else: # "all"
|
|
95
|
+
filter_expr = reduce(lambda x, y: x | y, conditions)
|
|
96
|
+
|
|
97
|
+
sql_expr = filter_expr.to_sql_expression({"c": new_query}, config)
|
|
98
|
+
if new_query.where is None:
|
|
99
|
+
new_query.where = sql_expr
|
|
100
|
+
else:
|
|
101
|
+
new_query.where = LogicalBinaryExpression(
|
|
102
|
+
type_=LogicalBinaryType.AND,
|
|
103
|
+
left=new_query.where,
|
|
104
|
+
right=sql_expr
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return new_query
|
|
108
|
+
|
|
109
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
110
|
+
base_pure = self.__base_frame.to_pure(config)
|
|
111
|
+
if self.__subset is not None:
|
|
112
|
+
cols_to_check = self.__subset
|
|
113
|
+
else:
|
|
114
|
+
cols_to_check = [c.get_name() for c in self.__base_frame.columns()]
|
|
115
|
+
|
|
116
|
+
if not cols_to_check:
|
|
117
|
+
if self.__how == 'all':
|
|
118
|
+
return f"{base_pure}{config.separator(1)}->filter(c|1!=1)"
|
|
119
|
+
return base_pure
|
|
120
|
+
|
|
121
|
+
tds_row = PandasApiTdsRow.from_tds_frame("c", self.__base_frame)
|
|
122
|
+
conditions = [tds_row[col].is_not_null() for col in cols_to_check]
|
|
123
|
+
|
|
124
|
+
if self.__how == "any":
|
|
125
|
+
filter_expr = reduce(lambda x, y: x & y, conditions)
|
|
126
|
+
else: # "all"
|
|
127
|
+
filter_expr = reduce(lambda x, y: x | y, conditions)
|
|
128
|
+
|
|
129
|
+
pure_expr = filter_expr.to_pure_expression(config)
|
|
130
|
+
return f"{base_pure}{config.separator(1)}->filter(c|{pure_expr})"
|
|
131
|
+
|
|
132
|
+
def base_frame(self) -> PandasApiBaseTdsFrame:
|
|
133
|
+
return self.__base_frame
|
|
134
|
+
|
|
135
|
+
def tds_frame_parameters(self) -> PyLegendList["PandasApiBaseTdsFrame"]:
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
139
|
+
return [c.copy() for c in self.__base_frame.columns()]
|
|
140
|
+
|
|
141
|
+
def validate(self) -> bool:
|
|
142
|
+
if self.__axis not in (0, 1, "index", "columns"):
|
|
143
|
+
raise ValueError(f"No axis named {self.__axis} for object type TdsFrame")
|
|
144
|
+
if self.__axis in (1, "columns"):
|
|
145
|
+
raise NotImplementedError("axis=1 is not supported yet in Pandas API dropna")
|
|
146
|
+
|
|
147
|
+
if self.__thresh is not None:
|
|
148
|
+
raise NotImplementedError("thresh parameter is not supported yet in Pandas API dropna")
|
|
149
|
+
|
|
150
|
+
if self.__how not in ("any", "all"):
|
|
151
|
+
raise ValueError(f"invalid how option: {self.__how}")
|
|
152
|
+
|
|
153
|
+
if self.__subset is not None:
|
|
154
|
+
if not isinstance(self.__subset, (list, tuple, set)):
|
|
155
|
+
raise TypeError(f"subset must be a list, tuple or set of column names. Got {type(self.__subset)}")
|
|
156
|
+
valid_cols = {c.get_name() for c in self.__base_frame.columns()}
|
|
157
|
+
invalid_cols = [s for s in self.__subset if s not in valid_cols]
|
|
158
|
+
if invalid_cols:
|
|
159
|
+
raise KeyError(f"{invalid_cols}")
|
|
160
|
+
|
|
161
|
+
if self.__inplace:
|
|
162
|
+
raise NotImplementedError("inplace=True is not supported yet in Pandas API dropna")
|
|
163
|
+
|
|
164
|
+
if self.__ignore_index:
|
|
165
|
+
raise NotImplementedError("ignore_index=True is not supported yet in Pandas API dropna")
|
|
166
|
+
|
|
167
|
+
return True
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright 2025 Goldman Sachs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from datetime import date, datetime
|
|
16
|
+
|
|
17
|
+
from pylegend._typing import (
|
|
18
|
+
PyLegendSequence,
|
|
19
|
+
PyLegendOptional,
|
|
20
|
+
PyLegendList,
|
|
21
|
+
PyLegendUnion,
|
|
22
|
+
PyLegendDict
|
|
23
|
+
)
|
|
24
|
+
from pylegend.core.language import (
|
|
25
|
+
convert_literal_to_literal_expression
|
|
26
|
+
)
|
|
27
|
+
from pylegend.core.language.pandas_api.pandas_api_tds_row import PandasApiTdsRow
|
|
28
|
+
from pylegend.core.sql.metamodel import (
|
|
29
|
+
QuerySpecification,
|
|
30
|
+
SingleColumn, FunctionCall, QualifiedName,
|
|
31
|
+
)
|
|
32
|
+
from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import (
|
|
33
|
+
PandasApiAppliedFunction,
|
|
34
|
+
)
|
|
35
|
+
from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import (
|
|
36
|
+
PandasApiBaseTdsFrame,
|
|
37
|
+
)
|
|
38
|
+
from pylegend.core.tds.sql_query_helpers import copy_query
|
|
39
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
40
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
|
|
41
|
+
|
|
42
|
+
__all__: PyLegendSequence[str] = ["PandasApiFillnaFunction"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PandasApiFillnaFunction(PandasApiAppliedFunction):
|
|
46
|
+
__base_frame: PandasApiBaseTdsFrame
|
|
47
|
+
__value: PyLegendUnion[
|
|
48
|
+
int, float, str, bool, date, datetime,
|
|
49
|
+
PyLegendDict[str, PyLegendUnion[int, float, str, bool, date, datetime]]
|
|
50
|
+
]
|
|
51
|
+
__axis: PyLegendOptional[PyLegendUnion[int, str]]
|
|
52
|
+
__inplace: bool
|
|
53
|
+
__limit: PyLegendOptional[int]
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def name(cls) -> str:
|
|
57
|
+
return "fillna" # pragma: no cover
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
base_frame: PandasApiBaseTdsFrame,
|
|
62
|
+
value: PyLegendUnion[
|
|
63
|
+
int, float, str, bool, date, datetime,
|
|
64
|
+
PyLegendDict[str, PyLegendUnion[int, float, str, bool, date, datetime]]
|
|
65
|
+
],
|
|
66
|
+
axis: PyLegendOptional[PyLegendUnion[int, str]],
|
|
67
|
+
inplace: bool,
|
|
68
|
+
limit: PyLegendOptional[int]
|
|
69
|
+
) -> None:
|
|
70
|
+
self.__base_frame = base_frame
|
|
71
|
+
self.__value = value
|
|
72
|
+
self.__axis = axis
|
|
73
|
+
self.__inplace = inplace
|
|
74
|
+
self.__limit = limit
|
|
75
|
+
|
|
76
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
77
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
78
|
+
new_query = copy_query(base_query)
|
|
79
|
+
|
|
80
|
+
tds_row = PandasApiTdsRow.from_tds_frame("c", self.__base_frame)
|
|
81
|
+
db_extension = config.sql_to_string_generator().get_db_extension()
|
|
82
|
+
select_items = []
|
|
83
|
+
|
|
84
|
+
for col in self.__base_frame.columns():
|
|
85
|
+
col_name = col.get_name()
|
|
86
|
+
fill_value = self.__value if not isinstance(self.__value, dict) else self.__value.get(col_name)
|
|
87
|
+
col_expr = tds_row[col_name]
|
|
88
|
+
col_sql_expr = col_expr.to_sql_expression({"c": new_query}, config)
|
|
89
|
+
|
|
90
|
+
if fill_value is not None:
|
|
91
|
+
fill_expr = convert_literal_to_literal_expression(fill_value)
|
|
92
|
+
fill_sql_expr = fill_expr.to_sql_expression({"c": new_query}, config)
|
|
93
|
+
sql_expr = FunctionCall(
|
|
94
|
+
name=QualifiedName(parts=['coalesce']),
|
|
95
|
+
distinct=False,
|
|
96
|
+
arguments=[col_sql_expr, fill_sql_expr],
|
|
97
|
+
filter_=None,
|
|
98
|
+
window=None
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
sql_expr = col_sql_expr # type: ignore
|
|
102
|
+
|
|
103
|
+
select_items.append(SingleColumn(alias=db_extension.quote_identifier(col_name), expression=sql_expr))
|
|
104
|
+
|
|
105
|
+
new_query.select.selectItems = select_items # type: ignore
|
|
106
|
+
return new_query
|
|
107
|
+
|
|
108
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
109
|
+
base_pure = self.__base_frame.to_pure(config)
|
|
110
|
+
projections = []
|
|
111
|
+
|
|
112
|
+
for col in self.__base_frame.columns():
|
|
113
|
+
col_name = col.get_name()
|
|
114
|
+
fill_value = self.__value if not isinstance(self.__value, dict) else self.__value.get(col_name)
|
|
115
|
+
|
|
116
|
+
if fill_value is not None:
|
|
117
|
+
fill_expr = convert_literal_to_literal_expression(fill_value)
|
|
118
|
+
fill_pure_expr = fill_expr.to_pure_expression(config)
|
|
119
|
+
projections.append(f"'{col_name}':c|coalesce($c.{col_name}, {fill_pure_expr})")
|
|
120
|
+
else:
|
|
121
|
+
projections.append(f"'{col_name}':c|$c.{col_name}")
|
|
122
|
+
|
|
123
|
+
projection_string = ", ".join(projections)
|
|
124
|
+
return f"{base_pure}{config.separator(1)}->project(~[{projection_string}])"
|
|
125
|
+
|
|
126
|
+
def base_frame(self) -> PandasApiBaseTdsFrame:
|
|
127
|
+
return self.__base_frame
|
|
128
|
+
|
|
129
|
+
def tds_frame_parameters(self) -> PyLegendList["PandasApiBaseTdsFrame"]:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
133
|
+
return [c.copy() for c in self.__base_frame.columns()]
|
|
134
|
+
|
|
135
|
+
def validate(self) -> bool:
|
|
136
|
+
if self.__value is None:
|
|
137
|
+
raise ValueError("Must specify a fill 'value'")
|
|
138
|
+
|
|
139
|
+
if not isinstance(self.__value, (int, float, str, bool, date, datetime, dict)):
|
|
140
|
+
raise TypeError(f"'value' parameter must be a scalar or dict, but you passed a {type(self.__value)}")
|
|
141
|
+
if isinstance(self.__value, dict):
|
|
142
|
+
for k, v in self.__value.items():
|
|
143
|
+
if not isinstance(k, str):
|
|
144
|
+
raise TypeError(
|
|
145
|
+
"All keys in 'value' dict must be strings representing column names, "
|
|
146
|
+
f"but found key of type {type(k)}"
|
|
147
|
+
)
|
|
148
|
+
if not isinstance(v, (int, float, str, bool, date, datetime)):
|
|
149
|
+
raise TypeError(f"Non-scalar value of type {type(v)} passed for column '{k}' in 'value' parameter")
|
|
150
|
+
|
|
151
|
+
if self.__axis not in (0, 1, "index", "columns"):
|
|
152
|
+
raise ValueError(f"No axis named {self.__axis} for object type TdsFrame")
|
|
153
|
+
if self.__axis in (1, "columns"):
|
|
154
|
+
raise NotImplementedError("axis=1 is not supported yet in Pandas API fillna")
|
|
155
|
+
|
|
156
|
+
if self.__inplace:
|
|
157
|
+
raise NotImplementedError("inplace=True is not supported yet in Pandas API fillna")
|
|
158
|
+
|
|
159
|
+
if self.__limit is not None:
|
|
160
|
+
raise NotImplementedError("limit parameter is not supported yet in Pandas API fillna")
|
|
161
|
+
|
|
162
|
+
return True
|
|
@@ -101,9 +101,13 @@ class PandasApiFilterFunction(PandasApiAppliedFunction):
|
|
|
101
101
|
new_cols_with_index: PyLegendList[PyLegendTuple[int, SelectItem]] = []
|
|
102
102
|
for col in base_query.select.selectItems:
|
|
103
103
|
if not isinstance(col, SingleColumn):
|
|
104
|
-
raise ValueError(
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"Select operation not supported for queries with columns other than SingleColumn"
|
|
106
|
+
) # pragma: no cover
|
|
105
107
|
if col.alias is None:
|
|
106
|
-
raise ValueError(
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"Select operation not supported for queries with SingleColumns with missing alias"
|
|
110
|
+
) # pragma: no cover
|
|
107
111
|
if col.alias in columns_to_retain:
|
|
108
112
|
new_cols_with_index.append((columns_to_retain.index(col.alias), col))
|
|
109
113
|
|
|
@@ -130,10 +134,11 @@ class PandasApiFilterFunction(PandasApiAppliedFunction):
|
|
|
130
134
|
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
131
135
|
base_cols = [c.copy() for c in self.__base_frame.columns()]
|
|
132
136
|
desired_col_names = self.__get_desired_columns([c.get_name() for c in base_cols])
|
|
137
|
+
base_col_map = {c.get_name(): c for c in base_cols}
|
|
133
138
|
return [
|
|
134
|
-
|
|
135
|
-
for
|
|
136
|
-
if
|
|
139
|
+
base_col_map[name].copy()
|
|
140
|
+
for name in desired_col_names
|
|
141
|
+
if name in base_col_map
|
|
137
142
|
]
|
|
138
143
|
|
|
139
144
|
def validate(self) -> bool:
|