pylegend 0.3.0__py3-none-any.whl → 0.5.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/__init__.py +16 -6
- pylegend/core/{databse → database}/sql_to_string/__init__.py +3 -3
- pylegend/core/{databse → database}/sql_to_string/db_extension.py +14 -5
- pylegend/core/{databse → database}/sql_to_string/generator.py +2 -2
- pylegend/core/language/__init__.py +12 -10
- pylegend/core/language/legacy_api/__init__.py +13 -0
- pylegend/core/language/{aggregate_specification.py → legacy_api/aggregate_specification.py} +10 -10
- pylegend/core/language/legacy_api/legacy_api_tds_row.py +32 -0
- pylegend/core/language/legendql_api/__init__.py +13 -0
- pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +541 -0
- pylegend/core/language/legendql_api/legendql_api_tds_row.py +292 -0
- pylegend/core/language/shared/__init__.py +13 -0
- pylegend/core/language/{column_expressions.py → shared/column_expressions.py} +32 -31
- pylegend/core/language/{expression.py → shared/expression.py} +8 -0
- pylegend/core/language/{functions.py → shared/functions.py} +12 -3
- pylegend/core/language/shared/helpers.py +75 -0
- pylegend/core/language/{literal_expressions.py → shared/literal_expressions.py} +39 -1
- pylegend/core/language/{operations → shared/operations}/binary_expression.py +34 -2
- pylegend/core/language/{operations → shared/operations}/boolean_operation_expressions.py +34 -6
- pylegend/core/language/{operations → shared/operations}/collection_operation_expressions.py +146 -26
- pylegend/core/language/{operations → shared/operations}/date_operation_expressions.py +288 -24
- pylegend/core/language/{operations → shared/operations}/float_operation_expressions.py +53 -8
- pylegend/core/language/{operations → shared/operations}/integer_operation_expressions.py +62 -9
- pylegend/core/language/{operations → shared/operations}/nullary_expression.py +9 -2
- pylegend/core/language/{operations → shared/operations}/number_operation_expressions.py +211 -30
- pylegend/core/language/shared/operations/primitive_operation_expressions.py +155 -0
- pylegend/core/language/{operations → shared/operations}/string_operation_expressions.py +194 -21
- pylegend/core/language/{operations → shared/operations}/unary_expression.py +10 -2
- pylegend/core/language/{primitive_collection.py → shared/primitive_collection.py} +2 -2
- pylegend/core/language/{primitives → shared/primitives}/__init__.py +9 -9
- pylegend/core/language/{primitives → shared/primitives}/boolean.py +9 -5
- pylegend/core/language/{primitives → shared/primitives}/date.py +60 -15
- pylegend/core/language/{primitives → shared/primitives}/datetime.py +4 -5
- pylegend/core/language/{primitives → shared/primitives}/float.py +6 -6
- pylegend/core/language/{primitives → shared/primitives}/integer.py +6 -6
- pylegend/core/language/{primitives → shared/primitives}/number.py +16 -13
- pylegend/core/language/{primitives → shared/primitives}/primitive.py +41 -5
- pylegend/core/language/{primitives → shared/primitives}/strictdate.py +4 -5
- pylegend/core/language/{primitives → shared/primitives}/string.py +18 -19
- pylegend/core/language/{tds_row.py → shared/tds_row.py} +46 -16
- pylegend/core/request/__init__.py +7 -1
- pylegend/core/request/auth.py +55 -1
- pylegend/core/request/legend_client.py +32 -0
- pylegend/core/sql/metamodel_extension.py +28 -0
- pylegend/core/tds/abstract/__init__.py +13 -0
- pylegend/core/tds/abstract/frames/__init__.py +13 -0
- pylegend/core/tds/{legend_api/frames/legend_api_applied_function_tds_frame.py → abstract/frames/applied_function_tds_frame.py} +19 -13
- pylegend/core/tds/abstract/frames/base_tds_frame.py +125 -0
- pylegend/core/tds/{legend_api/frames/legend_api_input_tds_frame.py → abstract/frames/input_tds_frame.py} +9 -12
- pylegend/core/tds/{legend_api/frames/functions → abstract}/function_helpers.py +1 -1
- pylegend/core/tds/{legend_api/frames/functions/concatenate_function.py → legacy_api/frames/functions/legacy_api_concatenate_function.py} +25 -13
- pylegend/core/tds/{legend_api/frames/functions/distinct_function.py → legacy_api/frames/functions/legacy_api_distinct_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/drop_function.py → legacy_api/frames/functions/legacy_api_drop_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/extend_function.py → legacy_api/frames/functions/legacy_api_extend_function.py} +36 -16
- pylegend/core/tds/{legend_api/frames/functions/filter_function.py → legacy_api/frames/functions/legacy_api_filter_function.py} +25 -13
- pylegend/core/tds/{legend_api/frames/functions/group_by_function.py → legacy_api/frames/functions/legacy_api_group_by_function.py} +44 -17
- pylegend/core/tds/{legend_api/frames/functions/head_function.py → legacy_api/frames/functions/legacy_api_head_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/join_by_columns_function.py → legacy_api/frames/functions/legacy_api_join_by_columns_function.py} +40 -13
- pylegend/core/tds/{legend_api/frames/functions/join_function.py → legacy_api/frames/functions/legacy_api_join_function.py} +44 -20
- pylegend/core/tds/{legend_api/frames/functions/rename_columns_function.py → legacy_api/frames/functions/legacy_api_rename_columns_function.py} +20 -8
- pylegend/core/tds/{legend_api/frames/functions/restrict_function.py → legacy_api/frames/functions/legacy_api_restrict_function.py} +17 -8
- pylegend/core/tds/{legend_api/frames/functions/slice_function.py → legacy_api/frames/functions/legacy_api_slice_function.py} +13 -8
- pylegend/core/tds/{legend_api/frames/functions/sort_function.py → legacy_api/frames/functions/legacy_api_sort_function.py} +19 -8
- pylegend/core/tds/legacy_api/frames/legacy_api_applied_function_tds_frame.py +37 -0
- pylegend/core/tds/legacy_api/frames/legacy_api_base_tds_frame.py +204 -0
- pylegend/core/tds/legacy_api/frames/legacy_api_input_tds_frame.py +51 -0
- pylegend/core/tds/{legend_api/frames/legend_api_tds_frame.py → legacy_api/frames/legacy_api_tds_frame.py} +28 -28
- pylegend/core/tds/legendql_api/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/functions/__init__.py +13 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_asofjoin_function.py +156 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_concatenate_function.py +139 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +69 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_drop_function.py +74 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_extend_function.py +256 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_filter_function.py +121 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_function_helpers.py +137 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_groupby_function.py +256 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_head_function.py +74 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_join_function.py +214 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_project_function.py +169 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_rename_function.py +189 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_select_function.py +131 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_slice_function.py +82 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_sort_function.py +93 -0
- pylegend/core/tds/legendql_api/frames/functions/legendql_api_window_extend_function.py +283 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_applied_function_tds_frame.py +37 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +419 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_input_tds_frame.py +50 -0
- pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +327 -0
- pylegend/core/tds/pandas_api/frames/functions/assign_function.py +6 -6
- pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +4 -0
- pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +11 -3
- pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +2 -2
- pylegend/core/tds/tds_frame.py +32 -2
- pylegend/extensions/database/vendors/postgres/postgres_sql_to_string.py +1 -1
- pylegend/extensions/tds/abstract/legend_function_input_frame.py +4 -0
- pylegend/extensions/tds/abstract/legend_service_input_frame.py +4 -0
- pylegend/extensions/tds/abstract/table_spec_input_frame.py +4 -0
- pylegend/extensions/tds/{legend_api/frames/legend_api_legend_function_input_frame.py → legacy_api/frames/legacy_api_legend_function_input_frame.py} +5 -5
- pylegend/extensions/tds/{legend_api/frames/legend_api_legend_service_input_frame.py → legacy_api/frames/legacy_api_legend_service_input_frame.py} +6 -6
- pylegend/extensions/tds/{legend_api/frames/legend_api_table_spec_input_frame.py → legacy_api/frames/legacy_api_table_spec_input_frame.py} +5 -5
- pylegend/extensions/tds/legendql_api/__init__.py +13 -0
- pylegend/extensions/tds/legendql_api/frames/__init__.py +13 -0
- pylegend/extensions/tds/legendql_api/frames/legendql_api_legend_service_input_frame.py +46 -0
- pylegend/extensions/tds/legendql_api/frames/legendql_api_table_spec_input_frame.py +36 -0
- pylegend/{legend_api_tds_client.py → legacy_api_tds_client.py} +15 -15
- {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/METADATA +7 -8
- pylegend-0.5.0.dist-info/NOTICE +5 -0
- pylegend-0.5.0.dist-info/RECORD +155 -0
- {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/WHEEL +1 -1
- pylegend/core/language/operations/primitive_operation_expressions.py +0 -56
- pylegend/core/tds/legend_api/frames/legend_api_base_tds_frame.py +0 -294
- pylegend-0.3.0.dist-info/RECORD +0 -115
- /pylegend/core/{databse → database}/__init__.py +0 -0
- /pylegend/core/{databse → database}/sql_to_string/config.py +0 -0
- /pylegend/core/language/{operations → shared/operations}/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
- /pylegend/core/tds/{legend_api → legacy_api}/frames/functions/__init__.py +0 -0
- /pylegend/extensions/tds/{legend_api → legacy_api}/__init__.py +0 -0
- /pylegend/extensions/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
- {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/LICENSE +0 -0
- {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/LICENSE.spdx +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
from pylegend._typing import (
|
|
17
|
+
PyLegendList,
|
|
18
|
+
PyLegendSequence,
|
|
19
|
+
PyLegendCallable,
|
|
20
|
+
PyLegendUnion,
|
|
21
|
+
PyLegendTuple,
|
|
22
|
+
)
|
|
23
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
24
|
+
from pylegend.core.sql.metamodel_extension import WindowExpression
|
|
25
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
26
|
+
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
|
|
27
|
+
from pylegend.core.sql.metamodel import (
|
|
28
|
+
QuerySpecification,
|
|
29
|
+
SingleColumn,
|
|
30
|
+
Window,
|
|
31
|
+
)
|
|
32
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
33
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
34
|
+
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
35
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
36
|
+
from pylegend.core.language import (
|
|
37
|
+
PyLegendPrimitive,
|
|
38
|
+
PyLegendPrimitiveOrPythonPrimitive,
|
|
39
|
+
convert_literal_to_literal_expression, PyLegendPrimitiveCollection, create_primitive_collection,
|
|
40
|
+
)
|
|
41
|
+
from pylegend.core.language.shared.helpers import generate_pure_lambda, escape_column_name
|
|
42
|
+
from pylegend.core.tds.abstract.function_helpers import tds_column_for_primitive
|
|
43
|
+
|
|
44
|
+
__all__: PyLegendSequence[str] = [
|
|
45
|
+
"LegendQLApiExtendFunction"
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LegendQLApiExtendFunction(LegendQLApiAppliedFunction):
|
|
50
|
+
__base_frame: LegendQLApiBaseTdsFrame
|
|
51
|
+
__new_column_expressions: PyLegendList[
|
|
52
|
+
PyLegendUnion[
|
|
53
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive],
|
|
54
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]
|
|
55
|
+
]
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def name(cls) -> str:
|
|
60
|
+
return "extend"
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
65
|
+
extend_columns: PyLegendUnion[
|
|
66
|
+
PyLegendTuple[
|
|
67
|
+
str,
|
|
68
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive]
|
|
69
|
+
],
|
|
70
|
+
PyLegendTuple[
|
|
71
|
+
str,
|
|
72
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
|
|
73
|
+
PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
|
|
74
|
+
],
|
|
75
|
+
PyLegendList[
|
|
76
|
+
PyLegendUnion[
|
|
77
|
+
PyLegendTuple[
|
|
78
|
+
str,
|
|
79
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive]
|
|
80
|
+
],
|
|
81
|
+
PyLegendTuple[
|
|
82
|
+
str,
|
|
83
|
+
PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
|
|
84
|
+
PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
|
|
85
|
+
]
|
|
86
|
+
]
|
|
87
|
+
]
|
|
88
|
+
]
|
|
89
|
+
) -> None:
|
|
90
|
+
self.__base_frame = base_frame
|
|
91
|
+
col_expressions: PyLegendList[
|
|
92
|
+
PyLegendUnion[
|
|
93
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive],
|
|
94
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]
|
|
95
|
+
]
|
|
96
|
+
] = []
|
|
97
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
|
|
98
|
+
for (i, extend_column) in enumerate(extend_columns if isinstance(extend_columns, list) else [extend_columns]):
|
|
99
|
+
if isinstance(extend_column, tuple) and len(extend_column) in [2, 3]:
|
|
100
|
+
if not isinstance(extend_column[0], str):
|
|
101
|
+
raise TypeError(
|
|
102
|
+
"'extend' function extend_columns argument incompatible. "
|
|
103
|
+
"First element in an extend tuple should be a string (new column name). "
|
|
104
|
+
"E.g - ('new col', lambda r: r.c1 + 1). "
|
|
105
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
106
|
+
)
|
|
107
|
+
if not isinstance(extend_column[1], type(lambda x: 0)) or (extend_column[1].__code__.co_argcount != 1):
|
|
108
|
+
raise TypeError(
|
|
109
|
+
"'extend' function extend_columns argument incompatible. "
|
|
110
|
+
"Second element in an extend tuple should be a lambda function which takes one argument "
|
|
111
|
+
"(LegendQLApiTdsRow) E.g - ('new col', lambda r: r.c1 + 1)."
|
|
112
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
result = extend_column[1](tds_row)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
"'extend' function extend_columns argument incompatible. "
|
|
119
|
+
f"Error occurred while evaluating extend lambda at index {i} (0-indexed). Message: " + str(e)
|
|
120
|
+
) from e
|
|
121
|
+
|
|
122
|
+
if not isinstance(result, (int, float, bool, str, date, datetime, PyLegendPrimitive)):
|
|
123
|
+
raise TypeError(
|
|
124
|
+
"'extend' function extend_columns argument incompatible. "
|
|
125
|
+
f"Extend lambda at index {i} (0-indexed) returns non-primitive - {str(type(result))}"
|
|
126
|
+
)
|
|
127
|
+
if len(extend_column) == 2:
|
|
128
|
+
col_expressions.append((extend_column[0], result))
|
|
129
|
+
else:
|
|
130
|
+
if (not isinstance(extend_column[2], type(lambda x: 0)) or
|
|
131
|
+
(extend_column[2].__code__.co_argcount != 1)):
|
|
132
|
+
raise TypeError(
|
|
133
|
+
"'extend' function extend_columns argument incompatible. "
|
|
134
|
+
"Third element in an extend tuple should be a lambda function which takes one argument "
|
|
135
|
+
"(collection) E.g - ('new col', lambda r: r.c1, lambda c: c.sum()). "
|
|
136
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
137
|
+
)
|
|
138
|
+
collection = create_primitive_collection(result)
|
|
139
|
+
try:
|
|
140
|
+
agg_result = extend_column[2](collection)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
"'extend' function extend_columns argument incompatible. "
|
|
144
|
+
f"Error occurred while evaluating aggregation lambda at index {i} (0-indexed). "
|
|
145
|
+
"Message: " + str(e)
|
|
146
|
+
) from e
|
|
147
|
+
if not isinstance(agg_result, PyLegendPrimitive):
|
|
148
|
+
raise TypeError(
|
|
149
|
+
"'extend' function extend_columns argument incompatible. "
|
|
150
|
+
f"Aggregation lambda at index {i} (0-indexed) "
|
|
151
|
+
f"returns non-primitive - {str(type(agg_result))}"
|
|
152
|
+
)
|
|
153
|
+
col_expressions.append((extend_column[0], result, agg_result))
|
|
154
|
+
else:
|
|
155
|
+
raise TypeError(
|
|
156
|
+
"'extend' function extend_columns argument should be a list of tuples with two/three elements - "
|
|
157
|
+
"first element being a string (new column name), second element being a lambda "
|
|
158
|
+
"function which takes one argument (LegendQLApiTdsRow) and third element being an optional "
|
|
159
|
+
"aggregation lambda function which takes one argument "
|
|
160
|
+
"E.g - [('new col1', lambda r: r.c1 + 1), ('new col2', lambda r: r.c2, lambda c: c.sum())]. "
|
|
161
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
162
|
+
)
|
|
163
|
+
self.__new_column_expressions = col_expressions
|
|
164
|
+
|
|
165
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
166
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
167
|
+
should_create_sub_query = len(base_query.groupBy) > 0
|
|
168
|
+
db_extension = config.sql_to_string_generator().get_db_extension()
|
|
169
|
+
|
|
170
|
+
new_query = (
|
|
171
|
+
create_sub_query(base_query, config, "root") if should_create_sub_query else
|
|
172
|
+
copy_query(base_query)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for c in self.__new_column_expressions:
|
|
176
|
+
if len(c) == 2:
|
|
177
|
+
if isinstance(c[1], (bool, int, float, str, date, datetime)):
|
|
178
|
+
col_sql_expr = convert_literal_to_literal_expression(c[1]).to_sql_expression(
|
|
179
|
+
{"r": new_query},
|
|
180
|
+
config
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
col_sql_expr = c[1].to_sql_expression({"r": new_query}, config)
|
|
184
|
+
|
|
185
|
+
new_query.select.selectItems.append(
|
|
186
|
+
SingleColumn(alias=db_extension.quote_identifier(c[0]), expression=col_sql_expr)
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
agg_sql_expr = c[2].to_sql_expression({"r": new_query}, config)
|
|
190
|
+
window_expr = WindowExpression(
|
|
191
|
+
nested=agg_sql_expr,
|
|
192
|
+
window=Window(windowRef=None, partitions=[], orderBy=[], windowFrame=None)
|
|
193
|
+
)
|
|
194
|
+
new_query.select.selectItems.append(
|
|
195
|
+
SingleColumn(alias=db_extension.quote_identifier(c[0]), expression=window_expr)
|
|
196
|
+
)
|
|
197
|
+
return new_query
|
|
198
|
+
|
|
199
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
200
|
+
def render_single_column_expression(
|
|
201
|
+
c: PyLegendUnion[
|
|
202
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive],
|
|
203
|
+
PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]
|
|
204
|
+
]
|
|
205
|
+
) -> str:
|
|
206
|
+
escaped_col_name = escape_column_name(c[0])
|
|
207
|
+
expr_str = (c[1].to_pure_expression(config) if isinstance(c[1], PyLegendPrimitive) else
|
|
208
|
+
convert_literal_to_literal_expression(c[1]).to_pure_expression(config))
|
|
209
|
+
if len(c) == 2:
|
|
210
|
+
return f"{escaped_col_name}:{generate_pure_lambda('r', expr_str)}"
|
|
211
|
+
else:
|
|
212
|
+
agg_expr_str = c[2].to_pure_expression(config).replace(expr_str, "$c")
|
|
213
|
+
return (f"{escaped_col_name}:"
|
|
214
|
+
f"{generate_pure_lambda('r', expr_str)}:"
|
|
215
|
+
f"{generate_pure_lambda('c', agg_expr_str)}")
|
|
216
|
+
|
|
217
|
+
if all([len(t) == 2 for t in self.__new_column_expressions]) or all(
|
|
218
|
+
[len(t) == 3 for t in self.__new_column_expressions]):
|
|
219
|
+
if len(self.__new_column_expressions) == 1:
|
|
220
|
+
extend_str = f"->extend(~{render_single_column_expression(self.__new_column_expressions[0])})"
|
|
221
|
+
else:
|
|
222
|
+
extend_str = (f"->extend(~[{config.separator(2)}" +
|
|
223
|
+
("," + config.separator(2, True)).join(
|
|
224
|
+
[render_single_column_expression(x) for x in self.__new_column_expressions]
|
|
225
|
+
) +
|
|
226
|
+
f"{config.separator(1)}])")
|
|
227
|
+
return f"{self.__base_frame.to_pure(config)}{config.separator(1)}" + extend_str
|
|
228
|
+
else:
|
|
229
|
+
extend_str = self.__base_frame.to_pure(config)
|
|
230
|
+
for c in self.__new_column_expressions:
|
|
231
|
+
extend_str += f"{config.separator(1)}->extend(~{render_single_column_expression(c)})"
|
|
232
|
+
return extend_str
|
|
233
|
+
|
|
234
|
+
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
235
|
+
return self.__base_frame
|
|
236
|
+
|
|
237
|
+
def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
241
|
+
new_columns = [c.copy() for c in self.__base_frame.columns()]
|
|
242
|
+
for c in self.__new_column_expressions:
|
|
243
|
+
new_columns.append(tds_column_for_primitive(c[0], c[1] if len(c) == 2 else c[2]))
|
|
244
|
+
return new_columns
|
|
245
|
+
|
|
246
|
+
def validate(self) -> bool:
|
|
247
|
+
col_names = [x[0] for x in self.__new_column_expressions]
|
|
248
|
+
|
|
249
|
+
if len(col_names) != len(set(col_names)):
|
|
250
|
+
raise ValueError(f"Extend column names list has duplicates: {col_names}")
|
|
251
|
+
|
|
252
|
+
for c in col_names:
|
|
253
|
+
if c in [c.get_name() for c in self.__base_frame.columns()]:
|
|
254
|
+
raise ValueError(f"Extend column name - '{c}' already exists in base frame")
|
|
255
|
+
|
|
256
|
+
return True
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
PyLegendSequence,
|
|
18
|
+
PyLegendCallable,
|
|
19
|
+
PyLegendUnion,
|
|
20
|
+
)
|
|
21
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
22
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
|
|
23
|
+
from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
|
|
24
|
+
from pylegend.core.sql.metamodel import (
|
|
25
|
+
QuerySpecification,
|
|
26
|
+
LogicalBinaryType,
|
|
27
|
+
LogicalBinaryExpression,
|
|
28
|
+
)
|
|
29
|
+
from pylegend.core.tds.tds_column import TdsColumn
|
|
30
|
+
from pylegend.core.tds.tds_frame import FrameToSqlConfig
|
|
31
|
+
from pylegend.core.tds.tds_frame import FrameToPureConfig
|
|
32
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
33
|
+
from pylegend.core.language import (
|
|
34
|
+
PyLegendBoolean,
|
|
35
|
+
PyLegendBooleanLiteralExpression,
|
|
36
|
+
PyLegendPrimitive,
|
|
37
|
+
convert_literal_to_literal_expression,
|
|
38
|
+
)
|
|
39
|
+
from pylegend.core.language.shared.helpers import generate_pure_lambda
|
|
40
|
+
|
|
41
|
+
__all__: PyLegendSequence[str] = [
|
|
42
|
+
"LegendQLApiFilterFunction"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LegendQLApiFilterFunction(LegendQLApiAppliedFunction):
|
|
47
|
+
__base_frame: LegendQLApiBaseTdsFrame
|
|
48
|
+
__filter_function: PyLegendCallable[[LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]]
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def name(cls) -> str:
|
|
52
|
+
return "filter"
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
57
|
+
filter_function: PyLegendCallable[[LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]]
|
|
58
|
+
) -> None:
|
|
59
|
+
self.__base_frame = base_frame
|
|
60
|
+
self.__filter_function = filter_function
|
|
61
|
+
|
|
62
|
+
def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
|
|
63
|
+
base_query = self.__base_frame.to_sql_query_object(config)
|
|
64
|
+
should_create_sub_query = (len(base_query.groupBy) > 0) or \
|
|
65
|
+
(base_query.offset is not None) or (base_query.limit is not None)
|
|
66
|
+
new_query = (
|
|
67
|
+
create_sub_query(base_query, config, "root") if should_create_sub_query else
|
|
68
|
+
copy_query(base_query)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", self.__base_frame)
|
|
72
|
+
filter_expr = self.__filter_function(tds_row)
|
|
73
|
+
if isinstance(filter_expr, bool):
|
|
74
|
+
filter_expr = PyLegendBoolean(PyLegendBooleanLiteralExpression(filter_expr))
|
|
75
|
+
filter_sql_expr = filter_expr.to_sql_expression(
|
|
76
|
+
{"frame": new_query},
|
|
77
|
+
config
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if new_query.where is None:
|
|
81
|
+
new_query.where = filter_sql_expr
|
|
82
|
+
else:
|
|
83
|
+
new_query.where = LogicalBinaryExpression(LogicalBinaryType.AND, new_query.where, filter_sql_expr)
|
|
84
|
+
|
|
85
|
+
return new_query
|
|
86
|
+
|
|
87
|
+
def to_pure(self, config: FrameToPureConfig) -> str:
|
|
88
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
|
|
89
|
+
filter_expr = self.__filter_function(tds_row)
|
|
90
|
+
filter_expr_string = (filter_expr.to_pure_expression(config) if isinstance(filter_expr, PyLegendPrimitive) else
|
|
91
|
+
convert_literal_to_literal_expression(filter_expr).to_pure_expression(config))
|
|
92
|
+
return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}" +
|
|
93
|
+
f"->filter({generate_pure_lambda('r', filter_expr_string)})")
|
|
94
|
+
|
|
95
|
+
def base_frame(self) -> LegendQLApiBaseTdsFrame:
|
|
96
|
+
return self.__base_frame
|
|
97
|
+
|
|
98
|
+
def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
|
|
102
|
+
return [c.copy() for c in self.__base_frame.columns()]
|
|
103
|
+
|
|
104
|
+
def validate(self) -> bool:
|
|
105
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", self.__base_frame)
|
|
106
|
+
|
|
107
|
+
copy = self.__filter_function # For MyPy
|
|
108
|
+
if not isinstance(copy, type(lambda x: 0)) or (copy.__code__.co_argcount != 1):
|
|
109
|
+
raise TypeError("Filter function should be a lambda which takes one argument (TDSRow)")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = self.__filter_function(tds_row)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Filter function incompatible. Error occurred while evaluating. Message: " + str(e)
|
|
116
|
+
) from e
|
|
117
|
+
|
|
118
|
+
if not isinstance(result, (bool, PyLegendBoolean)):
|
|
119
|
+
raise RuntimeError("Filter function incompatible. Returns non boolean - " + str(type(result)))
|
|
120
|
+
|
|
121
|
+
return True
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
PyLegendSequence,
|
|
18
|
+
PyLegendCallable,
|
|
19
|
+
PyLegendUnion,
|
|
20
|
+
)
|
|
21
|
+
from pylegend.core.language import PyLegendColumnExpression
|
|
22
|
+
from pylegend.core.language.legendql_api.legendql_api_custom_expressions import (
|
|
23
|
+
LegendQLApiPrimitive,
|
|
24
|
+
LegendQLApiSortInfo,
|
|
25
|
+
LegendQLApiSortDirection,
|
|
26
|
+
)
|
|
27
|
+
from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
|
|
28
|
+
from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__: PyLegendSequence[str] = [
|
|
32
|
+
"infer_columns_from_frame",
|
|
33
|
+
"infer_sorts_from_frame",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def infer_columns_from_frame(
|
|
38
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
39
|
+
columns: PyLegendUnion[
|
|
40
|
+
str,
|
|
41
|
+
PyLegendList[str],
|
|
42
|
+
PyLegendCallable[
|
|
43
|
+
[LegendQLApiTdsRow],
|
|
44
|
+
PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
|
|
45
|
+
]
|
|
46
|
+
],
|
|
47
|
+
context: str
|
|
48
|
+
) -> PyLegendList[str]:
|
|
49
|
+
if isinstance(columns, str):
|
|
50
|
+
return [columns]
|
|
51
|
+
if isinstance(columns, list) and all([isinstance(c, str) for c in columns]):
|
|
52
|
+
return columns
|
|
53
|
+
|
|
54
|
+
if isinstance(columns, type(lambda x: 0)) and (columns.__code__.co_argcount == 1):
|
|
55
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", base_frame)
|
|
56
|
+
try:
|
|
57
|
+
result = columns(tds_row)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"{context} argument lambda incompatible. Error occurred while evaluating. Message: " + str(e)
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
list_result = result if isinstance(result, list) else [result]
|
|
64
|
+
|
|
65
|
+
columns_list = []
|
|
66
|
+
for (i, r) in enumerate(list_result):
|
|
67
|
+
if isinstance(r, LegendQLApiPrimitive) and isinstance(r.value(), PyLegendColumnExpression):
|
|
68
|
+
col_expr: PyLegendColumnExpression = r.value()
|
|
69
|
+
columns_list.append(col_expr.get_column())
|
|
70
|
+
else:
|
|
71
|
+
raise TypeError(
|
|
72
|
+
f"{context} argument lambda incompatible. Columns can be simple column expressions "
|
|
73
|
+
f"(E.g - lambda r: [r.column1, r.column2, r['column with spaces']). "
|
|
74
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
75
|
+
)
|
|
76
|
+
return columns_list
|
|
77
|
+
|
|
78
|
+
raise TypeError(f"{context} argument can either be a list of strings (column names) or "
|
|
79
|
+
f"a lambda function which takes one argument (LegendQLApiTdsRow)")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def infer_sorts_from_frame(
|
|
83
|
+
base_frame: LegendQLApiBaseTdsFrame,
|
|
84
|
+
sort_infos: PyLegendUnion[
|
|
85
|
+
str,
|
|
86
|
+
PyLegendList[str],
|
|
87
|
+
PyLegendCallable[
|
|
88
|
+
[LegendQLApiTdsRow],
|
|
89
|
+
PyLegendUnion[
|
|
90
|
+
LegendQLApiPrimitive,
|
|
91
|
+
LegendQLApiSortInfo,
|
|
92
|
+
PyLegendList[PyLegendUnion[LegendQLApiPrimitive, LegendQLApiSortInfo]],
|
|
93
|
+
]
|
|
94
|
+
]
|
|
95
|
+
],
|
|
96
|
+
context: str
|
|
97
|
+
) -> PyLegendList[LegendQLApiSortInfo]:
|
|
98
|
+
tds_row = LegendQLApiTdsRow.from_tds_frame("frame", base_frame)
|
|
99
|
+
sort_info_list = []
|
|
100
|
+
|
|
101
|
+
if isinstance(sort_infos, str):
|
|
102
|
+
col_expr1: PyLegendColumnExpression = tds_row[sort_infos].value() # type: ignore
|
|
103
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr1, LegendQLApiSortDirection.ASC))
|
|
104
|
+
|
|
105
|
+
elif isinstance(sort_infos, list) and all([isinstance(s, str) for s in sort_infos]):
|
|
106
|
+
for s in sort_infos:
|
|
107
|
+
col_expr2: PyLegendColumnExpression = tds_row[s].value() # type: ignore
|
|
108
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr2, LegendQLApiSortDirection.ASC))
|
|
109
|
+
|
|
110
|
+
elif isinstance(sort_infos, type(lambda x: 0)) and (sort_infos.__code__.co_argcount == 1):
|
|
111
|
+
try:
|
|
112
|
+
result = sort_infos(tds_row)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
f"{context} argument lambda incompatible. "
|
|
116
|
+
"Error occurred while evaluating. Message: " + str(e)
|
|
117
|
+
) from e
|
|
118
|
+
|
|
119
|
+
list_result = result if isinstance(result, list) else [result]
|
|
120
|
+
for (i, r) in enumerate(list_result):
|
|
121
|
+
if isinstance(r, LegendQLApiPrimitive) and isinstance(r.value(), PyLegendColumnExpression):
|
|
122
|
+
col_expr3: PyLegendColumnExpression = r.value()
|
|
123
|
+
sort_info_list.append(LegendQLApiSortInfo(col_expr3, LegendQLApiSortDirection.ASC))
|
|
124
|
+
elif isinstance(r, LegendQLApiSortInfo):
|
|
125
|
+
sort_info_list.append(r)
|
|
126
|
+
else:
|
|
127
|
+
raise TypeError(
|
|
128
|
+
f"{context} argument lambda incompatible. Columns can be simple column "
|
|
129
|
+
f"expressions or sort infos. "
|
|
130
|
+
f"(E.g - lambda r: [r.column1, r['column with spaces'].descending(), r.column3.ascending()). "
|
|
131
|
+
f"Element at index {i} (0-indexed) is incompatible"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
raise TypeError(f"{context} argument can either be a list of strings (column names) or "
|
|
135
|
+
"a lambda function which takes one argument (LegendQLApiTdsRow)")
|
|
136
|
+
|
|
137
|
+
return sort_info_list
|