pylegend 0.10.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/aggregate_function.py +221 -96
- 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/merge.py +513 -0
- pylegend/core/tds/pandas_api/frames/functions/rename.py +214 -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 +559 -18
- pylegend/core/tds/pandas_api/frames/pandas_api_groupby_tds_frame.py +325 -0
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +218 -12
- pylegend/extensions/tds/abstract/csv_tds_frame.py +95 -0
- pylegend/extensions/tds/legendql_api/frames/legendql_api_csv_input_frame.py +36 -0
- 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.10.0.dist-info → pylegend-0.12.0.dist-info}/METADATA +1 -1
- {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/RECORD +31 -24
- {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/WHEEL +0 -0
- {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE.spdx +0 -0
- {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -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:
|