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,1008 @@
|
|
|
1
|
+
# For render_logo
|
|
2
|
+
import base64
|
|
3
|
+
import typing
|
|
4
|
+
from builtins import float
|
|
5
|
+
from datetime import date, datetime, time
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from os.path import splitext
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from time import sleep
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import streamlit as st
|
|
14
|
+
import validators
|
|
15
|
+
from pandas.api.types import is_datetime64_any_dtype
|
|
16
|
+
from st_aggrid import AgGrid, ColumnsAutoSizeMode, DataReturnMode, GridOptionsBuilder, GridUpdateMode, JsCode
|
|
17
|
+
from streamlit_extras.no_default_selectbox import selectbox
|
|
18
|
+
|
|
19
|
+
import testgen.common.date_service as date_service
|
|
20
|
+
import testgen.ui.services.authentication_service as authentication_service
|
|
21
|
+
import testgen.ui.services.database_service as db
|
|
22
|
+
from testgen.ui.components import widgets as testgen
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
Shared rendering of UI elements
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
logo_file = (Path(__file__).parent.parent / "assets/dk_logo.svg").as_posix()
|
|
29
|
+
help_icon = (Path(__file__).parent.parent / "assets/question_mark.png").as_posix()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FormWidget(Enum):
|
|
33
|
+
text_md = 1
|
|
34
|
+
text_input = 2
|
|
35
|
+
text_area = 3
|
|
36
|
+
number_input = 4
|
|
37
|
+
selectbox = 5
|
|
38
|
+
date_input = 6
|
|
39
|
+
radio = 7
|
|
40
|
+
checkbox = 8
|
|
41
|
+
multiselect = 9 # TODO: implement
|
|
42
|
+
hidden = 99
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FieldSpec:
|
|
46
|
+
field_label = None
|
|
47
|
+
column_name = None
|
|
48
|
+
widget = None
|
|
49
|
+
value_original = None
|
|
50
|
+
init_value = None
|
|
51
|
+
display_only = False
|
|
52
|
+
required = False
|
|
53
|
+
key_order = 0
|
|
54
|
+
|
|
55
|
+
# Entry Options
|
|
56
|
+
max_chars = None
|
|
57
|
+
num_min = None
|
|
58
|
+
num_max = None
|
|
59
|
+
text_multi_lines = 3
|
|
60
|
+
|
|
61
|
+
# Selectbox Options
|
|
62
|
+
df_options = None
|
|
63
|
+
show_column_name = None
|
|
64
|
+
return_column_name = None
|
|
65
|
+
|
|
66
|
+
# Radio options
|
|
67
|
+
lst_option_text: typing.ClassVar = []
|
|
68
|
+
lst_option_values: typing.ClassVar = []
|
|
69
|
+
show_horizontal = True
|
|
70
|
+
|
|
71
|
+
value = None
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
str_label,
|
|
76
|
+
str_column_name,
|
|
77
|
+
form_widget,
|
|
78
|
+
orig_val=None,
|
|
79
|
+
init_val=None,
|
|
80
|
+
read_only=False,
|
|
81
|
+
required=False,
|
|
82
|
+
int_key=0,
|
|
83
|
+
max_chars=None,
|
|
84
|
+
num_min=None,
|
|
85
|
+
num_max=None,
|
|
86
|
+
text_multi_lines=3,
|
|
87
|
+
):
|
|
88
|
+
self.field_label = str_label
|
|
89
|
+
self.column_name = str_column_name
|
|
90
|
+
self.value_original = orig_val
|
|
91
|
+
self.init_value = init_val if init_val else orig_val
|
|
92
|
+
self.widget = form_widget
|
|
93
|
+
self.display_only = read_only
|
|
94
|
+
self.required = required
|
|
95
|
+
self.key_order = int_key
|
|
96
|
+
self.max_chars = max_chars
|
|
97
|
+
self.num_min = num_min
|
|
98
|
+
self.num_max = num_max
|
|
99
|
+
self.text_multi_lines = text_multi_lines
|
|
100
|
+
|
|
101
|
+
def set_select_choices(self, df_options, str_show_column_name, str_return_column_name):
|
|
102
|
+
if self.widget in [FormWidget.selectbox, FormWidget.multiselect]:
|
|
103
|
+
self.df_options = df_options
|
|
104
|
+
self.show_column_name = str_show_column_name
|
|
105
|
+
self.return_column_name = str_return_column_name
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(f"Can't set Select Choices for widget {self.widget}")
|
|
108
|
+
|
|
109
|
+
def render_widget(self, boo_form_display_only=False):
|
|
110
|
+
# if either form-level or field-level display-only is true, then widget is display-only
|
|
111
|
+
boo_display_only = boo_form_display_only or self.display_only
|
|
112
|
+
|
|
113
|
+
match self.widget:
|
|
114
|
+
case FormWidget.text_md:
|
|
115
|
+
st.markdown(f"**{self.field_label}**")
|
|
116
|
+
st.markdown(self.init_value)
|
|
117
|
+
|
|
118
|
+
case FormWidget.text_input:
|
|
119
|
+
self.value = st.text_input(
|
|
120
|
+
label=self.field_label, value=self.init_value, disabled=boo_display_only, max_chars=self.max_chars
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
case FormWidget.text_area:
|
|
124
|
+
box_height = 26 * self.text_multi_lines
|
|
125
|
+
self.value = st.text_area(
|
|
126
|
+
label=self.field_label,
|
|
127
|
+
value=self.init_value,
|
|
128
|
+
disabled=boo_display_only,
|
|
129
|
+
max_chars=self.max_chars,
|
|
130
|
+
height=box_height,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
case FormWidget.number_input:
|
|
134
|
+
self.value = st.number_input(
|
|
135
|
+
label=self.field_label,
|
|
136
|
+
value=self.init_value,
|
|
137
|
+
min_value=self.num_min,
|
|
138
|
+
max_value=self.num_max,
|
|
139
|
+
disabled=boo_display_only,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
case FormWidget.selectbox:
|
|
143
|
+
self.value = render_select(
|
|
144
|
+
self.field_label,
|
|
145
|
+
self.df_options,
|
|
146
|
+
self.show_column_name,
|
|
147
|
+
not self.return_column_name,
|
|
148
|
+
self.required,
|
|
149
|
+
self.init_value,
|
|
150
|
+
self.display_only,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
case FormWidget.date_input:
|
|
154
|
+
self.value = render_select_date(self.field_label, boo_disabled=boo_display_only)
|
|
155
|
+
|
|
156
|
+
case FormWidget.radio:
|
|
157
|
+
# If no init_value, or if init_value is None (NULL), the first value will be selected by default
|
|
158
|
+
self.value = render_radio(
|
|
159
|
+
self.field_label,
|
|
160
|
+
self.lst_option_text,
|
|
161
|
+
self.lst_option_values if self.lst_option_values else self.lst_option_text,
|
|
162
|
+
self.init_value,
|
|
163
|
+
boo_display_only,
|
|
164
|
+
self.show_horizontal,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
case FormWidget.checkbox:
|
|
168
|
+
self.value = render_checkbox(
|
|
169
|
+
self.field_label, self.lst_option_values, self.init_value, boo_display_only
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
case FormWidget.hidden:
|
|
173
|
+
self.value = self.init_value
|
|
174
|
+
|
|
175
|
+
case _:
|
|
176
|
+
raise ValueError(f"Widget {self.widget} is not supported.")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@st.cache_data(show_spinner=False)
|
|
180
|
+
def _generate_excel_export(
|
|
181
|
+
df_data, lst_export_columns, str_title=None, str_caption=None, lst_wrap_columns=None, lst_column_headers=None
|
|
182
|
+
):
|
|
183
|
+
if lst_export_columns:
|
|
184
|
+
# Filter the DataFrame to keep only the columns in lst_export_columns
|
|
185
|
+
df_to_export = df_data[lst_export_columns]
|
|
186
|
+
else:
|
|
187
|
+
lst_export_columns = list(df_data.columns)
|
|
188
|
+
df_to_export = df_data
|
|
189
|
+
|
|
190
|
+
dct_col_to_header = dict(zip(lst_export_columns, lst_column_headers, strict=True)) if lst_column_headers else None
|
|
191
|
+
|
|
192
|
+
if not str_title:
|
|
193
|
+
str_title = "TestGen Data Export"
|
|
194
|
+
start_row = 4 if str_caption else 3
|
|
195
|
+
|
|
196
|
+
# Create a BytesIO buffer to hold the Excel file
|
|
197
|
+
output = BytesIO()
|
|
198
|
+
|
|
199
|
+
# Create a Pandas Excel writer using XlsxWriter as the engine
|
|
200
|
+
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
|
201
|
+
# Write the DataFrame to an Excel file, starting from the fourth row
|
|
202
|
+
df_to_export.to_excel(writer, index=False, sheet_name="Sheet1", startrow=start_row)
|
|
203
|
+
|
|
204
|
+
# Access the XlsxWriter workbook and worksheet objects from the dataframe
|
|
205
|
+
workbook = writer.book
|
|
206
|
+
worksheet = writer.sheets["Sheet1"]
|
|
207
|
+
|
|
208
|
+
# Add table formatting
|
|
209
|
+
(max_row, max_col) = df_to_export.shape
|
|
210
|
+
if dct_col_to_header:
|
|
211
|
+
column_settings = [{"header": dct_col_to_header[column]} for column in df_to_export.columns]
|
|
212
|
+
else:
|
|
213
|
+
column_settings = [{"header": column} for column in df_to_export.columns]
|
|
214
|
+
worksheet.add_table(
|
|
215
|
+
start_row,
|
|
216
|
+
0,
|
|
217
|
+
max_row + start_row,
|
|
218
|
+
max_col - 1,
|
|
219
|
+
{"columns": column_settings, "style": "Table Style Medium 16"},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Define the format for wrapped text
|
|
223
|
+
wrap_format = workbook.add_format(
|
|
224
|
+
{
|
|
225
|
+
"text_wrap": True,
|
|
226
|
+
"valign": "top", # Align to the top to better display wrapped text
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
valign_format = workbook.add_format({"valign": "top"})
|
|
230
|
+
|
|
231
|
+
# Autofit the worksheet (before adding title or settingwrapped column width)
|
|
232
|
+
worksheet.set_column(0, 1000, None, valign_format)
|
|
233
|
+
worksheet.autofit()
|
|
234
|
+
|
|
235
|
+
# Set a fixed column width for wrapped columns and apply wrap format
|
|
236
|
+
approx_width = 60
|
|
237
|
+
for col_idx, column in enumerate(df_to_export[lst_export_columns].columns):
|
|
238
|
+
if column in lst_wrap_columns:
|
|
239
|
+
# Set column width and format for wrapping
|
|
240
|
+
worksheet.set_column(col_idx, col_idx, approx_width, wrap_format)
|
|
241
|
+
|
|
242
|
+
# Add a cell format for the title
|
|
243
|
+
title_format = workbook.add_format({"bold": True, "size": 14})
|
|
244
|
+
# Write the title in cell A2 with formatting
|
|
245
|
+
worksheet.write("A2", str_title, title_format)
|
|
246
|
+
|
|
247
|
+
if str_caption:
|
|
248
|
+
str_caption = str_caption.replace("{TIMESTAMP}", date_service.get_timezoned_now(st.session_state))
|
|
249
|
+
caption_format = workbook.add_format({"italic": True, "size": 9, "valign": "top"})
|
|
250
|
+
worksheet.write("A3", str_caption, caption_format)
|
|
251
|
+
|
|
252
|
+
# Rewind the buffer
|
|
253
|
+
output.seek(0)
|
|
254
|
+
|
|
255
|
+
# Return the Excel file
|
|
256
|
+
return output.getvalue()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def render_excel_export(
|
|
260
|
+
df, lst_export_columns, str_export_title=None, str_caption=None, lst_wrap_columns=None, lst_column_headers=None
|
|
261
|
+
):
|
|
262
|
+
# Set up the download button
|
|
263
|
+
st.download_button(
|
|
264
|
+
label=":blue[**⤓**]",
|
|
265
|
+
use_container_width=True,
|
|
266
|
+
help="Download to Excel",
|
|
267
|
+
data=_generate_excel_export(
|
|
268
|
+
df, lst_export_columns, str_export_title, str_caption, lst_wrap_columns, lst_column_headers
|
|
269
|
+
),
|
|
270
|
+
file_name=f"{str_export_title}.xlsx",
|
|
271
|
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def render_refresh_button(button_container):
|
|
276
|
+
with button_container:
|
|
277
|
+
do_refresh = st.button(":blue[**⟳**]", help="Refresh page data", use_container_width=False)
|
|
278
|
+
if do_refresh:
|
|
279
|
+
reset_post_updates("Refreshing page", True, True)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def show_prompt(str_prompt=None):
|
|
283
|
+
if str_prompt:
|
|
284
|
+
st.markdown(f":blue[{str_prompt}]")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def show_header(str_header=None):
|
|
288
|
+
if str_header:
|
|
289
|
+
st.header(f":green[{str_header}]")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def show_subheader(str_text=None):
|
|
293
|
+
if str_text:
|
|
294
|
+
st.subheader(f":green[{str_text}]")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _show_section_header(str_section_header=None):
|
|
298
|
+
if str_section_header:
|
|
299
|
+
st.markdown(f":green[**{str_section_header}**]")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def render_form_by_field_specs(
|
|
303
|
+
str_form_name, str_table_name, lst_field_specs, str_text_display=None, boo_display_only=False, str_caption=None
|
|
304
|
+
):
|
|
305
|
+
show_header(str_form_name)
|
|
306
|
+
|
|
307
|
+
if str_text_display:
|
|
308
|
+
layout_column_1, layout_column_2 = st.columns([0.7, 0.3])
|
|
309
|
+
else:
|
|
310
|
+
layout_column_1, layout_column_2 = st.columns([0.95, 0.05])
|
|
311
|
+
|
|
312
|
+
if str_text_display:
|
|
313
|
+
with layout_column_2:
|
|
314
|
+
st.markdown(str_text_display)
|
|
315
|
+
|
|
316
|
+
with layout_column_1:
|
|
317
|
+
# Render form
|
|
318
|
+
layout_container = st.container() if boo_display_only else st.form(str_form_name, clear_on_submit=True)
|
|
319
|
+
with layout_container:
|
|
320
|
+
if str_caption:
|
|
321
|
+
st.caption(f":green[{str_caption}]")
|
|
322
|
+
|
|
323
|
+
# Render all widgets
|
|
324
|
+
for field in lst_field_specs:
|
|
325
|
+
field.render_widget(boo_display_only)
|
|
326
|
+
|
|
327
|
+
submit = (
|
|
328
|
+
False
|
|
329
|
+
if boo_display_only
|
|
330
|
+
else st.form_submit_button("Save Changes", disabled=authentication_service.current_user_has_read_role())
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if submit and not boo_display_only:
|
|
334
|
+
# Process Results
|
|
335
|
+
changes = []
|
|
336
|
+
keys = []
|
|
337
|
+
|
|
338
|
+
# Construct SQL UPDATE statement based on the changed values
|
|
339
|
+
lst_field_specs_by_key = sorted(lst_field_specs, key=lambda x: x.key_order)
|
|
340
|
+
for field in lst_field_specs_by_key:
|
|
341
|
+
if field.key_order > 0:
|
|
342
|
+
keys.append(f"{field.column_name} = '{field.value}'")
|
|
343
|
+
elif not field.display_only and field.value is None and field.value_original is not None:
|
|
344
|
+
changes.append(f"{field.column_name} = NULL")
|
|
345
|
+
elif not field.display_only and field.value != field.value_original:
|
|
346
|
+
changes.append(f"{field.column_name} = '{field.value}'")
|
|
347
|
+
# If there are any changes, construct and run the SQL statement
|
|
348
|
+
if changes:
|
|
349
|
+
str_schema = st.session_state["dbschema"]
|
|
350
|
+
str_sql = (
|
|
351
|
+
f"UPDATE {str_schema}.{str_table_name} SET {', '.join(changes)} WHERE {' AND '.join(keys)};"
|
|
352
|
+
)
|
|
353
|
+
db.execute_sql(str_sql)
|
|
354
|
+
reset_post_updates("Changes have been saved.")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def ut_prettify_header(str_header, expand=False):
|
|
358
|
+
# First drop underscores and make title-case
|
|
359
|
+
str_new = str_header.replace("_", " ").title()
|
|
360
|
+
|
|
361
|
+
if expand:
|
|
362
|
+
# Second, expand abbreviaqtions
|
|
363
|
+
PRETTY_DICT = {
|
|
364
|
+
" Ct": " Count",
|
|
365
|
+
"Min ": "Minimum ",
|
|
366
|
+
"Max ": "Maximum ",
|
|
367
|
+
"Avg ": "Average ",
|
|
368
|
+
"Std ": "Standard ",
|
|
369
|
+
}
|
|
370
|
+
for old, new in PRETTY_DICT.items():
|
|
371
|
+
str_new = str_new.replace(old, new)
|
|
372
|
+
|
|
373
|
+
return str_new
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def reset_post_updates(str_message=None, as_toast=False, clear_cache=True, lst_cached_functions=None):
|
|
377
|
+
if str_message:
|
|
378
|
+
if as_toast:
|
|
379
|
+
st.toast(str_message)
|
|
380
|
+
else:
|
|
381
|
+
st.success(str_message)
|
|
382
|
+
sleep(0.5)
|
|
383
|
+
|
|
384
|
+
if clear_cache:
|
|
385
|
+
if lst_cached_functions:
|
|
386
|
+
for fcn in lst_cached_functions:
|
|
387
|
+
fcn.clear()
|
|
388
|
+
else:
|
|
389
|
+
st.cache_data.clear()
|
|
390
|
+
st.experimental_rerun()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def render_page_header(
|
|
394
|
+
str_page_title, str_help_link=None, str_description=None, lst_breadcrumbs=None, boo_show_refresh=False
|
|
395
|
+
):
|
|
396
|
+
hcol1, hcol2 = st.columns([9, 1])
|
|
397
|
+
hcol1.subheader(str_page_title, anchor=False)
|
|
398
|
+
if str_help_link:
|
|
399
|
+
with hcol2:
|
|
400
|
+
st.caption(" ")
|
|
401
|
+
render_icon_link(str_help_link)
|
|
402
|
+
st.write(
|
|
403
|
+
'<hr style="background-color: #21c354; margin-top: 0;'
|
|
404
|
+
' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">',
|
|
405
|
+
unsafe_allow_html=True,
|
|
406
|
+
)
|
|
407
|
+
if str_description:
|
|
408
|
+
st.caption(str_description)
|
|
409
|
+
|
|
410
|
+
if "last_page" in st.session_state:
|
|
411
|
+
if str_page_title != st.session_state["last_page"]:
|
|
412
|
+
st.cache_data.clear()
|
|
413
|
+
st.session_state["last_page"] = str_page_title
|
|
414
|
+
|
|
415
|
+
if lst_breadcrumbs:
|
|
416
|
+
if boo_show_refresh:
|
|
417
|
+
bcol1, bcol2, bcol3, _ = st.columns([875, 60, 60, 5])
|
|
418
|
+
render_refresh_button(bcol3)
|
|
419
|
+
else:
|
|
420
|
+
bcol1, bcol2, _ = st.columns([95, 4, 1])
|
|
421
|
+
with bcol1:
|
|
422
|
+
testgen.breadcrumbs(breadcrumbs=lst_breadcrumbs)
|
|
423
|
+
return bcol2
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def render_modal_header(str_title, str_help_link=None, str_prompt=None):
|
|
427
|
+
hcol1, hcol2 = st.columns([9, 1])
|
|
428
|
+
hcol1.markdown(f"#### {str_title}")
|
|
429
|
+
if str_help_link:
|
|
430
|
+
with hcol2:
|
|
431
|
+
st.caption(" ")
|
|
432
|
+
render_icon_link(str_help_link)
|
|
433
|
+
st.write(
|
|
434
|
+
'<hr style="background-color: #21c354; margin-top: 0;'
|
|
435
|
+
' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">',
|
|
436
|
+
unsafe_allow_html=True,
|
|
437
|
+
)
|
|
438
|
+
show_prompt(str_prompt)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def render_select(
|
|
442
|
+
str_label, df_options, str_show_column, str_return_column, boo_required=True, str_default=None, boo_disabled=False
|
|
443
|
+
):
|
|
444
|
+
# Assemble conditional arguments for selectbox
|
|
445
|
+
kwargs = {"label": str_label, "options": df_options[str_show_column], "disabled": boo_disabled}
|
|
446
|
+
if str_default:
|
|
447
|
+
# Conditionally select index based on index of default value
|
|
448
|
+
if str_default not in df_options[str_show_column].values:
|
|
449
|
+
message = f"Label: {str_label} - Option: {str_default} not available. Click the refresh button."
|
|
450
|
+
st.markdown(f":orange[{message}]")
|
|
451
|
+
else:
|
|
452
|
+
kwargs["index"] = int(df_options[df_options[str_show_column] == str_default].index[0])
|
|
453
|
+
str_choice_name = st.selectbox(**kwargs) if boo_required else selectbox(**kwargs)
|
|
454
|
+
# Assign return-value from selected show-value
|
|
455
|
+
if str_choice_name:
|
|
456
|
+
return df_options.loc[df_options[str_show_column] == str_choice_name, str_return_column].iloc[0]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def render_select_date(str_label, dt_min_date=None, dt_max_date=None, boo_disabled=False, dt_default=None):
|
|
460
|
+
dt_select = st.date_input(
|
|
461
|
+
label=str_label,
|
|
462
|
+
value=dt_default,
|
|
463
|
+
min_value=dt_min_date,
|
|
464
|
+
max_value=dt_max_date,
|
|
465
|
+
format="YYYY-MM-DD",
|
|
466
|
+
disabled=boo_disabled,
|
|
467
|
+
)
|
|
468
|
+
return dt_select
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def render_radio(
|
|
472
|
+
str_label, lst_option_text, lst_option_values=None, init_value=None, boo_disabled=False, boo_horizontal=True
|
|
473
|
+
):
|
|
474
|
+
if init_value:
|
|
475
|
+
# Lookup index for init value
|
|
476
|
+
i = next((i for i, x in enumerate(lst_option_values) if x == init_value), -1)
|
|
477
|
+
i = i if i > 0 else 0
|
|
478
|
+
else:
|
|
479
|
+
# If no init_value, or if init_value is None (NULL), the first value will be selected by default
|
|
480
|
+
i = 0
|
|
481
|
+
str_choice_text = st.radio(
|
|
482
|
+
str_label, options=lst_option_text, index=i, disabled=boo_disabled, horizontal=boo_horizontal
|
|
483
|
+
)
|
|
484
|
+
if lst_option_values:
|
|
485
|
+
# Lookup choice -- get value
|
|
486
|
+
i = next((i for i, x in enumerate(lst_option_text) if x == str_choice_text), -1)
|
|
487
|
+
val_select = lst_option_values[i]
|
|
488
|
+
else:
|
|
489
|
+
val_select = str_choice_text
|
|
490
|
+
|
|
491
|
+
return val_select
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def render_checkbox(str_label, lst_true_false_values, boo_init_state=False, boo_disabled=False):
|
|
495
|
+
boo_value = st.checkbox(str_label, boo_init_state, disabled=boo_disabled)
|
|
496
|
+
return lst_true_false_values[0] if boo_value else lst_true_false_values[1]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def render_html_list(dct_row, lst_columns, str_section_header=None, int_data_width=300, lst_labels=None):
|
|
500
|
+
# Renders sets of values as vertical markdown list
|
|
501
|
+
|
|
502
|
+
if str_section_header:
|
|
503
|
+
# Header
|
|
504
|
+
_show_section_header(str_section_header)
|
|
505
|
+
|
|
506
|
+
# Subtract the padding-left and right from the width
|
|
507
|
+
if int_data_width > 0:
|
|
508
|
+
int_data_width += -20
|
|
509
|
+
|
|
510
|
+
str_block = "block" if int_data_width == 0 else "inline-block"
|
|
511
|
+
|
|
512
|
+
str_markdown = """
|
|
513
|
+
<style>
|
|
514
|
+
.dk-field-label {
|
|
515
|
+
display: inline-block;
|
|
516
|
+
width: 180px;
|
|
517
|
+
vertical-align: top;
|
|
518
|
+
font-weight: bold;
|
|
519
|
+
}
|
|
520
|
+
.dk-text-value {
|
|
521
|
+
display: <<BLOCK>>;
|
|
522
|
+
width: <<WIDTH>>px;
|
|
523
|
+
background-color: var(--dk-text-value-background);
|
|
524
|
+
text-align: left;
|
|
525
|
+
font-family: 'Courier New', monospace;
|
|
526
|
+
padding-left: 10px;
|
|
527
|
+
padding-right: 10px;
|
|
528
|
+
box-sizing: border-box;
|
|
529
|
+
}
|
|
530
|
+
.dk-num-value {
|
|
531
|
+
display: <<BLOCK>>;
|
|
532
|
+
width: <<WIDTH>>px;
|
|
533
|
+
background-color: var(--dk-text-value-background);
|
|
534
|
+
text-align: right;
|
|
535
|
+
font-family: 'Courier New', monospace;
|
|
536
|
+
padding-left: 10px;
|
|
537
|
+
padding-right: 10px;
|
|
538
|
+
box-sizing: border-box;
|
|
539
|
+
}
|
|
540
|
+
</style>
|
|
541
|
+
"""
|
|
542
|
+
str_data_width = "100%" if int_data_width == 0 else str(int_data_width)
|
|
543
|
+
str_markdown = str_markdown.replace("<<WIDTH>>", str_data_width)
|
|
544
|
+
str_markdown = str_markdown.replace("<<BLOCK>>", str_block)
|
|
545
|
+
|
|
546
|
+
# Prep labels
|
|
547
|
+
if not lst_labels:
|
|
548
|
+
lst_labels = [ut_prettify_header(label, expand=True) for label in lst_columns]
|
|
549
|
+
|
|
550
|
+
for col, label in zip(lst_columns, lst_labels, strict=True):
|
|
551
|
+
str_use_class = "num" if type(dct_row[col]) is (int | float) else "text"
|
|
552
|
+
str_markdown += f"""<div><span class="dk-field-label">{label}</span><span class="dk-{str_use_class}-value">{dct_row[col]!s}</span></div>"""
|
|
553
|
+
|
|
554
|
+
with st.container():
|
|
555
|
+
st.markdown(str_markdown, unsafe_allow_html=True)
|
|
556
|
+
st.divider()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def render_markdown_list(dct_row, lst_columns, str_header=None):
|
|
560
|
+
# Renders sets of values as vertical markdown list
|
|
561
|
+
|
|
562
|
+
str_blank_line = "<br>" # chr(10) + chr(10)
|
|
563
|
+
|
|
564
|
+
if str_header:
|
|
565
|
+
# Header with extra line
|
|
566
|
+
str_markdown = f":green[**{str_header}**]" + str_blank_line
|
|
567
|
+
else:
|
|
568
|
+
str_markdown = ""
|
|
569
|
+
|
|
570
|
+
for col in lst_columns:
|
|
571
|
+
# Column: Value with extra line
|
|
572
|
+
str_markdown += f"**{ut_prettify_header(col)}**: `{dct_row[col]!s}`" + str_blank_line
|
|
573
|
+
|
|
574
|
+
# Drop last blank line
|
|
575
|
+
i = str_markdown.rfind(str_blank_line)
|
|
576
|
+
if i != -1:
|
|
577
|
+
str_markdown = str_markdown[:i]
|
|
578
|
+
|
|
579
|
+
with st.container():
|
|
580
|
+
st.markdown(str_markdown, unsafe_allow_html=True)
|
|
581
|
+
st.divider()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def render_markdown_table(df, lst_columns):
|
|
585
|
+
# Filter the DataFrame to include only the specified columns
|
|
586
|
+
|
|
587
|
+
df_filtered = df[lst_columns]
|
|
588
|
+
|
|
589
|
+
# Initialize markdown string
|
|
590
|
+
md_str = ""
|
|
591
|
+
# Add headers
|
|
592
|
+
headers = "|".join([f" {ut_prettify_header(col)} " for col in lst_columns])
|
|
593
|
+
md_str += f"|{headers}|\n"
|
|
594
|
+
# Add alignment row
|
|
595
|
+
alignments = []
|
|
596
|
+
for col in lst_columns:
|
|
597
|
+
if pd.api.types.is_numeric_dtype(df_filtered[col]):
|
|
598
|
+
alignments.append("---:")
|
|
599
|
+
else:
|
|
600
|
+
alignments.append(":---")
|
|
601
|
+
md_str += f"|{'|'.join(alignments)}|\n"
|
|
602
|
+
|
|
603
|
+
# Add rows
|
|
604
|
+
for _, row in df_filtered.iterrows():
|
|
605
|
+
row_str = []
|
|
606
|
+
for col in lst_columns:
|
|
607
|
+
if pd.api.types.is_numeric_dtype(df_filtered[col]):
|
|
608
|
+
row_str.append(f" {row[col]} ")
|
|
609
|
+
else:
|
|
610
|
+
row_str.append(f" {row[col]} ")
|
|
611
|
+
md_str += f"|{'|'.join(row_str)}|\n"
|
|
612
|
+
|
|
613
|
+
st.markdown(md_str)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def render_column_list(row_selected, lst_columns, str_prompt):
|
|
617
|
+
with st.container():
|
|
618
|
+
show_prompt(str_prompt)
|
|
619
|
+
|
|
620
|
+
for column in lst_columns:
|
|
621
|
+
column_type = type(row_selected[column])
|
|
622
|
+
if column_type is str:
|
|
623
|
+
st.text_input(label=ut_prettify_header(column), value=row_selected[column], disabled=True)
|
|
624
|
+
elif column_type is (int | float):
|
|
625
|
+
st.number_input(label=ut_prettify_header(column), value=row_selected[column], disabled=True)
|
|
626
|
+
elif column_type is (date | datetime):
|
|
627
|
+
st.date_input(label=ut_prettify_header(column), value=row_selected[column], disabled=True)
|
|
628
|
+
elif column_type is time:
|
|
629
|
+
st.time_input(label=ut_prettify_header(column), value=row_selected[column], disabled=True)
|
|
630
|
+
else:
|
|
631
|
+
st.text_input(label=ut_prettify_header(column), value=row_selected[column], disabled=True)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def render_grid_form(
|
|
635
|
+
str_form_name,
|
|
636
|
+
df_data,
|
|
637
|
+
str_table_name,
|
|
638
|
+
lst_key_columns,
|
|
639
|
+
lst_show_columns,
|
|
640
|
+
lst_disabled_columns,
|
|
641
|
+
lst_no_update_columns,
|
|
642
|
+
dct_hard_default_columns,
|
|
643
|
+
dct_column_config,
|
|
644
|
+
str_prompt=None,
|
|
645
|
+
):
|
|
646
|
+
show_header(str_form_name)
|
|
647
|
+
with st.form(str_form_name, clear_on_submit=True):
|
|
648
|
+
show_prompt(str_prompt)
|
|
649
|
+
df_edits = st.data_editor(
|
|
650
|
+
df_data,
|
|
651
|
+
column_order=lst_show_columns,
|
|
652
|
+
column_config=dct_column_config,
|
|
653
|
+
disabled=lst_disabled_columns,
|
|
654
|
+
num_rows="dynamic",
|
|
655
|
+
hide_index=True,
|
|
656
|
+
)
|
|
657
|
+
submit = st.form_submit_button("Save Changes", disabled=authentication_service.current_user_has_read_role())
|
|
658
|
+
if submit:
|
|
659
|
+
booStatus = db.apply_df_edits(
|
|
660
|
+
df_data, df_edits, str_table_name, lst_key_columns, lst_no_update_columns, dct_hard_default_columns
|
|
661
|
+
)
|
|
662
|
+
if booStatus:
|
|
663
|
+
reset_post_updates("Changes have been saved.")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def render_edit_form(
|
|
667
|
+
str_form_name,
|
|
668
|
+
row_selected,
|
|
669
|
+
str_table_name,
|
|
670
|
+
lst_show_columns,
|
|
671
|
+
lst_key_columns,
|
|
672
|
+
lst_disabled=None,
|
|
673
|
+
str_text_display=None,
|
|
674
|
+
form_unique_key: str | None = None,
|
|
675
|
+
):
|
|
676
|
+
show_header(str_form_name)
|
|
677
|
+
|
|
678
|
+
layout_column_1 = st.empty()
|
|
679
|
+
if str_text_display:
|
|
680
|
+
layout_column_1, layout_column_2 = st.columns([0.7, 0.3])
|
|
681
|
+
|
|
682
|
+
dct_mods = {}
|
|
683
|
+
if not lst_disabled:
|
|
684
|
+
lst_disabled = lst_key_columns
|
|
685
|
+
# Retrieve data types
|
|
686
|
+
row_selected.map(type)
|
|
687
|
+
|
|
688
|
+
if str_text_display:
|
|
689
|
+
with layout_column_2:
|
|
690
|
+
st.markdown(str_text_display)
|
|
691
|
+
|
|
692
|
+
with layout_column_1:
|
|
693
|
+
with st.form(form_unique_key or str_form_name, clear_on_submit=True):
|
|
694
|
+
for column, value in row_selected.items():
|
|
695
|
+
if column in lst_show_columns:
|
|
696
|
+
column_type = type(value)
|
|
697
|
+
if column_type is str:
|
|
698
|
+
dct_mods[column] = st.text_input(
|
|
699
|
+
label=ut_prettify_header(column),
|
|
700
|
+
value=row_selected[column],
|
|
701
|
+
disabled=(column in lst_disabled),
|
|
702
|
+
)
|
|
703
|
+
elif column_type in (int, float):
|
|
704
|
+
dct_mods[column] = st.number_input(
|
|
705
|
+
label=ut_prettify_header(column),
|
|
706
|
+
value=row_selected[column],
|
|
707
|
+
disabled=(column in lst_disabled),
|
|
708
|
+
)
|
|
709
|
+
elif column_type in (date, datetime, datetime.date):
|
|
710
|
+
dct_mods[column] = st.date_input(
|
|
711
|
+
label=ut_prettify_header(column),
|
|
712
|
+
value=row_selected[column],
|
|
713
|
+
disabled=(column in lst_disabled),
|
|
714
|
+
)
|
|
715
|
+
elif column_type is time:
|
|
716
|
+
dct_mods[column] = st.time_input(
|
|
717
|
+
label=ut_prettify_header(column),
|
|
718
|
+
value=row_selected[column],
|
|
719
|
+
disabled=(column in lst_disabled),
|
|
720
|
+
)
|
|
721
|
+
else:
|
|
722
|
+
dct_mods[column] = st.text_input(
|
|
723
|
+
label=ut_prettify_header(column),
|
|
724
|
+
value=row_selected[column],
|
|
725
|
+
disabled=(column in lst_disabled),
|
|
726
|
+
)
|
|
727
|
+
else:
|
|
728
|
+
# If Hidden, add directly to dct_mods for updates
|
|
729
|
+
dct_mods[column] = row_selected[column]
|
|
730
|
+
submit = st.form_submit_button("Save Changes", disabled=authentication_service.current_user_has_read_role())
|
|
731
|
+
|
|
732
|
+
if submit:
|
|
733
|
+
# Construct SQL UPDATE statement based on the changed columns
|
|
734
|
+
changes = []
|
|
735
|
+
keys = []
|
|
736
|
+
for col, val in dct_mods.items():
|
|
737
|
+
if col in lst_key_columns:
|
|
738
|
+
keys.append(f"{col} = {db.make_value_db_friendly(val)}")
|
|
739
|
+
if val != row_selected[col]:
|
|
740
|
+
changes.append(f"{col} = {db.make_value_db_friendly(val)}")
|
|
741
|
+
|
|
742
|
+
# If there are any changes, construct and run the SQL statement
|
|
743
|
+
if changes:
|
|
744
|
+
str_schema = st.session_state["dbschema"]
|
|
745
|
+
str_sql = (
|
|
746
|
+
f"UPDATE {str_schema}.{str_table_name} SET {', '.join(changes)} WHERE {' AND '.join(keys)};"
|
|
747
|
+
)
|
|
748
|
+
db.execute_sql(str_sql)
|
|
749
|
+
reset_post_updates("Changes have been saved.")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def render_insert_form(
|
|
753
|
+
str_form_name,
|
|
754
|
+
lst_columns,
|
|
755
|
+
str_table_name,
|
|
756
|
+
dct_default_values=None,
|
|
757
|
+
lst_hidden=None,
|
|
758
|
+
lst_disabled=None,
|
|
759
|
+
form_unique_key: str | None = None,
|
|
760
|
+
on_cancel=None,
|
|
761
|
+
):
|
|
762
|
+
show_header(str_form_name)
|
|
763
|
+
dct_mods = {}
|
|
764
|
+
|
|
765
|
+
with st.form(form_unique_key or str_form_name, clear_on_submit=True):
|
|
766
|
+
for column in lst_columns:
|
|
767
|
+
if column not in (lst_hidden or []):
|
|
768
|
+
val = "" if column not in (dct_default_values or []) else dct_default_values[column]
|
|
769
|
+
input_type_by_default_value = {
|
|
770
|
+
date: st.date_input,
|
|
771
|
+
}
|
|
772
|
+
is_disabled = column in (lst_disabled or [])
|
|
773
|
+
input_type = input_type_by_default_value.get(type(val), st.text_input)
|
|
774
|
+
|
|
775
|
+
dct_mods[column] = input_type(label=ut_prettify_header(column), value=val, disabled=is_disabled)
|
|
776
|
+
else:
|
|
777
|
+
dct_mods[column] = dct_default_values[column]
|
|
778
|
+
|
|
779
|
+
_, col1, col2 = st.columns([0.7, 0.1, 0.2])
|
|
780
|
+
with col2:
|
|
781
|
+
submit = st.form_submit_button("Insert Record", use_container_width=True)
|
|
782
|
+
if on_cancel:
|
|
783
|
+
with col1:
|
|
784
|
+
st.form_submit_button("Cancel", on_click=on_cancel, use_container_width=True)
|
|
785
|
+
|
|
786
|
+
if submit:
|
|
787
|
+
str_schema = st.session_state["dbschema"]
|
|
788
|
+
# Construct SQL INSERT statement based on all columns
|
|
789
|
+
insert_cols = []
|
|
790
|
+
insert_vals = []
|
|
791
|
+
for col, val in dct_mods.items():
|
|
792
|
+
insert_cols.append(col)
|
|
793
|
+
insert_vals.append(f"'{val}'")
|
|
794
|
+
str_sql = f"INSERT INTO {str_schema}.{str_table_name} ({', '.join(insert_cols)}) VALUES ({', '.join(insert_vals)})"
|
|
795
|
+
db.execute_sql(str_sql)
|
|
796
|
+
reset_post_updates("New record created.")
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def render_grid_select(
|
|
800
|
+
df,
|
|
801
|
+
show_columns,
|
|
802
|
+
str_prompt=None,
|
|
803
|
+
int_height=400,
|
|
804
|
+
do_multi_select=False,
|
|
805
|
+
show_column_headers=None,
|
|
806
|
+
render_highlights=True,
|
|
807
|
+
):
|
|
808
|
+
show_prompt(str_prompt)
|
|
809
|
+
|
|
810
|
+
# Set grid formatting
|
|
811
|
+
cellstyle_jscode = JsCode(
|
|
812
|
+
"""
|
|
813
|
+
function(params) {
|
|
814
|
+
let style = {
|
|
815
|
+
'text-align': 'center',
|
|
816
|
+
'vertical-align': 'middle',
|
|
817
|
+
'border': '2px solid',
|
|
818
|
+
'borderRadius': '15px',
|
|
819
|
+
'display': 'inline-block'
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (['Failed', 'Error'].includes(params.value)) {
|
|
823
|
+
style.color = 'black';
|
|
824
|
+
style.borderColor = 'mistyrose';
|
|
825
|
+
style.backgroundColor = "mistyrose";
|
|
826
|
+
style.fontWeight = 'bolder';
|
|
827
|
+
return style;
|
|
828
|
+
} else if (params.value === 'Warning') {
|
|
829
|
+
style.color = 'black';
|
|
830
|
+
style.borderColor = 'seashell';
|
|
831
|
+
style.backgroundColor = "seashell";
|
|
832
|
+
return style;
|
|
833
|
+
} else if (params.value === 'Passed') {
|
|
834
|
+
style.color = 'black';
|
|
835
|
+
style.borderColor = 'honeydew';
|
|
836
|
+
style.backgroundColor = "honeydew";
|
|
837
|
+
return style;
|
|
838
|
+
} else if (params.value === '✓') {
|
|
839
|
+
return {
|
|
840
|
+
// 'color': 'green',
|
|
841
|
+
'text-align' : 'center',
|
|
842
|
+
'fontWeight' : 'bolder',
|
|
843
|
+
'fontSize' : "1.2em",
|
|
844
|
+
};
|
|
845
|
+
} else if (params.value === '✘') {
|
|
846
|
+
return {
|
|
847
|
+
// 'color': 'red',
|
|
848
|
+
'text-align' : 'center',
|
|
849
|
+
'fontWeight' : 'bolder',
|
|
850
|
+
'fontSize' : "1.2em",
|
|
851
|
+
};
|
|
852
|
+
} else if (params.value === '🚫') {
|
|
853
|
+
return {
|
|
854
|
+
'text-align' : 'center',
|
|
855
|
+
'fontWeight' : 'bolder',
|
|
856
|
+
'fontSize' : "1.2em",
|
|
857
|
+
};
|
|
858
|
+
} else if (params.value === '🔇') {
|
|
859
|
+
return {
|
|
860
|
+
'text-align' : 'center',
|
|
861
|
+
// 'fontWeight' : 'bolder',
|
|
862
|
+
'fontSize' : "1.2em",
|
|
863
|
+
};
|
|
864
|
+
} else if (params.value === '⌀') {
|
|
865
|
+
return {
|
|
866
|
+
'color': 'gray',
|
|
867
|
+
'text-align' : 'center',
|
|
868
|
+
'fontWeight' : 'bolder',
|
|
869
|
+
'fontSize' : "1.2em",
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
"""
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
dct_col_to_header = dict(zip(show_columns, show_column_headers, strict=True)) if show_column_headers else None
|
|
877
|
+
|
|
878
|
+
gb = GridOptionsBuilder.from_dataframe(df)
|
|
879
|
+
selection_mode = "multiple" if do_multi_select else "single"
|
|
880
|
+
gb.configure_selection(selection_mode=selection_mode, use_checkbox=do_multi_select)
|
|
881
|
+
|
|
882
|
+
all_columns = list(df.columns)
|
|
883
|
+
|
|
884
|
+
for column in all_columns:
|
|
885
|
+
# Define common kwargs for all columns: NOTE THAT FIRST COLUMN HOLDS CHECKBOX AND SHOULD BE SHOWN!
|
|
886
|
+
str_header = dct_col_to_header.get(column) if dct_col_to_header else None
|
|
887
|
+
common_kwargs = {
|
|
888
|
+
"field": column,
|
|
889
|
+
"header_name": str_header if str_header else ut_prettify_header(column),
|
|
890
|
+
"hide": column not in show_columns,
|
|
891
|
+
"headerCheckboxSelection": do_multi_select and column == show_columns[0],
|
|
892
|
+
"headerCheckboxSelectionFilteredOnly": do_multi_select and column == show_columns[0],
|
|
893
|
+
}
|
|
894
|
+
highlight_kwargs = {"cellStyle": cellstyle_jscode}
|
|
895
|
+
|
|
896
|
+
# Check if the column is a date-time column
|
|
897
|
+
if is_datetime64_any_dtype(df[column]):
|
|
898
|
+
if (df[column].dt.time == pd.Timestamp("00:00:00").time()).all():
|
|
899
|
+
format_string = "yyyy-MM-dd"
|
|
900
|
+
else:
|
|
901
|
+
format_string = "yyyy-MM-dd HH:mm"
|
|
902
|
+
# Additional kwargs for date-time columns
|
|
903
|
+
date_time_kwargs = {"type": ["customDateTimeFormat"], "custom_format_string": format_string}
|
|
904
|
+
|
|
905
|
+
# Merge common and date-time specific kwargs
|
|
906
|
+
all_kwargs = {**common_kwargs, **date_time_kwargs}
|
|
907
|
+
else:
|
|
908
|
+
if render_highlights == True:
|
|
909
|
+
# Merge common and highlight-specific kwargs
|
|
910
|
+
all_kwargs = {**common_kwargs, **highlight_kwargs}
|
|
911
|
+
else:
|
|
912
|
+
all_kwargs = common_kwargs
|
|
913
|
+
|
|
914
|
+
# Apply configuration using kwargs
|
|
915
|
+
gb.configure_column(**all_kwargs)
|
|
916
|
+
|
|
917
|
+
grid_options = gb.build()
|
|
918
|
+
|
|
919
|
+
# Render Grid: custom_css fixes spacing bug and tightens empty space at top of grid
|
|
920
|
+
grid_data = AgGrid(
|
|
921
|
+
df,
|
|
922
|
+
gridOptions=grid_options,
|
|
923
|
+
theme="balham",
|
|
924
|
+
enable_enterprise_modules=False,
|
|
925
|
+
allow_unsafe_jscode=True,
|
|
926
|
+
update_mode=GridUpdateMode.SELECTION_CHANGED,
|
|
927
|
+
data_return_mode=DataReturnMode.FILTERED_AND_SORTED,
|
|
928
|
+
columns_auto_size_mode=ColumnsAutoSizeMode.FIT_CONTENTS,
|
|
929
|
+
height=int_height,
|
|
930
|
+
custom_css={
|
|
931
|
+
"#gridToolBar": {
|
|
932
|
+
"padding-bottom": "0px !important",
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
if len(grid_data["selected_rows"]):
|
|
938
|
+
return grid_data["selected_rows"]
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def render_logo(logo_path: str = logo_file):
|
|
942
|
+
st.markdown(
|
|
943
|
+
f"""<img class="dk-logo-img" src="data:image/svg+xml;base64,{base64.b64encode(open(logo_path, "rb").read()).decode()}">""",
|
|
944
|
+
unsafe_allow_html=True,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def render_icon_link(target_url, width=20, height=20, icon_path=help_icon):
|
|
949
|
+
# left, right = st.columns([0.5, 0.5])
|
|
950
|
+
# with left:
|
|
951
|
+
|
|
952
|
+
# Check if the icon_path is a URL or a local path
|
|
953
|
+
if validators.url(icon_path):
|
|
954
|
+
img_data = icon_path
|
|
955
|
+
else:
|
|
956
|
+
# If local path, convert the image to base64
|
|
957
|
+
img_data = base64.b64encode(Path(icon_path).read_bytes()).decode()
|
|
958
|
+
|
|
959
|
+
# Get the image extension
|
|
960
|
+
img_format = splitext(icon_path)[-1].replace(".", "")
|
|
961
|
+
|
|
962
|
+
base_html = f"""
|
|
963
|
+
<a href="{target_url}" style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
|
964
|
+
<img src="{{}}" style="width:{width}px; height:{height}px;" />
|
|
965
|
+
</a>
|
|
966
|
+
"""
|
|
967
|
+
if validators.url(icon_path):
|
|
968
|
+
html_code = base_html.format(img_data)
|
|
969
|
+
else:
|
|
970
|
+
html_code = base_html.format(f"data:image/{img_format};base64,{img_data}")
|
|
971
|
+
|
|
972
|
+
st.markdown(html_code, unsafe_allow_html=True)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def render_icon_link_new(target_url, width=20, height=20, icon_path=help_icon):
|
|
976
|
+
# FIXME: Why doesn't this work?
|
|
977
|
+
|
|
978
|
+
# Check if the icon_path is a URL or a local path
|
|
979
|
+
if validators.url(icon_path):
|
|
980
|
+
img_data = icon_path
|
|
981
|
+
else:
|
|
982
|
+
# If local path, convert the image to base64
|
|
983
|
+
img_data = base64.b64encode(Path(icon_path).read_bytes()).decode()
|
|
984
|
+
|
|
985
|
+
# Get the image extension
|
|
986
|
+
img_format = splitext(icon_path)[-1].replace(".", "")
|
|
987
|
+
|
|
988
|
+
if not validators.url(icon_path):
|
|
989
|
+
img_data = f"data:image/{img_format};base64,{img_data}"
|
|
990
|
+
|
|
991
|
+
html_code = f"""
|
|
992
|
+
<a href="#" onclick="DKlowerRightPopup('{target_url}'); return false;">
|
|
993
|
+
<img src="{img_data}" style="width:{width}px; height:{height}px;" />
|
|
994
|
+
</a>
|
|
995
|
+
<script>
|
|
996
|
+
function DKlowerRightPopup(url) {{
|
|
997
|
+
let win_width = 300;
|
|
998
|
+
let win_height = 400;
|
|
999
|
+
let offsetX = 20;
|
|
1000
|
+
let offsetY = 20;
|
|
1001
|
+
let left = screen.width - win_width - offsetX;
|
|
1002
|
+
let top = screen.height - win_height - offsetY;
|
|
1003
|
+
window.open(url, 'PopupWindow', `width=${{win_width}},height=${{win_height}},left=${{left}},top=${{top}},scrollbars=yes,resizable=yes`);
|
|
1004
|
+
}}
|
|
1005
|
+
</script>
|
|
1006
|
+
"""
|
|
1007
|
+
|
|
1008
|
+
st.markdown(html_code, unsafe_allow_html=True)
|