analytics-toolkit 1.3.7.3__tar.gz → 1.3.8.2__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 (154) hide show
  1. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/PKG-INFO +106 -8
  2. analytics_toolkit-1.3.8.2/README.md +153 -0
  3. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/api.py +42 -2
  4. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/cuped.py +70 -11
  5. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/metrics.py +4 -0
  6. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/parallel.py +41 -2
  7. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/rows.py +200 -0
  8. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/cli.py +34 -1
  9. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/__init__.py +19 -58
  10. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/base.py +5 -1
  11. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/clickhouse.py +3 -1
  12. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/__init__.py +2 -2
  13. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/lifecycle.py +33 -15
  14. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/options.py +1 -24
  15. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/__init__.py +1 -6
  16. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/get_sql_connection.py +3 -26
  17. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/__init__.py +0 -4
  18. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/ddl/api.py +635 -0
  19. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/models.py +3 -6
  20. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/__init__.py +6 -10
  21. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/__init__.py +2 -2
  22. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/io/cancel_queries.py +211 -0
  23. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_read.py +4 -4
  24. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/execute_sql.py +4 -4
  25. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/read_sql.py +8 -8
  26. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_df.py +20 -33
  27. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/models.py +0 -1
  28. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/stage.py +7 -6
  29. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/__init__.py +6 -19
  30. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/_basic_ops.py +5 -0
  31. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/api.py +3 -4
  32. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/ch_create_table_as.py +73 -69
  33. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/create_table_from_sql.py +33 -20
  34. analytics_toolkit-1.3.8.2/analytics_toolkit/sql/dml/table/drop_tables.py +411 -0
  35. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/maintenance.py +13 -5
  36. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/models.py +4 -23
  37. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/partitions.py +34 -21
  38. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/write_modes.py +11 -11
  39. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/api.py +3 -15
  40. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/finalize.py +0 -3
  41. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/models.py +0 -1
  42. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/tasks.py +21 -0
  43. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/PKG-INFO +106 -8
  44. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/SOURCES.txt +5 -6
  45. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/pyproject.toml +1 -1
  46. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_metrics.py +161 -0
  47. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_parallel.py +172 -7
  48. analytics_toolkit-1.3.8.2/tests/test_rag_docs.py +264 -0
  49. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_architecture.py +89 -12
  50. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_async.py +204 -69
  51. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_backend_adapters.py +26 -14
  52. analytics_toolkit-1.3.8.2/tests/test_sql_cancel_queries.py +363 -0
  53. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_ch_create_table_as.py +48 -1
  54. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_connection_config.py +53 -60
  55. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_create_table_from_sql.py +117 -2
  56. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_drop_many_partitions.py +39 -75
  57. analytics_toolkit-1.3.7.3/tests/test_sql_ch_drop_table.py → analytics_toolkit-1.3.8.2/tests/test_sql_drop_tables.py +96 -44
  58. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_gp_create_partitions.py +101 -85
  59. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_improvements.py +115 -67
  60. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_load_table.py +26 -24
  61. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_parallel.py +179 -48
  62. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_retries.py +9 -2
  63. analytics_toolkit-1.3.7.3/README.md +0 -55
  64. analytics_toolkit-1.3.7.3/analytics_toolkit/sql/ddl/api.py +0 -246
  65. analytics_toolkit-1.3.7.3/analytics_toolkit/sql/dml/io/gp_cancel.py +0 -83
  66. analytics_toolkit-1.3.7.3/analytics_toolkit/sql/dml/table/ch_drop_table.py +0 -214
  67. analytics_toolkit-1.3.7.3/analytics_toolkit/sql/dml/table/ch_full_table_move.py +0 -1096
  68. analytics_toolkit-1.3.7.3/tests/test_sql_ch_full_table_move.py +0 -463
  69. analytics_toolkit-1.3.7.3/tests/test_sql_gp_cancel.py +0 -223
  70. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/LICENSE +0 -0
  71. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/__init__.py +0 -0
  72. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/__init__.py +0 -0
  73. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/bootstrap.py +0 -0
  74. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/constants.py +0 -0
  75. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/formatter.py +0 -0
  76. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/outliers.py +0 -0
  77. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/planning.py +0 -0
  78. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/ratio.py +0 -0
  79. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/split.py +0 -0
  80. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/stats.py +0 -0
  81. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/ab_utils/validation.py +0 -0
  82. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/__init__.py +0 -0
  83. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/dates.py +0 -0
  84. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/dates/functions.py +0 -0
  85. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/__init__.py +0 -0
  86. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/excel/long_format.py +0 -0
  87. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/__init__.py +0 -0
  88. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/logging.py +0 -0
  89. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/general/read_file.py +0 -0
  90. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/__init__.py +0 -0
  91. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/dbapi.py +0 -0
  92. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/gp.py +0 -0
  93. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/registry.py +0 -0
  94. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/trino.py +0 -0
  95. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/_backend_adapters/utils.py +0 -0
  96. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/backend_adapters.py +0 -0
  97. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/clickhouse/wait.py +0 -0
  98. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/config.py +0 -0
  99. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/errors.py +0 -0
  100. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/connection/protocols.py +0 -0
  101. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/__init__.py +0 -0
  102. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/capabilities.py +0 -0
  103. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/identifiers.py +0 -0
  104. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/core/types.py +0 -0
  105. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/builders.py +0 -0
  106. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/clickhouse.py +0 -0
  107. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/extract_ddl.py +0 -0
  108. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/identifiers.py +0 -0
  109. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/ddl/schema.py +0 -0
  110. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/io/models.py +0 -0
  111. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/__init__.py +0 -0
  112. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/load/load_sql_table.py +0 -0
  113. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/table/table_validation.py +0 -0
  114. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/__init__.py +0 -0
  115. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/__init__.py +0 -0
  116. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/attempt.py +0 -0
  117. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/estimate.py +0 -0
  118. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/flow/stage.py +0 -0
  119. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/__init__.py +0 -0
  120. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/io/source.py +0 -0
  121. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/__init__.py +0 -0
  122. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/runtime/retry.py +0 -0
  123. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/dml/transfer/schema.py +0 -0
  124. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/__init__.py +0 -0
  125. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/labels.py +0 -0
  126. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/operation_runner.py +0 -0
  127. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plan_steps.py +0 -0
  128. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/plans.py +0 -0
  129. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/execution/query_timing.py +0 -0
  130. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/__init__.py +0 -0
  131. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/show_tables.py +0 -0
  132. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/metadata/table_info.py +0 -0
  133. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/__init__.py +0 -0
  134. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/async_sql.py +0 -0
  135. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/parallel_sql.py +0 -0
  136. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit/sql/orchestration/pipeline.py +0 -0
  137. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/dependency_links.txt +0 -0
  138. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/entry_points.txt +0 -0
  139. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/requires.txt +0 -0
  140. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/analytics_toolkit.egg-info/top_level.txt +0 -0
  141. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/setup.cfg +0 -0
  142. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_format.py +0 -0
  143. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_ab_utils_split.py +0 -0
  144. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_dates.py +0 -0
  145. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_excel_long_format.py +0 -0
  146. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_general_logging.py +0 -0
  147. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_general_read_file.py +0 -0
  148. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_execute_read.py +0 -0
  149. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_extract_ddl.py +0 -0
  150. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_show_tables.py +0 -0
  151. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_clickhouse.py +0 -0
  152. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_row_batches.py +0 -0
  153. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_schema.py +0 -0
  154. {analytics_toolkit-1.3.7.3 → analytics_toolkit-1.3.8.2}/tests/test_sql_transfer_source_batches.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: analytics-toolkit
3
- Version: 1.3.7.3
3
+ Version: 1.3.8.2
4
4
  Summary: Shared Python utilities for SQL, Excel, and date helpers.
5
5
  Author: analytics_toolkit contributors
6
6
  License: MIT License
@@ -75,7 +75,7 @@ Python toolkit for AB-test analysis, SQL workflows, Excel reports, and date help
75
75
 
76
76
  [Documentation Overview](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/README.md)
77
77
 
78
- **Version:** `1.3.7.3`<br>
78
+ **Version:** `1.3.8.2`<br>
79
79
  **Depends:** Python (`>=3.8,<3.15`)<br>
80
80
  **Imports:** [clickhouse-connect](https://pypi.org/project/clickhouse-connect/) (`>=0.5.14,<1`), [lz4](https://pypi.org/project/lz4/) (`>=4.3.2,<5`), [numpy](https://pypi.org/project/numpy/) (`>=1.24.2,<2`), [openpyxl](https://pypi.org/project/openpyxl/) (`>=3.1.1,<4`), [orjson](https://pypi.org/project/orjson/) (`>=3.8.7,<4`), [pandas](https://pypi.org/project/pandas/) (`>=1.4.4,<3`), [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) (`>=2.9.5,<3`), [python-dateutil](https://pypi.org/project/python-dateutil/) (`>=2.8.2,<3`), [pytz](https://pypi.org/project/pytz/) (`>=2022.7`), [requests](https://pypi.org/project/requests/) (`>=2.28.2,<3`), [scipy](https://pypi.org/project/scipy/) (`>=1.10.1,<2`), [sqlglot](https://pypi.org/project/sqlglot/) (`>=20,<31`), [sqlparse](https://pypi.org/project/sqlparse/) (`>=0.4.3,<1`), [tqdm](https://pypi.org/project/tqdm/) (`>=4.65.0,<5`), [trino](https://pypi.org/project/trino/) (`>=0.320,<1`), [zstandard](https://pypi.org/project/zstandard/) (`>=0.20.0,<1`)<br>
81
81
  **Suggests:** [apache-airflow](https://pypi.org/project/apache-airflow/) (`>=2.4,<3`; optional extra `airflow`)<br>
@@ -107,13 +107,111 @@ pip install git+https://github.com/Karapsin/analytics_toolkit.git
107
107
  - `analytics_toolkit.dates`: date and period helpers.
108
108
  - `analytics_toolkit.general`: shared logging and file path helpers.
109
109
 
110
- ## SQL Import Policy
110
+ ## SQL Workflows
111
111
 
112
- Supported SQL imports are `from analytics_toolkit import sql` or
113
- `import analytics_toolkit.sql as sql`. Deep imports under
114
- `analytics_toolkit.sql.*` are internal implementation details and may change;
115
- call SQL helpers through the `sql` facade, for example `sql.create_sql_table(...)`
116
- or `sql.transfer(...)`.
112
+ [All SQL functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/sql/functions/index.md)
113
+
114
+ [SQL module guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/sql/index.md)
115
+
116
+ `sql.transfer` streams query results between configured SQL backends, with
117
+ batching, retries, and table creation or replacement handled by one call.
118
+
119
+ ```python
120
+ from analytics_toolkit import sql
121
+
122
+ rows = sql.transfer(
123
+ from_db="trino",
124
+ to_db="gp",
125
+ from_sql="select user_id, order_id, amount from iceberg.analytics.orders",
126
+ to_table="sandbox.orders_copy",
127
+ write_mode="replace",
128
+ batch_size=50_000,
129
+ progress=True,
130
+ )
131
+ ```
132
+
133
+ Aliases can point to the same backend type, so Greenplum-to-Greenplum transfers
134
+ work the same way.
135
+
136
+ ```python
137
+ rows = sql.transfer(
138
+ from_db="gp_sales",
139
+ to_db="gp_finance",
140
+ from_sql="select user_id, order_id, amount from mart.sales_orders",
141
+ to_table="finance.sales_orders_copy",
142
+ write_mode="replace",
143
+ batch_size=50_000,
144
+ )
145
+ ```
146
+
147
+ - `sql.read`: run a query and return a dataframe.
148
+ - `sql.execute`: run DDL or DML without returning a dataframe.
149
+ - `sql.execute_read`: run setup SQL and return the final result as a dataframe.
150
+ - `sql.load_df`: load a pandas dataframe into a configured backend table.
151
+ - `sql.transfer`: move rows from a source query to a target table across backends.
152
+
153
+ ## AB Metrics
154
+
155
+ [All AB functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/ab_utils/functions/index.md)
156
+
157
+ [AB utilities guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/ab_utils/index.md)
158
+
159
+ `compute_test_metrics` compares experiment groups across mean and ratio metrics,
160
+ with optional CUPED statistics and bootstrap multiple-comparison adjustment.
161
+
162
+ ```python
163
+ from analytics_toolkit.ab_utils import compute_test_metrics
164
+
165
+ result = compute_test_metrics(
166
+ experiment_df,
167
+ group="group_name",
168
+ control="control",
169
+ user_id="user_id",
170
+ ratio_metrics=[
171
+ {"name": "ctr", "numerator": "clicks", "denominator": "views"},
172
+ ],
173
+ pre_exp_metrics_df=pre_experiment_df,
174
+ multiple_comparisons_adjustment=True,
175
+ multiple_comparisons_adjustment_resamples=1000,
176
+ )
177
+ ```
178
+
179
+ Example output with CUPED and bootstrap columns enabled:
180
+
181
+ | metric_type | group_1 | group_2 | metric_name | n0 | n1 | outliers_cutoff | outliers_n_control | outliers_n_test | metric_control | metric_test | variance_control | variance_test | delta_abs | delta_relative | mde_abs | mde_relative | s.e. | p-value | s.e. CUPED | p-value CUPED | mde_abs CUPED | mde_relative CUPED | s.e. bootstrap | bootstrap_adj_p |
182
+ | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
183
+ | mean | test | control | revenue | 10000 | 10050 | 250.0 | 3 | 4 | 12.40 | 13.10 | 45.20 | 47.80 | 0.70 | 0.056 | 0.42 | 0.034 | 0.15 | 0.003 | 0.11 | 0.001 | 0.31 | 0.025 | 0.14 | 0.012 |
184
+ | ratio | test | control | ctr | 10000 | 10050 | 1.0 | 0 | 0 | 0.082 | 0.087 | 0.0009 | 0.0010 | 0.005 | 0.061 | 0.003 | 0.037 | 0.001 | 0.008 | 0.001 | 0.006 | 0.002 | 0.024 | 0.001 | 0.019 |
185
+
186
+ ## Date Helpers
187
+
188
+ [All date functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/dates/functions/index.md)
189
+
190
+ [Date helpers guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/dates/index.md)
191
+
192
+ Date helpers cover reporting ranges, period boundaries, offsets, and stable
193
+ string formatting for SQL and filenames.
194
+
195
+ ```python
196
+ from analytics_toolkit.dates import add_days, first_day, gen_dates_list, last_day
197
+
198
+ report_days = gen_dates_list("2026-06-01", "2026-06-07")
199
+ # ["2026-06-01", "2026-06-02", "2026-06-03", "2026-06-04", "2026-06-05", "2026-06-06", "2026-06-07"]
200
+
201
+ month_start = first_day("2026-06-08", "month")
202
+ # "2026-06-01"
203
+
204
+ month_end = last_day("2026-06-08", "month")
205
+ # "2026-06-30"
206
+
207
+ next_run = add_days("2026-06-08", 1)
208
+ # "2026-06-09"
209
+ ```
210
+
211
+ - `gen_dates_list`: build daily, weekly, monthly, or quarterly sequences.
212
+ - `first_day` / `last_day`: get week, month, or quarter boundaries.
213
+ - `add_days`, `add_weeks`, `add_months`, `add_quarters`: shift dates.
214
+ - `sanitize_date`: convert a date to compact `YYYYMMDD` text.
117
215
 
118
216
  ## Documentation
119
217
 
@@ -0,0 +1,153 @@
1
+ # analytics_toolkit
2
+
3
+ Python toolkit for AB-test analysis, SQL workflows, Excel reports, and date helpers.
4
+
5
+ [Quick Start Guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/QUICK_START.md)
6
+
7
+ [Documentation Overview](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/README.md)
8
+
9
+ **Version:** `1.3.8.2`<br>
10
+ **Depends:** Python (`>=3.8,<3.15`)<br>
11
+ **Imports:** [clickhouse-connect](https://pypi.org/project/clickhouse-connect/) (`>=0.5.14,<1`), [lz4](https://pypi.org/project/lz4/) (`>=4.3.2,<5`), [numpy](https://pypi.org/project/numpy/) (`>=1.24.2,<2`), [openpyxl](https://pypi.org/project/openpyxl/) (`>=3.1.1,<4`), [orjson](https://pypi.org/project/orjson/) (`>=3.8.7,<4`), [pandas](https://pypi.org/project/pandas/) (`>=1.4.4,<3`), [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) (`>=2.9.5,<3`), [python-dateutil](https://pypi.org/project/python-dateutil/) (`>=2.8.2,<3`), [pytz](https://pypi.org/project/pytz/) (`>=2022.7`), [requests](https://pypi.org/project/requests/) (`>=2.28.2,<3`), [scipy](https://pypi.org/project/scipy/) (`>=1.10.1,<2`), [sqlglot](https://pypi.org/project/sqlglot/) (`>=20,<31`), [sqlparse](https://pypi.org/project/sqlparse/) (`>=0.4.3,<1`), [tqdm](https://pypi.org/project/tqdm/) (`>=4.65.0,<5`), [trino](https://pypi.org/project/trino/) (`>=0.320,<1`), [zstandard](https://pypi.org/project/zstandard/) (`>=0.20.0,<1`)<br>
12
+ **Suggests:** [apache-airflow](https://pypi.org/project/apache-airflow/) (`>=2.4,<3`; optional extra `airflow`)<br>
13
+ **Install:** `pip install analytics-toolkit`<br>
14
+ **PyPI:** [pypi.org/project/analytics-toolkit](https://pypi.org/project/analytics-toolkit/)<br>
15
+ **License:** MIT<br>
16
+ **Source:** [github.com/Karapsin/analytics_toolkit](https://github.com/Karapsin/analytics_toolkit)<br>
17
+ **Issues:** [GitHub Issues](https://github.com/Karapsin/analytics_toolkit/issues)
18
+
19
+ ## Installation
20
+
21
+ From PyPI:
22
+
23
+ ```bash
24
+ pip install analytics-toolkit
25
+ ```
26
+
27
+ From GitHub:
28
+
29
+ ```bash
30
+ pip install git+https://github.com/Karapsin/analytics_toolkit.git
31
+ ```
32
+
33
+ ## Areas
34
+
35
+ - `analytics_toolkit.ab_utils`: AB-test metric comparison helpers.
36
+ - `analytics_toolkit.sql`: SQL read, execute, load, and transfer helpers.
37
+ - `analytics_toolkit.excel`: Excel report helpers for long-format dataframes.
38
+ - `analytics_toolkit.dates`: date and period helpers.
39
+ - `analytics_toolkit.general`: shared logging and file path helpers.
40
+
41
+ ## SQL Workflows
42
+
43
+ [All SQL functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/sql/functions/index.md)
44
+
45
+ [SQL module guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/sql/index.md)
46
+
47
+ `sql.transfer` streams query results between configured SQL backends, with
48
+ batching, retries, and table creation or replacement handled by one call.
49
+
50
+ ```python
51
+ from analytics_toolkit import sql
52
+
53
+ rows = sql.transfer(
54
+ from_db="trino",
55
+ to_db="gp",
56
+ from_sql="select user_id, order_id, amount from iceberg.analytics.orders",
57
+ to_table="sandbox.orders_copy",
58
+ write_mode="replace",
59
+ batch_size=50_000,
60
+ progress=True,
61
+ )
62
+ ```
63
+
64
+ Aliases can point to the same backend type, so Greenplum-to-Greenplum transfers
65
+ work the same way.
66
+
67
+ ```python
68
+ rows = sql.transfer(
69
+ from_db="gp_sales",
70
+ to_db="gp_finance",
71
+ from_sql="select user_id, order_id, amount from mart.sales_orders",
72
+ to_table="finance.sales_orders_copy",
73
+ write_mode="replace",
74
+ batch_size=50_000,
75
+ )
76
+ ```
77
+
78
+ - `sql.read`: run a query and return a dataframe.
79
+ - `sql.execute`: run DDL or DML without returning a dataframe.
80
+ - `sql.execute_read`: run setup SQL and return the final result as a dataframe.
81
+ - `sql.load_df`: load a pandas dataframe into a configured backend table.
82
+ - `sql.transfer`: move rows from a source query to a target table across backends.
83
+
84
+ ## AB Metrics
85
+
86
+ [All AB functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/ab_utils/functions/index.md)
87
+
88
+ [AB utilities guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/ab_utils/index.md)
89
+
90
+ `compute_test_metrics` compares experiment groups across mean and ratio metrics,
91
+ with optional CUPED statistics and bootstrap multiple-comparison adjustment.
92
+
93
+ ```python
94
+ from analytics_toolkit.ab_utils import compute_test_metrics
95
+
96
+ result = compute_test_metrics(
97
+ experiment_df,
98
+ group="group_name",
99
+ control="control",
100
+ user_id="user_id",
101
+ ratio_metrics=[
102
+ {"name": "ctr", "numerator": "clicks", "denominator": "views"},
103
+ ],
104
+ pre_exp_metrics_df=pre_experiment_df,
105
+ multiple_comparisons_adjustment=True,
106
+ multiple_comparisons_adjustment_resamples=1000,
107
+ )
108
+ ```
109
+
110
+ Example output with CUPED and bootstrap columns enabled:
111
+
112
+ | metric_type | group_1 | group_2 | metric_name | n0 | n1 | outliers_cutoff | outliers_n_control | outliers_n_test | metric_control | metric_test | variance_control | variance_test | delta_abs | delta_relative | mde_abs | mde_relative | s.e. | p-value | s.e. CUPED | p-value CUPED | mde_abs CUPED | mde_relative CUPED | s.e. bootstrap | bootstrap_adj_p |
113
+ | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
114
+ | mean | test | control | revenue | 10000 | 10050 | 250.0 | 3 | 4 | 12.40 | 13.10 | 45.20 | 47.80 | 0.70 | 0.056 | 0.42 | 0.034 | 0.15 | 0.003 | 0.11 | 0.001 | 0.31 | 0.025 | 0.14 | 0.012 |
115
+ | ratio | test | control | ctr | 10000 | 10050 | 1.0 | 0 | 0 | 0.082 | 0.087 | 0.0009 | 0.0010 | 0.005 | 0.061 | 0.003 | 0.037 | 0.001 | 0.008 | 0.001 | 0.006 | 0.002 | 0.024 | 0.001 | 0.019 |
116
+
117
+ ## Date Helpers
118
+
119
+ [All date functions](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/dates/functions/index.md)
120
+
121
+ [Date helpers guide](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/dates/index.md)
122
+
123
+ Date helpers cover reporting ranges, period boundaries, offsets, and stable
124
+ string formatting for SQL and filenames.
125
+
126
+ ```python
127
+ from analytics_toolkit.dates import add_days, first_day, gen_dates_list, last_day
128
+
129
+ report_days = gen_dates_list("2026-06-01", "2026-06-07")
130
+ # ["2026-06-01", "2026-06-02", "2026-06-03", "2026-06-04", "2026-06-05", "2026-06-06", "2026-06-07"]
131
+
132
+ month_start = first_day("2026-06-08", "month")
133
+ # "2026-06-01"
134
+
135
+ month_end = last_day("2026-06-08", "month")
136
+ # "2026-06-30"
137
+
138
+ next_run = add_days("2026-06-08", 1)
139
+ # "2026-06-09"
140
+ ```
141
+
142
+ - `gen_dates_list`: build daily, weekly, monthly, or quarterly sequences.
143
+ - `first_day` / `last_day`: get week, month, or quarter boundaries.
144
+ - `add_days`, `add_weeks`, `add_months`, `add_quarters`: shift dates.
145
+ - `sanitize_date`: convert a date to compact `YYYYMMDD` text.
146
+
147
+ ## Documentation
148
+
149
+ - [Documentation Overview](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/README.md)
150
+ - [Quick Start](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/QUICK_START.md)
151
+ - [Airflow SQL Manual](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/AIRFLOW_SQL_MANUAL.md)
152
+ - [Module documentation](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/modules/README.md)
153
+ - [Changelog](https://github.com/Karapsin/analytics_toolkit/blob/main/docs/CHANGELOG.md)
@@ -6,10 +6,15 @@ from analytics_toolkit.general import time_print
6
6
 
7
7
  from .bootstrap import _apply_multiple_comparisons_adjustment
8
8
  from .constants import DEFAULT_ALPHA, DEFAULT_POWER
9
- from .cuped import _compute_cuped_statistics
9
+ from .cuped import _compute_cuped_statistics, _prepare_cuped_context
10
10
  from .outliers import _build_outlier_context
11
11
  from .ratio import _normalize_ratio_metrics
12
- from .rows import _build_comparisons, _build_metric_definitions, _build_metric_row
12
+ from .rows import (
13
+ _build_comparisons,
14
+ _build_metric_definitions,
15
+ _build_metric_row,
16
+ _prepare_metric_context,
17
+ )
13
18
  from .stats import _compute_mde_from_standard_error, _safe_relative
14
19
  from .validation import (
15
20
  _validate_input_columns,
@@ -105,6 +110,15 @@ def compute_test_metrics(
105
110
  ratio_specs = _normalize_ratio_metrics(df, ratio_metrics, reserved_columns={group, user_id})
106
111
  comparisons = _build_comparisons(group_names, control, test_vs_test=test_vs_test)
107
112
  metric_definitions = _build_metric_definitions(metric_columns, ratio_specs)
113
+ group_values = df[group].to_numpy()
114
+ group_masks = {group_name: group_values == group_name for group_name in group_names}
115
+ comparison_frames = {
116
+ (test_group, baseline_group): df.loc[
117
+ group_masks[test_group] | group_masks[baseline_group],
118
+ [user_id, group],
119
+ ].copy()
120
+ for test_group, baseline_group in comparisons
121
+ }
108
122
  time_print(
109
123
  "compute_test_metrics: setup complete "
110
124
  f"groups={len(group_names)} comparisons={len(comparisons)} "
@@ -129,6 +143,26 @@ def compute_test_metrics(
129
143
  )
130
144
  time_print("compute_test_metrics: outlier contexts complete")
131
145
 
146
+ prepared_metric_contexts: dict[str, dict[str, object]] = {}
147
+ prepared_cuped_contexts: dict[str, dict[str, object]] = {}
148
+ for metric_definition in metric_definitions:
149
+ metric_key = str(metric_definition["metric_key"])
150
+ outlier_context = metric_definition.get("_outlier_context")
151
+ prepared_metric_contexts[metric_key] = _prepare_metric_context(
152
+ df=df,
153
+ metric_definition=metric_definition,
154
+ outlier_context=outlier_context,
155
+ )
156
+ if pre_exp_metrics_df is not None:
157
+ prepared_cuped_contexts[metric_key] = _prepare_cuped_context(
158
+ df=df,
159
+ pre_exp_metrics_df=pre_exp_metrics_df,
160
+ user_id_column=user_id,
161
+ metric_definition=metric_definition,
162
+ outlier_context=outlier_context,
163
+ pre_outlier_context=metric_definition.get("_pre_outlier_context"),
164
+ )
165
+
132
166
  rows: list[dict[str, object]] = []
133
167
  for test_group, baseline_group in comparisons:
134
168
  time_print(f"compute_test_metrics: comparison {test_group} vs {baseline_group}")
@@ -146,6 +180,8 @@ def compute_test_metrics(
146
180
  mde_alpha=mde_alpha,
147
181
  mde_power=mde_power,
148
182
  outlier_context=outlier_context,
183
+ prepared_metric_context=prepared_metric_contexts.get(metric_name),
184
+ group_masks=group_masks,
149
185
  )
150
186
  row["_comparison_key"] = (test_group, baseline_group)
151
187
  if pre_exp_metrics_df is not None:
@@ -160,6 +196,10 @@ def compute_test_metrics(
160
196
  metric_definition=metric_definition,
161
197
  outlier_context=outlier_context,
162
198
  pre_outlier_context=metric_definition.get("_pre_outlier_context"),
199
+ prepared_cuped_context={
200
+ **prepared_cuped_contexts[metric_name],
201
+ "comparison_frame": comparison_frames[(test_group, baseline_group)],
202
+ },
163
203
  )
164
204
  row["mde_abs CUPED"] = _compute_mde_from_standard_error(
165
205
  standard_error=row["s.e. CUPED"],
@@ -41,29 +41,68 @@ def _compute_cuped_p_value(
41
41
  return p_value
42
42
 
43
43
 
44
- def _compute_cuped_statistics(
44
+ def _prepare_cuped_context(
45
45
  df: pd.DataFrame,
46
46
  pre_exp_metrics_df: pd.DataFrame,
47
- group_column: str,
48
47
  user_id_column: str,
49
- baseline_group: str,
50
- test_group: str,
51
48
  metric_definition: dict[str, object],
52
49
  outlier_context: dict[str, object] | None = None,
53
50
  pre_outlier_context: dict[str, object] | None = None,
54
- ) -> tuple[float, float]:
55
- cuped_frame, reason = _build_cuped_frame(
51
+ ) -> dict[str, object]:
52
+ exp_values, exp_error = _build_metric_values_by_user(
56
53
  df=df,
57
- pre_exp_metrics_df=pre_exp_metrics_df,
58
54
  user_id_column=user_id_column,
59
- group_column=group_column,
60
- baseline_group=baseline_group,
61
- test_group=test_group,
62
55
  metric_definition=metric_definition,
56
+ value_column="metric_exp",
63
57
  outlier_context=outlier_context,
64
- pre_outlier_context=pre_outlier_context,
65
58
  )
59
+ pre_values, pre_error = _build_metric_values_by_user(
60
+ df=pre_exp_metrics_df,
61
+ user_id_column=user_id_column,
62
+ metric_definition=metric_definition,
63
+ value_column="metric_pre",
64
+ outlier_context=pre_outlier_context,
65
+ )
66
+ return {
67
+ "exp_values": exp_values.set_index(user_id_column) if exp_error is None else None,
68
+ "pre_values": pre_values.set_index(user_id_column) if pre_error is None else None,
69
+ "exp_error": exp_error,
70
+ "pre_error": pre_error,
71
+ }
72
+
73
+
74
+ def _compute_cuped_statistics(
75
+ df: pd.DataFrame,
76
+ pre_exp_metrics_df: pd.DataFrame,
77
+ group_column: str,
78
+ user_id_column: str,
79
+ baseline_group: str,
80
+ test_group: str,
81
+ metric_definition: dict[str, object],
82
+ outlier_context: dict[str, object] | None = None,
83
+ pre_outlier_context: dict[str, object] | None = None,
84
+ prepared_cuped_context: dict[str, object] | None = None,
85
+ ) -> tuple[float, float]:
66
86
  metric_name = str(metric_definition["metric_key"])
87
+ if prepared_cuped_context is None:
88
+ cuped_frame, reason = _build_cuped_frame(
89
+ df=df,
90
+ pre_exp_metrics_df=pre_exp_metrics_df,
91
+ user_id_column=user_id_column,
92
+ group_column=group_column,
93
+ baseline_group=baseline_group,
94
+ test_group=test_group,
95
+ metric_definition=metric_definition,
96
+ outlier_context=outlier_context,
97
+ pre_outlier_context=pre_outlier_context,
98
+ )
99
+ else:
100
+ cuped_frame, reason = _build_cuped_frame_from_prepared_context(
101
+ comparison_frame=prepared_cuped_context["comparison_frame"],
102
+ user_id_column=user_id_column,
103
+ prepared_cuped_context=prepared_cuped_context,
104
+ )
105
+
67
106
  if reason is not None:
68
107
  warnings.warn(
69
108
  (
@@ -93,6 +132,26 @@ def _compute_cuped_statistics(
93
132
  return p_value, standard_error
94
133
 
95
134
 
135
+ def _build_cuped_frame_from_prepared_context(
136
+ comparison_frame: pd.DataFrame,
137
+ user_id_column: str,
138
+ prepared_cuped_context: dict[str, object],
139
+ ) -> tuple[pd.DataFrame | None, str | None]:
140
+ exp_values = prepared_cuped_context.get("exp_values")
141
+ if exp_values is None:
142
+ return None, f"experiment metric values are unavailable: {prepared_cuped_context.get('exp_error')}"
143
+ pre_values = prepared_cuped_context.get("pre_values")
144
+ if pre_values is None:
145
+ return None, f"pre-experiment metric values are unavailable: {prepared_cuped_context.get('pre_error')}"
146
+
147
+ cuped_frame = comparison_frame.copy()
148
+ cuped_frame = cuped_frame.join(exp_values, on=user_id_column).join(pre_values, on=user_id_column)
149
+ cuped_frame = cuped_frame.dropna(subset=["metric_exp", "metric_pre"]).reset_index(drop=True)
150
+ if cuped_frame.empty:
151
+ return None, "no overlapping non-missing experiment/pre-experiment observations"
152
+ return cuped_frame, None
153
+
154
+
96
155
  def _build_cuped_frame(
97
156
  df: pd.DataFrame,
98
157
  pre_exp_metrics_df: pd.DataFrame,
@@ -29,6 +29,7 @@ from .cuped import (
29
29
  _compute_cuped_p_value_from_frame,
30
30
  _compute_cuped_statistics,
31
31
  _compute_cuped_statistics_from_frame,
32
+ _prepare_cuped_context,
32
33
  )
33
34
  from .outliers import (
34
35
  _apply_outliers_to_agg_ratio_components,
@@ -59,6 +60,7 @@ from .rows import (
59
60
  _build_metric_definitions,
60
61
  _build_metric_row,
61
62
  _build_ratio_metric_row,
63
+ _prepare_metric_context,
62
64
  )
63
65
  from .stats import (
64
66
  _both_present,
@@ -110,6 +112,8 @@ __all__ = [
110
112
  "_build_ratio_valid_mask",
111
113
  "_build_ratio_valid_mask_from_arrays",
112
114
  "_build_value_outlier_mask",
115
+ "_prepare_cuped_context",
116
+ "_prepare_metric_context",
113
117
  "_compute_agg_ratio_diff_standard_error",
114
118
  "_compute_agg_ratio_group_stats",
115
119
  "_compute_agg_ratio_group_stats_arrays",
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextvars
4
+ import inspect
4
5
  from collections.abc import Mapping
5
6
  from contextlib import ExitStack
6
7
  from concurrent.futures import Future, ThreadPoolExecutor, as_completed
@@ -20,6 +21,9 @@ from .api import compute_test_metrics
20
21
  async_sql = sql_facade.async_sql
21
22
 
22
23
  _SQL_DATAFRAME_FIELDS = frozenset({"df", "pre_exp_df", "pre_exp_metrics_df"})
24
+ _COMPUTE_TEST_METRICS_FIELDS = frozenset(
25
+ inspect.signature(compute_test_metrics).parameters
26
+ )
23
27
  _DEFAULT_HARD_CONCURRENCY_CAP = 10
24
28
  _CONCURRENCY_STATE: contextvars.ContextVar["_ConcurrencyState | None"] = (
25
29
  contextvars.ContextVar("analytics_toolkit_ab_parallel_concurrency", default=None)
@@ -127,9 +131,11 @@ def parallel_compute_metrics_from_sql(
127
131
  soft_concurrency_cap: int | None = None,
128
132
  hard_concurrency_cap: int = _DEFAULT_HARD_CONCURRENCY_CAP,
129
133
  progress: bool = False,
134
+ **metric_defaults: Any,
130
135
  ) -> dict[str, pd.DataFrame | str]:
131
136
  """Load SQL-backed task dataframes, then run ``parallel_compute_metrics``."""
132
137
  _validate_start_comment("start_comment", start_comment)
138
+ metric_defaults = _validate_metric_defaults(metric_defaults)
133
139
  task_defs = _validate_sql_tasks(tasks)
134
140
  _validate_concurrency(concurrency)
135
141
  _validate_optional_soft_concurrency_cap(soft_concurrency_cap)
@@ -191,7 +197,8 @@ def parallel_compute_metrics_from_sql(
191
197
  sql_failures[name] = df_result
192
198
  continue
193
199
 
194
- metric_kwargs = dict(kwargs)
200
+ metric_kwargs = dict(metric_defaults)
201
+ metric_kwargs.update(kwargs)
195
202
  metric_kwargs["df"] = df_result
196
203
  if pre_exp_sql is not None:
197
204
  pre_exp_df_result = sql_results[_sql_read_task_name(name, "pre_exp_sql")]
@@ -424,6 +431,38 @@ def _validate_sql_task_spec(
424
431
  return name, kwargs, sql, pre_exp_sql, start_comment
425
432
 
426
433
 
434
+ def _validate_metric_defaults(metric_defaults: Mapping[str, Any]) -> dict[str, Any]:
435
+ if not metric_defaults:
436
+ return {}
437
+
438
+ dataframe_fields = sorted(_SQL_DATAFRAME_FIELDS.intersection(metric_defaults))
439
+ if dataframe_fields:
440
+ fields = ", ".join(dataframe_fields)
441
+ raise ValueError(
442
+ "parallel_compute_metrics_from_sql top-level metric defaults cannot "
443
+ f"define SQL-backed dataframe field(s): {fields}."
444
+ )
445
+
446
+ supported_fields = _COMPUTE_TEST_METRICS_FIELDS - {
447
+ "df",
448
+ "pre_exp_metrics_df",
449
+ }
450
+ unexpected_fields = sorted(set(metric_defaults) - supported_fields)
451
+ if unexpected_fields:
452
+ quoted_fields = ", ".join(repr(field) for field in unexpected_fields)
453
+ if len(unexpected_fields) == 1:
454
+ raise TypeError(
455
+ "parallel_compute_metrics_from_sql() got an unexpected keyword "
456
+ f"argument {quoted_fields}"
457
+ )
458
+ raise TypeError(
459
+ "parallel_compute_metrics_from_sql() got unexpected keyword "
460
+ f"arguments: {quoted_fields}"
461
+ )
462
+
463
+ return dict(metric_defaults)
464
+
465
+
427
466
  def _make_sql_read_task(
428
467
  *,
429
468
  name: str,
@@ -435,7 +474,7 @@ def _make_sql_read_task(
435
474
  task = {
436
475
  "name": _sql_read_task_name(name, field),
437
476
  "type": "read",
438
- "connection_type": db,
477
+ "db_key": db,
439
478
  "query": query,
440
479
  }
441
480
  if start_comment is not _MISSING: