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.
Files changed (31) hide show
  1. pylegend/core/database/sql_to_string/db_extension.py +68 -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/sql/metamodel.py +4 -1
  5. pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +53 -7
  6. pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
  7. pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
  8. pylegend/core/tds/pandas_api/frames/functions/aggregate_function.py +221 -96
  9. pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
  10. pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
  11. pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
  12. pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
  13. pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
  14. pylegend/core/tds/pandas_api/frames/functions/merge.py +513 -0
  15. pylegend/core/tds/pandas_api/frames/functions/rename.py +214 -0
  16. pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
  17. pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
  18. pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +559 -18
  19. pylegend/core/tds/pandas_api/frames/pandas_api_groupby_tds_frame.py +325 -0
  20. pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +218 -12
  21. pylegend/extensions/tds/abstract/csv_tds_frame.py +95 -0
  22. pylegend/extensions/tds/legendql_api/frames/legendql_api_csv_input_frame.py +36 -0
  23. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
  24. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
  25. pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
  26. {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/METADATA +1 -1
  27. {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/RECORD +31 -24
  28. {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/WHEEL +0 -0
  29. {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE +0 -0
  30. {pylegend-0.10.0.dist-info → pylegend-0.12.0.dist-info}/licenses/LICENSE.spdx +0 -0
  31. {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("Select operation not supported for queries with columns other than SingleColumn")
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("Select operation not supported for queries with SingleColumns with missing alias")
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
- base_col.copy()
135
- for base_col in base_cols
136
- if base_col.get_name() in desired_col_names
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: