pylegend 0.10.0__tar.gz → 0.12.0__tar.gz

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 (181) hide show
  1. {pylegend-0.10.0 → pylegend-0.12.0}/PKG-INFO +1 -1
  2. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/database/sql_to_string/db_extension.py +68 -6
  3. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legendql_api/legendql_api_custom_expressions.py +190 -5
  4. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/pandas_api/pandas_api_series.py +3 -0
  5. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/sql/metamodel.py +4 -1
  6. pylegend-0.12.0/pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +115 -0
  7. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/legendql_api_base_tds_frame.py +146 -4
  8. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/legendql_api_tds_frame.py +33 -2
  9. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/functions/aggregate_function.py +441 -0
  10. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/assign_function.py +65 -23
  11. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/drop.py +3 -3
  12. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/functions/dropna.py +167 -0
  13. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/functions/fillna.py +162 -0
  14. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/filter.py +10 -5
  15. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/functions/merge.py +513 -0
  16. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/functions/rename.py +214 -0
  17. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/truncate_function.py +151 -120
  18. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/pandas_api_applied_function_tds_frame.py +7 -3
  19. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +889 -0
  20. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/pandas_api_groupby_tds_frame.py +325 -0
  21. pylegend-0.12.0/pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +336 -0
  22. pylegend-0.12.0/pylegend/extensions/tds/abstract/csv_tds_frame.py +95 -0
  23. pylegend-0.12.0/pylegend/extensions/tds/legendql_api/frames/legendql_api_csv_input_frame.py +36 -0
  24. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_function_input_frame.py +9 -4
  25. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/pandas_api/frames/pandas_api_legend_service_input_frame.py +12 -5
  26. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/pandas_api/frames/pandas_api_table_spec_input_frame.py +12 -4
  27. {pylegend-0.10.0 → pylegend-0.12.0}/pyproject.toml +1 -1
  28. pylegend-0.10.0/pylegend/core/tds/legendql_api/frames/functions/legendql_api_distinct_function.py +0 -69
  29. pylegend-0.10.0/pylegend/core/tds/pandas_api/frames/functions/aggregate_function.py +0 -316
  30. pylegend-0.10.0/pylegend/core/tds/pandas_api/frames/pandas_api_base_tds_frame.py +0 -348
  31. pylegend-0.10.0/pylegend/core/tds/pandas_api/frames/pandas_api_tds_frame.py +0 -130
  32. {pylegend-0.10.0 → pylegend-0.12.0}/LICENSE +0 -0
  33. {pylegend-0.10.0 → pylegend-0.12.0}/LICENSE.spdx +0 -0
  34. {pylegend-0.10.0 → pylegend-0.12.0}/NOTICE +0 -0
  35. {pylegend-0.10.0 → pylegend-0.12.0}/README.md +0 -0
  36. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/__init__.py +0 -0
  37. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/_typing.py +0 -0
  38. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/__init__.py +0 -0
  39. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/database/__init__.py +0 -0
  40. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/database/sql_to_string/__init__.py +0 -0
  41. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/database/sql_to_string/config.py +0 -0
  42. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/database/sql_to_string/generator.py +0 -0
  43. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/__init__.py +0 -0
  44. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legacy_api/__init__.py +0 -0
  45. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legacy_api/aggregate_specification.py +0 -0
  46. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legacy_api/legacy_api_tds_row.py +0 -0
  47. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legendql_api/__init__.py +0 -0
  48. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/legendql_api/legendql_api_tds_row.py +0 -0
  49. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/pandas_api/__init__.py +0 -0
  50. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/pandas_api/pandas_api_aggregate_specification.py +0 -0
  51. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/pandas_api/pandas_api_custom_expressions.py +0 -0
  52. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/pandas_api/pandas_api_tds_row.py +0 -0
  53. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/__init__.py +0 -0
  54. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/column_expressions.py +0 -0
  55. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/expression.py +0 -0
  56. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/functions.py +0 -0
  57. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/helpers.py +0 -0
  58. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/literal_expressions.py +0 -0
  59. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/__init__.py +0 -0
  60. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/binary_expression.py +0 -0
  61. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/boolean_operation_expressions.py +0 -0
  62. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/collection_operation_expressions.py +0 -0
  63. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/date_operation_expressions.py +0 -0
  64. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/float_operation_expressions.py +0 -0
  65. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/integer_operation_expressions.py +0 -0
  66. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/nary_expression.py +0 -0
  67. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/nullary_expression.py +0 -0
  68. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/number_operation_expressions.py +0 -0
  69. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/primitive_operation_expressions.py +0 -0
  70. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/string_operation_expressions.py +0 -0
  71. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/operations/unary_expression.py +0 -0
  72. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/pct_helpers.py +0 -0
  73. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitive_collection.py +0 -0
  74. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/__init__.py +0 -0
  75. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/boolean.py +0 -0
  76. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/date.py +0 -0
  77. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/datetime.py +0 -0
  78. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/float.py +0 -0
  79. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/integer.py +0 -0
  80. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/number.py +0 -0
  81. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/primitive.py +0 -0
  82. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/strictdate.py +0 -0
  83. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/primitives/string.py +0 -0
  84. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/language/shared/tds_row.py +0 -0
  85. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/project_cooridnates.py +0 -0
  86. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/request/__init__.py +0 -0
  87. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/request/auth.py +0 -0
  88. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/request/legend_client.py +0 -0
  89. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/request/response_reader.py +0 -0
  90. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/request/service_client.py +0 -0
  91. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/sql/__init__.py +0 -0
  92. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/sql/metamodel_extension.py +0 -0
  93. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/__init__.py +0 -0
  94. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/__init__.py +0 -0
  95. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/frames/__init__.py +0 -0
  96. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/frames/applied_function_tds_frame.py +0 -0
  97. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/frames/base_tds_frame.py +0 -0
  98. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/frames/input_tds_frame.py +0 -0
  99. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/abstract/function_helpers.py +0 -0
  100. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/__init__.py +0 -0
  101. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/__init__.py +0 -0
  102. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/__init__.py +0 -0
  103. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_concatenate_function.py +0 -0
  104. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_distinct_function.py +0 -0
  105. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_drop_function.py +0 -0
  106. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_extend_function.py +0 -0
  107. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_filter_function.py +0 -0
  108. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_group_by_function.py +0 -0
  109. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_head_function.py +0 -0
  110. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_join_by_columns_function.py +0 -0
  111. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_join_function.py +0 -0
  112. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_rename_columns_function.py +0 -0
  113. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_restrict_function.py +0 -0
  114. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_slice_function.py +0 -0
  115. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/functions/legacy_api_sort_function.py +0 -0
  116. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/legacy_api_applied_function_tds_frame.py +0 -0
  117. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/legacy_api_base_tds_frame.py +0 -0
  118. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/legacy_api_input_tds_frame.py +0 -0
  119. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legacy_api/frames/legacy_api_tds_frame.py +0 -0
  120. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/__init__.py +0 -0
  121. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/__init__.py +0 -0
  122. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/__init__.py +0 -0
  123. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_asofjoin_function.py +0 -0
  124. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_concatenate_function.py +0 -0
  125. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_drop_function.py +0 -0
  126. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_extend_function.py +0 -0
  127. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_filter_function.py +0 -0
  128. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_function_helpers.py +0 -0
  129. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_groupby_function.py +0 -0
  130. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_head_function.py +0 -0
  131. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_join_function.py +0 -0
  132. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_project_function.py +0 -0
  133. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_rename_function.py +0 -0
  134. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_select_function.py +0 -0
  135. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_slice_function.py +0 -0
  136. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_sort_function.py +0 -0
  137. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/functions/legendql_api_window_extend_function.py +0 -0
  138. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/legendql_api_applied_function_tds_frame.py +0 -0
  139. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/legendql_api/frames/legendql_api_input_tds_frame.py +0 -0
  140. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/__init__.py +0 -0
  141. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/__init__.py +0 -0
  142. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/__init__.py +0 -0
  143. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/filtering.py +0 -0
  144. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/functions/sort_values_function.py +0 -0
  145. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/pandas_api/frames/pandas_api_input_tds_frame.py +0 -0
  146. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/result_handler/__init__.py +0 -0
  147. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/result_handler/result_handler.py +0 -0
  148. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/result_handler/to_csv_file_result_handler.py +0 -0
  149. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/result_handler/to_json_file_result_handler.py +0 -0
  150. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/result_handler/to_string_result_handler.py +0 -0
  151. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/sql_query_helpers.py +0 -0
  152. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/tds_column.py +0 -0
  153. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/core/tds/tds_frame.py +0 -0
  154. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/__init__.py +0 -0
  155. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/database/__init__.py +0 -0
  156. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/database/vendors/__init__.py +0 -0
  157. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/database/vendors/postgres/__init__.py +0 -0
  158. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/database/vendors/postgres/postgres_sql_to_string.py +0 -0
  159. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/__init__.py +0 -0
  160. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/abstract/__init__.py +0 -0
  161. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/abstract/legend_function_input_frame.py +0 -0
  162. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/abstract/legend_service_input_frame.py +0 -0
  163. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/abstract/table_spec_input_frame.py +0 -0
  164. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legacy_api/__init__.py +0 -0
  165. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legacy_api/frames/__init__.py +0 -0
  166. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legacy_api/frames/legacy_api_legend_function_input_frame.py +0 -0
  167. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legacy_api/frames/legacy_api_legend_service_input_frame.py +0 -0
  168. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legacy_api/frames/legacy_api_table_spec_input_frame.py +0 -0
  169. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legendql_api/__init__.py +0 -0
  170. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legendql_api/frames/__init__.py +0 -0
  171. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legendql_api/frames/legendql_api_legend_function_input_frame.py +0 -0
  172. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legendql_api/frames/legendql_api_legend_service_input_frame.py +0 -0
  173. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/legendql_api/frames/legendql_api_table_spec_input_frame.py +0 -0
  174. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/pandas_api/__init__.py +0 -0
  175. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/pandas_api/frames/__init__.py +0 -0
  176. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/result_handler/__init__.py +0 -0
  177. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/extensions/tds/result_handler/to_pandas_df_result_handler.py +0 -0
  178. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/legacy_api_tds_client.py +0 -0
  179. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/legendql_api_tds_client.py +0 -0
  180. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/utils/__init__.py +0 -0
  181. {pylegend-0.10.0 → pylegend-0.12.0}/pylegend/utils/class_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylegend
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary: Python language binding for Legend data management platform
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -72,6 +72,10 @@ from pylegend.core.sql.metamodel import (
72
72
  Window,
73
73
  TableFunction,
74
74
  Union,
75
+ WindowFrame,
76
+ WindowFrameMode,
77
+ FrameBound,
78
+ FrameBoundType
75
79
  )
76
80
  from pylegend.core.sql.metamodel_extension import (
77
81
  StringLengthExpression,
@@ -864,14 +868,27 @@ def window_processor(
864
868
  if window.windowRef:
865
869
  return window.windowRef
866
870
 
867
- partitions = "PARTITION BY " + (", ".join([extension.process_expression(e, config) for e in window.partitions])) \
868
- if window.partitions else ""
871
+ clauses: list[str] = []
869
872
 
870
- order_by = "ORDER BY " + (", ".join([extension.process_sort_item(o, config) for o in window.orderBy])) \
871
- if window.orderBy else ""
873
+ if window.partitions:
874
+ partition_clause = ", ".join(
875
+ extension.process_expression(expr, config)
876
+ for expr in window.partitions
877
+ )
878
+ clauses.append(f"PARTITION BY {partition_clause}")
879
+
880
+ if window.orderBy:
881
+ order_clause = ", ".join(
882
+ extension.process_sort_item(item, config)
883
+ for item in window.orderBy
884
+ )
885
+ clauses.append(f"ORDER BY {order_clause}")
872
886
 
873
- # TODO: Handle window frame
874
- return f"OVER ({partitions}{' ' if (partitions != '') and (order_by != '') else ''}{order_by}){''}"
887
+ if window.windowFrame:
888
+ frame_clause = extension.process_window_frame(window.windowFrame, config)
889
+ clauses.append(frame_clause)
890
+
891
+ return f"OVER ({' '.join(clauses)})"
875
892
 
876
893
 
877
894
  def table_function_processor(
@@ -901,6 +918,45 @@ def union_processor(
901
918
  return f"{left}{sep0}{union_str}{sep0}{right}"
902
919
 
903
920
 
921
+ def frame_bound_processor(
922
+ frame_bound: FrameBound,
923
+ extension: "SqlToStringDbExtension",
924
+ config: SqlToStringConfig,
925
+ ) -> str:
926
+ bound_sql = {
927
+ FrameBoundType.UNBOUNDED_PRECEDING: "UNBOUNDED PRECEDING",
928
+ FrameBoundType.PRECEDING: "PRECEDING",
929
+ FrameBoundType.FOLLOWING: "FOLLOWING",
930
+ FrameBoundType.CURRENT_ROW: "CURRENT ROW",
931
+ FrameBoundType.UNBOUNDED_FOLLOWING: "UNBOUNDED FOLLOWING",
932
+ }[frame_bound.type_]
933
+
934
+ if frame_bound.value is None:
935
+ return bound_sql
936
+
937
+ offset_expr = extension.process_expression(frame_bound.value, config)
938
+
939
+ offset_sql = (
940
+ f"INTERVAL '{offset_expr} {frame_bound.duration_unit.value}'"
941
+ if frame_bound.duration_unit
942
+ else offset_expr
943
+ )
944
+
945
+ return f"{offset_sql} {bound_sql}"
946
+
947
+
948
+ def window_frame_processor(
949
+ frame: WindowFrame,
950
+ extension: "SqlToStringDbExtension",
951
+ config: SqlToStringConfig,
952
+ ) -> str:
953
+ mode = "ROWS" if frame.mode == WindowFrameMode.ROWS else "RANGE"
954
+ start = extension.process_frame_bound(frame.start, config)
955
+ end = extension.process_frame_bound(frame.end, config) if frame.end else "UNBOUNDED FOLLOWING"
956
+
957
+ return f"{mode} BETWEEN {start} AND {end}"
958
+
959
+
904
960
  class SqlToStringDbExtension:
905
961
  @classmethod
906
962
  def reserved_keywords(cls) -> PyLegendList[str]:
@@ -1260,3 +1316,9 @@ class SqlToStringDbExtension:
1260
1316
 
1261
1317
  def process_union(self, union: Union, config: SqlToStringConfig, nested_subquery: bool = False) -> str:
1262
1318
  return union_processor(union, self, config, nested_subquery)
1319
+
1320
+ def process_window_frame(self, frame: WindowFrame, config: SqlToStringConfig) -> str:
1321
+ return window_frame_processor(frame, self, config)
1322
+
1323
+ def process_frame_bound(self, frame_bound: FrameBound, config: SqlToStringConfig) -> str:
1324
+ return frame_bound_processor(frame_bound, self, config)
@@ -27,12 +27,14 @@ from pylegend.core.language import (
27
27
  PyLegendColumnExpression,
28
28
  PyLegendExpressionIntegerReturn,
29
29
  PyLegendExpressionFloatReturn,
30
+ convert_literal_to_literal_expression,
30
31
  )
31
32
  from pylegend._typing import (
32
33
  PyLegendSequence,
33
34
  PyLegendOptional,
34
35
  PyLegendList,
35
36
  PyLegendDict,
37
+ PyLegendUnion,
36
38
  )
37
39
  from pylegend.core.language.shared.helpers import escape_column_name
38
40
  from pylegend.core.sql.metamodel import (
@@ -44,7 +46,13 @@ from pylegend.core.sql.metamodel import (
44
46
  SortItemNullOrdering,
45
47
  Window,
46
48
  FunctionCall,
47
- QualifiedName, IntegerLiteral
49
+ QualifiedName,
50
+ IntegerLiteral,
51
+ WindowFrame,
52
+ WindowFrameMode,
53
+ FrameBound,
54
+ FrameBoundType,
55
+ StringLiteral
48
56
  )
49
57
  from pylegend.core.tds.tds_frame import FrameToSqlConfig, FrameToPureConfig
50
58
  from typing import TYPE_CHECKING
@@ -64,6 +72,11 @@ __all__: PyLegendSequence[str] = [
64
72
  "LegendQLApiWindow",
65
73
  "LegendQLApiPartialFrame",
66
74
  "LegendQLApiWindowReference",
75
+ "LegendQLApiWindowFrameBound",
76
+ "LegendQLApiWindowFrameMode",
77
+ "LegendQLApiWindowFrame",
78
+ "LegendQLApiDurationUnit",
79
+ "LegendQLApiWindowFrameBoundType"
67
80
  ]
68
81
 
69
82
 
@@ -168,8 +181,174 @@ class LegendQLApiSortInfo:
168
181
  return f"{func}(~{escape_column_name(self.__column)})"
169
182
 
170
183
 
171
- class LegendQLApiWindowFrame(metaclass=ABCMeta):
172
- pass
184
+ class LegendQLApiWindowFrameMode(Enum):
185
+ ROWS = 1
186
+ RANGE = 2
187
+
188
+
189
+ class LegendQLApiWindowFrameBoundType(Enum):
190
+ UNBOUNDED_PRECEDING = 1
191
+ PRECEDING = 2
192
+ CURRENT_ROW = 3
193
+ FOLLOWING = 4
194
+ UNBOUNDED_FOLLOWING = 5
195
+
196
+ def to_sql_node(
197
+ self,
198
+ query: QuerySpecification,
199
+ config: FrameToSqlConfig,
200
+ ) -> FrameBoundType:
201
+ mapping = {
202
+ LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING: FrameBoundType.UNBOUNDED_PRECEDING,
203
+ LegendQLApiWindowFrameBoundType.PRECEDING: FrameBoundType.PRECEDING,
204
+ LegendQLApiWindowFrameBoundType.CURRENT_ROW: FrameBoundType.CURRENT_ROW,
205
+ LegendQLApiWindowFrameBoundType.FOLLOWING: FrameBoundType.FOLLOWING,
206
+ LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING: FrameBoundType.UNBOUNDED_FOLLOWING
207
+ }
208
+
209
+ return mapping[self]
210
+
211
+
212
+ class LegendQLApiDurationUnit(Enum):
213
+ YEARS = 1
214
+ MONTHS = 2
215
+ WEEKS = 3
216
+ DAYS = 4
217
+ HOURS = 5
218
+ MINUTES = 6
219
+ SECONDS = 7
220
+ MILLISECONDS = 8
221
+ MICROSECONDS = 9
222
+ NANOSECONDS = 10
223
+
224
+ def to_pure_expression(self, config: FrameToPureConfig) -> str:
225
+ return self.name
226
+
227
+ def to_sql_node(
228
+ self,
229
+ query: QuerySpecification,
230
+ config: FrameToSqlConfig
231
+ ) -> StringLiteral:
232
+ mapping = {
233
+ LegendQLApiDurationUnit.YEARS: "YEAR",
234
+ LegendQLApiDurationUnit.MONTHS: "MONTH",
235
+ LegendQLApiDurationUnit.WEEKS: "WEEK",
236
+ LegendQLApiDurationUnit.DAYS: "DAY",
237
+ LegendQLApiDurationUnit.HOURS: "HOUR",
238
+ LegendQLApiDurationUnit.MINUTES: "MINUTE",
239
+ LegendQLApiDurationUnit.SECONDS: "SECOND",
240
+ LegendQLApiDurationUnit.MILLISECONDS: "MILLISECOND",
241
+ LegendQLApiDurationUnit.MICROSECONDS: "MICROSECOND",
242
+ LegendQLApiDurationUnit.NANOSECONDS: "NANOSECOND",
243
+ }
244
+
245
+ return StringLiteral(mapping[self], quoted=False)
246
+
247
+ @classmethod
248
+ def from_string(cls, value: str) -> "LegendQLApiDurationUnit":
249
+ try:
250
+ return cls[value.upper()]
251
+ except KeyError:
252
+ raise ValueError(
253
+ f"Invalid duration unit '{value}'. "
254
+ f"Supported values: {[u.name.lower() for u in cls]}"
255
+ )
256
+
257
+
258
+ class LegendQLApiWindowFrameBound:
259
+ __bound_type: LegendQLApiWindowFrameBoundType
260
+ __row_offset: PyLegendOptional[PyLegendUnion[int, float]]
261
+ __duration_unit: PyLegendOptional[LegendQLApiDurationUnit]
262
+
263
+ def __init__(
264
+ self,
265
+ bound_type: LegendQLApiWindowFrameBoundType,
266
+ row_offset: PyLegendOptional[PyLegendUnion[int, float]] = None,
267
+ duration_unit: PyLegendOptional[LegendQLApiDurationUnit] = None,
268
+ ) -> None:
269
+ if bound_type in (
270
+ LegendQLApiWindowFrameBoundType.PRECEDING,
271
+ LegendQLApiWindowFrameBoundType.FOLLOWING,
272
+ ) and row_offset is None:
273
+ raise ValueError(f"row_offset must be provided for bound_type {bound_type.name}")
274
+
275
+ if bound_type not in (
276
+ LegendQLApiWindowFrameBoundType.PRECEDING,
277
+ LegendQLApiWindowFrameBoundType.FOLLOWING,
278
+ ) and row_offset is not None:
279
+ raise ValueError(f"row_offset is not allowed for bound_type {bound_type.name}")
280
+
281
+ self.__bound_type = bound_type
282
+ self.__row_offset = row_offset
283
+ self.__duration_unit = duration_unit
284
+
285
+ def to_pure_expression(self, config: FrameToPureConfig) -> str:
286
+ if (self.__bound_type == LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING
287
+ or self.__bound_type == LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING):
288
+ return "unbounded()"
289
+
290
+ elif self.__bound_type == LegendQLApiWindowFrameBoundType.CURRENT_ROW:
291
+ expr = "0"
292
+
293
+ else:
294
+ expr = convert_literal_to_literal_expression(self.__row_offset).to_pure_expression(
295
+ config) if self.__row_offset is not None else ""
296
+
297
+ if self.__duration_unit is not None:
298
+ expr += f", DurationUnit.{self.__duration_unit.to_pure_expression(config)}"
299
+
300
+ return expr
301
+
302
+ def to_sql_node(
303
+ self,
304
+ query: QuerySpecification,
305
+ config: FrameToSqlConfig,
306
+ ) -> FrameBound:
307
+ value = (convert_literal_to_literal_expression(abs(self.__row_offset))
308
+ .to_sql_expression({"w": query}, config)) \
309
+ if self.__row_offset is not None else None
310
+
311
+ frame_bound_type = self.__bound_type.to_sql_node(query, config)
312
+
313
+ duration_unit = self.__duration_unit.to_sql_node(
314
+ query,
315
+ config) if self.__duration_unit is not None else None
316
+
317
+ return FrameBound(frame_bound_type, value, duration_unit)
318
+
319
+
320
+ class LegendQLApiWindowFrame:
321
+ __mode: LegendQLApiWindowFrameMode
322
+ __start_bound: LegendQLApiWindowFrameBound
323
+ __end_bound: LegendQLApiWindowFrameBound
324
+
325
+ def __init__(
326
+ self,
327
+ mode: LegendQLApiWindowFrameMode,
328
+ start_bound: LegendQLApiWindowFrameBound,
329
+ end_bound: LegendQLApiWindowFrameBound,
330
+ ) -> None:
331
+ self.__mode = mode
332
+ self.__start_bound = start_bound
333
+ self.__end_bound = end_bound
334
+
335
+ def to_pure_expression(self, config: FrameToPureConfig) -> str:
336
+ mode_str = "rows" if self.__mode == LegendQLApiWindowFrameMode.ROWS else "_range"
337
+ start_expr = self.__start_bound.to_pure_expression(config)
338
+ end_expr = self.__end_bound.to_pure_expression(config)
339
+
340
+ return f"{mode_str}({start_expr}, {end_expr})"
341
+
342
+ def to_sql_node(
343
+ self,
344
+ query: QuerySpecification,
345
+ config: FrameToSqlConfig
346
+ ) -> WindowFrame:
347
+ return WindowFrame(
348
+ mode=WindowFrameMode.ROWS if self.__mode == LegendQLApiWindowFrameMode.ROWS else WindowFrameMode.RANGE,
349
+ start=self.__start_bound.to_sql_node(query, config),
350
+ end=self.__end_bound.to_sql_node(query, config),
351
+ )
173
352
 
174
353
 
175
354
  class LegendQLApiWindow:
@@ -211,7 +390,10 @@ class LegendQLApiWindow:
211
390
  [] if self.__order_by is None else
212
391
  [sort_info.to_sql_node(query, config) for sort_info in self.__order_by]
213
392
  ),
214
- windowFrame=None
393
+ windowFrame=(
394
+ None if self.__frame is None else
395
+ self.__frame.to_sql_node(query, config)
396
+ ),
215
397
  )
216
398
 
217
399
  @staticmethod
@@ -235,7 +417,10 @@ class LegendQLApiWindow:
235
417
  "[]" if self.__order_by is None or len(self.__order_by) == 0
236
418
  else "[" + (', '.join([s.to_pure_expression(config) for s in self.__order_by])) + "]"
237
419
  )
238
- return f"over({partitions_str}, {sorts_str})"
420
+
421
+ frame_str = f", {self.__frame.to_pure_expression(config)}" if self.__frame else ""
422
+
423
+ return f"over({partitions_str}, {sorts_str}{frame_str})"
239
424
 
240
425
 
241
426
  class LegendQLApiPartialFrame:
@@ -77,6 +77,9 @@ class Series(PyLegendColumnExpression, PyLegendPrimitive, BaseTdsFrame):
77
77
  def value(self) -> PyLegendColumnExpression:
78
78
  return self
79
79
 
80
+ def get_base_frame(self) -> "PandasApiTdsFrame":
81
+ return self.__base_frame
82
+
80
83
  def to_sql_expression(
81
84
  self,
82
85
  frame_name_to_base_query_map: PyLegendDict[str, QuerySpecification],
@@ -918,12 +918,15 @@ class WindowFrame(Node):
918
918
  class FrameBound(Node):
919
919
  type_: "FrameBoundType"
920
920
  value: "PyLegendOptional[Expression]"
921
+ duration_unit: "PyLegendOptional[StringLiteral]"
921
922
 
922
923
  def __init__(
923
924
  self,
924
925
  type_: "FrameBoundType",
925
- value: "PyLegendOptional[Expression]"
926
+ value: "PyLegendOptional[Expression]",
927
+ duration_unit: "PyLegendOptional[StringLiteral]" = None
926
928
  ) -> None:
927
929
  super().__init__(_type="frameBound")
928
930
  self.type_ = type_
929
931
  self.value = value
932
+ self.duration_unit = duration_unit
@@ -0,0 +1,115 @@
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
+ PyLegendOptional,
20
+ PyLegendCallable
21
+ )
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
+ )
27
+ from pylegend.core.tds.tds_column import TdsColumn
28
+ from pylegend.core.tds.tds_frame import FrameToSqlConfig
29
+ from pylegend.core.tds.tds_frame import FrameToPureConfig
30
+ from pylegend.core.tds.legendql_api.frames.legendql_api_base_tds_frame import LegendQLApiBaseTdsFrame
31
+ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import LegendQLApiPrimitive
32
+ from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
33
+ from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import infer_columns_from_frame
34
+ from pylegend.core.language.shared.helpers import escape_column_name
35
+
36
+ __all__: PyLegendSequence[str] = [
37
+ "LegendQLApiDistinctFunction"
38
+ ]
39
+
40
+
41
+ class LegendQLApiDistinctFunction(LegendQLApiAppliedFunction):
42
+ __base_frame: LegendQLApiBaseTdsFrame
43
+ __column_name_list: PyLegendOptional[PyLegendList[str]]
44
+
45
+ @classmethod
46
+ def name(cls) -> str:
47
+ return "distinct"
48
+
49
+ def __init__(
50
+ self,
51
+ base_frame: LegendQLApiBaseTdsFrame,
52
+ columns: PyLegendOptional[PyLegendUnion[
53
+ str,
54
+ PyLegendList[str],
55
+ PyLegendCallable[
56
+ [LegendQLApiTdsRow],
57
+ PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
58
+ ]
59
+ ]] = None
60
+ ) -> None:
61
+ self.__base_frame = base_frame
62
+ self.__column_name_list = infer_columns_from_frame(base_frame, columns,
63
+ "'distinct' function 'columns'") if columns is not None else None
64
+
65
+ def to_sql(self, config: FrameToSqlConfig) -> QuerySpecification:
66
+ base_query = self.__base_frame.to_sql_query_object(config)
67
+ should_create_sub_query = (base_query.offset is not None) or (base_query.limit is not None) or (
68
+ self.__column_name_list is not None)
69
+
70
+ quoted_columns = None
71
+ if self.__column_name_list is not None:
72
+ db_extension = config.sql_to_string_generator().get_db_extension()
73
+ quoted_columns = [
74
+ db_extension.quote_identifier(col)
75
+ for col in self.__column_name_list
76
+ ]
77
+
78
+ new_query = (
79
+ create_sub_query(base_query, config, "root", quoted_columns) if should_create_sub_query else
80
+ copy_query(base_query)
81
+ )
82
+ new_query.select.distinct = True
83
+
84
+ return new_query
85
+
86
+ def to_pure(self, config: FrameToPureConfig) -> str:
87
+ columns_expr = (
88
+ f"~[{', '.join(map(escape_column_name, self.__column_name_list))}]"
89
+ if self.__column_name_list else ""
90
+ )
91
+ return (f"{self.__base_frame.to_pure(config)}{config.separator(1)}"
92
+ f"->distinct({columns_expr})")
93
+
94
+ def base_frame(self) -> LegendQLApiBaseTdsFrame:
95
+ return self.__base_frame
96
+
97
+ def tds_frame_parameters(self) -> PyLegendList[LegendQLApiBaseTdsFrame]:
98
+ return []
99
+
100
+ def calculate_columns(self) -> PyLegendSequence[TdsColumn]:
101
+ if not self.__column_name_list:
102
+ return [c.copy() for c in self.__base_frame.columns()]
103
+
104
+ base_columns = self.__base_frame.columns()
105
+ new_columns = []
106
+ for name in self.__column_name_list:
107
+ for col in base_columns:
108
+ if col.get_name() == name:
109
+ new_columns.append(col.copy())
110
+ break
111
+
112
+ return new_columns
113
+
114
+ def validate(self) -> bool:
115
+ return True
@@ -30,6 +30,11 @@ from pylegend.core.language.legendql_api.legendql_api_custom_expressions import
30
30
  LegendQLApiWindow,
31
31
  LegendQLApiPartialFrame,
32
32
  LegendQLApiWindowReference,
33
+ LegendQLApiWindowFrame,
34
+ LegendQLApiWindowFrameMode,
35
+ LegendQLApiWindowFrameBound,
36
+ LegendQLApiWindowFrameBoundType,
37
+ LegendQLApiDurationUnit
33
38
  )
34
39
  from pylegend.core.language.legendql_api.legendql_api_tds_row import LegendQLApiTdsRow
35
40
  from pylegend.core.tds.abstract.frames.base_tds_frame import BaseTdsFrame
@@ -59,14 +64,23 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
59
64
  def limit(self, row_count: int = 5) -> "LegendQLApiTdsFrame":
60
65
  return self.head(row_count=row_count)
61
66
 
62
- def distinct(self) -> "LegendQLApiTdsFrame":
67
+ def distinct(
68
+ self,
69
+ columns: PyLegendOptional[PyLegendUnion[
70
+ str,
71
+ PyLegendList[str],
72
+ PyLegendCallable[
73
+ [LegendQLApiTdsRow],
74
+ PyLegendUnion[LegendQLApiPrimitive, PyLegendList[LegendQLApiPrimitive]]
75
+ ]
76
+ ]] = None) -> "LegendQLApiTdsFrame":
63
77
  from pylegend.core.tds.legendql_api.frames.legendql_api_applied_function_tds_frame import (
64
78
  LegendQLApiAppliedFunctionTdsFrame
65
79
  )
66
80
  from pylegend.core.tds.legendql_api.frames.functions.legendql_api_distinct_function import (
67
81
  LegendQLApiDistinctFunction
68
82
  )
69
- return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self))
83
+ return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiDistinctFunction(self, columns))
70
84
 
71
85
  def select(
72
86
  self,
@@ -304,6 +318,80 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
304
318
  LegendQLApiGroupByFunction(self, grouping_columns, aggregate_specifications)
305
319
  )
306
320
 
321
+ def rows(
322
+ self,
323
+ start: PyLegendUnion[str, int],
324
+ end: PyLegendUnion[str, int]) -> LegendQLApiWindowFrame:
325
+ return LegendQLApiWindowFrame(
326
+ LegendQLApiWindowFrameMode.ROWS,
327
+ _infer_window_frame_bound(start, is_start_bound=True),
328
+ _infer_window_frame_bound(end)
329
+ )
330
+
331
+ def range(
332
+ self,
333
+ *,
334
+ number_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
335
+ number_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
336
+ duration_start: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
337
+ duration_start_unit: PyLegendOptional[str] = None,
338
+ duration_end: PyLegendOptional[PyLegendUnion[str, int, float]] = None,
339
+ duration_end_unit: PyLegendOptional[str] = None) -> LegendQLApiWindowFrame:
340
+
341
+ has_number = number_start is not None or number_end is not None
342
+ has_duration = any([
343
+ duration_start is not None,
344
+ duration_end is not None,
345
+ duration_start_unit is not None,
346
+ duration_end_unit is not None,
347
+ ])
348
+
349
+ if not has_number and not has_duration:
350
+ raise ValueError(
351
+ "Either numeric range or duration range must be provided. "
352
+ "Specify number_start and number_end, or duration_start and duration_end "
353
+ "(with duration_start_unit and duration_end_unit as needed)."
354
+ )
355
+
356
+ if has_number and has_duration:
357
+ raise ValueError(
358
+ "Numeric range and duration range cannot be used together. "
359
+ "Use either (number_start, number_end) or (duration_start, duration_end)."
360
+ "(with duration_start_unit and duration_end_unit as needed)."
361
+ )
362
+
363
+ if has_number:
364
+ if number_start is None or number_end is None:
365
+ raise ValueError(
366
+ "Both number_start and number_end must be provided together."
367
+ )
368
+
369
+ return LegendQLApiWindowFrame(
370
+ LegendQLApiWindowFrameMode.RANGE,
371
+ _infer_window_frame_bound(number_start, is_start_bound=True),
372
+ _infer_window_frame_bound(number_end),
373
+ )
374
+
375
+ if duration_start is None or duration_end is None:
376
+ raise ValueError(
377
+ "Both duration_start and duration_end must be provided."
378
+ "(with duration_start_unit and duration_end_unit as needed).")
379
+
380
+ def is_unbounded(value: object) -> bool:
381
+ return isinstance(value, str) and value.lower() == "unbounded"
382
+
383
+ if not is_unbounded(duration_start) and duration_start_unit is None:
384
+ raise ValueError("duration_start_unit is required for bounded duration_start.")
385
+
386
+ if not is_unbounded(duration_end) and duration_end_unit is None:
387
+ raise ValueError("duration_end_unit is required for bounded duration_end.")
388
+
389
+ return LegendQLApiWindowFrame(
390
+ LegendQLApiWindowFrameMode.RANGE,
391
+ _infer_window_frame_bound(duration_start, is_start_bound=True, duration_unit=duration_start_unit),
392
+ _infer_window_frame_bound(duration_end, duration_unit=duration_end_unit)
393
+ )
394
+
307
395
  def window(
308
396
  self,
309
397
  partition_by: PyLegendOptional[
@@ -329,7 +417,8 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
329
417
  ]
330
418
  ]
331
419
  ]
332
- ] = None
420
+ ] = None,
421
+ frame: PyLegendOptional[LegendQLApiWindowFrame] = None
333
422
  ) -> "LegendQLApiWindow":
334
423
  from pylegend.core.tds.legendql_api.frames.functions.legendql_api_function_helpers import (
335
424
  infer_columns_from_frame,
@@ -344,7 +433,7 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
344
433
  None if order_by is None else
345
434
  infer_sorts_from_frame(self, order_by, "'window' function order_by")
346
435
  ),
347
- frame=None
436
+ frame=frame
348
437
  )
349
438
 
350
439
  def window_extend(
@@ -417,3 +506,56 @@ class LegendQLApiBaseTdsFrame(LegendQLApiTdsFrame, BaseTdsFrame, metaclass=ABCMe
417
506
  LegendQLApiProjectFunction
418
507
  )
419
508
  return LegendQLApiAppliedFunctionTdsFrame(LegendQLApiProjectFunction(self, project_columns))
509
+
510
+
511
+ def _infer_window_frame_bound(
512
+ value: PyLegendOptional[
513
+ PyLegendUnion[str, int, float]
514
+ ] = None,
515
+ *,
516
+ is_start_bound: bool = False,
517
+ duration_unit: PyLegendOptional[str] = None,
518
+ ) -> LegendQLApiWindowFrameBound:
519
+ if isinstance(value, str):
520
+ if value.lower() != "unbounded":
521
+ raise ValueError(
522
+ f"Invalid window frame boundary '{value}'. "
523
+ "Only 'unbounded' is supported as a string. "
524
+ "Otherwise, provide a numeric offset where "
525
+ "positive = FOLLOWING, negative = PRECEDING, "
526
+ "and 0 = CURRENT ROW."
527
+ )
528
+
529
+ bound_type = (
530
+ LegendQLApiWindowFrameBoundType.UNBOUNDED_PRECEDING
531
+ if is_start_bound
532
+ else LegendQLApiWindowFrameBoundType.UNBOUNDED_FOLLOWING
533
+ )
534
+
535
+ return LegendQLApiWindowFrameBound(bound_type)
536
+
537
+ if not isinstance(value, (int, float)):
538
+ raise TypeError(
539
+ f"Invalid type for window frame boundary: {type(value).__name__}. "
540
+ "Expected 'unbounded' (str) or numeric offset (int | float)."
541
+ )
542
+
543
+ duration_unit_enum = (
544
+ LegendQLApiDurationUnit.from_string(duration_unit)
545
+ if duration_unit
546
+ else None
547
+ )
548
+
549
+ if value == 0:
550
+ return LegendQLApiWindowFrameBound(
551
+ LegendQLApiWindowFrameBoundType.CURRENT_ROW,
552
+ row_offset=None,
553
+ duration_unit=duration_unit_enum
554
+ )
555
+
556
+ if value > 0:
557
+ bound_type = LegendQLApiWindowFrameBoundType.FOLLOWING
558
+ else:
559
+ bound_type = LegendQLApiWindowFrameBoundType.PRECEDING
560
+
561
+ return LegendQLApiWindowFrameBound(bound_type, value, duration_unit_enum)