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,119 @@
1
+ # ruff: noqa: S105
2
+
3
+ import datetime
4
+ import logging
5
+ import typing
6
+
7
+ import extra_streamlit_components as stx
8
+ import jwt
9
+ import streamlit as st
10
+
11
+ from testgen.common.encrypt import encrypt_ui_password
12
+ from testgen.ui.queries import authentication_queries
13
+ from testgen.ui.session import session
14
+
15
+ RoleType = typing.Literal["admin", "edit", "read"]
16
+
17
+ JWT_HASHING_KEY = "dk_signature_key"
18
+ AUTH_TOKEN_COOKIE_NAME = "dk_cookie_name"
19
+ AUTH_TOKEN_EXPIRATION_DAYS = 5
20
+
21
+ LOG = logging.getLogger("testgen")
22
+
23
+
24
+ def load_user_session() -> None:
25
+ cookies = stx.CookieManager(key="testgen.cookies.get")
26
+ token = cookies.get(AUTH_TOKEN_COOKIE_NAME)
27
+ if token is not None:
28
+ try:
29
+ token = jwt.decode(token, JWT_HASHING_KEY, algorithms=["HS256"])
30
+ if token["exp_date"] > datetime.datetime.utcnow().timestamp():
31
+ start_user_session(token["name"], token["username"])
32
+ except Exception:
33
+ LOG.debug("Invalid auth token found on cookies", exc_info=True, stack_info=True)
34
+
35
+
36
+ def start_user_session(name: str, username: str) -> None:
37
+ session.name = name
38
+ session.username = username
39
+ session.auth_role = get_role_for_user(get_auth_data(), username)
40
+ session.authentication_status = True
41
+ if not session.current_page or session.current_page == "login":
42
+ session.current_page = "overview"
43
+ session.current_page_args = {}
44
+ session.logging_out = False
45
+
46
+
47
+ def end_user_session() -> None:
48
+ session.auth_role = None
49
+ session.authentication_status = None
50
+ session.current_page = "login"
51
+ session.current_page_args = {}
52
+ session.logging_out = True
53
+
54
+ del session.name
55
+ del session.username
56
+
57
+
58
+ def add_user(user):
59
+ encrypted_password = encrypt_ui_password(user["password"])
60
+ schema = st.session_state["dbschema"]
61
+ authentication_queries.add_user(schema, user, encrypted_password)
62
+
63
+
64
+ def delete_users(user_ids):
65
+ schema = st.session_state["dbschema"]
66
+ return authentication_queries.delete_users(schema, user_ids)
67
+
68
+
69
+ def edit_user(user):
70
+ encrypted_password = encrypt_ui_password(user["password"])
71
+ schema = st.session_state["dbschema"]
72
+ authentication_queries.edit_user(schema, user, encrypted_password)
73
+
74
+
75
+ def get_auth_data():
76
+ auth_data = authentication_queries.get_users(session.dbschema)
77
+
78
+ usernames = {}
79
+ preauthorized_list = []
80
+
81
+ for item in auth_data.itertuples():
82
+ usernames[item.username] = {
83
+ "email": item.email,
84
+ "name": item.name,
85
+ "password": item.password,
86
+ "role": item.role,
87
+ }
88
+ if item.preauthorized:
89
+ preauthorized_list.append(item.email)
90
+
91
+ return {
92
+ "credentials": {"usernames": usernames},
93
+ "cookie": {"expiry_days": AUTH_TOKEN_EXPIRATION_DAYS, "key": JWT_HASHING_KEY, "name": AUTH_TOKEN_COOKIE_NAME},
94
+ "preauthorized": {"emails": preauthorized_list},
95
+ }
96
+
97
+
98
+ def get_users():
99
+ return authentication_queries.get_users(session.dbschema)
100
+
101
+
102
+ def get_role_for_user(auth_data, username):
103
+ return auth_data["credentials"]["usernames"][username]["role"]
104
+
105
+
106
+ def current_user_has_admin_role():
107
+ return session.auth_role == "admin"
108
+
109
+
110
+ def current_user_has_edit_role():
111
+ return session.auth_role in ("edit", "admin")
112
+
113
+
114
+ def current_user_has_read_role():
115
+ return not session.auth_role or session.auth_role == "read"
116
+
117
+
118
+ def current_user_has_role(role: RoleType) -> bool:
119
+ return session.auth_role == role
@@ -0,0 +1,220 @@
1
+ import streamlit as st
2
+
3
+ import testgen.ui.queries.connection_queries as connection_queries
4
+ import testgen.ui.services.table_group_service as table_group_service
5
+ from testgen.commands.run_profiling_bridge import InitializeProfilingSQL
6
+ from testgen.commands.run_setup_profiling_tools import run_setup_profiling_tools
7
+ from testgen.common.database.database_service import (
8
+ AssignConnectParms,
9
+ RetrieveDBResultsToList,
10
+ empty_cache,
11
+ get_db_type,
12
+ get_flavor_service,
13
+ )
14
+ from testgen.common.encrypt import DecryptText, EncryptText
15
+
16
+
17
+ def get_by_id(connection_id, hide_passwords: bool = True):
18
+ connections_df = connection_queries.get_by_id(connection_id)
19
+ decrypt_connections(connections_df, hide_passwords)
20
+ connection = connections_df.to_dict(orient="records")[0]
21
+ return connection
22
+
23
+
24
+ def get_connections(project_code, hide_passwords: bool = False):
25
+ connections = connection_queries.get_connections(project_code)
26
+ decrypt_connections(connections, hide_passwords)
27
+ return connections
28
+
29
+
30
+ def decrypt_connections(connections, hide_passwords: bool = False):
31
+ for index, connection in connections.iterrows():
32
+ if hide_passwords:
33
+ password = "***" # noqa S105
34
+ private_key = "***" # S105
35
+ private_key_passphrase = "***" # noqa S105
36
+ else:
37
+ password = DecryptText(connection["project_pw_encrypted"]) if connection["project_pw_encrypted"] else None
38
+ private_key = DecryptText(connection["private_key"]) if connection["private_key"] else None
39
+ private_key_passphrase = DecryptText(connection["private_key_passphrase"]) if connection["private_key_passphrase"] else ""
40
+ connections.at[index, "password"] = password
41
+ connections.at[index, "private_key"] = private_key
42
+ connections.at[index, "private_key_passphrase"] = private_key_passphrase
43
+
44
+
45
+ def encrypt_credentials(connection):
46
+ encrypted_password = EncryptText(connection["password"]) if connection["password"] else None
47
+ encrypted_private_key = EncryptText(connection["private_key"]) if connection["private_key"] else None
48
+ encrypted_private_key_passphrase = EncryptText(connection["private_key_passphrase"]) if connection["private_key_passphrase"] else None
49
+ return encrypted_password, encrypted_private_key, encrypted_private_key_passphrase
50
+
51
+
52
+ def edit_connection(connection):
53
+ empty_cache()
54
+ schema = st.session_state["dbschema"]
55
+ connection = pre_save_connection_process(connection)
56
+ encrypted_password, encrypted_private_key, encrypted_private_key_passphrase = encrypt_credentials(connection)
57
+ connection_queries.edit_connection(schema, connection, encrypted_password, encrypted_private_key, encrypted_private_key_passphrase)
58
+
59
+
60
+ def add_connection(connection):
61
+ empty_cache()
62
+ schema = st.session_state["dbschema"]
63
+ connection = pre_save_connection_process(connection)
64
+ encrypted_password, encrypted_private_key, encrypted_private_key_passphrase = encrypt_credentials(connection)
65
+ connection_queries.add_connection(schema, connection, encrypted_password, encrypted_private_key, encrypted_private_key_passphrase)
66
+
67
+
68
+ def pre_save_connection_process(connection):
69
+ if connection["connect_by_url"]:
70
+ url = connection["url"]
71
+ if url:
72
+ url_sections = url.split("/")
73
+ if len(url_sections) > 0:
74
+ host_port = url_sections[0]
75
+ host_port_sections = host_port.split(":")
76
+ if len(host_port_sections) > 0:
77
+ connection["project_host"] = host_port_sections[0]
78
+ connection["project_port"] = "".join(host_port_sections[1:])
79
+ else:
80
+ connection["project_host"] = host_port
81
+ connection["project_port"] = ""
82
+ if len(url_sections) > 1:
83
+ connection["project_db"] = url_sections[1]
84
+ return connection
85
+
86
+
87
+ def delete_connections(connection_ids):
88
+ empty_cache()
89
+ schema = st.session_state["dbschema"]
90
+ return connection_queries.delete_connections(schema, connection_ids)
91
+
92
+
93
+ def cascade_delete(connection_ids, dry_run=False):
94
+ schema = st.session_state["dbschema"]
95
+ can_be_deleted = True
96
+ table_group_names = get_table_group_names_by_connection(connection_ids)
97
+ connection_has_dependencies = table_group_names is not None and len(table_group_names) > 0
98
+ if connection_has_dependencies:
99
+ can_be_deleted = False
100
+ if not dry_run:
101
+ if connection_has_dependencies:
102
+ table_group_service.cascade_delete(table_group_names)
103
+ connection_queries.delete_connections(schema, connection_ids)
104
+ return can_be_deleted
105
+
106
+
107
+ def are_connections_in_use(connection_ids):
108
+ table_group_names = get_table_group_names_by_connection(connection_ids)
109
+ table_groups_in_use = table_group_service.are_table_groups_in_use(table_group_names)
110
+ return table_groups_in_use
111
+
112
+
113
+ def get_table_group_names_by_connection(connection_ids):
114
+ if not connection_ids:
115
+ return []
116
+ schema = st.session_state["dbschema"]
117
+ table_group_names = connection_queries.get_table_group_names_by_connection(schema, connection_ids)
118
+ return table_group_names.to_dict()["table_groups_name"].values()
119
+
120
+
121
+ def init_profiling_sql(project_code, connection, table_group_schema=None):
122
+ # get connection data
123
+ empty_cache()
124
+ connection_id = str(connection["connection_id"]) if connection["connection_id"] else None
125
+ sql_flavor = connection["sql_flavor"]
126
+ url = connection["url"]
127
+ connect_by_url = connection["connect_by_url"]
128
+ connect_by_key = connection["connect_by_key"]
129
+ private_key = connection["private_key"]
130
+ private_key_passphrase = connection["private_key_passphrase"]
131
+ project_host = connection["project_host"]
132
+ project_port = connection["project_port"]
133
+ project_db = connection["project_db"]
134
+ project_user = connection["project_user"]
135
+ project_qc_schema = connection["project_qc_schema"]
136
+ password = connection["password"]
137
+
138
+ # prepare the profiling query
139
+ clsProfiling = InitializeProfilingSQL(project_code, sql_flavor)
140
+
141
+ AssignConnectParms(
142
+ project_code,
143
+ connection_id,
144
+ project_host,
145
+ project_port,
146
+ project_db,
147
+ table_group_schema if table_group_schema else project_qc_schema,
148
+ project_user,
149
+ sql_flavor,
150
+ url,
151
+ connect_by_url,
152
+ connect_by_key,
153
+ private_key,
154
+ private_key_passphrase,
155
+ connectname="PROJECT",
156
+ password=password,
157
+ )
158
+
159
+ return clsProfiling
160
+
161
+
162
+ def test_qc_connection(project_code, connection, init_profiling=True):
163
+ qc_results = {}
164
+
165
+ if init_profiling:
166
+ init_profiling_sql(project_code, connection)
167
+
168
+ project_qc_schema = connection["project_qc_schema"]
169
+ query_isnum_true = f"select {project_qc_schema}.fndk_isnum('32')"
170
+ query_isnum_true_result_raw = RetrieveDBResultsToList("PROJECT", query_isnum_true)
171
+ isnum_true_result = query_isnum_true_result_raw[0][0][0] == 1
172
+ qc_results["isnum_true_result"] = isnum_true_result
173
+
174
+ query_isnum_false = f"select {project_qc_schema}.fndk_isnum('HELLO')"
175
+ query_isnum_false_result_raw = RetrieveDBResultsToList("PROJECT", query_isnum_false)
176
+ isnum_false_result = query_isnum_false_result_raw[0][0][0] == 0
177
+ qc_results["isnum_false_result"] = isnum_false_result
178
+
179
+ query_isdate_true = f"select {project_qc_schema}.fndk_isdate('2013-05-18')"
180
+ query_isdate_true_result_raw = RetrieveDBResultsToList("PROJECT", query_isdate_true)
181
+ isdate_true_result = query_isdate_true_result_raw[0][0][0] == 1
182
+ qc_results["isdate_true_result"] = isdate_true_result
183
+
184
+ query_isdate_false = f"select {project_qc_schema}.fndk_isdate('HELLO')"
185
+ query_isdate_false_result_raw = RetrieveDBResultsToList("PROJECT", query_isdate_false)
186
+ isdate_false_result = query_isdate_false_result_raw[0][0][0] == 0
187
+ qc_results["isdate_false_result"] = isdate_false_result
188
+
189
+ return qc_results
190
+
191
+
192
+ def create_qc_schema(connection_id, create_qc_schema, db_user, db_password, skip_granting_privileges, admin_private_key_passphrase=None, admin_private_key=None, user_role=None):
193
+ dry_run = False
194
+ empty_cache()
195
+ run_setup_profiling_tools(connection_id, dry_run, create_qc_schema, db_user, db_password, skip_granting_privileges, admin_private_key_passphrase, admin_private_key, user_role)
196
+
197
+
198
+ def form_overwritten_connection_url(connection):
199
+ flavor = connection["sql_flavor"]
200
+
201
+ connection_credentials = {
202
+ "flavor": flavor,
203
+ "user": "<user>",
204
+ "host": connection["project_host"],
205
+ "port": connection["project_port"],
206
+ "dbname": connection["project_db"],
207
+ "url": None,
208
+ "connect_by_url": None,
209
+ "connect_by_key": connection["connect_by_key"],
210
+ "private_key": None,
211
+ "private_key_passphrase": "",
212
+ "dbschema": "",
213
+ }
214
+
215
+ db_type = get_db_type(flavor)
216
+ flavor_service = get_flavor_service(db_type)
217
+ flavor_service.init(connection_credentials)
218
+ connection_string = flavor_service.get_connection_string("<password>")
219
+
220
+ return connection_string
@@ -0,0 +1,282 @@
1
+ from urllib.parse import quote_plus
2
+
3
+ import pandas as pd
4
+ from sqlalchemy import create_engine, text
5
+
6
+ from testgen.common.credentials import (
7
+ get_tg_db,
8
+ get_tg_host,
9
+ get_tg_password,
10
+ get_tg_port,
11
+ get_tg_schema,
12
+ get_tg_username,
13
+ )
14
+ from testgen.common.database.database_service import get_flavor_service
15
+ from testgen.common.encrypt import DecryptText
16
+
17
+ """
18
+ Shared database access and utility functions
19
+ """
20
+
21
+
22
+ def get_schema():
23
+ return get_tg_schema()
24
+
25
+
26
+ def _start_engine():
27
+ # TestGen database
28
+ dbhost = get_tg_host()
29
+ dbport = get_tg_port()
30
+ dbname = get_tg_db()
31
+ # User Information
32
+ dbuser = get_tg_username()
33
+ dbpw = get_tg_password()
34
+
35
+ conn_str = "postgresql://" + dbuser + ":" + quote_plus(dbpw) + "@" + dbhost + ":" + dbport + "/" + dbname
36
+ return create_engine(conn_str)
37
+
38
+
39
+ def _make_connection():
40
+ engine = _start_engine()
41
+ return engine
42
+
43
+
44
+ def make_header_db_friendly(str_header):
45
+ return str_header.replace(" ", "_").lower()
46
+
47
+
48
+ def make_value_db_friendly(value):
49
+ if value is None or pd.isna(value):
50
+ newval = "NULL"
51
+ else:
52
+ newval = str(value) if isinstance(value, int | float) else f"'{value}'"
53
+ return newval
54
+
55
+
56
+ def retrieve_data(str_sql):
57
+ tg_engine = _start_engine()
58
+ # Retrieve data from Postgres
59
+ return pd.read_sql_query(str_sql, tg_engine)
60
+
61
+
62
+ def retrieve_data_list(str_sql):
63
+ tg_engine = _start_engine()
64
+ # Retrieve data from Postgres
65
+ with tg_engine.connect() as con:
66
+ return con.execute(text(str_sql)).fetchall()
67
+
68
+
69
+ def retrieve_single_result(str_sql):
70
+ tg_engine = _start_engine()
71
+ with tg_engine.connect() as con:
72
+ lstResult = con.execute(text(str_sql)).fetchone()
73
+ if lstResult:
74
+ return lstResult[0]
75
+
76
+
77
+ def execute_sql(str_sql):
78
+ if str_sql > "":
79
+ tg_engine = _start_engine()
80
+ tg_engine.execute(text(str_sql))
81
+
82
+
83
+ def execute_sql_raw(str_sql):
84
+ # For special cases where SQLAlchemy can't handle query syntax
85
+ if str_sql > "":
86
+ tg_engine = _start_engine()
87
+ con = tg_engine.raw_connection()
88
+ with con.cursor() as cur:
89
+ cur.execute(str_sql)
90
+ con.commit()
91
+
92
+
93
+ def _get_df_edits(df_original: pd.DataFrame, df_edited: pd.DataFrame, lst_id_columns: list) -> tuple:
94
+ # Rows in df_edited that exist in df_original but have had any column changed
95
+ # based on composite ID columns
96
+
97
+ # Merge the two dataframes based on the composite ID columns
98
+ merged_df = df_edited.merge(df_original, on=lst_id_columns, how="outer", indicator=True, suffixes=("", "_original"))
99
+ # Filter the merged dataframe to only keep rows that are changed
100
+ # Step 1: Filter rows that exist in both dataframes
101
+ both_rows = merged_df[merged_df["_merge"] == "both"]
102
+
103
+ # Step 2: Identify changed rows
104
+ def has_changes(row):
105
+ for col in df_original.columns:
106
+ # Skip the ID columns
107
+ if col in lst_id_columns:
108
+ continue
109
+ if row[col] != row[col + "_original"]:
110
+ return True
111
+ return False
112
+
113
+ changed_rows_mask = both_rows.apply(has_changes, axis=1)
114
+
115
+ # Step 3: Combine the filters
116
+ changed_rows = both_rows[changed_rows_mask]
117
+
118
+ # All rows in df_edited that are newly created and don't exist in df_original
119
+ new_rows = merged_df[merged_df["_merge"] == "left_only"].drop(
120
+ columns=["_merge"] + [col + "_original" for col in df_original.columns if col not in lst_id_columns]
121
+ )
122
+
123
+ # All rows in df_original that have been deleted from df_edited
124
+ deleted_rows = merged_df[merged_df["_merge"] == "right_only"][df_original.columns]
125
+
126
+ return changed_rows, new_rows, deleted_rows
127
+
128
+
129
+ def _gen_df_update_sql(
130
+ changed_rows: pd.DataFrame, table_name: str, lst_id_columns: list, no_update_columns: list
131
+ ) -> list:
132
+ # Generate a list of SQL UPDATE statements based on the changed rows.
133
+
134
+ # Extract the original column names by removing the "_original" suffix
135
+ original_columns = [col.replace("_original", "") for col in changed_rows.columns if col.endswith("_original")]
136
+ # Drop columns we aren't updating from list
137
+ update_columns = [col for col in original_columns if col not in no_update_columns]
138
+
139
+ # Generate SQL UPDATE statements
140
+ sql_statements = []
141
+ for _, row in changed_rows.iterrows():
142
+ set_statements = []
143
+ for col in update_columns:
144
+ # If the value is different from the original value
145
+ if row[col] != row[col + "_original"]:
146
+ value = make_value_db_friendly(row[col])
147
+ set_statements.append(f"{col} = {value}")
148
+
149
+ # Handle composite keys for the WHERE clause
150
+ where_statements = []
151
+ for col in lst_id_columns:
152
+ value = make_value_db_friendly(row[col])
153
+ # value = f"'{row[col]}'" if isinstance(row[col], str) else row[col]
154
+ where_statements.append(f"{col} = {value}")
155
+
156
+ update_statement = f"UPDATE {get_schema()}.{table_name} SET {', '.join(set_statements)} WHERE {' AND '.join(where_statements)};"
157
+ sql_statements.append(update_statement)
158
+
159
+ return sql_statements
160
+
161
+
162
+ def _gen_df_delete_sql(deleted_rows: pd.DataFrame, table_name: str, lst_id_columns: list) -> list:
163
+ # Generate a list of SQL DELETE statements based on the deleted rows.
164
+
165
+ # Generate SQL DELETE statements
166
+ sql_statements = []
167
+ for _, row in deleted_rows.iterrows():
168
+ # Handle composite keys for the WHERE clause
169
+ where_statements = []
170
+ for col in lst_id_columns:
171
+ value = make_value_db_friendly(row[col])
172
+ # value = f"'{row[col]}'" if isinstance(row[col], str) else row[col]
173
+ where_statements.append(f"{col} = {value}")
174
+
175
+ delete_statement = f"DELETE FROM {get_schema()}.{table_name} WHERE {' AND '.join(where_statements)};"
176
+ sql_statements.append(delete_statement)
177
+
178
+ return sql_statements
179
+
180
+
181
+ def _gen_insert_sql(
182
+ new_rows: pd.DataFrame,
183
+ table_name: str,
184
+ lst_id_columns: list,
185
+ no_update_columns: list,
186
+ dct_hard_default_columns: dict,
187
+ ) -> str:
188
+ # Generate a SQL INSERT statement for the new rows, ensuring strings are properly quoted.
189
+
190
+ # Remove the id column as it will be generated by the server
191
+ if lst_id_columns:
192
+ new_rows = new_rows.drop(columns=lst_id_columns)
193
+ if no_update_columns:
194
+ # Remove columns we aren't updating
195
+ new_rows = new_rows.drop(columns=no_update_columns)
196
+ if dct_hard_default_columns:
197
+ # Add and default all columns
198
+ new_rows = new_rows.assign(**dct_hard_default_columns)
199
+
200
+ # Generate column names and values for the INSERT statement
201
+ columns = ", ".join(new_rows.columns)
202
+
203
+ # Ensure strings are quoted
204
+ values = []
205
+ for _, row in new_rows.iterrows():
206
+ row_values = []
207
+ for val in row:
208
+ row_values.append(make_value_db_friendly(val))
209
+ # if isinstance(val, str):
210
+ # row_values.append(f"'{val}'")
211
+ # else:
212
+ # row_values.append(str(val))
213
+ values.append(f"({', '.join(row_values)})")
214
+
215
+ if values:
216
+ values_str = ", ".join(values)
217
+ # Construct the SQL INSERT statement
218
+ sql_statement = f"INSERT INTO {get_schema()}.{table_name} ({columns}) VALUES {values_str};"
219
+ return sql_statement
220
+
221
+
222
+ def apply_df_edits(df_original, df_edited, str_table, lst_id_columns, no_update_columns, dct_hard_default_columns):
223
+ booStatus = False
224
+ df_changed, df_new, df_deleted = _get_df_edits(df_original, df_edited, lst_id_columns)
225
+
226
+ # Generate SQL UPDATE statements
227
+ lst_update_SQL = _gen_df_update_sql(df_changed, str_table, lst_id_columns, no_update_columns)
228
+ if lst_update_SQL:
229
+ for str_sql in lst_update_SQL:
230
+ execute_sql(str_sql)
231
+ booStatus = True
232
+ # Generate SQL DELETE statements
233
+ lst_delete_SQL = _gen_df_delete_sql(df_deleted, str_table, lst_id_columns)
234
+ if lst_delete_SQL:
235
+ for str_sql in lst_delete_SQL:
236
+ execute_sql(str_sql)
237
+ booStatus = True
238
+ # Generate SQL INSERT statements
239
+ str_insert_sql = _gen_insert_sql(df_new, str_table, lst_id_columns, no_update_columns, dct_hard_default_columns)
240
+ if str_insert_sql:
241
+ execute_sql(str_insert_sql)
242
+ booStatus = True
243
+
244
+ return booStatus
245
+
246
+
247
+ def _start_target_db_engine(flavor, host, port, db_name, user, password, url, connect_by_url, connect_by_key, private_key, private_key_passphrase):
248
+ connection_params = {
249
+ "flavor": flavor if flavor != "redshift" else "postgresql",
250
+ "user": user,
251
+ "host": host,
252
+ "port": port,
253
+ "dbname": db_name,
254
+ "url": url,
255
+ "connect_by_url": connect_by_url,
256
+ "connect_by_key": connect_by_key,
257
+ "private_key": private_key,
258
+ "private_key_passphrase": private_key_passphrase,
259
+ "dbschema": None,
260
+ }
261
+ flavor_service = get_flavor_service(flavor)
262
+ flavor_service.init(connection_params)
263
+ connection_string = flavor_service.get_connection_string(password)
264
+ connect_args = {"connect_timeout": 3600}
265
+ connect_args.update(flavor_service.get_connect_args())
266
+ return create_engine(connection_string, connect_args=connect_args)
267
+
268
+
269
+ def retrieve_target_db_data(flavor, host, port, db_name, user, password, url, connect_by_url, connect_by_key, private_key, private_key_passphrase, sql_query, decrypt=False):
270
+ if decrypt:
271
+ password = DecryptText(password)
272
+ db_engine = _start_target_db_engine(flavor, host, port, db_name, user, password, url, connect_by_url, connect_by_key, private_key, private_key_passphrase)
273
+ with db_engine.connect() as connection:
274
+ query_result = connection.execute(text(sql_query))
275
+ return query_result.fetchall()
276
+
277
+
278
+ def retrieve_target_db_df(flavor, host, port, db_name, user, password, sql_query, url, connect_by_url, connect_by_key, private_key, private_key_passphrase):
279
+ if password:
280
+ password = DecryptText(password)
281
+ db_engine = _start_target_db_engine(flavor, host, port, db_name, user, password, url, connect_by_url, connect_by_key, private_key, private_key_passphrase)
282
+ return pd.read_sql_query(text(sql_query), db_engine)