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.
Files changed (124) hide show
  1. pylegend/__init__.py +16 -6
  2. pylegend/core/{databse → database}/sql_to_string/__init__.py +3 -3
  3. pylegend/core/{databse → database}/sql_to_string/db_extension.py +14 -5
  4. pylegend/core/{databse → database}/sql_to_string/generator.py +2 -2
  5. pylegend/core/language/__init__.py +12 -10
  6. pylegend/core/language/legacy_api/__init__.py +13 -0
  7. pylegend/core/language/{aggregate_specification.py → legacy_api/aggregate_specification.py} +10 -10
  8. pylegend/core/language/legacy_api/legacy_api_tds_row.py +32 -0
  9. pylegend/core/language/legendql_api/__init__.py +13 -0
  10. pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +541 -0
  11. pylegend/core/language/legendql_api/legendql_api_tds_row.py +292 -0
  12. pylegend/core/language/shared/__init__.py +13 -0
  13. pylegend/core/language/{column_expressions.py → shared/column_expressions.py} +32 -31
  14. pylegend/core/language/{expression.py → shared/expression.py} +8 -0
  15. pylegend/core/language/{functions.py → shared/functions.py} +12 -3
  16. pylegend/core/language/shared/helpers.py +75 -0
  17. pylegend/core/language/{literal_expressions.py → shared/literal_expressions.py} +39 -1
  18. pylegend/core/language/{operations → shared/operations}/binary_expression.py +34 -2
  19. pylegend/core/language/{operations → shared/operations}/boolean_operation_expressions.py +34 -6
  20. pylegend/core/language/{operations → shared/operations}/collection_operation_expressions.py +146 -26
  21. pylegend/core/language/{operations → shared/operations}/date_operation_expressions.py +288 -24
  22. pylegend/core/language/{operations → shared/operations}/float_operation_expressions.py +53 -8
  23. pylegend/core/language/{operations → shared/operations}/integer_operation_expressions.py +62 -9
  24. pylegend/core/language/{operations → shared/operations}/nullary_expression.py +9 -2
  25. pylegend/core/language/{operations → shared/operations}/number_operation_expressions.py +211 -30
  26. pylegend/core/language/shared/operations/primitive_operation_expressions.py +155 -0
  27. pylegend/core/language/{operations → shared/operations}/string_operation_expressions.py +194 -21
  28. pylegend/core/language/{operations → shared/operations}/unary_expression.py +10 -2
  29. pylegend/core/language/{primitive_collection.py → shared/primitive_collection.py} +2 -2
  30. pylegend/core/language/{primitives → shared/primitives}/__init__.py +9 -9
  31. pylegend/core/language/{primitives → shared/primitives}/boolean.py +9 -5
  32. pylegend/core/language/{primitives → shared/primitives}/date.py +60 -15
  33. pylegend/core/language/{primitives → shared/primitives}/datetime.py +4 -5
  34. pylegend/core/language/{primitives → shared/primitives}/float.py +6 -6
  35. pylegend/core/language/{primitives → shared/primitives}/integer.py +6 -6
  36. pylegend/core/language/{primitives → shared/primitives}/number.py +16 -13
  37. pylegend/core/language/{primitives → shared/primitives}/primitive.py +41 -5
  38. pylegend/core/language/{primitives → shared/primitives}/strictdate.py +4 -5
  39. pylegend/core/language/{primitives → shared/primitives}/string.py +18 -19
  40. pylegend/core/language/{tds_row.py → shared/tds_row.py} +46 -16
  41. pylegend/core/request/__init__.py +7 -1
  42. pylegend/core/request/auth.py +55 -1
  43. pylegend/core/request/legend_client.py +32 -0
  44. pylegend/core/sql/metamodel_extension.py +28 -0
  45. pylegend/core/tds/abstract/__init__.py +13 -0
  46. pylegend/core/tds/abstract/frames/__init__.py +13 -0
  47. pylegend/core/tds/{legend_api/frames/legend_api_applied_function_tds_frame.py → abstract/frames/applied_function_tds_frame.py} +19 -13
  48. pylegend/core/tds/abstract/frames/base_tds_frame.py +125 -0
  49. pylegend/core/tds/{legend_api/frames/legend_api_input_tds_frame.py → abstract/frames/input_tds_frame.py} +9 -12
  50. pylegend/core/tds/{legend_api/frames/functions → abstract}/function_helpers.py +1 -1
  51. pylegend/core/tds/{legend_api/frames/functions/concatenate_function.py → legacy_api/frames/functions/legacy_api_concatenate_function.py} +25 -13
  52. pylegend/core/tds/{legend_api/frames/functions/distinct_function.py → legacy_api/frames/functions/legacy_api_distinct_function.py} +13 -8
  53. pylegend/core/tds/{legend_api/frames/functions/drop_function.py → legacy_api/frames/functions/legacy_api_drop_function.py} +13 -8
  54. pylegend/core/tds/{legend_api/frames/functions/extend_function.py → legacy_api/frames/functions/legacy_api_extend_function.py} +36 -16
  55. pylegend/core/tds/{legend_api/frames/functions/filter_function.py → legacy_api/frames/functions/legacy_api_filter_function.py} +25 -13
  56. pylegend/core/tds/{legend_api/frames/functions/group_by_function.py → legacy_api/frames/functions/legacy_api_group_by_function.py} +44 -17
  57. pylegend/core/tds/{legend_api/frames/functions/head_function.py → legacy_api/frames/functions/legacy_api_head_function.py} +13 -8
  58. 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
  59. pylegend/core/tds/{legend_api/frames/functions/join_function.py → legacy_api/frames/functions/legacy_api_join_function.py} +44 -20
  60. pylegend/core/tds/{legend_api/frames/functions/rename_columns_function.py → legacy_api/frames/functions/legacy_api_rename_columns_function.py} +20 -8
  61. pylegend/core/tds/{legend_api/frames/functions/restrict_function.py → legacy_api/frames/functions/legacy_api_restrict_function.py} +17 -8
  62. pylegend/core/tds/{legend_api/frames/functions/slice_function.py → legacy_api/frames/functions/legacy_api_slice_function.py} +13 -8
  63. pylegend/core/tds/{legend_api/frames/functions/sort_function.py → legacy_api/frames/functions/legacy_api_sort_function.py} +19 -8
  64. pylegend/core/tds/legacy_api/frames/legacy_api_applied_function_tds_frame.py +37 -0
  65. pylegend/core/tds/legacy_api/frames/legacy_api_base_tds_frame.py +204 -0
  66. pylegend/core/tds/legacy_api/frames/legacy_api_input_tds_frame.py +51 -0
  67. pylegend/core/tds/{legend_api/frames/legend_api_tds_frame.py → legacy_api/frames/legacy_api_tds_frame.py} +28 -28
  68. pylegend/core/tds/legendql_api/__init__.py +13 -0
  69. pylegend/core/tds/legendql_api/frames/__init__.py +13 -0
  70. pylegend/core/tds/legendql_api/frames/functions/__init__.py +13 -0
  71. pylegend/core/tds/legendql_api/frames/functions/legendql_api_asofjoin_function.py +156 -0
  72. pylegend/core/tds/legendql_api/frames/functions/legendql_api_concatenate_function.py +139 -0
  73. pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +69 -0
  74. pylegend/core/tds/legendql_api/frames/functions/legendql_api_drop_function.py +74 -0
  75. pylegend/core/tds/legendql_api/frames/functions/legendql_api_extend_function.py +256 -0
  76. pylegend/core/tds/legendql_api/frames/functions/legendql_api_filter_function.py +121 -0
  77. pylegend/core/tds/legendql_api/frames/functions/legendql_api_function_helpers.py +137 -0
  78. pylegend/core/tds/legendql_api/frames/functions/legendql_api_groupby_function.py +256 -0
  79. pylegend/core/tds/legendql_api/frames/functions/legendql_api_head_function.py +74 -0
  80. pylegend/core/tds/legendql_api/frames/functions/legendql_api_join_function.py +214 -0
  81. pylegend/core/tds/legendql_api/frames/functions/legendql_api_project_function.py +169 -0
  82. pylegend/core/tds/legendql_api/frames/functions/legendql_api_rename_function.py +189 -0
  83. pylegend/core/tds/legendql_api/frames/functions/legendql_api_select_function.py +131 -0
  84. pylegend/core/tds/legendql_api/frames/functions/legendql_api_slice_function.py +82 -0
  85. pylegend/core/tds/legendql_api/frames/functions/legendql_api_sort_function.py +93 -0
  86. pylegend/core/tds/legendql_api/frames/functions/legendql_api_window_extend_function.py +283 -0
  87. pylegend/core/tds/legendql_api/frames/legendql_api_applied_function_tds_frame.py +37 -0
  88. pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +419 -0
  89. pylegend/core/tds/legendql_api/frames/legendql_api_input_tds_frame.py +50 -0
  90. pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +327 -0
  91. pylegend/core/tds/pandas_api/frames/functions/assign_function.py +6 -6
  92. pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +4 -0
  93. pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +11 -3
  94. pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +2 -2
  95. pylegend/core/tds/tds_frame.py +32 -2
  96. pylegend/extensions/database/vendors/postgres/postgres_sql_to_string.py +1 -1
  97. pylegend/extensions/tds/abstract/legend_function_input_frame.py +4 -0
  98. pylegend/extensions/tds/abstract/legend_service_input_frame.py +4 -0
  99. pylegend/extensions/tds/abstract/table_spec_input_frame.py +4 -0
  100. 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
  101. 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
  102. 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
  103. pylegend/extensions/tds/legendql_api/__init__.py +13 -0
  104. pylegend/extensions/tds/legendql_api/frames/__init__.py +13 -0
  105. pylegend/extensions/tds/legendql_api/frames/legendql_api_legend_service_input_frame.py +46 -0
  106. pylegend/extensions/tds/legendql_api/frames/legendql_api_table_spec_input_frame.py +36 -0
  107. pylegend/{legend_api_tds_client.py → legacy_api_tds_client.py} +15 -15
  108. {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/METADATA +7 -8
  109. pylegend-0.5.0.dist-info/NOTICE +5 -0
  110. pylegend-0.5.0.dist-info/RECORD +155 -0
  111. {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/WHEEL +1 -1
  112. pylegend/core/language/operations/primitive_operation_expressions.py +0 -56
  113. pylegend/core/tds/legend_api/frames/legend_api_base_tds_frame.py +0 -294
  114. pylegend-0.3.0.dist-info/RECORD +0 -115
  115. /pylegend/core/{databse → database}/__init__.py +0 -0
  116. /pylegend/core/{databse → database}/sql_to_string/config.py +0 -0
  117. /pylegend/core/language/{operations → shared/operations}/__init__.py +0 -0
  118. /pylegend/core/tds/{legend_api → legacy_api}/__init__.py +0 -0
  119. /pylegend/core/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
  120. /pylegend/core/tds/{legend_api → legacy_api}/frames/functions/__init__.py +0 -0
  121. /pylegend/extensions/tds/{legend_api → legacy_api}/__init__.py +0 -0
  122. /pylegend/extensions/tds/{legend_api → legacy_api}/frames/__init__.py +0 -0
  123. {pylegend-0.3.0.dist-info → pylegend-0.5.0.dist-info}/LICENSE +0 -0
  124. {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
+ PyLegendTuple,
20
+ PyLegendCallable,
21
+ PyLegendUnion,
22
+ )
23
+ from pylegend.core.language import (
24
+ PyLegendPrimitiveOrPythonPrimitive,
25
+ PyLegendPrimitiveCollection,
26
+ PyLegendPrimitive,
27
+ create_primitive_collection,
28
+ convert_literal_to_literal_expression
29
+ )
30
+ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import LegendQLApiPrimitive
31
+ from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
32
+ from pylegend.core.tds.abstract.function_helpers import tds_column_for_primitive
33
+ from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import infer_columns_from_frame
34
+ from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
35
+ from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
36
+ from pylegend.core.sql.metamodel import (
37
+ QuerySpecification,
38
+ SingleColumn,
39
+ SelectItem
40
+ )
41
+ from pylegend.core.tds.tds_column import TdsColumn
42
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig
43
+ from pylegend.core.tds.tds_frame import FrameToPureConfig
44
+ from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
45
+ from pylegend.core.language.shared.helpers import escape_column_name, generate_pure_lambda
46
+
47
+ __all__: PyLegendSequence[str] = [
48
+ "LegendQLApiGroupByFunction"
49
+ ]
50
+
51
+
52
+ class LegendQLApiGroupByFunction(LegendQLApiAppliedFunction):
53
+ __base_frame: LegendQLApiBaseTdsFrame
54
+ __grouping_column_name_list: PyLegendList[str]
55
+ __aggregates_list: PyLegendList[PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]]
56
+
57
+ @classmethod
58
+ def name(cls) -> str:
59
+ return "groupBy"
60
+
61
+ def __init__(
62
+ self,
63
+ base_frame: LegendQLApiBaseTdsFrame,
64
+ grouping_columns: PyLegendUnion[
65
+ str,
66
+ PyLegendList[str],
67
+ PyLegendCallable[
68
+ [LegendQLApiTdsRow],
69
+ PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
70
+ ]
71
+ ],
72
+ aggregate_specifications: PyLegendUnion[
73
+ PyLegendTuple[
74
+ str,
75
+ PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
76
+ PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
77
+ ],
78
+ PyLegendList[
79
+ PyLegendTuple[
80
+ str,
81
+ PyLegendCallable[[LegendQLApiTdsRow], PyLegendPrimitiveOrPythonPrimitive],
82
+ PyLegendCallable[[PyLegendPrimitiveCollection], PyLegendPrimitive]
83
+ ]
84
+ ]
85
+ ]
86
+ ) -> None:
87
+ self.__base_frame = base_frame
88
+ self.__grouping_column_name_list = infer_columns_from_frame(
89
+ base_frame, grouping_columns, "'group_by' function grouping columns"
90
+ )
91
+
92
+ tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
93
+ list_result = (
94
+ aggregate_specifications if isinstance(aggregate_specifications, list) else [aggregate_specifications]
95
+ )
96
+ aggregates_list: PyLegendList[PyLegendTuple[str, PyLegendPrimitiveOrPythonPrimitive, PyLegendPrimitive]] = []
97
+ for (i, agg_spec) in enumerate(list_result):
98
+ error = (
99
+ "'group_by' function aggregate specifications incompatible. "
100
+ "Each aggregate specification should be a triplet with first element being the aggregation column "
101
+ "name, second element being a mapper function (single argument lambda) and third element being the "
102
+ "aggregation function (single argument lambda). "
103
+ "E.g - ('count_col', lambda r: r['col1'], lambda c: c.count()). "
104
+ f"Element at index {i} (0-indexed) is incompatible"
105
+ )
106
+
107
+ if isinstance(agg_spec, tuple) and isinstance(agg_spec[0], str):
108
+
109
+ if not isinstance(agg_spec[1], type(lambda x: 0)) or (agg_spec[1].__code__.co_argcount != 1):
110
+ raise TypeError(error)
111
+
112
+ try:
113
+ map_result = agg_spec[1](tds_row)
114
+ except Exception as e:
115
+ raise RuntimeError(
116
+ "'group_by' function aggregate specifications incompatible. "
117
+ f"Error occurred while evaluating mapper lambda in the aggregate specification at "
118
+ f"index {i} (0-indexed). Message: " + str(e)
119
+ ) from e
120
+
121
+ if not isinstance(map_result, (int, float, bool, str, date, datetime, PyLegendPrimitive)):
122
+ raise TypeError(
123
+ "'group_by' function aggregate specifications incompatible. "
124
+ f"Mapper lambda in the aggregate specification at index {i} (0-indexed) "
125
+ f"returns non-primitive - {str(type(map_result))}"
126
+ )
127
+
128
+ collection = create_primitive_collection(map_result)
129
+
130
+ if not isinstance(agg_spec[2], type(lambda x: 0)) or (agg_spec[2].__code__.co_argcount != 1):
131
+ raise TypeError(error)
132
+
133
+ try:
134
+ agg_result = agg_spec[2](collection)
135
+ except Exception as e:
136
+ raise RuntimeError(
137
+ "'group_by' function aggregate specifications incompatible. "
138
+ f"Error occurred while evaluating aggregation lambda in the aggregate specification at "
139
+ f"index {i} (0-indexed). Message: " + str(e)
140
+ ) from e
141
+
142
+ if not isinstance(agg_result, PyLegendPrimitive):
143
+ raise TypeError(
144
+ "'group_by' function aggregate specifications incompatible. "
145
+ f"Aggregation lambda in the aggregate specification at index {i} (0-indexed) "
146
+ f"returns non-primitive - {str(type(agg_result))}"
147
+ )
148
+
149
+ aggregates_list.append((agg_spec[0], map_result, agg_result))
150
+
151
+ else:
152
+ raise TypeError(error)
153
+
154
+ self.__aggregates_list = aggregates_list
155
+
156
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
157
+ db_extension = config.sql_to_string_generator().get_db_extension()
158
+ base_query = self.__base_frame.to_sql_query_object(config)
159
+
160
+ should_create_sub_query = (len(base_query.groupBy) > 0) or base_query.select.distinct or \
161
+ (base_query.offset is not None) or (base_query.limit is not None)
162
+
163
+ columns_to_retain = [db_extension.quote_identifier(x) for x in self.__grouping_column_name_list]
164
+ if should_create_sub_query:
165
+ new_query = create_sub_query(base_query, config, "root")
166
+ else:
167
+ new_query = copy_query(base_query)
168
+
169
+ new_cols_with_index: PyLegendList[PyLegendTuple[int, 'SelectItem']] = []
170
+ for col in new_query.select.selectItems:
171
+ if not isinstance(col, SingleColumn):
172
+ raise ValueError("Group By operation not supported for queries "
173
+ "with columns other than SingleColumn") # pragma: no cover
174
+ if col.alias is None:
175
+ raise ValueError("Group By operation not supported for queries "
176
+ "with SingleColumns with missing alias") # pragma: no cover
177
+ if col.alias in columns_to_retain:
178
+ new_cols_with_index.append((columns_to_retain.index(col.alias), col))
179
+
180
+ new_select_items = [y[1] for y in sorted(new_cols_with_index, key=lambda x: x[0])]
181
+
182
+ tds_row = LegendQLApiTdsRow.from_tds_frame("r", self.__base_frame)
183
+ for agg in self.__aggregates_list:
184
+ agg_sql_expr = agg[2].to_sql_expression({"r": new_query}, config)
185
+
186
+ new_select_items.append(
187
+ SingleColumn(alias=db_extension.quote_identifier(agg[0]), expression=agg_sql_expr)
188
+ )
189
+
190
+ new_query.select.selectItems = new_select_items
191
+ new_query.groupBy = [
192
+ (lambda x: x[c])(tds_row).to_sql_expression({"r": new_query}, config)
193
+ for c in self.__grouping_column_name_list
194
+ ]
195
+ return new_query
196
+
197
+ def to_pure(self, config: FrameToPureConfig) -> str:
198
+ group_strings = []
199
+ for col_name in self.__grouping_column_name_list:
200
+ group_strings.append(escape_column_name(col_name))
201
+
202
+ agg_strings = []
203
+ for agg in self.__aggregates_list:
204
+ map_expr_string = (agg[1].to_pure_expression(config) if isinstance(agg[1], PyLegendPrimitive)
205
+ else convert_literal_to_literal_expression(agg[1]).to_pure_expression(config))
206
+ agg_expr_string = agg[2].to_pure_expression(config).replace(map_expr_string, "$c")
207
+ agg_strings.append(f"{escape_column_name(agg[0])}:{generate_pure_lambda('r', map_expr_string)}:"
208
+ f"{generate_pure_lambda('c', agg_expr_string)}")
209
+
210
+ return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}" +
211
+ f"->groupBy({config.separator(2)}"
212
+ f"~[{', '.join(group_strings)}],{config.separator(2, True)}"
213
+ f"~[{', '.join(agg_strings)}]{config.separator(1)}"
214
+ f")")
215
+
216
+ def base_frame(self) -> LegendQLApiBaseTdsFrame:
217
+ return self.__base_frame
218
+
219
+ def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
220
+ return []
221
+
222
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
223
+ base_columns = self.__base_frame.columns()
224
+ new_columns = []
225
+ for c in self.__grouping_column_name_list:
226
+ for base_col in base_columns:
227
+ if base_col.get_name() == c:
228
+ new_columns.append(base_col.copy())
229
+ break
230
+ for agg in self.__aggregates_list:
231
+ new_columns.append(tds_column_for_primitive(agg[0], agg[2]))
232
+ return new_columns
233
+
234
+ def validate(self) -> bool:
235
+ base_columns = self.__base_frame.columns()
236
+ for c in self.__grouping_column_name_list:
237
+ found_col = False
238
+ for base_col in base_columns:
239
+ if base_col.get_name() == c:
240
+ found_col = True
241
+ break
242
+ if not found_col:
243
+ raise ValueError(
244
+ f"Column - '{c}' in group_by columns list doesn't exist in the current frame. "
245
+ f"Current frame columns: {[x.get_name() for x in base_columns]}"
246
+ )
247
+
248
+ agg_cols = [c[0] for c in self.__aggregates_list]
249
+ new_cols = self.__grouping_column_name_list + agg_cols
250
+ if len(new_cols) == 0:
251
+ raise ValueError("At-least one grouping column or aggregate specification must be provided "
252
+ "when using group_by function")
253
+ if len(new_cols) != len(set(new_cols)):
254
+ raise ValueError("Found duplicate column names in grouping columns and aggregation columns. "
255
+ f"Grouping columns - {self.__grouping_column_name_list}, Aggregation columns - {agg_cols}")
256
+ return True
@@ -0,0 +1,74 @@
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
+ )
19
+ from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import LegendQLApiAppliedFunction
20
+ from pylegend.core.tds.sql_query_helpers import copy_query, create_sub_query
21
+ from pylegend.core.sql.metamodel import (
22
+ QuerySpecification,
23
+ LongLiteral,
24
+ )
25
+ from pylegend.core.tds.tds_column import TdsColumn
26
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig
27
+ from pylegend.core.tds.tds_frame import FrameToPureConfig
28
+ from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
29
+
30
+
31
+ __all__: PyLegendSequence[str] = [
32
+ "LegendQLApiHeadFunction"
33
+ ]
34
+
35
+
36
+ class LegendQLApiHeadFunction(LegendQLApiAppliedFunction):
37
+ __base_frame: LegendQLApiBaseTdsFrame
38
+ __row_count: int
39
+
40
+ @classmethod
41
+ def name(cls) -> str:
42
+ return "head"
43
+
44
+ def __init__(self, base_frame: LegendQLApiBaseTdsFrame, row_count: int) -> None:
45
+ self.__base_frame = base_frame
46
+ self.__row_count = row_count
47
+
48
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
49
+ base_query = self.__base_frame.to_sql_query_object(config)
50
+ should_create_sub_query = (base_query.limit is not None)
51
+ new_query = (
52
+ create_sub_query(base_query, config, "root") if should_create_sub_query else
53
+ copy_query(base_query)
54
+ )
55
+ new_query.limit = LongLiteral(value=self.__row_count)
56
+ return new_query
57
+
58
+ def to_pure(self, config: FrameToPureConfig) -> str:
59
+ return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
60
+ f"->limit({self.__row_count})")
61
+
62
+ def base_frame(self) -> LegendQLApiBaseTdsFrame:
63
+ return self.__base_frame
64
+
65
+ def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
66
+ return []
67
+
68
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
69
+ return [c.copy() for c in self.__base_frame.columns()]
70
+
71
+ def validate(self) -> bool:
72
+ if self.__row_count < 0:
73
+ raise ValueError("Row count argument of head/limit function cannot be negative")
74
+ return True
@@ -0,0 +1,214 @@
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
+ PyLegendUnion,
19
+ PyLegendCallable,
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, extract_columns_for_subquery
24
+ from pylegend.core.sql.metamodel import (
25
+ QuerySpecification,
26
+ Select,
27
+ SelectItem,
28
+ SingleColumn,
29
+ AliasedRelation,
30
+ TableSubquery,
31
+ Query,
32
+ Join,
33
+ JoinType,
34
+ JoinOn,
35
+ QualifiedNameReference,
36
+ QualifiedName,
37
+ )
38
+ from pylegend.core.tds.tds_column import TdsColumn
39
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig
40
+ from pylegend.core.tds.tds_frame import FrameToPureConfig
41
+ from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
42
+ from pylegend.core.tds.legendql_api.frames.legendql_api_tds_frame import LegendQLApiTdsFrame
43
+ from pylegend.core.language import (
44
+ PyLegendBoolean,
45
+ PyLegendBooleanLiteralExpression,
46
+ PyLegendPrimitive,
47
+ convert_literal_to_literal_expression,
48
+ )
49
+ from pylegend.core.language.shared.helpers import generate_pure_lambda
50
+
51
+
52
+ __all__: PyLegendSequence[str] = [
53
+ "LegendQLApiJoinFunction"
54
+ ]
55
+
56
+
57
+ class LegendQLApiJoinFunction(LegendQLApiAppliedFunction):
58
+ __base_frame: LegendQLApiBaseTdsFrame
59
+ __other_frame: LegendQLApiBaseTdsFrame
60
+ __join_condition: PyLegendCallable[[LegendQLApiTdsRow, LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]]
61
+ __join_type: str
62
+
63
+ @classmethod
64
+ def name(cls) -> str:
65
+ return "join"
66
+
67
+ def __init__(
68
+ self,
69
+ base_frame: LegendQLApiBaseTdsFrame,
70
+ other_frame: LegendQLApiTdsFrame,
71
+ join_condition: PyLegendCallable[[LegendQLApiTdsRow, LegendQLApiTdsRow], PyLegendUnion[bool, PyLegendBoolean]],
72
+ join_type: str
73
+ ) -> None:
74
+ self.__base_frame = base_frame
75
+ if not isinstance(other_frame, LegendQLApiBaseTdsFrame):
76
+ raise ValueError("Expected LegendQLApiBaseTdsFrame") # pragma: no cover
77
+ self.__other_frame = other_frame
78
+ self.__join_condition = join_condition
79
+ self.__join_type = join_type
80
+
81
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
82
+ db_extension = config.sql_to_string_generator().get_db_extension()
83
+ base_query = copy_query(self.__base_frame.to_sql_query_object(config))
84
+ other_query = copy_query(self.__other_frame.to_sql_query_object(config))
85
+
86
+ join_type = (
87
+ JoinType.INNER if self.__join_type.lower() == 'inner' else (
88
+ JoinType.LEFT if self.__join_type.lower() in ('left_outer', 'leftouter') else
89
+ JoinType.RIGHT
90
+ )
91
+ )
92
+
93
+ left_row = LegendQLApiTdsRow.from_tds_frame('left', self.__base_frame)
94
+ right_row = LegendQLApiTdsRow.from_tds_frame('right', self.__other_frame)
95
+
96
+ join_expr = self.__join_condition(left_row, right_row)
97
+ if isinstance(join_expr, bool):
98
+ join_expr = PyLegendBoolean(PyLegendBooleanLiteralExpression(join_expr))
99
+ join_sql_expr = join_expr.to_sql_expression(
100
+ {
101
+ 'left': create_sub_query(base_query, config, 'left'),
102
+ 'right': create_sub_query(other_query, config, 'right'),
103
+ },
104
+ config
105
+ )
106
+
107
+ left_alias = db_extension.quote_identifier('left')
108
+ right_alias = db_extension.quote_identifier('right')
109
+ new_select_items: PyLegendList[SelectItem] = []
110
+ for c in self.__base_frame.columns():
111
+ q = db_extension.quote_identifier(c.get_name())
112
+ new_select_items.append(SingleColumn(q, QualifiedNameReference(name=QualifiedName(parts=[left_alias, q]))))
113
+ for c in self.__other_frame.columns():
114
+ q = db_extension.quote_identifier(c.get_name())
115
+ new_select_items.append(SingleColumn(q, QualifiedNameReference(name=QualifiedName(parts=[right_alias, q]))))
116
+
117
+ join_query = QuerySpecification(
118
+ select=Select(
119
+ selectItems=new_select_items,
120
+ distinct=False
121
+ ),
122
+ from_=[
123
+ Join(
124
+ type_=join_type,
125
+ left=AliasedRelation(
126
+ relation=TableSubquery(query=Query(queryBody=base_query, limit=None, offset=None, orderBy=[])),
127
+ alias=left_alias,
128
+ columnNames=extract_columns_for_subquery(base_query)
129
+ ),
130
+ right=AliasedRelation(
131
+ relation=TableSubquery(query=Query(queryBody=other_query, limit=None, offset=None, orderBy=[])),
132
+ alias=right_alias,
133
+ columnNames=extract_columns_for_subquery(other_query)
134
+ ),
135
+ criteria=JoinOn(expression=join_sql_expr)
136
+ )
137
+ ],
138
+ where=None,
139
+ groupBy=[],
140
+ having=None,
141
+ orderBy=[],
142
+ limit=None,
143
+ offset=None
144
+ )
145
+
146
+ wrapped_join_query = create_sub_query(join_query, config, "root")
147
+ return wrapped_join_query
148
+
149
+ def to_pure(self, config: FrameToPureConfig) -> str:
150
+ left_row = LegendQLApiTdsRow.from_tds_frame("l", self.__base_frame)
151
+ right_row = LegendQLApiTdsRow.from_tds_frame("r", self.__other_frame)
152
+ join_expr = self.__join_condition(left_row, right_row)
153
+ join_expr_string = (join_expr.to_pure_expression(config.push_indent(2))
154
+ if isinstance(join_expr, PyLegendPrimitive) else
155
+ convert_literal_to_literal_expression(join_expr).to_pure_expression(config.push_indent(2)))
156
+ join_kind = (
157
+ "INNER" if self.__join_type.lower() == 'inner' else
158
+ "LEFT" if self.__join_type.lower() in ('left_outer', 'leftouter') else
159
+ "RIGHT" if self.__join_type.lower() in ('right_outer', 'rightouter') else
160
+ "FULL"
161
+ )
162
+ return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}" +
163
+ f"->join({config.separator(2)}"
164
+ f"{self.__other_frame.to_pure(config.push_indent(2))},{config.separator(2, True)}"
165
+ f"JoinKind.{join_kind},{config.separator(2, True)}"
166
+ f"{generate_pure_lambda('l, r', join_expr_string)}{config.separator(1)}"
167
+ f")")
168
+
169
+ def base_frame(self) -> LegendQLApiBaseTdsFrame:
170
+ return self.__base_frame
171
+
172
+ def tds_frame_parameters(self) -> PyLegendList["LegendQLApiBaseTdsFrame"]:
173
+ return [self.__other_frame]
174
+
175
+ def calculate_columns(self) -> PyLegendSequence["TdsColumn"]:
176
+ return (
177
+ [c.copy() for c in self.__base_frame.columns()] +
178
+ [c.copy() for c in self.__other_frame.columns()]
179
+ )
180
+
181
+ def validate(self) -> bool:
182
+ copy = self.__join_condition # For MyPy
183
+ if not isinstance(copy, type(lambda x: 0)) or (copy.__code__.co_argcount != 2):
184
+ raise TypeError("Join condition function should be a lambda which takes two arguments (TDSRow, TDSRow)")
185
+
186
+ left_row = LegendQLApiTdsRow.from_tds_frame("left", self.__base_frame)
187
+ right_row = LegendQLApiTdsRow.from_tds_frame("right", self.__other_frame)
188
+
189
+ try:
190
+ result = self.__join_condition(left_row, right_row)
191
+ except Exception as e:
192
+ raise RuntimeError(
193
+ "Join condition function incompatible. Error occurred while evaluating. Message: " + str(e)
194
+ ) from e
195
+
196
+ if not isinstance(result, (bool, PyLegendBoolean)):
197
+ raise RuntimeError("Join condition function incompatible. Returns non boolean - " + str(type(result)))
198
+
199
+ left_cols = [c.get_name() for c in self.__base_frame.columns()]
200
+ right_cols = [c.get_name() for c in self.__other_frame.columns()]
201
+
202
+ final_cols = left_cols + right_cols
203
+ if len(final_cols) != len(set(final_cols)):
204
+ raise ValueError(
205
+ "Found duplicate columns in joined frames. Use rename function to ensure there are no duplicate columns "
206
+ f"in joined frames. Columns - Left Frame: {left_cols}, Right Frame: {right_cols}"
207
+ )
208
+
209
+ if self.__join_type.lower() not in ('inner', 'left_outer', 'right_outer', 'leftouter', 'rightouter'):
210
+ raise ValueError(
211
+ f"Unknown join type - {self.__join_type}. Supported types are - INNER, LEFT_OUTER, RIGHT_OUTER"
212
+ )
213
+
214
+ return True