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.
- dataops_testgen-2.2.0.dist-info/LICENSE +203 -0
- dataops_testgen-2.2.0.dist-info/METADATA +287 -0
- dataops_testgen-2.2.0.dist-info/NOTICE +5 -0
- dataops_testgen-2.2.0.dist-info/RECORD +270 -0
- dataops_testgen-2.2.0.dist-info/WHEEL +5 -0
- dataops_testgen-2.2.0.dist-info/entry_points.txt +2 -0
- dataops_testgen-2.2.0.dist-info/top_level.txt +1 -0
- testgen/__init__.py +0 -0
- testgen/__main__.py +770 -0
- testgen/commands/__init__.py +0 -0
- testgen/commands/queries/__init__.py +0 -0
- testgen/commands/queries/execute_cat_tests_query.py +95 -0
- testgen/commands/queries/execute_tests_query.py +160 -0
- testgen/commands/queries/generate_tests_query.py +94 -0
- testgen/commands/queries/profiling_query.py +366 -0
- testgen/commands/queries/test_parameter_validation_query.py +88 -0
- testgen/commands/run_execute_cat_tests.py +162 -0
- testgen/commands/run_execute_tests.py +168 -0
- testgen/commands/run_generate_tests.py +107 -0
- testgen/commands/run_get_entities.py +122 -0
- testgen/commands/run_launch_db_config.py +84 -0
- testgen/commands/run_observability_exporter.py +330 -0
- testgen/commands/run_profiling_bridge.py +495 -0
- testgen/commands/run_quick_start.py +168 -0
- testgen/commands/run_setup_profiling_tools.py +96 -0
- testgen/commands/run_test_definition.py +146 -0
- testgen/commands/run_test_parameter_validation.py +135 -0
- testgen/commands/run_upgrade_db_config.py +156 -0
- testgen/common/__init__.py +8 -0
- testgen/common/clean_sql.py +53 -0
- testgen/common/credentials.py +25 -0
- testgen/common/database/__init__.py +0 -0
- testgen/common/database/database_service.py +629 -0
- testgen/common/database/flavor/__init__.py +0 -0
- testgen/common/database/flavor/flavor_service.py +75 -0
- testgen/common/database/flavor/mssql_flavor_service.py +34 -0
- testgen/common/database/flavor/postgresql_flavor_service.py +5 -0
- testgen/common/database/flavor/redshift_flavor_service.py +22 -0
- testgen/common/database/flavor/snowflake_flavor_service.py +69 -0
- testgen/common/database/flavor/trino_flavor_service.py +21 -0
- testgen/common/date_service.py +68 -0
- testgen/common/display_service.py +85 -0
- testgen/common/docker_service.py +76 -0
- testgen/common/encrypt.py +55 -0
- testgen/common/get_pipeline_parms.py +57 -0
- testgen/common/logs.py +79 -0
- testgen/common/process_service.py +62 -0
- testgen/common/read_file.py +69 -0
- testgen/settings.py +440 -0
- testgen/template/dbsetup/010_create_base_schema.sql +2 -0
- testgen/template/dbsetup/020_create_standard_functions_sprocs.sql +179 -0
- testgen/template/dbsetup/030_initialize_new_schema_structure.sql +735 -0
- testgen/template/dbsetup/040_populate_new_schema_project.sql +59 -0
- testgen/template/dbsetup/050_populate_new_schema_metadata.sql +1517 -0
- testgen/template/dbsetup/060_create_standard_views.sql +248 -0
- testgen/template/dbsetup/070_create_default_users.sql +17 -0
- testgen/template/dbsetup/075_grant_role_rights.sql +43 -0
- testgen/template/dbsetup/080_set_current_revision.sql +5 -0
- testgen/template/dbupgrade/0100_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0101_incremental_upgrade.sql +15 -0
- testgen/template/dbupgrade/0102_incremental_upgrade.sql +4 -0
- testgen/template/dbupgrade/0103_incremental_upgrade.sql +22 -0
- testgen/template/dbupgrade/0104_incremental_upgrade.sql +44 -0
- testgen/template/dbupgrade/0105_incremental_upgrade.sql +1 -0
- testgen/template/dbupgrade/0106_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0107_incremental_upgrade.sql +3 -0
- testgen/template/dbupgrade_helpers/get_tg_revision.sql +2 -0
- testgen/template/exec_cat_tests/ex_cat_build_agg_table_tests.sql +116 -0
- testgen/template/exec_cat_tests/ex_cat_get_distinct_tables.sql +11 -0
- testgen/template/exec_cat_tests/ex_cat_results_parse.sql +69 -0
- testgen/template/exec_cat_tests/ex_cat_retrieve_agg_test_parms.sql +6 -0
- testgen/template/exec_cat_tests/ex_cat_test_query.sql +8 -0
- testgen/template/execution/ex_finalize_test_run_results.sql +37 -0
- testgen/template/execution/ex_get_tests_non_cat.sql +47 -0
- testgen/template/execution/ex_update_test_record_in_testrun_table.sql +27 -0
- testgen/template/execution/ex_write_test_record_to_testrun_table.sql +6 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_no_drops_generic.sql +48 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_num_incr_generic.sql +34 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_above_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_within_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_same_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_custom_query_generic.sql +39 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_2way_generic.sql +58 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_generic.sql +44 -0
- testgen/template/flavors/generic/exec_query_tests/ex_prior_match_generic.sql +37 -0
- testgen/template/flavors/generic/exec_query_tests/ex_relative_entropy_generic.sql +53 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_no_drops_generic.sql +46 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_same_generic.sql +59 -0
- testgen/template/flavors/generic/profiling/contingency_counts.sql +3 -0
- testgen/template/flavors/generic/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/flavors/mssql/exec_query_tests/ex_relative_entropy_mssql.sql +53 -0
- testgen/template/flavors/mssql/profiling/project_ddf_query_mssql.sql +35 -0
- testgen/template/flavors/mssql/profiling/project_profiling_query_mssql.yaml +246 -0
- testgen/template/flavors/mssql/profiling/project_secondary_profiling_query_mssql.sql +36 -0
- testgen/template/flavors/mssql/setup_profiling_tools/00_drop_existing_functions_mssql.sql +8 -0
- testgen/template/flavors/mssql/setup_profiling_tools/01_create_functions_mssql.sql +12 -0
- testgen/template/flavors/mssql/setup_profiling_tools/02_create_functions_mssql.sql +54 -0
- testgen/template/flavors/mssql/setup_profiling_tools/create_qc_schema_mssql.sql +4 -0
- testgen/template/flavors/mssql/setup_profiling_tools/grant_execute_privileges_mssql.sql +1 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_no_drops_postgresql.sql +46 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_same_postgresql.sql +59 -0
- testgen/template/flavors/postgresql/profiling/project_ddf_query_postgresql.sql +42 -0
- testgen/template/flavors/postgresql/profiling/project_profiling_query_postgresql.yaml +225 -0
- testgen/template/flavors/postgresql/profiling/project_secondary_profiling_query_postgresql.sql +28 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_functions_postgresql.sql +157 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_qc_schema_postgresql.sql +1 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/grant_execute_privileges_postgresql.sql +2 -0
- testgen/template/flavors/redshift/profiling/project_ddf_query_redshift.sql +38 -0
- testgen/template/flavors/redshift/profiling/project_profiling_query_redshift.yaml +221 -0
- testgen/template/flavors/redshift/profiling/project_secondary_profiling_query_redshift.sql +29 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_functions_redshift.sql +115 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_qc_schema_redshift.sql +1 -0
- testgen/template/flavors/redshift/setup_profiling_tools/grant_execute_privileges_redshift.sql +2 -0
- testgen/template/flavors/snowflake/profiling/project_ddf_query_snowflake.sql +38 -0
- testgen/template/flavors/snowflake/profiling/project_profiling_query_snowflake.yaml +220 -0
- testgen/template/flavors/snowflake/profiling/project_secondary_profiling_query_snowflake.sql +29 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_functions_snowflake.sql +69 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_qc_schema_snowflake.sql +1 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/grant_execute_privileges_snowflake.sql +6 -0
- testgen/template/flavors/trino/profiling/project_profiling_query_trino.yaml +219 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_functions_trino.sql +92 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_qc_schema_trino.sql +1 -0
- testgen/template/gen_funny_cat_tests/gen_test_constant.sql +104 -0
- testgen/template/gen_funny_cat_tests/gen_test_distinct_value_ct.sql +98 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct.sql +57 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct_pct.sql +59 -0
- testgen/template/generation/gen_delete_old_tests.sql +5 -0
- testgen/template/generation/gen_insert_test_suite.sql +5 -0
- testgen/template/generation/gen_retrieve_or_insert_test_suite.sql +58 -0
- testgen/template/generation/gen_standard_test_type_list.sql +13 -0
- testgen/template/generation/gen_standard_tests.sql +48 -0
- testgen/template/get_entities/get_connection.sql +21 -0
- testgen/template/get_entities/get_connections_list.sql +9 -0
- testgen/template/get_entities/get_latest.sql +4 -0
- testgen/template/get_entities/get_profile.sql +12 -0
- testgen/template/get_entities/get_profile_info.sql +17 -0
- testgen/template/get_entities/get_profile_list.sql +17 -0
- testgen/template/get_entities/get_profile_screen.sql +275 -0
- testgen/template/get_entities/get_project_list.sql +6 -0
- testgen/template/get_entities/get_table_group_list.sql +10 -0
- testgen/template/get_entities/get_test_generation_list.sql +18 -0
- testgen/template/get_entities/get_test_info.sql +41 -0
- testgen/template/get_entities/get_test_results_for_run_cli.sql +16 -0
- testgen/template/get_entities/get_test_run_list.sql +24 -0
- testgen/template/get_entities/get_test_suite.sql +13 -0
- testgen/template/get_entities/get_test_suite_list.sql +18 -0
- testgen/template/get_entities/list_test_types.sql +4 -0
- testgen/template/observability/get_event_data.sql +23 -0
- testgen/template/observability/get_test_results.sql +41 -0
- testgen/template/observability/update_test_results_exported_to_observability.sql +12 -0
- testgen/template/parms/parms_profiling.sql +34 -0
- testgen/template/parms/parms_test_execution.sql +13 -0
- testgen/template/parms/parms_test_gen.sql +23 -0
- testgen/template/profiling/contingency_columns.sql +7 -0
- testgen/template/profiling/datatype_suggestions.sql +56 -0
- testgen/template/profiling/functional_datatype.sql +523 -0
- testgen/template/profiling/functional_tabletype_stage.sql +48 -0
- testgen/template/profiling/functional_tabletype_update.sql +8 -0
- testgen/template/profiling/pii_flag.sql +133 -0
- testgen/template/profiling/profile_anomalies_screen_column.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_multi_column.sql +58 -0
- testgen/template/profiling/profile_anomalies_screen_table.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_table_dates.sql +30 -0
- testgen/template/profiling/profile_anomalies_screen_variants.sql +40 -0
- testgen/template/profiling/profile_anomaly_types_get.sql +3 -0
- testgen/template/profiling/project_get_table_sample_count.sql +22 -0
- testgen/template/profiling/project_profile_run_record_insert.sql +8 -0
- testgen/template/profiling/project_profile_run_record_update.sql +5 -0
- testgen/template/profiling/project_profile_run_record_update_status.sql +5 -0
- testgen/template/profiling/project_update_profile_results_to_estimates.sql +32 -0
- testgen/template/profiling/refresh_anomalies.sql +33 -0
- testgen/template/profiling/refresh_data_chars_from_profiling.sql +156 -0
- testgen/template/profiling/secondary_profiling_columns.sql +12 -0
- testgen/template/profiling/secondary_profiling_delete.sql +4 -0
- testgen/template/profiling/secondary_profiling_update.sql +18 -0
- testgen/template/quick_start/populate_target_data.sql +1077 -0
- testgen/template/quick_start/recreate_target_data_schema.sql +167 -0
- testgen/template/quick_start/update_target_data.sql +100 -0
- testgen/template/updates/create_tmp_test_definition.sql +19 -0
- testgen/template/updates/get_test_def_parms.sql +38 -0
- testgen/template/updates/populate_stg_test_definitions.sql +184 -0
- testgen/template/validate_tests/ex_disable_tests_test_definitions.sql +5 -0
- testgen/template/validate_tests/ex_flag_tests_test_definitions.sql +64 -0
- testgen/template/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/validate_tests/ex_get_test_column_list_tg.sql +65 -0
- testgen/template/validate_tests/ex_write_test_val_errors.sql +22 -0
- testgen/ui/__init__.py +0 -0
- testgen/ui/app.py +98 -0
- testgen/ui/assets/dk_logo.svg +46 -0
- testgen/ui/assets/question_mark.png +0 -0
- testgen/ui/assets/scripts.js +68 -0
- testgen/ui/assets/style.css +140 -0
- testgen/ui/bootstrap.py +109 -0
- testgen/ui/components/__init__.py +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu4mxK.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 +0 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.css +24 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.woff2 +0 -0
- testgen/ui/components/frontend/css/roboto-font-faces.css +35 -0
- testgen/ui/components/frontend/css/shared.css +36 -0
- testgen/ui/components/frontend/img/dk_logo.svg +46 -0
- testgen/ui/components/frontend/index.html +17 -0
- testgen/ui/components/frontend/js/components/breadcrumbs.js +86 -0
- testgen/ui/components/frontend/js/components/button.js +66 -0
- testgen/ui/components/frontend/js/components/location.js +62 -0
- testgen/ui/components/frontend/js/components/select.js +75 -0
- testgen/ui/components/frontend/js/components/sidebar.js +358 -0
- testgen/ui/components/frontend/js/main.js +99 -0
- testgen/ui/components/frontend/js/streamlit.js +19 -0
- testgen/ui/components/frontend/js/van.min.js +1 -0
- testgen/ui/components/utils/__init__.py +0 -0
- testgen/ui/components/utils/callbacks.py +51 -0
- testgen/ui/components/utils/component.py +13 -0
- testgen/ui/components/widgets/__init__.py +6 -0
- testgen/ui/components/widgets/breadcrumbs.py +32 -0
- testgen/ui/components/widgets/location.py +65 -0
- testgen/ui/components/widgets/modal.py +97 -0
- testgen/ui/components/widgets/sidebar.py +69 -0
- testgen/ui/navigation/__init__.py +0 -0
- testgen/ui/navigation/menu.py +42 -0
- testgen/ui/navigation/page.py +20 -0
- testgen/ui/navigation/router.py +63 -0
- testgen/ui/queries/__init__.py +0 -0
- testgen/ui/queries/authentication_queries.py +47 -0
- testgen/ui/queries/connection_queries.py +121 -0
- testgen/ui/queries/profiling_queries.py +148 -0
- testgen/ui/queries/project_queries.py +9 -0
- testgen/ui/queries/table_group_queries.py +186 -0
- testgen/ui/queries/test_definition_queries.py +270 -0
- testgen/ui/queries/test_run_queries.py +32 -0
- testgen/ui/queries/test_suite_queries.py +145 -0
- testgen/ui/scripts/__init__.py +0 -0
- testgen/ui/scripts/patch_streamlit.py +111 -0
- testgen/ui/services/__init__.py +0 -0
- testgen/ui/services/authentication_service.py +119 -0
- testgen/ui/services/connection_service.py +220 -0
- testgen/ui/services/database_service.py +282 -0
- testgen/ui/services/form_service.py +1008 -0
- testgen/ui/services/javascript_service.py +44 -0
- testgen/ui/services/query_service.py +316 -0
- testgen/ui/services/string_service.py +12 -0
- testgen/ui/services/table_group_service.py +130 -0
- testgen/ui/services/test_definition_service.py +117 -0
- testgen/ui/services/test_run_service.py +13 -0
- testgen/ui/services/test_suite_service.py +76 -0
- testgen/ui/services/toolbar_service.py +77 -0
- testgen/ui/session.py +46 -0
- testgen/ui/views/__init__.py +0 -0
- testgen/ui/views/app_log_modal.py +92 -0
- testgen/ui/views/connections.py +72 -0
- testgen/ui/views/connections_base.py +367 -0
- testgen/ui/views/login.py +40 -0
- testgen/ui/views/not_found.py +16 -0
- testgen/ui/views/overview.py +34 -0
- testgen/ui/views/profiling_anomalies.py +501 -0
- testgen/ui/views/profiling_details.py +335 -0
- testgen/ui/views/profiling_modal.py +40 -0
- testgen/ui/views/profiling_results.py +206 -0
- testgen/ui/views/profiling_summary.py +177 -0
- testgen/ui/views/project_settings.py +74 -0
- testgen/ui/views/table_groups.py +530 -0
- testgen/ui/views/test_definitions.py +1020 -0
- testgen/ui/views/test_results.py +908 -0
- testgen/ui/views/test_runs.py +195 -0
- testgen/ui/views/test_suites.py +545 -0
- testgen/utils/__init__.py +0 -0
- testgen/utils/plugins.py +17 -0
- 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)
|