pylegend 0.11.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. pylegend/core/database/sql_to_string/db_extension.py +244 -6
  2. pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +190 -5
  3. pylegend/core/language/pandas_api/pandas_api_series.py +3 -0
  4. pylegend/core/language/shared/expression.py +5 -0
  5. pylegend/core/language/shared/literal_expressions.py +22 -1
  6. pylegend/core/language/shared/operations/boolean_operation_expressions.py +144 -0
  7. pylegend/core/language/shared/operations/date_operation_expressions.py +91 -0
  8. pylegend/core/language/shared/operations/integer_operation_expressions.py +183 -1
  9. pylegend/core/language/shared/operations/string_operation_expressions.py +31 -1
  10. pylegend/core/language/shared/primitives/boolean.py +40 -0
  11. pylegend/core/language/shared/primitives/date.py +39 -0
  12. pylegend/core/language/shared/primitives/datetime.py +18 -0
  13. pylegend/core/language/shared/primitives/integer.py +54 -1
  14. pylegend/core/language/shared/primitives/strictdate.py +25 -1
  15. pylegend/core/language/shared/primitives/string.py +16 -2
  16. pylegend/core/sql/metamodel.py +54 -2
  17. pylegend/core/sql/metamodel_extension.py +77 -1
  18. pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +53 -7
  19. pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
  20. pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
  21. pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
  22. pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
  23. pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
  24. pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
  25. pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
  26. pylegend/core/tds/pandas_api/frames/functions/iloc.py +99 -0
  27. pylegend/core/tds/pandas_api/frames/functions/loc.py +136 -0
  28. pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
  29. pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
  30. pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +340 -34
  31. pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +90 -9
  32. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
  33. pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
  34. pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
  35. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/METADATA +1 -1
  36. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/RECORD +40 -36
  37. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/WHEEL +1 -1
  38. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE +0 -0
  39. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/LICENSE.spdx +0 -0
  40. {pylegend-0.11.0.dist-info → pylegend-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -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(self) -> "LegendQLApiTdsFrame":
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
- tds_row = PandasApiTdsRow.from_tds_frame("frame", self.__base_frame)
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 not isinstance(res, PyLegendPrimitive):
81
- raise RuntimeError("Constants not supported")
82
- new_col_expr = res.to_sql_expression(
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
- new_query.select.selectItems.append(
87
- SingleColumn(alias=db_extension.quote_identifier(col), expression=new_col_expr)
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
- raise NotImplementedError("to_pure is not implemented yet") # pragma: no cover
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
- res = func(tds_row)
105
- if isinstance(res, (int, PyLegendInteger)):
106
- new_cols.append(PrimitiveTdsColumn.integer_column(col))
107
- elif isinstance(res, (float, PyLegendFloat)):
108
- new_cols.append(PrimitiveTdsColumn.float_column(col))
109
- elif isinstance(res, PyLegendNumber):
110
- new_cols.append(PrimitiveTdsColumn.number_column(col))
111
- elif isinstance(res, (bool, PyLegendBoolean)):
112
- new_cols.append(PrimitiveTdsColumn.boolean_column(col))
113
- elif isinstance(res, (str, PyLegendString)):
114
- new_cols.append(PrimitiveTdsColumn.string_column(col))
115
- else:
116
- raise RuntimeError("Type not supported")
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 False:
164
- raise NotImplementedError(f"Only inplace=True is supported. Got inplace={self.__inplace!r}")
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 True. Got inplace={self.__inplace!r}") # pragma: no cover
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("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:
@@ -0,0 +1,99 @@
1
+ # Copyright 2026 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 typing import TYPE_CHECKING
16
+ from pylegend._typing import (
17
+ PyLegendUnion,
18
+ PyLegendTuple,
19
+ PyLegendSequence,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import PandasApiBaseTdsFrame
24
+ from pylegend.core.tds.pandas_api.frames.pandas_api_tds_frame import PandasApiTdsFrame
25
+
26
+
27
+ __all__: PyLegendSequence[str] = [
28
+ "PandasApiIlocIndexer"
29
+ ]
30
+
31
+
32
+ class PandasApiIlocIndexer:
33
+ _frame: "PandasApiBaseTdsFrame"
34
+
35
+ def __init__(self, frame: "PandasApiBaseTdsFrame") -> None:
36
+ self._frame = frame
37
+
38
+ def __getitem__( # type: ignore
39
+ self,
40
+ key: PyLegendUnion[int, slice, PyLegendTuple[PyLegendUnion[int, slice], ...]]
41
+ ) -> "PandasApiTdsFrame":
42
+ if isinstance(key, tuple):
43
+ if len(key) > 2:
44
+ raise IndexError("Too many indexers")
45
+ elif len(key) == 1:
46
+ rows, cols = key[0], slice(None, None, None)
47
+ else:
48
+ rows, cols = key # type: ignore
49
+ else:
50
+ rows, cols = key, slice(None, None, None)
51
+
52
+ # Row selection
53
+ row_frame = self._handle_row_selection(rows)
54
+
55
+ # Column selection
56
+ return self._handle_column_selection(row_frame, cols)
57
+
58
+ def _handle_row_selection(self, rows: PyLegendUnion[int, slice]) -> "PandasApiTdsFrame": # type: ignore
59
+ if isinstance(rows, slice):
60
+ if rows.step is not None and rows.step != 1:
61
+ raise NotImplementedError("iloc with slice step other than 1 is not supported yet in Pandas Api")
62
+
63
+ start = rows.start
64
+ stop = rows.stop
65
+ after = stop - 1 if stop is not None else None
66
+ return self._frame.truncate(before=start, after=after)
67
+
68
+ elif isinstance(rows, int):
69
+ return self._frame.truncate(before=rows, after=rows)
70
+
71
+ else:
72
+ raise NotImplementedError(
73
+ f"iloc supports integer, slice, or tuple of these, but got indexer of type: {type(rows)}"
74
+ )
75
+
76
+ def _handle_column_selection( # type: ignore
77
+ self,
78
+ frame: "PandasApiTdsFrame",
79
+ cols: PyLegendUnion[int, slice]
80
+ ) -> "PandasApiTdsFrame":
81
+ if isinstance(cols, slice):
82
+ if cols.step is not None and cols.step != 1:
83
+ raise NotImplementedError("iloc with slice step other than 1 is not supported yet in Pandas Api")
84
+
85
+ all_columns = [c.get_name() for c in frame.columns()]
86
+ selected_columns = all_columns[cols]
87
+ return frame.filter(items=selected_columns)
88
+
89
+ elif isinstance(cols, int):
90
+ all_columns = [c.get_name() for c in frame.columns()]
91
+ if not -len(all_columns) <= cols < len(all_columns):
92
+ raise IndexError("single positional indexer is out-of-bounds")
93
+ selected_column = all_columns[cols]
94
+ return frame.filter(items=[selected_column])
95
+
96
+ else:
97
+ raise NotImplementedError(
98
+ f"iloc supports integer, slice, or tuple of these, but got indexer of type: {type(cols)}"
99
+ )