dataops-testgen 2.2.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 (270) hide show
  1. dataops_testgen-2.2.0.dist-info/LICENSE +203 -0
  2. dataops_testgen-2.2.0.dist-info/METADATA +287 -0
  3. dataops_testgen-2.2.0.dist-info/NOTICE +5 -0
  4. dataops_testgen-2.2.0.dist-info/RECORD +270 -0
  5. dataops_testgen-2.2.0.dist-info/WHEEL +5 -0
  6. dataops_testgen-2.2.0.dist-info/entry_points.txt +2 -0
  7. dataops_testgen-2.2.0.dist-info/top_level.txt +1 -0
  8. testgen/__init__.py +0 -0
  9. testgen/__main__.py +770 -0
  10. testgen/commands/__init__.py +0 -0
  11. testgen/commands/queries/__init__.py +0 -0
  12. testgen/commands/queries/execute_cat_tests_query.py +95 -0
  13. testgen/commands/queries/execute_tests_query.py +160 -0
  14. testgen/commands/queries/generate_tests_query.py +94 -0
  15. testgen/commands/queries/profiling_query.py +366 -0
  16. testgen/commands/queries/test_parameter_validation_query.py +88 -0
  17. testgen/commands/run_execute_cat_tests.py +162 -0
  18. testgen/commands/run_execute_tests.py +168 -0
  19. testgen/commands/run_generate_tests.py +107 -0
  20. testgen/commands/run_get_entities.py +122 -0
  21. testgen/commands/run_launch_db_config.py +84 -0
  22. testgen/commands/run_observability_exporter.py +330 -0
  23. testgen/commands/run_profiling_bridge.py +495 -0
  24. testgen/commands/run_quick_start.py +168 -0
  25. testgen/commands/run_setup_profiling_tools.py +96 -0
  26. testgen/commands/run_test_definition.py +146 -0
  27. testgen/commands/run_test_parameter_validation.py +135 -0
  28. testgen/commands/run_upgrade_db_config.py +156 -0
  29. testgen/common/__init__.py +8 -0
  30. testgen/common/clean_sql.py +53 -0
  31. testgen/common/credentials.py +25 -0
  32. testgen/common/database/__init__.py +0 -0
  33. testgen/common/database/database_service.py +629 -0
  34. testgen/common/database/flavor/__init__.py +0 -0
  35. testgen/common/database/flavor/flavor_service.py +75 -0
  36. testgen/common/database/flavor/mssql_flavor_service.py +34 -0
  37. testgen/common/database/flavor/postgresql_flavor_service.py +5 -0
  38. testgen/common/database/flavor/redshift_flavor_service.py +22 -0
  39. testgen/common/database/flavor/snowflake_flavor_service.py +69 -0
  40. testgen/common/database/flavor/trino_flavor_service.py +21 -0
  41. testgen/common/date_service.py +68 -0
  42. testgen/common/display_service.py +85 -0
  43. testgen/common/docker_service.py +76 -0
  44. testgen/common/encrypt.py +55 -0
  45. testgen/common/get_pipeline_parms.py +57 -0
  46. testgen/common/logs.py +79 -0
  47. testgen/common/process_service.py +62 -0
  48. testgen/common/read_file.py +69 -0
  49. testgen/settings.py +440 -0
  50. testgen/template/dbsetup/010_create_base_schema.sql +2 -0
  51. testgen/template/dbsetup/020_create_standard_functions_sprocs.sql +179 -0
  52. testgen/template/dbsetup/030_initialize_new_schema_structure.sql +735 -0
  53. testgen/template/dbsetup/040_populate_new_schema_project.sql +59 -0
  54. testgen/template/dbsetup/050_populate_new_schema_metadata.sql +1517 -0
  55. testgen/template/dbsetup/060_create_standard_views.sql +248 -0
  56. testgen/template/dbsetup/070_create_default_users.sql +17 -0
  57. testgen/template/dbsetup/075_grant_role_rights.sql +43 -0
  58. testgen/template/dbsetup/080_set_current_revision.sql +5 -0
  59. testgen/template/dbupgrade/0100_incremental_upgrade.sql +5 -0
  60. testgen/template/dbupgrade/0101_incremental_upgrade.sql +15 -0
  61. testgen/template/dbupgrade/0102_incremental_upgrade.sql +4 -0
  62. testgen/template/dbupgrade/0103_incremental_upgrade.sql +22 -0
  63. testgen/template/dbupgrade/0104_incremental_upgrade.sql +44 -0
  64. testgen/template/dbupgrade/0105_incremental_upgrade.sql +1 -0
  65. testgen/template/dbupgrade/0106_incremental_upgrade.sql +5 -0
  66. testgen/template/dbupgrade/0107_incremental_upgrade.sql +3 -0
  67. testgen/template/dbupgrade_helpers/get_tg_revision.sql +2 -0
  68. testgen/template/exec_cat_tests/ex_cat_build_agg_table_tests.sql +116 -0
  69. testgen/template/exec_cat_tests/ex_cat_get_distinct_tables.sql +11 -0
  70. testgen/template/exec_cat_tests/ex_cat_results_parse.sql +69 -0
  71. testgen/template/exec_cat_tests/ex_cat_retrieve_agg_test_parms.sql +6 -0
  72. testgen/template/exec_cat_tests/ex_cat_test_query.sql +8 -0
  73. testgen/template/execution/ex_finalize_test_run_results.sql +37 -0
  74. testgen/template/execution/ex_get_tests_non_cat.sql +47 -0
  75. testgen/template/execution/ex_update_test_record_in_testrun_table.sql +27 -0
  76. testgen/template/execution/ex_write_test_record_to_testrun_table.sql +6 -0
  77. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_no_drops_generic.sql +48 -0
  78. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_num_incr_generic.sql +34 -0
  79. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_above_generic.sql +49 -0
  80. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_within_generic.sql +49 -0
  81. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_same_generic.sql +49 -0
  82. testgen/template/flavors/generic/exec_query_tests/ex_custom_query_generic.sql +39 -0
  83. testgen/template/flavors/generic/exec_query_tests/ex_data_match_2way_generic.sql +58 -0
  84. testgen/template/flavors/generic/exec_query_tests/ex_data_match_generic.sql +44 -0
  85. testgen/template/flavors/generic/exec_query_tests/ex_prior_match_generic.sql +37 -0
  86. testgen/template/flavors/generic/exec_query_tests/ex_relative_entropy_generic.sql +53 -0
  87. testgen/template/flavors/generic/exec_query_tests/ex_window_match_no_drops_generic.sql +46 -0
  88. testgen/template/flavors/generic/exec_query_tests/ex_window_match_same_generic.sql +59 -0
  89. testgen/template/flavors/generic/profiling/contingency_counts.sql +3 -0
  90. testgen/template/flavors/generic/validate_tests/ex_get_project_column_list_generic.sql +3 -0
  91. testgen/template/flavors/mssql/exec_query_tests/ex_relative_entropy_mssql.sql +53 -0
  92. testgen/template/flavors/mssql/profiling/project_ddf_query_mssql.sql +35 -0
  93. testgen/template/flavors/mssql/profiling/project_profiling_query_mssql.yaml +246 -0
  94. testgen/template/flavors/mssql/profiling/project_secondary_profiling_query_mssql.sql +36 -0
  95. testgen/template/flavors/mssql/setup_profiling_tools/00_drop_existing_functions_mssql.sql +8 -0
  96. testgen/template/flavors/mssql/setup_profiling_tools/01_create_functions_mssql.sql +12 -0
  97. testgen/template/flavors/mssql/setup_profiling_tools/02_create_functions_mssql.sql +54 -0
  98. testgen/template/flavors/mssql/setup_profiling_tools/create_qc_schema_mssql.sql +4 -0
  99. testgen/template/flavors/mssql/setup_profiling_tools/grant_execute_privileges_mssql.sql +1 -0
  100. testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_no_drops_postgresql.sql +46 -0
  101. testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_same_postgresql.sql +59 -0
  102. testgen/template/flavors/postgresql/profiling/project_ddf_query_postgresql.sql +42 -0
  103. testgen/template/flavors/postgresql/profiling/project_profiling_query_postgresql.yaml +225 -0
  104. testgen/template/flavors/postgresql/profiling/project_secondary_profiling_query_postgresql.sql +28 -0
  105. testgen/template/flavors/postgresql/setup_profiling_tools/create_functions_postgresql.sql +157 -0
  106. testgen/template/flavors/postgresql/setup_profiling_tools/create_qc_schema_postgresql.sql +1 -0
  107. testgen/template/flavors/postgresql/setup_profiling_tools/grant_execute_privileges_postgresql.sql +2 -0
  108. testgen/template/flavors/redshift/profiling/project_ddf_query_redshift.sql +38 -0
  109. testgen/template/flavors/redshift/profiling/project_profiling_query_redshift.yaml +221 -0
  110. testgen/template/flavors/redshift/profiling/project_secondary_profiling_query_redshift.sql +29 -0
  111. testgen/template/flavors/redshift/setup_profiling_tools/create_functions_redshift.sql +115 -0
  112. testgen/template/flavors/redshift/setup_profiling_tools/create_qc_schema_redshift.sql +1 -0
  113. testgen/template/flavors/redshift/setup_profiling_tools/grant_execute_privileges_redshift.sql +2 -0
  114. testgen/template/flavors/snowflake/profiling/project_ddf_query_snowflake.sql +38 -0
  115. testgen/template/flavors/snowflake/profiling/project_profiling_query_snowflake.yaml +220 -0
  116. testgen/template/flavors/snowflake/profiling/project_secondary_profiling_query_snowflake.sql +29 -0
  117. testgen/template/flavors/snowflake/setup_profiling_tools/create_functions_snowflake.sql +69 -0
  118. testgen/template/flavors/snowflake/setup_profiling_tools/create_qc_schema_snowflake.sql +1 -0
  119. testgen/template/flavors/snowflake/setup_profiling_tools/grant_execute_privileges_snowflake.sql +6 -0
  120. testgen/template/flavors/trino/profiling/project_profiling_query_trino.yaml +219 -0
  121. testgen/template/flavors/trino/setup_profiling_tools/create_functions_trino.sql +92 -0
  122. testgen/template/flavors/trino/setup_profiling_tools/create_qc_schema_trino.sql +1 -0
  123. testgen/template/gen_funny_cat_tests/gen_test_constant.sql +104 -0
  124. testgen/template/gen_funny_cat_tests/gen_test_distinct_value_ct.sql +98 -0
  125. testgen/template/gen_funny_cat_tests/gen_test_row_ct.sql +57 -0
  126. testgen/template/gen_funny_cat_tests/gen_test_row_ct_pct.sql +59 -0
  127. testgen/template/generation/gen_delete_old_tests.sql +5 -0
  128. testgen/template/generation/gen_insert_test_suite.sql +5 -0
  129. testgen/template/generation/gen_retrieve_or_insert_test_suite.sql +58 -0
  130. testgen/template/generation/gen_standard_test_type_list.sql +13 -0
  131. testgen/template/generation/gen_standard_tests.sql +48 -0
  132. testgen/template/get_entities/get_connection.sql +21 -0
  133. testgen/template/get_entities/get_connections_list.sql +9 -0
  134. testgen/template/get_entities/get_latest.sql +4 -0
  135. testgen/template/get_entities/get_profile.sql +12 -0
  136. testgen/template/get_entities/get_profile_info.sql +17 -0
  137. testgen/template/get_entities/get_profile_list.sql +17 -0
  138. testgen/template/get_entities/get_profile_screen.sql +275 -0
  139. testgen/template/get_entities/get_project_list.sql +6 -0
  140. testgen/template/get_entities/get_table_group_list.sql +10 -0
  141. testgen/template/get_entities/get_test_generation_list.sql +18 -0
  142. testgen/template/get_entities/get_test_info.sql +41 -0
  143. testgen/template/get_entities/get_test_results_for_run_cli.sql +16 -0
  144. testgen/template/get_entities/get_test_run_list.sql +24 -0
  145. testgen/template/get_entities/get_test_suite.sql +13 -0
  146. testgen/template/get_entities/get_test_suite_list.sql +18 -0
  147. testgen/template/get_entities/list_test_types.sql +4 -0
  148. testgen/template/observability/get_event_data.sql +23 -0
  149. testgen/template/observability/get_test_results.sql +41 -0
  150. testgen/template/observability/update_test_results_exported_to_observability.sql +12 -0
  151. testgen/template/parms/parms_profiling.sql +34 -0
  152. testgen/template/parms/parms_test_execution.sql +13 -0
  153. testgen/template/parms/parms_test_gen.sql +23 -0
  154. testgen/template/profiling/contingency_columns.sql +7 -0
  155. testgen/template/profiling/datatype_suggestions.sql +56 -0
  156. testgen/template/profiling/functional_datatype.sql +523 -0
  157. testgen/template/profiling/functional_tabletype_stage.sql +48 -0
  158. testgen/template/profiling/functional_tabletype_update.sql +8 -0
  159. testgen/template/profiling/pii_flag.sql +133 -0
  160. testgen/template/profiling/profile_anomalies_screen_column.sql +22 -0
  161. testgen/template/profiling/profile_anomalies_screen_multi_column.sql +58 -0
  162. testgen/template/profiling/profile_anomalies_screen_table.sql +22 -0
  163. testgen/template/profiling/profile_anomalies_screen_table_dates.sql +30 -0
  164. testgen/template/profiling/profile_anomalies_screen_variants.sql +40 -0
  165. testgen/template/profiling/profile_anomaly_types_get.sql +3 -0
  166. testgen/template/profiling/project_get_table_sample_count.sql +22 -0
  167. testgen/template/profiling/project_profile_run_record_insert.sql +8 -0
  168. testgen/template/profiling/project_profile_run_record_update.sql +5 -0
  169. testgen/template/profiling/project_profile_run_record_update_status.sql +5 -0
  170. testgen/template/profiling/project_update_profile_results_to_estimates.sql +32 -0
  171. testgen/template/profiling/refresh_anomalies.sql +33 -0
  172. testgen/template/profiling/refresh_data_chars_from_profiling.sql +156 -0
  173. testgen/template/profiling/secondary_profiling_columns.sql +12 -0
  174. testgen/template/profiling/secondary_profiling_delete.sql +4 -0
  175. testgen/template/profiling/secondary_profiling_update.sql +18 -0
  176. testgen/template/quick_start/populate_target_data.sql +1077 -0
  177. testgen/template/quick_start/recreate_target_data_schema.sql +167 -0
  178. testgen/template/quick_start/update_target_data.sql +100 -0
  179. testgen/template/updates/create_tmp_test_definition.sql +19 -0
  180. testgen/template/updates/get_test_def_parms.sql +38 -0
  181. testgen/template/updates/populate_stg_test_definitions.sql +184 -0
  182. testgen/template/validate_tests/ex_disable_tests_test_definitions.sql +5 -0
  183. testgen/template/validate_tests/ex_flag_tests_test_definitions.sql +64 -0
  184. testgen/template/validate_tests/ex_get_project_column_list_generic.sql +3 -0
  185. testgen/template/validate_tests/ex_get_test_column_list_tg.sql +65 -0
  186. testgen/template/validate_tests/ex_write_test_val_errors.sql +22 -0
  187. testgen/ui/__init__.py +0 -0
  188. testgen/ui/app.py +98 -0
  189. testgen/ui/assets/dk_logo.svg +46 -0
  190. testgen/ui/assets/question_mark.png +0 -0
  191. testgen/ui/assets/scripts.js +68 -0
  192. testgen/ui/assets/style.css +140 -0
  193. testgen/ui/bootstrap.py +109 -0
  194. testgen/ui/components/__init__.py +0 -0
  195. testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 +0 -0
  196. testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 +0 -0
  197. testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu4mxK.woff2 +0 -0
  198. testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 +0 -0
  199. testgen/ui/components/frontend/css/material-symbols-rounded.css +24 -0
  200. testgen/ui/components/frontend/css/material-symbols-rounded.woff2 +0 -0
  201. testgen/ui/components/frontend/css/roboto-font-faces.css +35 -0
  202. testgen/ui/components/frontend/css/shared.css +36 -0
  203. testgen/ui/components/frontend/img/dk_logo.svg +46 -0
  204. testgen/ui/components/frontend/index.html +17 -0
  205. testgen/ui/components/frontend/js/components/breadcrumbs.js +86 -0
  206. testgen/ui/components/frontend/js/components/button.js +66 -0
  207. testgen/ui/components/frontend/js/components/location.js +62 -0
  208. testgen/ui/components/frontend/js/components/select.js +75 -0
  209. testgen/ui/components/frontend/js/components/sidebar.js +358 -0
  210. testgen/ui/components/frontend/js/main.js +99 -0
  211. testgen/ui/components/frontend/js/streamlit.js +19 -0
  212. testgen/ui/components/frontend/js/van.min.js +1 -0
  213. testgen/ui/components/utils/__init__.py +0 -0
  214. testgen/ui/components/utils/callbacks.py +51 -0
  215. testgen/ui/components/utils/component.py +13 -0
  216. testgen/ui/components/widgets/__init__.py +6 -0
  217. testgen/ui/components/widgets/breadcrumbs.py +32 -0
  218. testgen/ui/components/widgets/location.py +65 -0
  219. testgen/ui/components/widgets/modal.py +97 -0
  220. testgen/ui/components/widgets/sidebar.py +69 -0
  221. testgen/ui/navigation/__init__.py +0 -0
  222. testgen/ui/navigation/menu.py +42 -0
  223. testgen/ui/navigation/page.py +20 -0
  224. testgen/ui/navigation/router.py +63 -0
  225. testgen/ui/queries/__init__.py +0 -0
  226. testgen/ui/queries/authentication_queries.py +47 -0
  227. testgen/ui/queries/connection_queries.py +121 -0
  228. testgen/ui/queries/profiling_queries.py +148 -0
  229. testgen/ui/queries/project_queries.py +9 -0
  230. testgen/ui/queries/table_group_queries.py +186 -0
  231. testgen/ui/queries/test_definition_queries.py +270 -0
  232. testgen/ui/queries/test_run_queries.py +32 -0
  233. testgen/ui/queries/test_suite_queries.py +145 -0
  234. testgen/ui/scripts/__init__.py +0 -0
  235. testgen/ui/scripts/patch_streamlit.py +111 -0
  236. testgen/ui/services/__init__.py +0 -0
  237. testgen/ui/services/authentication_service.py +119 -0
  238. testgen/ui/services/connection_service.py +220 -0
  239. testgen/ui/services/database_service.py +282 -0
  240. testgen/ui/services/form_service.py +1008 -0
  241. testgen/ui/services/javascript_service.py +44 -0
  242. testgen/ui/services/query_service.py +316 -0
  243. testgen/ui/services/string_service.py +12 -0
  244. testgen/ui/services/table_group_service.py +130 -0
  245. testgen/ui/services/test_definition_service.py +117 -0
  246. testgen/ui/services/test_run_service.py +13 -0
  247. testgen/ui/services/test_suite_service.py +76 -0
  248. testgen/ui/services/toolbar_service.py +77 -0
  249. testgen/ui/session.py +46 -0
  250. testgen/ui/views/__init__.py +0 -0
  251. testgen/ui/views/app_log_modal.py +92 -0
  252. testgen/ui/views/connections.py +72 -0
  253. testgen/ui/views/connections_base.py +367 -0
  254. testgen/ui/views/login.py +40 -0
  255. testgen/ui/views/not_found.py +16 -0
  256. testgen/ui/views/overview.py +34 -0
  257. testgen/ui/views/profiling_anomalies.py +501 -0
  258. testgen/ui/views/profiling_details.py +335 -0
  259. testgen/ui/views/profiling_modal.py +40 -0
  260. testgen/ui/views/profiling_results.py +206 -0
  261. testgen/ui/views/profiling_summary.py +177 -0
  262. testgen/ui/views/project_settings.py +74 -0
  263. testgen/ui/views/table_groups.py +530 -0
  264. testgen/ui/views/test_definitions.py +1020 -0
  265. testgen/ui/views/test_results.py +908 -0
  266. testgen/ui/views/test_runs.py +195 -0
  267. testgen/ui/views/test_suites.py +545 -0
  268. testgen/utils/__init__.py +0 -0
  269. testgen/utils/plugins.py +17 -0
  270. testgen/utils/singleton.py +14 -0
@@ -0,0 +1,908 @@
1
+ import typing
2
+ from datetime import date
3
+
4
+ import pandas as pd
5
+ import plotly.express as px
6
+ import plotly.graph_objects as go
7
+ import streamlit as st
8
+
9
+ import testgen.ui.services.database_service as db
10
+ import testgen.ui.services.form_service as fm
11
+ import testgen.ui.services.query_service as dq
12
+ import testgen.ui.services.toolbar_service as tb
13
+ from testgen.common import ConcatColumnList, date_service
14
+ from testgen.ui.components import widgets as testgen
15
+ from testgen.ui.navigation.page import Page
16
+ from testgen.ui.services.string_service import empty_if_null
17
+ from testgen.ui.session import session
18
+ from testgen.ui.views.profiling_modal import view_profiling_modal
19
+ from testgen.ui.views.test_definitions import show_add_edit_modal_by_test_definition
20
+
21
+ ALWAYS_SPIN = False
22
+
23
+
24
+ class TestResultsPage(Page):
25
+ path = "tests/results"
26
+ can_activate: typing.ClassVar = [
27
+ lambda: session.authentication_status or "login",
28
+ lambda: session.project != None or "overview",
29
+ ]
30
+
31
+ def render(self) -> None:
32
+ export_container = fm.render_page_header(
33
+ "Test Results",
34
+ "https://docs.datakitchen.io/article/dataops-testgen-help/test-results",
35
+ lst_breadcrumbs=[
36
+ {"label": "Overview", "path": "overview"},
37
+ {"label": "Test Runs", "path": "tests/runs"},
38
+ {"label": "Test Results", "path": None},
39
+ ],
40
+ )
41
+
42
+ str_project = st.session_state["project"] if "project" in st.session_state else None
43
+
44
+ # Look for drill-down from another page
45
+ if "drill_test_run" in st.session_state:
46
+ str_sel_test_run = st.session_state["drill_test_run"]
47
+ else:
48
+ str_sel_test_run = None
49
+
50
+ if not str_project:
51
+ st.write("Choose a Project from the menu.")
52
+ else:
53
+ # Setup Toolbar
54
+ tool_bar = tb.ToolBar(3, 1, 4, None)
55
+
56
+ # Lookup Test Run
57
+ if str_sel_test_run:
58
+ df = get_drill_test_run(str_sel_test_run)
59
+ if not df.empty:
60
+ with tool_bar.long_slots[0]:
61
+ time_columns = ["test_date"]
62
+ date_service.accommodate_dataframe_to_timezone(df, st.session_state, time_columns)
63
+ df["description"] = df["test_date"] + " | " + df["test_suite_description"]
64
+ str_sel_test_run = fm.render_select(
65
+ "Test Run", df, "description", "test_run_id", boo_required=True, boo_disabled=True
66
+ )
67
+
68
+ if str_sel_test_run:
69
+ with tool_bar.long_slots[1]:
70
+ lst_status_options = [
71
+ "Failures and Warnings",
72
+ "Failed Tests",
73
+ "Tests with Warnings",
74
+ "Passed Tests",
75
+ ]
76
+ str_sel_status = st.selectbox("Result Priority", lst_status_options)
77
+
78
+ with tool_bar.short_slots[0]:
79
+ str_help = "Toggle on to perform actions on multiple results"
80
+ do_multi_select = st.toggle("Multi-Select", help=str_help)
81
+
82
+ match str_sel_status:
83
+ case "Failures and Warnings":
84
+ str_sel_status = "'Failed','Warning'"
85
+ case "Failed Tests":
86
+ str_sel_status = "'Failed'"
87
+ case "Tests with Warnings":
88
+ str_sel_status = "'Warning'"
89
+ case "Passed Tests":
90
+ str_sel_status = "'Passed'"
91
+
92
+ # Display main grid and retrieve selection
93
+ selected = show_result_detail(str_sel_test_run, str_sel_status, do_multi_select, export_container)
94
+
95
+ # Need to render toolbar buttons after grid, so selection status is maintained
96
+ disable_dispo = True if not selected or str_sel_status == "'Passed'" else False
97
+ if tool_bar.button_slots[0].button(
98
+ "✓", help="Confirm this issue as relevant for this run", disabled=disable_dispo
99
+ ):
100
+ fm.reset_post_updates(
101
+ do_disposition_update(selected, "Confirmed"),
102
+ as_toast=True,
103
+ clear_cache=True,
104
+ lst_cached_functions=[get_test_disposition],
105
+ )
106
+ if tool_bar.button_slots[1].button(
107
+ "✘", help="Dismiss this issue as not relevant for this run", disabled=disable_dispo
108
+ ):
109
+ fm.reset_post_updates(
110
+ do_disposition_update(selected, "Dismissed"),
111
+ as_toast=True,
112
+ clear_cache=True,
113
+ lst_cached_functions=[get_test_disposition],
114
+ )
115
+ if tool_bar.button_slots[2].button(
116
+ "🔇", help="Mute this test to deactivate it for future runs", disabled=not selected
117
+ ):
118
+ fm.reset_post_updates(
119
+ do_disposition_update(selected, "Inactive"),
120
+ as_toast=True,
121
+ clear_cache=True,
122
+ lst_cached_functions=[get_test_disposition],
123
+ )
124
+ if tool_bar.button_slots[3].button("⟲", help="Clear action", disabled=not selected):
125
+ fm.reset_post_updates(
126
+ do_disposition_update(selected, "No Decision"),
127
+ as_toast=True,
128
+ clear_cache=True,
129
+ lst_cached_functions=[get_test_disposition],
130
+ )
131
+
132
+ # Help Links
133
+ st.markdown(
134
+ "[Help on Test Types](https://docs.datakitchen.io/article/dataops-testgen-help/testgen-test-types)"
135
+ )
136
+
137
+ # with st.sidebar:
138
+ # st.divider()
139
+
140
+
141
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
142
+ def run_test_suite_lookup_by_project_query(str_project_code):
143
+ str_schema = st.session_state["dbschema"]
144
+ return dq.run_test_suite_lookup_by_project_query(str_schema, str_project_code)
145
+
146
+
147
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
148
+ def run_test_run_lookup_by_date(str_project_code, str_run_date):
149
+ str_schema = st.session_state["dbschema"]
150
+ return dq.run_test_run_lookup_by_date(str_schema, str_project_code, str_run_date)
151
+
152
+
153
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
154
+ def get_drill_test_run(str_test_run_id):
155
+ str_schema = st.session_state["dbschema"]
156
+ str_sql = f"""
157
+ SELECT id::VARCHAR as test_run_id,
158
+ test_starttime as test_date,
159
+ test_suite as test_suite_description
160
+ FROM {str_schema}.test_runs
161
+ WHERE id = '{str_test_run_id}'::UUID;
162
+ """
163
+ return db.retrieve_data(str_sql)
164
+
165
+
166
+ @st.cache_data(show_spinner="Retrieving Results")
167
+ def get_test_results(str_run_id, str_sel_test_status):
168
+ schema = st.session_state["dbschema"]
169
+ return get_test_results_uncached(schema, str_run_id, str_sel_test_status)
170
+
171
+
172
+ def get_test_results_uncached(str_schema, str_run_id, str_sel_test_status):
173
+ # First visible row first, so multi-select checkbox will render
174
+ str_sql = f"""
175
+ WITH run_results
176
+ AS (SELECT *
177
+ FROM {str_schema}.test_results r
178
+ WHERE r.test_run_id = '{str_run_id}'
179
+ AND r.result_status IN ({str_sel_test_status})
180
+ )
181
+ SELECT r.table_name,
182
+ p.project_name, ts.test_suite, tg.table_groups_name, cn.connection_name, cn.project_host, cn.sql_flavor,
183
+ tt.dq_dimension, tt.test_scope,
184
+ r.schema_name, r.column_names, r.test_time::DATE as test_date, r.test_type, tt.id as test_type_id,
185
+ tt.test_name_short, tt.test_name_long, r.test_description, tt.measure_uom, tt.measure_uom_description,
186
+ c.test_operator, r.threshold_value::NUMERIC(16, 5), r.result_measure::NUMERIC(16, 5), r.result_status,
187
+ CASE
188
+ WHEN r.result_code <> 1 THEN r.disposition
189
+ ELSE 'Passed'
190
+ END as disposition,
191
+ NULL::VARCHAR(1) as action,
192
+ r.input_parameters, r.result_message, CASE WHEN result_code <> 1 THEN r.severity END as severity,
193
+ r.result_code as passed_ct,
194
+ (1 - r.result_code)::INTEGER as exception_ct,
195
+ CASE
196
+ WHEN result_status = 'Warning'
197
+ AND result_message NOT ILIKE 'ERROR - TEST COLUMN MISSING%%' THEN 1
198
+ END::INTEGER as warning_ct,
199
+ CASE
200
+ WHEN result_status = 'Failed'
201
+ AND result_message NOT ILIKE 'ERROR - TEST COLUMN MISSING%%' THEN 1
202
+ END::INTEGER as failed_ct,
203
+ CASE
204
+ WHEN result_message ILIKE 'ERROR - TEST COLUMN MISSING%%' THEN 1
205
+ END as execution_error_ct,
206
+ r.project_code, r.table_groups_id::VARCHAR,
207
+ r.id::VARCHAR as test_result_id, r.test_run_id::VARCHAR,
208
+ c.id::VARCHAR as connection_id, r.test_suite_id::VARCHAR,
209
+ r.test_definition_id::VARCHAR as test_definition_id_runtime,
210
+ CASE
211
+ WHEN r.auto_gen = TRUE THEN d.id
212
+ ELSE r.test_definition_id
213
+ END::VARCHAR as test_definition_id_current,
214
+ r.auto_gen
215
+ FROM run_results r
216
+ INNER JOIN {str_schema}.test_types tt
217
+ ON (r.test_type = tt.test_type)
218
+ LEFT JOIN {str_schema}.test_definitions rd
219
+ ON (r.test_definition_id = rd.id)
220
+ LEFT JOIN {str_schema}.test_definitions d
221
+ ON (r.test_suite_id = d.test_suite_id
222
+ AND r.table_name = d.table_name
223
+ AND r.column_names = COALESCE(d.column_name, 'N/A')
224
+ AND r.test_type = d.test_type
225
+ AND r.auto_gen = TRUE
226
+ AND d.last_auto_gen_date IS NOT NULL)
227
+ INNER JOIN {str_schema}.test_suites ts
228
+ ON (r.project_code = ts.project_code
229
+ AND r.test_suite = ts.test_suite)
230
+ INNER JOIN {str_schema}.projects p
231
+ ON (r.project_code = p.project_code)
232
+ INNER JOIN {str_schema}.table_groups tg
233
+ ON (ts.table_groups_id = tg.id)
234
+ INNER JOIN {str_schema}.connections cn
235
+ ON (tg.connection_id = cn.connection_id)
236
+ LEFT JOIN {str_schema}.cat_test_conditions c
237
+ ON (cn.sql_flavor = c.sql_flavor
238
+ AND r.test_type = c.test_type)
239
+ ORDER BY schema_name, table_name, column_names, test_type;
240
+ """
241
+ df = db.retrieve_data(str_sql)
242
+
243
+ # Clean Up
244
+ df["test_date"] = pd.to_datetime(df["test_date"])
245
+
246
+ return df
247
+
248
+
249
+ @st.cache_data(show_spinner="Retrieving Status")
250
+ def get_test_disposition(str_run_id):
251
+ str_schema = st.session_state["dbschema"]
252
+ str_sql = f"""
253
+ SELECT id::VARCHAR, disposition
254
+ FROM {str_schema}.test_results
255
+ WHERE test_run_id = '{str_run_id}';
256
+ """
257
+
258
+ df = db.retrieve_data(str_sql)
259
+
260
+ dct_replace = {"Confirmed": "✓", "Dismissed": "✘", "Inactive": "🔇", "Passed": ""}
261
+ df["action"] = df["disposition"].replace(dct_replace)
262
+
263
+ return df[["id", "action"]]
264
+
265
+
266
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
267
+ def get_test_result_summary(str_run_id):
268
+ str_schema = st.session_state["dbschema"]
269
+ str_sql = f"""
270
+ SELECT test_ct as result_ct,
271
+ COALESCE(error_ct, 0) as error_ct,
272
+ failed_ct + warning_ct as exception_ct, warning_ct,
273
+ ROUND({str_schema}.fn_pct(warning_ct, test_ct), 1) as warning_pct,
274
+ failed_ct,
275
+ ROUND({str_schema}.fn_pct(failed_ct, test_ct), 1) as failed_pct,
276
+ passed_ct,
277
+ ROUND({str_schema}.fn_pct(passed_ct, test_ct), 1) as passed_pct
278
+ FROM {str_schema}.test_runs
279
+ WHERE id = '{str_run_id}'::UUID;
280
+ """
281
+ df = db.retrieve_data(str_sql)
282
+
283
+ return df
284
+
285
+
286
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
287
+ def get_test_result_history(str_test_type, str_test_suite_id, str_table_name, str_column_names,
288
+ str_test_definition_id, auto_gen):
289
+ str_schema = st.session_state["dbschema"]
290
+
291
+ if auto_gen:
292
+ str_where = f"""
293
+ WHERE test_suite_id = '{str_test_suite_id}'
294
+ AND table_name = '{str_table_name}'
295
+ AND column_names = '{str_column_names}'
296
+ AND test_type = '{str_test_type}'
297
+ AND auto_gen = TRUE
298
+ """
299
+ else:
300
+ str_where = f"""
301
+ WHERE test_definition_id_runtime = '{str_test_definition_id}'
302
+ """
303
+
304
+ str_sql = f"""
305
+ SELECT test_date, test_type,
306
+ test_name_short, test_name_long, measure_uom, test_operator,
307
+ threshold_value::NUMERIC, result_measure, result_status
308
+ FROM {str_schema}.v_test_results {str_where}
309
+ ORDER BY test_date DESC;
310
+ """
311
+
312
+ df = db.retrieve_data(str_sql)
313
+ # Clean Up
314
+ df["test_date"] = pd.to_datetime(df["test_date"])
315
+
316
+ return df
317
+
318
+
319
+ @st.cache_data(show_spinner=ALWAYS_SPIN)
320
+ def get_test_definition(str_test_def_id):
321
+ str_schema = st.session_state["dbschema"]
322
+ return get_test_definition_uncached(str_schema, str_test_def_id)
323
+
324
+
325
+ def get_test_definition_uncached(str_schema, str_test_def_id):
326
+ str_sql = f"""
327
+ SELECT d.id::VARCHAR, tt.test_name_short as test_name, tt.test_name_long as full_name,
328
+ tt.test_description as description, tt.usage_notes,
329
+ d.column_name,
330
+ d.baseline_value, d.baseline_ct, d.baseline_avg, d.baseline_sd, d.threshold_value,
331
+ d.subset_condition, d.groupby_names, d.having_condition, d.match_schema_name,
332
+ d.match_table_name, d.match_column_names, d.match_subset_condition,
333
+ d.match_groupby_names, d.match_having_condition,
334
+ d.window_date_column, d.window_days::VARCHAR as window_days,
335
+ d.custom_query,
336
+ d.severity, tt.default_severity,
337
+ d.test_active, d.lock_refresh, d.last_manual_update
338
+ FROM {str_schema}.test_definitions d
339
+ INNER JOIN {str_schema}.test_types tt
340
+ ON (d.test_type = tt.test_type)
341
+ WHERE d.id = '{str_test_def_id}';
342
+ """
343
+ return db.retrieve_data(str_sql)
344
+
345
+
346
+ @st.cache_data(show_spinner=False)
347
+ def do_source_data_lookup(selected_row):
348
+ schema = st.session_state["dbschema"]
349
+ return do_source_data_lookup_uncached(schema, selected_row)
350
+
351
+
352
+ def do_source_data_lookup_uncached(str_schema, selected_row, sql_only=False):
353
+ # Define the query
354
+ str_sql = f"""
355
+ SELECT t.lookup_query, tg.table_group_schema, c.project_qc_schema,
356
+ c.sql_flavor, c.project_host, c.project_port, c.project_db, c.project_user, c.project_pw_encrypted,
357
+ c.url, c.connect_by_url,
358
+ c.connect_by_key, c.private_key, c.private_key_passphrase
359
+ FROM {str_schema}.target_data_lookups t
360
+ INNER JOIN {str_schema}.table_groups tg
361
+ ON ('{selected_row["table_groups_id"]}'::UUID = tg.id)
362
+ INNER JOIN {str_schema}.connections c
363
+ ON (tg.connection_id = c.connection_id)
364
+ AND (t.sql_flavor = c.sql_flavor)
365
+ WHERE t.error_type = 'Test Results'
366
+ AND t.test_id = '{selected_row["test_type_id"]}'
367
+ AND t.lookup_query > '';
368
+ """
369
+
370
+ def replace_parms(df_test, str_query):
371
+ if df_test.empty:
372
+ raise ValueError("This test definition is no longer present.")
373
+
374
+ str_query = str_query.replace("{TARGET_SCHEMA}", empty_if_null(lst_query[0]["table_group_schema"]))
375
+ str_query = str_query.replace("{TABLE_NAME}", empty_if_null(selected_row["table_name"]))
376
+ str_query = str_query.replace("{COLUMN_NAME}", empty_if_null(selected_row["column_names"]))
377
+ str_query = str_query.replace("{DATA_QC_SCHEMA}", empty_if_null(lst_query[0]["project_qc_schema"]))
378
+ str_query = str_query.replace("{TEST_DATE}", str(empty_if_null(selected_row["test_date"])))
379
+
380
+ str_query = str_query.replace("{CUSTOM_QUERY}", empty_if_null(df_test.at[0, "custom_query"]))
381
+ str_query = str_query.replace("{BASELINE_VALUE}", empty_if_null(df_test.at[0, "baseline_value"]))
382
+ str_query = str_query.replace("{BASELINE_CT}", empty_if_null(df_test.at[0, "baseline_ct"]))
383
+ str_query = str_query.replace("{BASELINE_AVG}", empty_if_null(df_test.at[0, "baseline_avg"]))
384
+ str_query = str_query.replace("{BASELINE_SD}", empty_if_null(df_test.at[0, "baseline_sd"]))
385
+ str_query = str_query.replace("{THRESHOLD_VALUE}", empty_if_null(df_test.at[0, "threshold_value"]))
386
+
387
+ str_substitute = empty_if_null(df_test.at[0, "subset_condition"])
388
+ str_substitute = "1=1" if str_substitute == "" else str_substitute
389
+ str_query = str_query.replace("{SUBSET_CONDITION}", str_substitute)
390
+
391
+ str_query = str_query.replace("{GROUPBY_NAMES}", empty_if_null(df_test.at[0, "groupby_names"]))
392
+ str_query = str_query.replace("{HAVING_CONDITION}", empty_if_null(df_test.at[0, "having_condition"]))
393
+ str_query = str_query.replace("{MATCH_SCHEMA_NAME}", empty_if_null(df_test.at[0, "match_schema_name"]))
394
+ str_query = str_query.replace("{MATCH_TABLE_NAME}", empty_if_null(df_test.at[0, "match_table_name"]))
395
+ str_query = str_query.replace("{MATCH_COLUMN_NAMES}", empty_if_null(df_test.at[0, "match_column_names"]))
396
+
397
+ str_substitute = empty_if_null(df_test.at[0, "match_subset_condition"])
398
+ str_substitute = "1=1" if str_substitute == "" else str_substitute
399
+ str_query = str_query.replace("{MATCH_SUBSET_CONDITION}", str_substitute)
400
+
401
+ str_query = str_query.replace("{MATCH_GROUPBY_NAMES}", empty_if_null(df_test.at[0, "match_groupby_names"]))
402
+ str_query = str_query.replace("{MATCH_HAVING_CONDITION}", empty_if_null(df_test.at[0, "match_having_condition"]))
403
+ str_query = str_query.replace("{COLUMN_NAME_NO_QUOTES}", empty_if_null(selected_row["column_names"]))
404
+
405
+ str_query = str_query.replace("{WINDOW_DATE_COLUMN}", empty_if_null(df_test.at[0, "window_date_column"]))
406
+ str_query = str_query.replace("{WINDOW_DAYS}", empty_if_null(df_test.at[0, "window_days"]))
407
+
408
+ str_substitute = ConcatColumnList(selected_row["column_names"], "<NULL>")
409
+ str_query = str_query.replace("{CONCAT_COLUMNS}", str_substitute)
410
+ str_substitute = ConcatColumnList(df_test.at[0, "match_groupby_names"], "<NULL>")
411
+ str_query = str_query.replace("{CONCAT_MATCH_GROUPBY}", str_substitute)
412
+
413
+ if str_query is None or str_query == "":
414
+ raise ValueError("Lookup query is not defined for this Test Type.")
415
+ return str_query
416
+
417
+ try:
418
+ # Retrieve SQL for customer lookup
419
+ lst_query = db.retrieve_data_list(str_sql)
420
+
421
+ if sql_only:
422
+ return lst_query, replace_parms, None
423
+
424
+ # Retrieve and return data as df
425
+ if lst_query:
426
+ df_test = get_test_definition(selected_row["test_definition_id_current"])
427
+
428
+ str_sql = replace_parms(df_test, lst_query[0]["lookup_query"])
429
+ df = db.retrieve_target_db_df(
430
+ lst_query[0]["sql_flavor"],
431
+ lst_query[0]["project_host"],
432
+ lst_query[0]["project_port"],
433
+ lst_query[0]["project_db"],
434
+ lst_query[0]["project_user"],
435
+ lst_query[0]["project_pw_encrypted"],
436
+ str_sql,
437
+ lst_query[0]["url"],
438
+ lst_query[0]["connect_by_url"],
439
+ lst_query[0]["connect_by_key"],
440
+ lst_query[0]["private_key"],
441
+ lst_query[0]["private_key_passphrase"],
442
+ )
443
+ if df.empty:
444
+ return "ND", "Data that violates Test criteria is not present in the current dataset.", None
445
+ else:
446
+ return "OK", None, df
447
+ else:
448
+ return "NA", "A source data lookup for this Test is not available.", None
449
+
450
+ except Exception as e:
451
+ return "ERR", f"Source data lookup query caused an error:\n\n{e.args[0]}\n\n{str_sql}", None
452
+
453
+
454
+ @st.cache_data(show_spinner=False)
455
+ def do_source_data_lookup_custom(selected_row):
456
+ str_schema = st.session_state["dbschema"]
457
+ # Define the query
458
+ str_sql = f"""
459
+ SELECT d.custom_query as lookup_query, tg.table_group_schema, c.project_qc_schema,
460
+ c.sql_flavor, c.project_host, c.project_port, c.project_db, c.project_user, c.project_pw_encrypted,
461
+ c.url, c.connect_by_url, c.connect_by_key, c.private_key, c.private_key_passphrase
462
+ FROM {str_schema}.test_definitions d
463
+ INNER JOIN {str_schema}.table_groups tg
464
+ ON ('{selected_row["table_groups_id"]}'::UUID = tg.id)
465
+ INNER JOIN {str_schema}.connections c
466
+ ON (tg.connection_id = c.connection_id)
467
+ WHERE d.id = '{selected_row["test_definition_id_current"]}';
468
+ """
469
+
470
+ try:
471
+ # Retrieve SQL for customer lookup
472
+ lst_query = db.retrieve_data_list(str_sql)
473
+
474
+ # Retrieve and return data as df
475
+ if lst_query:
476
+ str_sql = lst_query[0]["lookup_query"]
477
+ str_sql = str_sql.replace("{DATA_SCHEMA}", empty_if_null(lst_query[0]["table_group_schema"]))
478
+ df = db.retrieve_target_db_df(
479
+ lst_query[0]["sql_flavor"],
480
+ lst_query[0]["project_host"],
481
+ lst_query[0]["project_port"],
482
+ lst_query[0]["project_db"],
483
+ lst_query[0]["project_user"],
484
+ lst_query[0]["project_pw_encrypted"],
485
+ str_sql,
486
+ lst_query[0]["url"],
487
+ lst_query[0]["connect_by_url"],
488
+ lst_query[0]["connect_by_key"],
489
+ lst_query[0]["private_key"],
490
+ lst_query[0]["private_key_passphrase"],
491
+ )
492
+ if df.empty:
493
+ return "ND", "Data that violates Test criteria is not present in the current dataset.", None
494
+ else:
495
+ return "OK", None, df
496
+ else:
497
+ return "NA", "A source data lookup for this Test is not available.", None
498
+
499
+ except Exception as e:
500
+ return "ERR", f"Source data lookup query caused an error:\n\n{e.args[0]}\n\n{str_sql}", None
501
+
502
+
503
+ def show_test_def_detail(str_test_def_id):
504
+ df = get_test_definition(str_test_def_id)
505
+
506
+ specs = []
507
+ if not df.empty:
508
+ # Get First Row
509
+ row = df.iloc[0]
510
+
511
+ specs.append(
512
+ fm.FieldSpec(
513
+ "Usage Notes",
514
+ "usage_notes",
515
+ fm.FormWidget.text_area,
516
+ row["usage_notes"],
517
+ read_only=True,
518
+ text_multi_lines=7,
519
+ )
520
+ )
521
+ specs.append(
522
+ fm.FieldSpec(
523
+ "Threshold Value",
524
+ "threshold_value",
525
+ fm.FormWidget.number_input,
526
+ float(row["threshold_value"]) if row["threshold_value"] else None,
527
+ required=True,
528
+ )
529
+ )
530
+
531
+ default_severity_choice = f"Test Default ({row['default_severity']})"
532
+
533
+ spec = fm.FieldSpec("Test Result Urgency", "severity", fm.FormWidget.radio, row["severity"], required=True)
534
+ spec.lst_option_text = [default_severity_choice, "Warning", "Fail", "Log"]
535
+ spec.lst_option_values = [None, "Warning", "Fail", "Ignore"]
536
+ spec.show_horizontal = True
537
+ specs.append(spec)
538
+
539
+ spec = fm.FieldSpec(
540
+ "Perform Test in Future Runs", "test_active", fm.FormWidget.radio, row["test_active"], required=True
541
+ )
542
+ spec.lst_option_text = ["Yes", "No"]
543
+ spec.lst_option_values = ["Y", "N"]
544
+ spec.show_horizontal = True
545
+ specs.append(spec)
546
+
547
+ spec = fm.FieldSpec(
548
+ "Lock from Refresh", "lock_refresh", fm.FormWidget.radio, row["lock_refresh"], required=True
549
+ )
550
+ spec.lst_option_text = ["Unlocked", "Locked"]
551
+ spec.lst_option_values = ["N", "Y"]
552
+ spec.show_horizontal = True
553
+ specs.append(spec)
554
+
555
+ specs.append(fm.FieldSpec("", "id", form_widget=fm.FormWidget.hidden, int_key=1, init_val=row["id"]))
556
+
557
+ specs.append(
558
+ fm.FieldSpec(
559
+ "Last Manual Update",
560
+ "last_manual_update",
561
+ fm.FormWidget.date_input,
562
+ row["last_manual_update"],
563
+ date.today().strftime("%Y-%m-%d hh:mm:ss"),
564
+ read_only=True,
565
+ )
566
+ )
567
+ fm.render_form_by_field_specs(
568
+ None,
569
+ "test_definitions",
570
+ specs,
571
+ boo_display_only=True,
572
+ )
573
+
574
+
575
+ def show_result_detail(str_run_id, str_sel_test_status, do_multi_select, export_container):
576
+ # Retrieve summary counts
577
+ df_sum = get_test_result_summary(str_run_id)
578
+ if not df_sum.empty:
579
+ if (df_sum.at[0, "result_ct"] or 0) > 0:
580
+ write_summary_graph(df_sum)
581
+
582
+ # Retrieve test results (always cached, action as null)
583
+ df = get_test_results(str_run_id, str_sel_test_status)
584
+ # Retrieve disposition action (cache refreshed)
585
+ df_action = get_test_disposition(str_run_id)
586
+ # Update action from disposition df
587
+ action_map = df_action.set_index("id")["action"].to_dict()
588
+ df["action"] = df["test_result_id"].map(action_map).fillna(df["action"])
589
+
590
+ lst_show_columns = [
591
+ "table_name",
592
+ "column_names",
593
+ "test_name_short",
594
+ "result_measure",
595
+ "measure_uom",
596
+ "result_status",
597
+ "action",
598
+ ]
599
+
600
+ lst_show_headers = [
601
+ "Table Name",
602
+ "Columns/Focus",
603
+ "Test Type",
604
+ "Result Measure",
605
+ "UOM",
606
+ "Status",
607
+ "Action",
608
+ ]
609
+
610
+ selected_rows = fm.render_grid_select(
611
+ df, lst_show_columns, do_multi_select=do_multi_select, show_column_headers=lst_show_headers
612
+ )
613
+
614
+ with export_container:
615
+ lst_export_columns = [
616
+ "schema_name",
617
+ "table_name",
618
+ "column_names",
619
+ "test_name_short",
620
+ "test_description",
621
+ "dq_dimension",
622
+ "measure_uom",
623
+ "measure_uom_description",
624
+ "threshold_value",
625
+ "severity",
626
+ "result_measure",
627
+ "result_status",
628
+ "result_message",
629
+ "action",
630
+ ]
631
+ lst_wrap_colunns = ["test_description"]
632
+ lst_export_headers = [
633
+ "Schema Name",
634
+ "Table Name",
635
+ "Columns/Focus",
636
+ "Test Type",
637
+ "Test Description",
638
+ "DQ Dimension",
639
+ "UOM",
640
+ "UOM Description",
641
+ "Threshold Value",
642
+ "Severity",
643
+ "Result Measure",
644
+ "Status",
645
+ "Message",
646
+ "Action",
647
+ ]
648
+ fm.render_excel_export(
649
+ df, lst_export_columns, "Test Results", "{TIMESTAMP}", lst_wrap_colunns, lst_export_headers
650
+ )
651
+
652
+ # Display history and detail for selected row
653
+ if not selected_rows:
654
+ st.markdown(":orange[Select a record to see more information.]")
655
+ else:
656
+ selected_row = selected_rows[len(selected_rows) - 1]
657
+ dfh = get_test_result_history(
658
+ selected_row["test_type"],
659
+ selected_row["test_suite_id"],
660
+ selected_row["table_name"],
661
+ selected_row["column_names"],
662
+ selected_row["test_definition_id_runtime"],
663
+ selected_row["auto_gen"]
664
+ )
665
+ show_hist_columns = ["test_date", "threshold_value", "result_measure", "result_status"]
666
+
667
+ time_columns = ["test_date"]
668
+ date_service.accommodate_dataframe_to_timezone(dfh, st.session_state, time_columns)
669
+
670
+ pg_col1, pg_col2 = st.columns([0.5, 0.5])
671
+
672
+ with pg_col2:
673
+ v_col1, v_col2, v_col3 = st.columns([0.33, 0.33, 0.33])
674
+ view_edit_test(v_col1, selected_row["test_definition_id_current"])
675
+ if selected_row["test_scope"] == "column":
676
+ view_profiling_modal(
677
+ v_col2, selected_row["table_name"], selected_row["column_names"],
678
+ str_table_groups_id=selected_row["table_groups_id"]
679
+ )
680
+ view_bad_data(v_col3, selected_row)
681
+
682
+ with pg_col1:
683
+ fm.show_subheader(selected_row["test_name_short"])
684
+ st.markdown(f"###### {selected_row['test_description']}")
685
+ st.caption(empty_if_null(selected_row["measure_uom_description"]))
686
+ fm.render_grid_select(dfh, show_hist_columns)
687
+ with pg_col2:
688
+ ut_tab1, ut_tab2 = st.tabs(["History", "Test Definition"])
689
+ with ut_tab1:
690
+ if dfh.empty:
691
+ st.write("Test history not available.")
692
+ else:
693
+ write_history_graph(dfh)
694
+ with ut_tab2:
695
+ show_test_def_detail(selected_row["test_definition_id_current"])
696
+ return selected_rows
697
+
698
+
699
+ def write_summary_graph(df_sum):
700
+ df_graph = df_sum[["passed_ct", "error_ct", "warning_ct", "failed_ct"]]
701
+
702
+ str_error_caption = f"Errors: {df_sum.at[0, 'error_ct']}, " if df_sum.at[0, "error_ct"] > 0 else ""
703
+ str_graph_caption = f"<i>Passed: {df_sum.at[0, 'passed_ct']} ({df_sum.at[0, 'passed_pct']}%), {str_error_caption}Warnings: {df_sum.at[0, 'warning_ct']} ({df_sum.at[0, 'warning_pct']}%), Failed: {df_sum.at[0, 'failed_ct']} ({df_sum.at[0, 'failed_pct']}%)</i>"
704
+
705
+ fig = px.bar(
706
+ df_graph,
707
+ orientation="h",
708
+ title=None,
709
+ # labels={'value': 'Tests', 'variable': 'Result Status'},
710
+ color_discrete_sequence=["green", "gray", "yellow", "red"],
711
+ barmode="stack",
712
+ )
713
+
714
+ fig.update_traces(
715
+ # hoverinfo='y+name', # Display the y value and the trace name
716
+ # hovertemplate='Count: %{y}<br>Type: %{name}', # Custom template for hover text
717
+ hovertemplate="%{x}"
718
+ # hovertemplate=None
719
+ )
720
+
721
+ fig.update_layout(
722
+ showlegend=False,
723
+ legend_orientation="h",
724
+ legend_y=-0.2, # This value might need to be adjusted based on other chart elements
725
+ legend_x=0.5,
726
+ legend_xanchor="right",
727
+ legend_title_text="",
728
+ yaxis={
729
+ "showticklabels": False, # hides y-axis labels
730
+ "showgrid": False, # removes grid lines
731
+ "zeroline": False, # removes the zero line
732
+ "showline": False, # hides the axis line
733
+ "title_text": "",
734
+ },
735
+ xaxis={
736
+ "showticklabels": False, # hides y-axis labels
737
+ "showgrid": False, # removes grid lines
738
+ "zeroline": False, # removes the zero line
739
+ "showline": False, # hides the axis line
740
+ "title_text": "",
741
+ },
742
+ hovermode="closest",
743
+ height=100,
744
+ width=800,
745
+ margin={"l": 0, "r": 10, "b": 10, "t": 10}, # adjust margins around the plot
746
+ paper_bgcolor="rgba(0,0,0,0)",
747
+ plot_bgcolor="rgba(0,0,0,0)",
748
+ )
749
+
750
+ fig.add_annotation(
751
+ text=str_graph_caption,
752
+ xref="paper",
753
+ yref="paper", # 'paper' coordinates are relative to the layout, with (0,0) at the bottom left and (1,1) at the top right
754
+ x=0,
755
+ y=0,
756
+ xanchor="left",
757
+ yanchor="top",
758
+ showarrow=False,
759
+ font={"size": 15, "color": "black"},
760
+ )
761
+
762
+ config = {"displayModeBar": False}
763
+ st.plotly_chart(fig, config=config)
764
+
765
+
766
+ def write_history_graph(dfh):
767
+ y_min = min(dfh["result_measure"].min(), dfh["threshold_value"].min())
768
+ y_max = max(dfh["result_measure"].max(), dfh["threshold_value"].max())
769
+ str_uom = dfh.at[0, "measure_uom"]
770
+
771
+ fig = px.line(
772
+ dfh,
773
+ x="test_date",
774
+ y="result_measure",
775
+ title=None,
776
+ labels={"test_date": "Test Date", "result_measure": str_uom},
777
+ line_shape="linear",
778
+ )
779
+
780
+ # Add dots at every observation
781
+ fig.add_scatter(x=dfh["test_date"], y=dfh["result_measure"], mode="markers", name="Observations")
782
+
783
+ if all(dfh["test_operator"].isin(["<", "<="])):
784
+ # Add shaded region below: exception if under threshold
785
+ fig.add_trace(
786
+ go.Scatter(
787
+ x=dfh["test_date"],
788
+ y=dfh["threshold_value"],
789
+ fill="tozeroy",
790
+ fillcolor="rgba(255,182,193,0.5)",
791
+ line_color="rgba(255,182,193,0.5)",
792
+ mode="none",
793
+ name="Threshold",
794
+ )
795
+ )
796
+ elif all(dfh["test_operator"].isin([">", ">="])):
797
+ # Add shaded region above: exception if over threshold
798
+ fig.add_trace(
799
+ go.Scatter(
800
+ x=dfh["test_date"],
801
+ y=[max(dfh["threshold_value"]) * 1.1] * len(dfh["test_date"]), # some value above the maximum threshold
802
+ mode="lines",
803
+ line={"width": 0}, # making this line invisible
804
+ showlegend=False,
805
+ )
806
+ )
807
+
808
+ # Now, fill between this auxiliary line and the threshold line
809
+ fig.add_trace(
810
+ go.Scatter(
811
+ x=dfh["test_date"],
812
+ y=dfh["threshold_value"],
813
+ fill="tonexty",
814
+ fillcolor="rgba(255,182,193,0.5)",
815
+ line_color="rgba(255,182,193,0.5)",
816
+ mode="none",
817
+ name="Threshold",
818
+ )
819
+ )
820
+ elif all(dfh["test_operator"].isin(["=", "<>"])):
821
+ # Show line instead of shaded region: pink/exception if equal, green/exception if not equal
822
+ str_line_color = "rgba(255,182,193,0.5)" if all(dfh["test_operator"]) == "=" else "rgba(144, 238, 144, 1)"
823
+ fig.add_trace(
824
+ go.Scatter(
825
+ x=dfh["test_date"],
826
+ y=dfh["threshold_value"],
827
+ line_color=str_line_color,
828
+ mode="lines", # only lines, no markers
829
+ line={"width": 5},
830
+ name="Threshold",
831
+ )
832
+ )
833
+ # Update the Y-Axis to start from the minimum value
834
+
835
+ if y_min > 0 and y_max - y_min < 0.1 * y_max:
836
+ fig.update_layout(yaxis={"range": [y_min, y_max]})
837
+
838
+ fig.update_layout(legend={"x": 0.5, "y": 1.1, "xanchor": "center", "yanchor": "top", "orientation": "h"})
839
+ fig.update_layout(width=500, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)")
840
+
841
+ st.plotly_chart(fig)
842
+
843
+
844
+ def do_disposition_update(selected, str_new_status):
845
+ str_result = None
846
+ if selected:
847
+ if len(selected) > 1:
848
+ str_which = f"of {len(selected)} results to {str_new_status}"
849
+ elif len(selected) == 1:
850
+ str_which = f"of one result to {str_new_status}"
851
+
852
+ str_schema = st.session_state["dbschema"]
853
+ if not dq.update_result_disposition(selected, str_schema, str_new_status):
854
+ str_result = f":red[**The update {str_which} did not succeed.**]"
855
+
856
+ return str_result
857
+
858
+
859
+ def view_bad_data(button_container, selected_row):
860
+ str_header = f"Column: {selected_row['column_names']}, Table: {selected_row['table_name']}"
861
+ bad_data_modal = testgen.Modal(title=None, key="dk-test-data-modal", max_width=1100)
862
+
863
+ with button_container:
864
+ if st.button(
865
+ ":green[Source Data →]", help="Review current source data for highlighted result", use_container_width=True
866
+ ):
867
+ bad_data_modal.open()
868
+
869
+ if bad_data_modal.is_open():
870
+ with bad_data_modal.container():
871
+ fm.render_modal_header(selected_row["test_name_short"], None)
872
+ st.caption(selected_row["test_description"])
873
+ fm.show_prompt(str_header)
874
+
875
+ # Show detail
876
+ fm.render_html_list(
877
+ selected_row, ["input_parameters", "result_message"], None, 700, ["Test Parameters", "Result Detail"]
878
+ )
879
+
880
+ with st.spinner("Retrieving source data..."):
881
+ if selected_row["test_type"] == "CUSTOM":
882
+ bad_data_status, bad_data_msg, df_bad = do_source_data_lookup_custom(selected_row)
883
+ else:
884
+ bad_data_status, bad_data_msg, df_bad = do_source_data_lookup(selected_row)
885
+ if bad_data_status in {"ND", "NA"}:
886
+ st.info(bad_data_msg)
887
+ elif bad_data_status == "ERR":
888
+ st.error(bad_data_msg)
889
+ elif df_bad is None:
890
+ st.error("An unknown error was encountered.")
891
+ else:
892
+ if bad_data_msg:
893
+ st.info(bad_data_msg)
894
+ # Pretify the dataframe
895
+ df_bad.columns = [col.replace("_", " ").title() for col in df_bad.columns]
896
+ df_bad.fillna("[NULL]", inplace=True)
897
+ # Display the dataframe
898
+ st.dataframe(df_bad, height=500, width=1050, hide_index=True)
899
+
900
+
901
+ def view_edit_test(button_container, test_definition_id):
902
+ edit_test_definition_modal = testgen.Modal(title=None, key="dk-test-definition-edit-modal", max_width=1100)
903
+ with button_container:
904
+ if st.button("🖊️ Edit Test", help="Edit the Test Definition", use_container_width=True):
905
+ edit_test_definition_modal.open()
906
+
907
+ if edit_test_definition_modal.is_open():
908
+ show_add_edit_modal_by_test_definition(edit_test_definition_modal, test_definition_id)