pylegend 0.7.0__py3-none-any.whl → 0.9.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 (35) hide show
  1. pylegend/_typing.py +6 -0
  2. pylegend/core/database/sql_to_string/db_extension.py +35 -6
  3. pylegend/core/language/pandas_api/__init__.py +13 -0
  4. pylegend/core/language/pandas_api/pandas_api_aggregate_specification.py +54 -0
  5. pylegend/core/language/pandas_api/pandas_api_custom_expressions.py +85 -0
  6. pylegend/core/language/pandas_api/pandas_api_series.py +174 -0
  7. pylegend/core/language/pandas_api/pandas_api_tds_row.py +74 -0
  8. pylegend/core/language/shared/literal_expressions.py +2 -2
  9. pylegend/core/language/shared/operations/integer_operation_expressions.py +35 -0
  10. pylegend/core/language/shared/operations/nary_expression.py +104 -0
  11. pylegend/core/language/shared/operations/primitive_operation_expressions.py +30 -0
  12. pylegend/core/language/shared/operations/string_operation_expressions.py +624 -1
  13. pylegend/core/language/shared/pct_helpers.py +56 -0
  14. pylegend/core/language/shared/primitives/integer.py +6 -0
  15. pylegend/core/language/shared/primitives/primitive.py +6 -0
  16. pylegend/core/language/shared/primitives/string.py +129 -1
  17. pylegend/core/sql/metamodel.py +3 -1
  18. pylegend/core/sql/metamodel_extension.py +18 -0
  19. pylegend/core/tds/pandas_api/frames/functions/aggregate_function.py +316 -0
  20. pylegend/core/tds/pandas_api/frames/functions/assign_function.py +20 -15
  21. pylegend/core/tds/pandas_api/frames/functions/drop.py +171 -0
  22. pylegend/core/tds/pandas_api/frames/functions/filter.py +193 -0
  23. pylegend/core/tds/pandas_api/frames/functions/filtering.py +85 -0
  24. pylegend/core/tds/pandas_api/frames/functions/sort_values_function.py +189 -0
  25. pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +120 -0
  26. pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +5 -1
  27. pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +204 -7
  28. pylegend/core/tds/pandas_api/frames/pandas_api_input_tds_frame.py +5 -3
  29. pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +90 -3
  30. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/METADATA +1 -1
  31. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/RECORD +35 -22
  32. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/WHEEL +0 -0
  33. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/licenses/LICENSE +0 -0
  34. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/licenses/LICENSE.spdx +0 -0
  35. {pylegend-0.7.0.dist-info → pylegend-0.9.0.dist-info}/licenses/NOTICE +0 -0
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from datetime import date, datetime
16
+
16
17
  from pylegend._typing import (
17
18
  PyLegendList,
18
19
  PyLegendSequence,
@@ -20,17 +21,7 @@ from pylegend._typing import (
20
21
  PyLegendCallable,
21
22
  PyLegendUnion,
22
23
  )
23
- from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import PandasApiAppliedFunction
24
- from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
25
- from pylegend.core.sql.metamodel import (
26
- QuerySpecification,
27
- SingleColumn,
28
- )
29
- from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import PandasApiBaseTdsFrame
30
- from pylegend.core.tds.tds_column import TdsColumn, PrimitiveTdsColumn
31
- from pylegend.core.tds.tds_frame import FrameToSqlConfig
32
24
  from pylegend.core.language import (
33
- LegacyApiTdsRow,
34
25
  PyLegendPrimitive,
35
26
  PyLegendInteger,
36
27
  PyLegendFloat,
@@ -38,13 +29,23 @@ from pylegend.core.language import (
38
29
  PyLegendBoolean,
39
30
  PyLegendString,
40
31
  )
32
+ from pylegend.core.language.pandas_api.pandas_api_tds_row import PandasApiTdsRow
33
+ from pylegend.core.sql.metamodel import (
34
+ QuerySpecification,
35
+ SingleColumn,
36
+ )
37
+ from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import PandasApiAppliedFunction
38
+ from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import PandasApiBaseTdsFrame
39
+ from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
40
+ from pylegend.core.tds.tds_column import TdsColumn, PrimitiveTdsColumn
41
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
41
42
 
42
43
 
43
44
  class AssignFunction(PandasApiAppliedFunction):
44
45
  __base_frame: PandasApiBaseTdsFrame
45
46
  __col_definitions: PyLegendDict[
46
47
  str,
47
- PyLegendCallable[[LegacyApiTdsRow], PyLegendUnion[int, float, bool, str, date, datetime, PyLegendPrimitive]],
48
+ PyLegendCallable[[PandasApiTdsRow], PyLegendUnion[int, float, bool, str, date, datetime, PyLegendPrimitive]],
48
49
  ]
49
50
 
50
51
  @classmethod
@@ -56,7 +57,8 @@ class AssignFunction(PandasApiAppliedFunction):
56
57
  base_frame: PandasApiBaseTdsFrame,
57
58
  col_definitions: PyLegendDict[
58
59
  str,
59
- PyLegendCallable[[LegacyApiTdsRow], PyLegendUnion[int, float, bool, str, date, datetime, PyLegendPrimitive]],
60
+ PyLegendCallable[
61
+ [PandasApiTdsRow], PyLegendUnion[int, float, bool, str, date, datetime, PyLegendPrimitive]],
60
62
  ]
61
63
  ) -> None:
62
64
  self.__base_frame = base_frame
@@ -72,7 +74,7 @@ class AssignFunction(PandasApiAppliedFunction):
72
74
  copy_query(base_query)
73
75
  )
74
76
 
75
- tds_row = LegacyApiTdsRow.from_tds_frame("frame", self.__base_frame)
77
+ tds_row = PandasApiTdsRow.from_tds_frame("frame", self.__base_frame)
76
78
  for col, func in self.__col_definitions.items():
77
79
  res = func(tds_row)
78
80
  if not isinstance(res, PyLegendPrimitive):
@@ -86,6 +88,9 @@ class AssignFunction(PandasApiAppliedFunction):
86
88
  )
87
89
  return new_query
88
90
 
91
+ def to_pure(self, config: FrameToPureConfig) -> str:
92
+ raise NotImplementedError("to_pure is not implemented yet") # pragma: no cover
93
+
89
94
  def base_frame(self) -> PandasApiBaseTdsFrame:
90
95
  return self.__base_frame
91
96
 
@@ -94,7 +99,7 @@ class AssignFunction(PandasApiAppliedFunction):
94
99
 
95
100
  def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
96
101
  new_cols = [c.copy() for c in self.__base_frame.columns()]
97
- tds_row = LegacyApiTdsRow.from_tds_frame("frame", self.__base_frame)
102
+ tds_row = PandasApiTdsRow.from_tds_frame("frame", self.__base_frame)
98
103
  for col, func in self.__col_definitions.items():
99
104
  res = func(tds_row)
100
105
  if isinstance(res, (int, PyLegendInteger)):
@@ -112,7 +117,7 @@ class AssignFunction(PandasApiAppliedFunction):
112
117
  return new_cols
113
118
 
114
119
  def validate(self) -> bool:
115
- tds_row = LegacyApiTdsRow.from_tds_frame("frame", self.__base_frame)
120
+ tds_row = PandasApiTdsRow.from_tds_frame("frame", self.__base_frame)
116
121
  for col, f in self.__col_definitions.items():
117
122
  f(tds_row)
118
123
  return True
@@ -0,0 +1,171 @@
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 pylegend._typing import (
16
+ PyLegendList,
17
+ PyLegendSet,
18
+ PyLegendSequence,
19
+ PyLegendUnion,
20
+ PyLegendOptional
21
+ )
22
+ from pylegend.core.language.shared.primitives.boolean import PyLegendBoolean
23
+ from pylegend.core.language.shared.primitives.integer import PyLegendInteger
24
+ from pylegend.core.sql.metamodel import (
25
+ QuerySpecification
26
+ )
27
+ from pylegend.core.tds.pandas_api.frames.functions.filter import PandasApiFilterFunction
28
+ from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import PandasApiAppliedFunction
29
+ from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import PandasApiBaseTdsFrame
30
+ from pylegend.core.tds.tds_column import TdsColumn
31
+ from pylegend.core.tds.tds_frame import FrameToPureConfig, FrameToSqlConfig
32
+
33
+ __all__: PyLegendSequence[str] = [
34
+ "PandasApiDropFunction"
35
+ ]
36
+
37
+
38
+ class PandasApiDropFunction(PandasApiAppliedFunction):
39
+ __base_frame: PandasApiBaseTdsFrame
40
+ __labels: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]]
41
+ __axis: PyLegendUnion[str, int, PyLegendInteger]
42
+ __index: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]]
43
+ __columns: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]]
44
+ __level: PyLegendOptional[PyLegendUnion[int, PyLegendInteger, str]]
45
+ __inplace: PyLegendUnion[bool, PyLegendBoolean]
46
+ __errors: str
47
+
48
+ @classmethod
49
+ def name(cls) -> str:
50
+ return "drop" # pragma: no cover
51
+
52
+ def __init__(
53
+ self,
54
+ base_frame: PandasApiBaseTdsFrame,
55
+ labels: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]],
56
+ axis: PyLegendUnion[str, int, PyLegendInteger],
57
+ index: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]],
58
+ columns: PyLegendOptional[PyLegendUnion[str, PyLegendSequence[str], PyLegendSet[str]]],
59
+ level: PyLegendOptional[PyLegendUnion[int, PyLegendInteger, str]],
60
+ inplace: PyLegendUnion[bool, PyLegendBoolean],
61
+ errors: str
62
+ ) -> None:
63
+ self.__base_frame = base_frame
64
+ self.__labels = labels
65
+ self.__axis = axis
66
+ self.__index = index
67
+ self.__columns = columns
68
+ self.__level = level
69
+ self.__inplace = inplace
70
+ self.__errors = errors
71
+
72
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
73
+ base_cols = [c.get_name() for c in self.__base_frame.columns()]
74
+
75
+ if self.__errors == "raise":
76
+ not_found = [col for col in self.__columns if col not in base_cols] # type: ignore
77
+ if not_found:
78
+ raise KeyError(f"{not_found} not found in axis")
79
+
80
+ columns_to_retain = [col for col in base_cols if col not in self.__columns] # type: ignore
81
+ filter_func = PandasApiFilterFunction(
82
+ base_frame=self.__base_frame,
83
+ items=columns_to_retain,
84
+ like=None,
85
+ regex=None,
86
+ axis=1
87
+ )
88
+ return filter_func.to_sql(config)
89
+
90
+ def to_pure(self, config: FrameToPureConfig) -> str:
91
+ base_cols = [c.get_name() for c in self.__base_frame.columns()]
92
+ if self.__errors == "raise":
93
+ not_found = [col for col in self.__columns if col not in base_cols] # type: ignore
94
+ if not_found:
95
+ raise KeyError(f"{not_found} not found in axis")
96
+
97
+ columns_to_retain = [col for col in base_cols if col not in self.__columns] # type: ignore
98
+ filter_func = PandasApiFilterFunction(
99
+ base_frame=self.__base_frame,
100
+ items=columns_to_retain,
101
+ like=None,
102
+ regex=None,
103
+ axis=1
104
+ )
105
+ return filter_func.to_pure(config)
106
+
107
+ def base_frame(self) -> PandasApiBaseTdsFrame:
108
+ return self.__base_frame
109
+
110
+ def tds_frame_parameters(self) -> PyLegendList["PandasApiBaseTdsFrame"]:
111
+ return []
112
+
113
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
114
+ base_cols = [c.copy() for c in self.__base_frame.columns()]
115
+ if self.__columns is not None:
116
+ new_cols = []
117
+ for col in base_cols:
118
+ if col.get_name() not in self.__columns:
119
+ new_cols.append(col.copy())
120
+ return new_cols
121
+ return base_cols # pragma: no cover
122
+
123
+ def validate(self) -> bool:
124
+ valid_paramters: int = 0
125
+ if self.__axis is not None:
126
+ if isinstance(self.__axis, (str, int, PyLegendInteger)):
127
+ if self.__axis != 1 and self.__axis != "columns":
128
+ if self.__axis == 0 or self.__axis == "index":
129
+ raise NotImplementedError(
130
+ f"Axis {self.__axis} is not supported for 'drop' function in PandasApi")
131
+ else:
132
+ raise ValueError(f"No axis named {self.__axis} for object type Tds DataFrame")
133
+ else:
134
+ raise TypeError(f"No axis named {self.__axis} for object type Tds DataFrame") # pragma: no cover
135
+ if self.__level is not None:
136
+ raise NotImplementedError("'level' parameter is not supported for 'drop' function in PandasApi")
137
+
138
+ if self.__index is not None:
139
+ raise NotImplementedError("'index' parameter is not supported for 'drop' function in PandasApi")
140
+
141
+ if self.__labels is not None:
142
+ valid_paramters += 1
143
+
144
+ if self.__columns is None:
145
+ self.__columns = self.__labels
146
+ else:
147
+ raise ValueError("Cannot specify both 'labels' and 'columns'")
148
+
149
+ if self.__columns is not None:
150
+ def _normalize_columns(columns): # type: ignore
151
+ if columns is None:
152
+ return [] # pragma: no cover
153
+ if isinstance(columns, str):
154
+ return [columns]
155
+ if isinstance(columns, (PyLegendSequence, PyLegendSet)):
156
+ return list(columns)
157
+ raise TypeError(f"Unsupported type for columns: {type(columns)}")
158
+
159
+ valid_paramters += 1
160
+ self.__columns = _normalize_columns(self.__columns) # type: ignore
161
+
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}")
165
+ else:
166
+ raise TypeError(f"Inplace must be True. Got inplace={self.__inplace!r}") # pragma: no cover
167
+
168
+ if valid_paramters == 0:
169
+ raise ValueError("Need to specify at least one of 'labels' or 'columns'")
170
+
171
+ return True
@@ -0,0 +1,193 @@
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
+ import re
16
+
17
+ from pylegend._typing import (
18
+ PyLegendUnion,
19
+ PyLegendOptional,
20
+ PyLegendSequence,
21
+ PyLegendList,
22
+ PyLegendTuple
23
+ )
24
+ from pylegend.core.language import (
25
+ PyLegendInteger,
26
+ )
27
+ from pylegend.core.language.shared.helpers import escape_column_name
28
+ from pylegend.core.sql.metamodel import (
29
+ QuerySpecification,
30
+ SingleColumn,
31
+ SelectItem
32
+ )
33
+ from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import (
34
+ PandasApiAppliedFunction,
35
+ )
36
+ from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import (
37
+ PandasApiBaseTdsFrame,
38
+ )
39
+ from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
40
+ from pylegend.core.tds.tds_column import TdsColumn
41
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
42
+
43
+ __all__: PyLegendSequence[str] = ["PandasApiFilterFunction"]
44
+
45
+
46
+ class PandasApiFilterFunction(PandasApiAppliedFunction):
47
+ __base_frame: PandasApiBaseTdsFrame
48
+ __items: PyLegendOptional[PyLegendList[str]]
49
+ __like: PyLegendOptional[str]
50
+ __regex: PyLegendOptional[str]
51
+ __axis: PyLegendOptional[PyLegendUnion[str, int, PyLegendInteger]]
52
+
53
+ @classmethod
54
+ def name(cls) -> str:
55
+ return "filter" # pragma: no cover
56
+
57
+ def __init__(
58
+ self,
59
+ base_frame: PandasApiBaseTdsFrame,
60
+ items: PyLegendOptional[PyLegendList[str]],
61
+ like: PyLegendOptional[str],
62
+ regex: PyLegendOptional[str],
63
+ axis: PyLegendOptional[PyLegendUnion[str, int, PyLegendInteger]],
64
+ ) -> None:
65
+ self.__base_frame = base_frame
66
+ self.__items = items
67
+ self.__like = like
68
+ self.__regex = regex
69
+ self.__axis = 1 if axis is None else axis
70
+
71
+ def __get_desired_columns(
72
+ self, col_names: PyLegendSequence[str]
73
+ ) -> PyLegendSequence[str]:
74
+ if self.__items is not None:
75
+ return self.__items
76
+ elif self.__like is not None:
77
+ return [col for col in col_names if self.__like in col]
78
+ elif self.__regex is not None:
79
+ regex_pattern = re.compile(self.__regex)
80
+ return [col for col in col_names if regex_pattern.search(col)]
81
+
82
+ return [] # pragma: no cover
83
+
84
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
85
+ base_query = self.__base_frame.to_sql_query_object(config)
86
+ db_extension = config.sql_to_string_generator().get_db_extension()
87
+ columns_to_retain = [db_extension.quote_identifier(x) for x in
88
+ self.__get_desired_columns([c.get_name() for c in self.__base_frame.columns()])]
89
+
90
+ sub_query_required = (
91
+ len(base_query.groupBy) > 0 or
92
+ len(base_query.orderBy) > 0 or
93
+ base_query.having is not None or
94
+ base_query.select.distinct
95
+ )
96
+
97
+ if sub_query_required:
98
+ new_query = create_sub_query(base_query, config, "root", columns_to_retain=columns_to_retain)
99
+ return new_query
100
+ else:
101
+ new_cols_with_index: PyLegendList[PyLegendTuple[int, SelectItem]] = []
102
+ for col in base_query.select.selectItems:
103
+ if not isinstance(col, SingleColumn):
104
+ raise ValueError("Select operation not supported for queries with columns other than SingleColumn")
105
+ if col.alias is None:
106
+ raise ValueError("Select operation not supported for queries with SingleColumns with missing alias")
107
+ if col.alias in columns_to_retain:
108
+ new_cols_with_index.append((columns_to_retain.index(col.alias), col))
109
+
110
+ new_select_items = [y[1] for y in sorted(new_cols_with_index, key=lambda x: x[0])]
111
+ new_query = copy_query(base_query)
112
+ new_query.select.selectItems = new_select_items
113
+ return new_query
114
+
115
+ def to_pure(self, config: FrameToPureConfig) -> str:
116
+ col_names = [c.get_name() for c in self.__base_frame.columns()]
117
+ desired_columns = self.__get_desired_columns(col_names)
118
+ escaped_columns = [escape_column_name(col_name) for col_name in desired_columns]
119
+ return (
120
+ f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
121
+ f"->select(~[{', '.join(escaped_columns)}])"
122
+ )
123
+
124
+ def base_frame(self) -> PandasApiBaseTdsFrame:
125
+ return self.__base_frame
126
+
127
+ def tds_frame_parameters(self) -> PyLegendList["PandasApiBaseTdsFrame"]:
128
+ return []
129
+
130
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
131
+ base_cols = [c.copy() for c in self.__base_frame.columns()]
132
+ desired_col_names = self.__get_desired_columns([c.get_name() for c in base_cols])
133
+ return [
134
+ base_col.copy()
135
+ for base_col in base_cols
136
+ if base_col.get_name() in desired_col_names
137
+ ]
138
+
139
+ def validate(self) -> bool:
140
+ mutual_exclusion = sum(
141
+ [
142
+ self.__items is not None,
143
+ self.__like is not None,
144
+ self.__regex is not None,
145
+ ]
146
+ )
147
+ if mutual_exclusion > 1:
148
+ raise TypeError(
149
+ "Keyword arguments `items`, `like`, or `regex` are mutually exclusive"
150
+ )
151
+ if mutual_exclusion == 0:
152
+ raise TypeError("Must pass either `items`, `like`, or `regex`")
153
+
154
+ base_cols = [c.get_name() for c in self.__base_frame.columns()]
155
+ if self.__items is not None:
156
+ if not isinstance(self.__items, (list, PyLegendList)):
157
+ raise TypeError(
158
+ f"Index(...) must be called with a collection, got '{self.__items}'"
159
+ )
160
+ invalid_cols = [item for item in self.__items if item not in base_cols]
161
+ if invalid_cols:
162
+ raise ValueError(
163
+ f"Columns {invalid_cols} in `filter` items list do not exist. Available: {base_cols}"
164
+ )
165
+
166
+ if self.__like is not None:
167
+ if not isinstance(self.__like, str):
168
+ raise TypeError(f"'like' must be a string, got {type(self.__like)}")
169
+ if not any(self.__like in col for col in base_cols):
170
+ raise ValueError(
171
+ f"No columns match the pattern '{self.__like}'. Available: {base_cols}"
172
+ )
173
+
174
+ if self.__regex is not None:
175
+ if not isinstance(self.__regex, str):
176
+ raise TypeError(f"'regex' must be a string, got {type(self.__regex)}")
177
+ try:
178
+ regex_pattern = re.compile(self.__regex)
179
+ except re.error as e:
180
+ raise ValueError(f"Invalid regex pattern '{self.__regex}': {e}")
181
+ if not any(regex_pattern.search(col) for col in base_cols):
182
+ raise ValueError(
183
+ f"No columns match the regex '{self.__regex}'. Available: {base_cols}"
184
+ )
185
+
186
+ if not isinstance(
187
+ self.__axis, (str, int, PyLegendInteger)
188
+ ) or self.__axis not in [1, "columns"]:
189
+ raise ValueError(
190
+ f"Unsupported axis value: {self.__axis}. Expected 1 or 'columns'"
191
+ )
192
+
193
+ return True
@@ -0,0 +1,85 @@
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 pylegend._typing import (
16
+ PyLegendSequence,
17
+ PyLegendList
18
+ )
19
+ from pylegend.core.language import PyLegendBoolean
20
+ from pylegend.core.sql.metamodel import (
21
+ QuerySpecification,
22
+ LogicalBinaryExpression,
23
+ LogicalBinaryType,
24
+ )
25
+ from pylegend.core.tds.pandas_api.frames.pandas_api_applied_function_tds_frame import PandasApiAppliedFunction
26
+ from pylegend.core.tds.pandas_api.frames.pandas_api_base_tds_frame import PandasApiBaseTdsFrame
27
+ from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
28
+ from pylegend.core.tds.tds_column import TdsColumn
29
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
30
+
31
+ __all__: PyLegendSequence[str] = ["PandasApiFilteringFunction"]
32
+
33
+
34
+ class PandasApiFilteringFunction(PandasApiAppliedFunction):
35
+ __base_frame: PandasApiBaseTdsFrame
36
+ __filter_expr: PyLegendBoolean
37
+
38
+ @classmethod
39
+ def name(cls) -> str:
40
+ return "boolean_filter" # pragma: no cover
41
+
42
+ def __init__(
43
+ self,
44
+ base_frame: PandasApiBaseTdsFrame,
45
+ filter_expr: PyLegendBoolean
46
+ ) -> None:
47
+ self.__base_frame = base_frame
48
+ self.__filter_expr = filter_expr
49
+
50
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
51
+ base_query = self.__base_frame.to_sql_query_object(config)
52
+ should_create_sub_query = (len(base_query.groupBy) > 0) or \
53
+ (base_query.offset is not None) or (base_query.limit is not None)
54
+ new_query = (
55
+ create_sub_query(base_query, config, "root") if should_create_sub_query else
56
+ copy_query(base_query)
57
+ )
58
+
59
+ sql_expr = self.__filter_expr.to_sql_expression({"c": new_query}, config)
60
+
61
+ if new_query.where is None:
62
+ new_query.where = sql_expr
63
+ else:
64
+ new_query.where = LogicalBinaryExpression(
65
+ type_=LogicalBinaryType.AND,
66
+ left=new_query.where,
67
+ right=sql_expr
68
+ )
69
+ return new_query
70
+
71
+ def to_pure(self, config: FrameToPureConfig) -> str:
72
+ pure_expr = self.__filter_expr.to_pure_expression(config)
73
+ return f"{self.__base_frame.to_pure(config)}{config.separator(1)}->filter(c|{pure_expr})"
74
+
75
+ def base_frame(self) -> PandasApiBaseTdsFrame:
76
+ return self.__base_frame
77
+
78
+ def tds_frame_parameters(self) -> PyLegendList["PandasApiBaseTdsFrame"]:
79
+ return []
80
+
81
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
82
+ return [c.copy() for c in self.__base_frame.columns()]
83
+
84
+ def validate(self) -> bool:
85
+ return True