dataops-testgen 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. dataops_testgen-2.2.0.dist-info/LICENSE +203 -0
  2. dataops_testgen-2.2.0.dist-info/METADATA +287 -0
  3. dataops_testgen-2.2.0.dist-info/NOTICE +5 -0
  4. dataops_testgen-2.2.0.dist-info/RECORD +270 -0
  5. dataops_testgen-2.2.0.dist-info/WHEEL +5 -0
  6. dataops_testgen-2.2.0.dist-info/entry_points.txt +2 -0
  7. dataops_testgen-2.2.0.dist-info/top_level.txt +1 -0
  8. testgen/__init__.py +0 -0
  9. testgen/__main__.py +770 -0
  10. testgen/commands/__init__.py +0 -0
  11. testgen/commands/queries/__init__.py +0 -0
  12. testgen/commands/queries/execute_cat_tests_query.py +95 -0
  13. testgen/commands/queries/execute_tests_query.py +160 -0
  14. testgen/commands/queries/generate_tests_query.py +94 -0
  15. testgen/commands/queries/profiling_query.py +366 -0
  16. testgen/commands/queries/test_parameter_validation_query.py +88 -0
  17. testgen/commands/run_execute_cat_tests.py +162 -0
  18. testgen/commands/run_execute_tests.py +168 -0
  19. testgen/commands/run_generate_tests.py +107 -0
  20. testgen/commands/run_get_entities.py +122 -0
  21. testgen/commands/run_launch_db_config.py +84 -0
  22. testgen/commands/run_observability_exporter.py +330 -0
  23. testgen/commands/run_profiling_bridge.py +495 -0
  24. testgen/commands/run_quick_start.py +168 -0
  25. testgen/commands/run_setup_profiling_tools.py +96 -0
  26. testgen/commands/run_test_definition.py +146 -0
  27. testgen/commands/run_test_parameter_validation.py +135 -0
  28. testgen/commands/run_upgrade_db_config.py +156 -0
  29. testgen/common/__init__.py +8 -0
  30. testgen/common/clean_sql.py +53 -0
  31. testgen/common/credentials.py +25 -0
  32. testgen/common/database/__init__.py +0 -0
  33. testgen/common/database/database_service.py +629 -0
  34. testgen/common/database/flavor/__init__.py +0 -0
  35. testgen/common/database/flavor/flavor_service.py +75 -0
  36. testgen/common/database/flavor/mssql_flavor_service.py +34 -0
  37. testgen/common/database/flavor/postgresql_flavor_service.py +5 -0
  38. testgen/common/database/flavor/redshift_flavor_service.py +22 -0
  39. testgen/common/database/flavor/snowflake_flavor_service.py +69 -0
  40. testgen/common/database/flavor/trino_flavor_service.py +21 -0
  41. testgen/common/date_service.py +68 -0
  42. testgen/common/display_service.py +85 -0
  43. testgen/common/docker_service.py +76 -0
  44. testgen/common/encrypt.py +55 -0
  45. testgen/common/get_pipeline_parms.py +57 -0
  46. testgen/common/logs.py +79 -0
  47. testgen/common/process_service.py +62 -0
  48. testgen/common/read_file.py +69 -0
  49. testgen/settings.py +440 -0
  50. testgen/template/dbsetup/010_create_base_schema.sql +2 -0
  51. testgen/template/dbsetup/020_create_standard_functions_sprocs.sql +179 -0
  52. testgen/template/dbsetup/030_initialize_new_schema_structure.sql +735 -0
  53. testgen/template/dbsetup/040_populate_new_schema_project.sql +59 -0
  54. testgen/template/dbsetup/050_populate_new_schema_metadata.sql +1517 -0
  55. testgen/template/dbsetup/060_create_standard_views.sql +248 -0
  56. testgen/template/dbsetup/070_create_default_users.sql +17 -0
  57. testgen/template/dbsetup/075_grant_role_rights.sql +43 -0
  58. testgen/template/dbsetup/080_set_current_revision.sql +5 -0
  59. testgen/template/dbupgrade/0100_incremental_upgrade.sql +5 -0
  60. testgen/template/dbupgrade/0101_incremental_upgrade.sql +15 -0
  61. testgen/template/dbupgrade/0102_incremental_upgrade.sql +4 -0
  62. testgen/template/dbupgrade/0103_incremental_upgrade.sql +22 -0
  63. testgen/template/dbupgrade/0104_incremental_upgrade.sql +44 -0
  64. testgen/template/dbupgrade/0105_incremental_upgrade.sql +1 -0
  65. testgen/template/dbupgrade/0106_incremental_upgrade.sql +5 -0
  66. testgen/template/dbupgrade/0107_incremental_upgrade.sql +3 -0
  67. testgen/template/dbupgrade_helpers/get_tg_revision.sql +2 -0
  68. testgen/template/exec_cat_tests/ex_cat_build_agg_table_tests.sql +116 -0
  69. testgen/template/exec_cat_tests/ex_cat_get_distinct_tables.sql +11 -0
  70. testgen/template/exec_cat_tests/ex_cat_results_parse.sql +69 -0
  71. testgen/template/exec_cat_tests/ex_cat_retrieve_agg_test_parms.sql +6 -0
  72. testgen/template/exec_cat_tests/ex_cat_test_query.sql +8 -0
  73. testgen/template/execution/ex_finalize_test_run_results.sql +37 -0
  74. testgen/template/execution/ex_get_tests_non_cat.sql +47 -0
  75. testgen/template/execution/ex_update_test_record_in_testrun_table.sql +27 -0
  76. testgen/template/execution/ex_write_test_record_to_testrun_table.sql +6 -0
  77. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_no_drops_generic.sql +48 -0
  78. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_num_incr_generic.sql +34 -0
  79. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_above_generic.sql +49 -0
  80. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_within_generic.sql +49 -0
  81. testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_same_generic.sql +49 -0
  82. testgen/template/flavors/generic/exec_query_tests/ex_custom_query_generic.sql +39 -0
  83. testgen/template/flavors/generic/exec_query_tests/ex_data_match_2way_generic.sql +58 -0
  84. testgen/template/flavors/generic/exec_query_tests/ex_data_match_generic.sql +44 -0
  85. testgen/template/flavors/generic/exec_query_tests/ex_prior_match_generic.sql +37 -0
  86. testgen/template/flavors/generic/exec_query_tests/ex_relative_entropy_generic.sql +53 -0
  87. testgen/template/flavors/generic/exec_query_tests/ex_window_match_no_drops_generic.sql +46 -0
  88. testgen/template/flavors/generic/exec_query_tests/ex_window_match_same_generic.sql +59 -0
  89. testgen/template/flavors/generic/profiling/contingency_counts.sql +3 -0
  90. testgen/template/flavors/generic/validate_tests/ex_get_project_column_list_generic.sql +3 -0
  91. testgen/template/flavors/mssql/exec_query_tests/ex_relative_entropy_mssql.sql +53 -0
  92. testgen/template/flavors/mssql/profiling/project_ddf_query_mssql.sql +35 -0
  93. testgen/template/flavors/mssql/profiling/project_profiling_query_mssql.yaml +246 -0
  94. testgen/template/flavors/mssql/profiling/project_secondary_profiling_query_mssql.sql +36 -0
  95. testgen/template/flavors/mssql/setup_profiling_tools/00_drop_existing_functions_mssql.sql +8 -0
  96. testgen/template/flavors/mssql/setup_profiling_tools/01_create_functions_mssql.sql +12 -0
  97. testgen/template/flavors/mssql/setup_profiling_tools/02_create_functions_mssql.sql +54 -0
  98. testgen/template/flavors/mssql/setup_profiling_tools/create_qc_schema_mssql.sql +4 -0
  99. testgen/template/flavors/mssql/setup_profiling_tools/grant_execute_privileges_mssql.sql +1 -0
  100. testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_no_drops_postgresql.sql +46 -0
  101. testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_same_postgresql.sql +59 -0
  102. testgen/template/flavors/postgresql/profiling/project_ddf_query_postgresql.sql +42 -0
  103. testgen/template/flavors/postgresql/profiling/project_profiling_query_postgresql.yaml +225 -0
  104. testgen/template/flavors/postgresql/profiling/project_secondary_profiling_query_postgresql.sql +28 -0
  105. testgen/template/flavors/postgresql/setup_profiling_tools/create_functions_postgresql.sql +157 -0
  106. testgen/template/flavors/postgresql/setup_profiling_tools/create_qc_schema_postgresql.sql +1 -0
  107. testgen/template/flavors/postgresql/setup_profiling_tools/grant_execute_privileges_postgresql.sql +2 -0
  108. testgen/template/flavors/redshift/profiling/project_ddf_query_redshift.sql +38 -0
  109. testgen/template/flavors/redshift/profiling/project_profiling_query_redshift.yaml +221 -0
  110. testgen/template/flavors/redshift/profiling/project_secondary_profiling_query_redshift.sql +29 -0
  111. testgen/template/flavors/redshift/setup_profiling_tools/create_functions_redshift.sql +115 -0
  112. testgen/template/flavors/redshift/setup_profiling_tools/create_qc_schema_redshift.sql +1 -0
  113. testgen/template/flavors/redshift/setup_profiling_tools/grant_execute_privileges_redshift.sql +2 -0
  114. testgen/template/flavors/snowflake/profiling/project_ddf_query_snowflake.sql +38 -0
  115. testgen/template/flavors/snowflake/profiling/project_profiling_query_snowflake.yaml +220 -0
  116. testgen/template/flavors/snowflake/profiling/project_secondary_profiling_query_snowflake.sql +29 -0
  117. testgen/template/flavors/snowflake/setup_profiling_tools/create_functions_snowflake.sql +69 -0
  118. testgen/template/flavors/snowflake/setup_profiling_tools/create_qc_schema_snowflake.sql +1 -0
  119. testgen/template/flavors/snowflake/setup_profiling_tools/grant_execute_privileges_snowflake.sql +6 -0
  120. testgen/template/flavors/trino/profiling/project_profiling_query_trino.yaml +219 -0
  121. testgen/template/flavors/trino/setup_profiling_tools/create_functions_trino.sql +92 -0
  122. testgen/template/flavors/trino/setup_profiling_tools/create_qc_schema_trino.sql +1 -0
  123. testgen/template/gen_funny_cat_tests/gen_test_constant.sql +104 -0
  124. testgen/template/gen_funny_cat_tests/gen_test_distinct_value_ct.sql +98 -0
  125. testgen/template/gen_funny_cat_tests/gen_test_row_ct.sql +57 -0
  126. testgen/template/gen_funny_cat_tests/gen_test_row_ct_pct.sql +59 -0
  127. testgen/template/generation/gen_delete_old_tests.sql +5 -0
  128. testgen/template/generation/gen_insert_test_suite.sql +5 -0
  129. testgen/template/generation/gen_retrieve_or_insert_test_suite.sql +58 -0
  130. testgen/template/generation/gen_standard_test_type_list.sql +13 -0
  131. testgen/template/generation/gen_standard_tests.sql +48 -0
  132. testgen/template/get_entities/get_connection.sql +21 -0
  133. testgen/template/get_entities/get_connections_list.sql +9 -0
  134. testgen/template/get_entities/get_latest.sql +4 -0
  135. testgen/template/get_entities/get_profile.sql +12 -0
  136. testgen/template/get_entities/get_profile_info.sql +17 -0
  137. testgen/template/get_entities/get_profile_list.sql +17 -0
  138. testgen/template/get_entities/get_profile_screen.sql +275 -0
  139. testgen/template/get_entities/get_project_list.sql +6 -0
  140. testgen/template/get_entities/get_table_group_list.sql +10 -0
  141. testgen/template/get_entities/get_test_generation_list.sql +18 -0
  142. testgen/template/get_entities/get_test_info.sql +41 -0
  143. testgen/template/get_entities/get_test_results_for_run_cli.sql +16 -0
  144. testgen/template/get_entities/get_test_run_list.sql +24 -0
  145. testgen/template/get_entities/get_test_suite.sql +13 -0
  146. testgen/template/get_entities/get_test_suite_list.sql +18 -0
  147. testgen/template/get_entities/list_test_types.sql +4 -0
  148. testgen/template/observability/get_event_data.sql +23 -0
  149. testgen/template/observability/get_test_results.sql +41 -0
  150. testgen/template/observability/update_test_results_exported_to_observability.sql +12 -0
  151. testgen/template/parms/parms_profiling.sql +34 -0
  152. testgen/template/parms/parms_test_execution.sql +13 -0
  153. testgen/template/parms/parms_test_gen.sql +23 -0
  154. testgen/template/profiling/contingency_columns.sql +7 -0
  155. testgen/template/profiling/datatype_suggestions.sql +56 -0
  156. testgen/template/profiling/functional_datatype.sql +523 -0
  157. testgen/template/profiling/functional_tabletype_stage.sql +48 -0
  158. testgen/template/profiling/functional_tabletype_update.sql +8 -0
  159. testgen/template/profiling/pii_flag.sql +133 -0
  160. testgen/template/profiling/profile_anomalies_screen_column.sql +22 -0
  161. testgen/template/profiling/profile_anomalies_screen_multi_column.sql +58 -0
  162. testgen/template/profiling/profile_anomalies_screen_table.sql +22 -0
  163. testgen/template/profiling/profile_anomalies_screen_table_dates.sql +30 -0
  164. testgen/template/profiling/profile_anomalies_screen_variants.sql +40 -0
  165. testgen/template/profiling/profile_anomaly_types_get.sql +3 -0
  166. testgen/template/profiling/project_get_table_sample_count.sql +22 -0
  167. testgen/template/profiling/project_profile_run_record_insert.sql +8 -0
  168. testgen/template/profiling/project_profile_run_record_update.sql +5 -0
  169. testgen/template/profiling/project_profile_run_record_update_status.sql +5 -0
  170. testgen/template/profiling/project_update_profile_results_to_estimates.sql +32 -0
  171. testgen/template/profiling/refresh_anomalies.sql +33 -0
  172. testgen/template/profiling/refresh_data_chars_from_profiling.sql +156 -0
  173. testgen/template/profiling/secondary_profiling_columns.sql +12 -0
  174. testgen/template/profiling/secondary_profiling_delete.sql +4 -0
  175. testgen/template/profiling/secondary_profiling_update.sql +18 -0
  176. testgen/template/quick_start/populate_target_data.sql +1077 -0
  177. testgen/template/quick_start/recreate_target_data_schema.sql +167 -0
  178. testgen/template/quick_start/update_target_data.sql +100 -0
  179. testgen/template/updates/create_tmp_test_definition.sql +19 -0
  180. testgen/template/updates/get_test_def_parms.sql +38 -0
  181. testgen/template/updates/populate_stg_test_definitions.sql +184 -0
  182. testgen/template/validate_tests/ex_disable_tests_test_definitions.sql +5 -0
  183. testgen/template/validate_tests/ex_flag_tests_test_definitions.sql +64 -0
  184. testgen/template/validate_tests/ex_get_project_column_list_generic.sql +3 -0
  185. testgen/template/validate_tests/ex_get_test_column_list_tg.sql +65 -0
  186. testgen/template/validate_tests/ex_write_test_val_errors.sql +22 -0
  187. testgen/ui/__init__.py +0 -0
  188. testgen/ui/app.py +98 -0
  189. testgen/ui/assets/dk_logo.svg +46 -0
  190. testgen/ui/assets/question_mark.png +0 -0
  191. testgen/ui/assets/scripts.js +68 -0
  192. testgen/ui/assets/style.css +140 -0
  193. testgen/ui/bootstrap.py +109 -0
  194. testgen/ui/components/__init__.py +0 -0
  195. testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 +0 -0
  196. testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 +0 -0
  197. testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu4mxK.woff2 +0 -0
  198. testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 +0 -0
  199. testgen/ui/components/frontend/css/material-symbols-rounded.css +24 -0
  200. testgen/ui/components/frontend/css/material-symbols-rounded.woff2 +0 -0
  201. testgen/ui/components/frontend/css/roboto-font-faces.css +35 -0
  202. testgen/ui/components/frontend/css/shared.css +36 -0
  203. testgen/ui/components/frontend/img/dk_logo.svg +46 -0
  204. testgen/ui/components/frontend/index.html +17 -0
  205. testgen/ui/components/frontend/js/components/breadcrumbs.js +86 -0
  206. testgen/ui/components/frontend/js/components/button.js +66 -0
  207. testgen/ui/components/frontend/js/components/location.js +62 -0
  208. testgen/ui/components/frontend/js/components/select.js +75 -0
  209. testgen/ui/components/frontend/js/components/sidebar.js +358 -0
  210. testgen/ui/components/frontend/js/main.js +99 -0
  211. testgen/ui/components/frontend/js/streamlit.js +19 -0
  212. testgen/ui/components/frontend/js/van.min.js +1 -0
  213. testgen/ui/components/utils/__init__.py +0 -0
  214. testgen/ui/components/utils/callbacks.py +51 -0
  215. testgen/ui/components/utils/component.py +13 -0
  216. testgen/ui/components/widgets/__init__.py +6 -0
  217. testgen/ui/components/widgets/breadcrumbs.py +32 -0
  218. testgen/ui/components/widgets/location.py +65 -0
  219. testgen/ui/components/widgets/modal.py +97 -0
  220. testgen/ui/components/widgets/sidebar.py +69 -0
  221. testgen/ui/navigation/__init__.py +0 -0
  222. testgen/ui/navigation/menu.py +42 -0
  223. testgen/ui/navigation/page.py +20 -0
  224. testgen/ui/navigation/router.py +63 -0
  225. testgen/ui/queries/__init__.py +0 -0
  226. testgen/ui/queries/authentication_queries.py +47 -0
  227. testgen/ui/queries/connection_queries.py +121 -0
  228. testgen/ui/queries/profiling_queries.py +148 -0
  229. testgen/ui/queries/project_queries.py +9 -0
  230. testgen/ui/queries/table_group_queries.py +186 -0
  231. testgen/ui/queries/test_definition_queries.py +270 -0
  232. testgen/ui/queries/test_run_queries.py +32 -0
  233. testgen/ui/queries/test_suite_queries.py +145 -0
  234. testgen/ui/scripts/__init__.py +0 -0
  235. testgen/ui/scripts/patch_streamlit.py +111 -0
  236. testgen/ui/services/__init__.py +0 -0
  237. testgen/ui/services/authentication_service.py +119 -0
  238. testgen/ui/services/connection_service.py +220 -0
  239. testgen/ui/services/database_service.py +282 -0
  240. testgen/ui/services/form_service.py +1008 -0
  241. testgen/ui/services/javascript_service.py +44 -0
  242. testgen/ui/services/query_service.py +316 -0
  243. testgen/ui/services/string_service.py +12 -0
  244. testgen/ui/services/table_group_service.py +130 -0
  245. testgen/ui/services/test_definition_service.py +117 -0
  246. testgen/ui/services/test_run_service.py +13 -0
  247. testgen/ui/services/test_suite_service.py +76 -0
  248. testgen/ui/services/toolbar_service.py +77 -0
  249. testgen/ui/session.py +46 -0
  250. testgen/ui/views/__init__.py +0 -0
  251. testgen/ui/views/app_log_modal.py +92 -0
  252. testgen/ui/views/connections.py +72 -0
  253. testgen/ui/views/connections_base.py +367 -0
  254. testgen/ui/views/login.py +40 -0
  255. testgen/ui/views/not_found.py +16 -0
  256. testgen/ui/views/overview.py +34 -0
  257. testgen/ui/views/profiling_anomalies.py +501 -0
  258. testgen/ui/views/profiling_details.py +335 -0
  259. testgen/ui/views/profiling_modal.py +40 -0
  260. testgen/ui/views/profiling_results.py +206 -0
  261. testgen/ui/views/profiling_summary.py +177 -0
  262. testgen/ui/views/project_settings.py +74 -0
  263. testgen/ui/views/table_groups.py +530 -0
  264. testgen/ui/views/test_definitions.py +1020 -0
  265. testgen/ui/views/test_results.py +908 -0
  266. testgen/ui/views/test_runs.py +195 -0
  267. testgen/ui/views/test_suites.py +545 -0
  268. testgen/utils/__init__.py +0 -0
  269. testgen/utils/plugins.py +17 -0
  270. testgen/utils/singleton.py +14 -0
@@ -0,0 +1,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)}**:&nbsp;&nbsp;`{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)