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,629 @@
1
+ import concurrent.futures
2
+ import csv
3
+ import importlib
4
+ import logging
5
+ import queue as qu
6
+ import threading
7
+ from contextlib import suppress
8
+ from io import StringIO
9
+ from urllib.parse import quote_plus
10
+
11
+ from sqlalchemy import create_engine, text
12
+ from sqlalchemy.exc import ProgrammingError, SQLAlchemyError
13
+
14
+ from testgen import settings
15
+ from testgen.common.credentials import (
16
+ get_tg_db,
17
+ get_tg_host,
18
+ get_tg_password,
19
+ get_tg_port,
20
+ get_tg_schema,
21
+ get_tg_username,
22
+ )
23
+ from testgen.common.encrypt import DecryptText
24
+ from testgen.common.read_file import get_template_files
25
+
26
+ LOG = logging.getLogger("testgen")
27
+
28
+
29
+ class CConnectParms:
30
+ connectname = ""
31
+ projectcode = ""
32
+ connectid = ""
33
+ hostname = ""
34
+ port = ""
35
+ dbname = ""
36
+ schemaname = ""
37
+ username = ""
38
+ sql_flavor = ""
39
+ url = ""
40
+ connect_by_url = ""
41
+ connect_by_key = ""
42
+ private_key = ""
43
+ private_key_passphrase = ""
44
+ password = None
45
+
46
+ def __init__(self, connectname):
47
+ self.connectname = connectname
48
+
49
+
50
+ # Initialize variables global to this script
51
+ clsConnectParms = CConnectParms("NONE")
52
+ dctDBEngines = {}
53
+
54
+
55
+ def QuoteCSVItems(str_csv_row, char_quote='"'):
56
+ if str_csv_row:
57
+ lst_values = str_csv_row.split(",")
58
+ # Process each value individually, quoting it if not already quoted
59
+ str_quoted_values = ",".join(
60
+ [
61
+ (
62
+ f"{char_quote}{value}{char_quote}"
63
+ if not (value.startswith(char_quote) and value.endswith(char_quote))
64
+ else value
65
+ )
66
+ for value in lst_values
67
+ ]
68
+ )
69
+ return str_quoted_values
70
+ return str_csv_row
71
+
72
+
73
+ def empty_cache():
74
+ global dctDBEngines
75
+ dctDBEngines = {}
76
+
77
+
78
+ def AssignConnectParms(
79
+ projectcode,
80
+ connectid,
81
+ host,
82
+ port,
83
+ dbname,
84
+ schema,
85
+ user,
86
+ flavor,
87
+ url,
88
+ connect_by_url,
89
+ connect_by_key,
90
+ private_key,
91
+ private_key_passphrase,
92
+ connectname="PROJECT",
93
+ password=None,
94
+ ):
95
+ global clsConnectParms
96
+
97
+ clsConnectParms.connectname = connectname
98
+ clsConnectParms.projectcode = projectcode
99
+ clsConnectParms.connectid = connectid
100
+ clsConnectParms.hostname = host
101
+ clsConnectParms.port = port
102
+ clsConnectParms.dbname = dbname
103
+ clsConnectParms.schemaname = schema
104
+ clsConnectParms.username = user
105
+ clsConnectParms.sql_flavor = flavor
106
+ clsConnectParms.password = password
107
+ clsConnectParms.url = url
108
+ clsConnectParms.connect_by_url = connect_by_url
109
+ clsConnectParms.connect_by_key = connect_by_key
110
+ clsConnectParms.private_key = private_key
111
+ clsConnectParms.private_key_passphrase = private_key_passphrase
112
+
113
+
114
+ def _RetrieveProjectPW(strProjectCode, strConnID):
115
+ strSQL = """ SELECT project_pw_encrypted
116
+ FROM connections cc
117
+ WHERE cc.project_code = '{PROJECT_CODE}' AND cc.connection_id = {CONNECTION_ID}; """
118
+
119
+ # Replace Parameters
120
+ strSQL = strSQL.replace("{PROJECT_CODE}", strProjectCode)
121
+ strSQL = strSQL.replace("{CONNECTION_ID}", str(strConnID))
122
+ # Execute Query
123
+ strPW = RetrieveSingleResultValue("DKTG", strSQL)
124
+ # Convert Postgres bytea to Python byte array
125
+ strPW = bytes(strPW) if strPW else None
126
+
127
+ # Perform Decryption
128
+ strPW = DecryptText(strPW)
129
+ return strPW
130
+
131
+
132
+ def _GetDBPassword(strCredentialSet):
133
+ global clsConnectParms
134
+
135
+ if strCredentialSet == "PROJECT":
136
+ if not clsConnectParms.password:
137
+ strPW = _RetrieveProjectPW(clsConnectParms.projectcode, clsConnectParms.connectid)
138
+ else:
139
+ strPW = clsConnectParms.password
140
+ elif strCredentialSet == "DKTG":
141
+ strPW = get_tg_password()
142
+ else:
143
+ raise ValueError('Credential Set "' + strCredentialSet + '" is unknown.')
144
+
145
+ if strPW == "":
146
+ raise ValueError('Password for Credential Set "' + strCredentialSet + '" is unknown.')
147
+ else:
148
+ return strPW
149
+
150
+
151
+ def get_db_type(sql_flavor):
152
+ # This is for connection purposes. sqlalchemy 1.4.46 uses postgresql to connect to redshift database
153
+ if sql_flavor == "redshift":
154
+ return "postgresql"
155
+ else:
156
+ return sql_flavor
157
+
158
+
159
+ def _GetDBCredentials(strCredentialSet):
160
+ global clsConnectParms
161
+
162
+ if strCredentialSet == "PROJECT":
163
+ # Check for unassigned parms
164
+ if clsConnectParms.connectname == "NONE":
165
+ raise ValueError("Project Connection Parameters were not set.")
166
+
167
+ strConnectflavor = get_db_type(clsConnectParms.sql_flavor)
168
+
169
+ # Get project credentials from clsConnectParms
170
+ dctCredentials = {
171
+ "name": strCredentialSet,
172
+ "host": clsConnectParms.hostname,
173
+ "port": clsConnectParms.port,
174
+ "dbname": clsConnectParms.dbname,
175
+ "dbschema": clsConnectParms.schemaname,
176
+ "user": clsConnectParms.username,
177
+ "flavor": strConnectflavor,
178
+ "dbtype": clsConnectParms.sql_flavor,
179
+ "url": clsConnectParms.url,
180
+ "connect_by_url": clsConnectParms.connect_by_url,
181
+ "connect_by_key": clsConnectParms.connect_by_key,
182
+ "private_key": clsConnectParms.private_key,
183
+ "private_key_passphrase": clsConnectParms.private_key_passphrase,
184
+ }
185
+ elif strCredentialSet == "DKTG":
186
+ # Get credentials from functions in my_dk_credentials.py
187
+ dctCredentials = {
188
+ "name": strCredentialSet,
189
+ "host": get_tg_host(),
190
+ "port": get_tg_port(),
191
+ "dbname": get_tg_db(),
192
+ "dbschema": get_tg_schema(),
193
+ "user": get_tg_username(),
194
+ "flavor": "postgresql",
195
+ "dbtype": "postgresql",
196
+ }
197
+ else:
198
+ raise ValueError("Credentials for " + strCredentialSet + " are not defined.")
199
+
200
+ return dctCredentials
201
+
202
+
203
+ def get_flavor_service(flavor):
204
+ module_path = f"testgen.common.database.flavor.{flavor}_flavor_service"
205
+ class_name = f"{flavor.capitalize()}FlavorService"
206
+ module = importlib.import_module(module_path)
207
+ flavor_class = getattr(module, class_name)
208
+ return flavor_class()
209
+
210
+
211
+ def _InitDBConnection(strCredentialSet, strRaw="N", strAdmin="N", user_override=None, pwd_override=None):
212
+ # Get DB Credentials
213
+ dctCredentials = _GetDBCredentials(strCredentialSet)
214
+
215
+ if strCredentialSet == "DKTG":
216
+ con = _InitDBConnection_appdb(dctCredentials, strCredentialSet, strRaw, strAdmin, user_override, pwd_override)
217
+ else:
218
+ flavor_service = get_flavor_service(dctCredentials["dbtype"])
219
+ flavor_service.init(dctCredentials)
220
+ con = _InitDBConnection_target_db(flavor_service, strCredentialSet, strRaw, user_override, pwd_override)
221
+ return con
222
+
223
+
224
+ def _InitDBConnection_appdb(
225
+ dctCredentials, strCredentialSet, strRaw="N", strAdmin="N", user_override=None, pwd_override=None
226
+ ):
227
+ # Get DB Credentials
228
+ dctCredentials = _GetDBCredentials(strCredentialSet)
229
+
230
+ # Set DB Credential Overrides for Admin connections
231
+ # strAdmin = "N": Log into DB/schema for normal stuff
232
+ # strAdmin = "D": Log into postgres/public to create DB via override user/password
233
+ # strAdmin = "S": Log into DB/public to create schema and run scripts via override user/password
234
+ if strAdmin in {"D", "S"}:
235
+ dctCredentials["user"] = user_override
236
+ dctCredentials["dbschema"] = "public"
237
+ if strAdmin == "D":
238
+ dctCredentials["dbname"] = "postgres"
239
+
240
+ # Get DBEngine using credentials
241
+ if strCredentialSet in dctDBEngines and strAdmin == "N":
242
+ # Retrieve existing engine from store
243
+ dbEngine = dctDBEngines[strCredentialSet]
244
+ else:
245
+ # Handle Admin overrides or circumstantial password override
246
+ if strAdmin in {"D", "S"} or pwd_override is not None:
247
+ strPW = pwd_override
248
+ else:
249
+ strPW = _GetDBPassword(strCredentialSet)
250
+
251
+ # Open a new engine with appropriate connection parms
252
+ # STANDARD FORMAT: strConnect = 'flavor://username:password@host:port/database'
253
+ strConnect = "{}://{}:{}@{}:{}/{}".format(
254
+ dctCredentials["flavor"],
255
+ dctCredentials["user"],
256
+ quote_plus(strPW),
257
+ dctCredentials["host"],
258
+ dctCredentials["port"],
259
+ dctCredentials["dbname"],
260
+ )
261
+ try:
262
+ # Timeout in seconds: 1 hour = 60 * 60 second = 3600
263
+ dbEngine = create_engine(strConnect, connect_args={"connect_timeout": 3600})
264
+ dctDBEngines[strCredentialSet] = dbEngine
265
+
266
+ except SQLAlchemyError as e:
267
+ raise ValueError(
268
+ f"Failed to create engine (Admin={strAdmin}) \
269
+ for database {dctCredentials['dbname']}"
270
+ ) from e
271
+
272
+ # Second, create a connection from our engine
273
+ try:
274
+ if strRaw == "N":
275
+ con = dbEngine.connect()
276
+ if strAdmin == "N":
277
+ strSchemaSQL = f"SET SEARCH_PATH = {dctCredentials['dbschema']};"
278
+ con.execute(text(strSchemaSQL))
279
+ else:
280
+ con = dbEngine.raw_connection()
281
+ strSchemaSQL = "SET SEARCH_PATH = " + dctCredentials["dbschema"]
282
+ with con.cursor() as cur:
283
+ cur.execute(strSchemaSQL)
284
+ con.commit()
285
+ except SQLAlchemyError as e:
286
+ raise ValueError("Failed to connect to database " + dctCredentials["dbname"]) from e
287
+
288
+ return con
289
+
290
+
291
+ def _InitDBConnection_target_db(flavor_service, strCredentialSet, strRaw="N", user_override=None, pwd_override=None):
292
+ # Get DBEngine using credentials
293
+ if strCredentialSet in dctDBEngines:
294
+ # Retrieve existing engine from store
295
+ dbEngine = dctDBEngines[strCredentialSet]
296
+ else:
297
+ # Handle user override
298
+ if user_override is not None:
299
+ flavor_service.override_user(user_override)
300
+ # Handle password override
301
+ if pwd_override is not None:
302
+ strPW = pwd_override
303
+ elif not flavor_service.is_connect_by_key():
304
+ strPW = _GetDBPassword(strCredentialSet)
305
+ else:
306
+ strPW = None
307
+
308
+ # Open a new engine with appropriate connection parms
309
+ is_password_overwritten = pwd_override is not None
310
+ strConnect = flavor_service.get_connection_string(strPW, is_password_overwritten)
311
+
312
+ connect_args = {"connect_timeout": 3600}
313
+ connect_args.update(flavor_service.get_connect_args(is_password_overwritten))
314
+
315
+ try:
316
+ # Timeout in seconds: 1 hour = 60 * 60 second = 3600
317
+ dbEngine = create_engine(strConnect, connect_args=connect_args)
318
+ dctDBEngines[strCredentialSet] = dbEngine
319
+
320
+ except SQLAlchemyError as e:
321
+ raise ValueError(f"Failed to create engine for database {flavor_service.get_db_name}") from e
322
+
323
+ # Second, create a connection from our engine
324
+ queries = flavor_service.get_pre_connection_queries()
325
+ if strRaw == "N":
326
+ connection = dbEngine.connect()
327
+ for query in queries:
328
+ try:
329
+ connection.execute(text(query))
330
+ except Exception:
331
+ LOG.warning(
332
+ f"failed executing pre connection query: `{query}`",
333
+ exc_info=settings.IS_DEBUG,
334
+ stack_info=settings.IS_DEBUG,
335
+ )
336
+ else:
337
+ connection = dbEngine.raw_connection()
338
+ with connection.cursor() as cur:
339
+ for query in queries:
340
+ try:
341
+ cur.execute(query)
342
+ except Exception:
343
+ LOG.warning(
344
+ f"failed executing pre connection query: `{query}`",
345
+ exc_info=settings.IS_DEBUG,
346
+ stack_info=settings.IS_DEBUG,
347
+ )
348
+ connection.commit()
349
+
350
+ return connection
351
+
352
+
353
+ def CreateDatabaseIfNotExists(strDBName: str, params_mapping: dict, delete_db: bool, drop_users_and_roles: bool = True):
354
+ LOG.info("CurrentDB Operation: CreateDatabase. Creds: DKTG Admin")
355
+
356
+ con = _InitDBConnection(
357
+ "DKTG",
358
+ strAdmin="D",
359
+ user_override=params_mapping["TESTGEN_ADMIN_USER"],
360
+ pwd_override=params_mapping["TESTGEN_ADMIN_PASSWORD"],
361
+ )
362
+ con.execute("commit")
363
+
364
+ # Catch and ignore error if database already exists
365
+ with con:
366
+ if delete_db:
367
+ con.execute(
368
+ f"SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '{strDBName}'"
369
+ )
370
+ con.execute("commit")
371
+ con.execute(f"DROP DATABASE IF EXISTS {strDBName}")
372
+ con.execute("commit")
373
+ if drop_users_and_roles:
374
+ con.execute(replace_params("DROP USER IF EXISTS {TESTGEN_USER}", params_mapping))
375
+ con.execute(replace_params("DROP USER IF EXISTS {TESTGEN_REPORT_USER}", params_mapping))
376
+ con.execute("DROP ROLE IF EXISTS testgen_execute_role")
377
+ con.execute("DROP ROLE IF EXISTS testgen_report_role")
378
+ con.execute("commit")
379
+ with suppress(ProgrammingError):
380
+ con.execute("create database " + strDBName)
381
+ con.close()
382
+
383
+
384
+ def RunActionQueryList(strCredentialSet, lstQueries, strAdminNDS="N", user_override=None, pwd_override=None):
385
+ LOG.info("CurrentDB Operation: RunActionQueryList. Creds: %s", strCredentialSet)
386
+
387
+ with _InitDBConnection(
388
+ strCredentialSet, strAdmin=strAdminNDS, user_override=user_override, pwd_override=pwd_override
389
+ ) as con:
390
+ i = 0
391
+ n = len(lstQueries)
392
+ if n == 0:
393
+ LOG.info("No queries to process")
394
+ for q in lstQueries:
395
+ i += 1
396
+ LOG.debug(f"LastQuery = {q}")
397
+ LOG.info(f"(Processing {i} of {n})")
398
+ tx = con.begin()
399
+ exQ = con.execute(text(q))
400
+ if exQ.rowcount == -1:
401
+ strMsg = "Action query processed no records."
402
+ else:
403
+ strMsg = str(exQ.rowcount) + " records processed."
404
+ tx.commit()
405
+ LOG.info(strMsg)
406
+
407
+
408
+ def RunRetrievalQueryList(strCredentialSet, lstQueries):
409
+ LOG.info("CurrentDB Operation: RunRetrievalQueryList. Creds: %s", strCredentialSet)
410
+
411
+ with _InitDBConnection(strCredentialSet) as con:
412
+ colNames = None
413
+ lstResults = []
414
+ i = 0
415
+ n = len(lstQueries)
416
+ if n == 0:
417
+ LOG.info("No queries to process")
418
+ for q in lstQueries:
419
+ i += 1
420
+ LOG.debug("LastQuery = %s", q)
421
+ LOG.info("(Processing %s of %s)", i, n)
422
+
423
+ exQ = con.execute(text(q))
424
+ lstOneResult = exQ.fetchall()
425
+ if not colNames:
426
+ colNames = exQ.keys()
427
+ strRows = str(exQ.rowcount)
428
+ lstResults.extend(lstOneResult)
429
+
430
+ LOG.info("%s records retrieved.", strRows)
431
+
432
+ return lstResults, colNames
433
+
434
+
435
+ class _CThreadedFetch:
436
+ def __init__(self, strCredentialSet, count_lock):
437
+ self.strCredentialSet = strCredentialSet
438
+ self.count_lock = count_lock
439
+ self.count = 0
440
+
441
+ def __call__(self, strQuery):
442
+ colNames = None
443
+ lstResult = None
444
+ booError = False
445
+
446
+ with self.count_lock:
447
+ self.count += 1
448
+ i = self.count
449
+
450
+ try:
451
+ with _InitDBConnection(self.strCredentialSet) as con:
452
+ try:
453
+ exQ = con.execute(text(strQuery))
454
+ lstResult = exQ.fetchall()
455
+ if not colNames:
456
+ colNames = exQ.keys()
457
+ LOG.info("(Processed Threaded Query %s on thread %s)", i, threading.current_thread().name)
458
+ except Exception:
459
+ LOG.exception(f"Failed Query. LastQuery: {strQuery}")
460
+ booError = True
461
+ except Exception as e:
462
+ LOG.info("LastQuery: %s", strQuery)
463
+ raise ValueError(f"Failed to execute threaded query: {e}") from e
464
+ else:
465
+ return lstResult, colNames, booError
466
+
467
+
468
+ def RunThreadedRetrievalQueryList(strCredentialSet, lstQueries, intMaxThreads, spinner):
469
+ LOG.info("CurrentDB Operation: RunThreadedRetrievalQueryList. Creds: %s", strCredentialSet)
470
+
471
+ lstResults = []
472
+ colNames = []
473
+ intErrors = 0
474
+
475
+ if intMaxThreads is None:
476
+ intMaxThreads = 4
477
+ elif intMaxThreads < 1 or intMaxThreads > 10:
478
+ intMaxThreads = 4
479
+
480
+ qq = qu.Queue()
481
+
482
+ for query in lstQueries:
483
+ qq.put(query)
484
+
485
+ # Initialize count and lock
486
+ count_lock = threading.Lock()
487
+
488
+ clsThreadedFetch = _CThreadedFetch(strCredentialSet, count_lock)
489
+
490
+ with concurrent.futures.ThreadPoolExecutor(max_workers=intMaxThreads) as executor:
491
+ try:
492
+ futures = []
493
+ while not qq.empty():
494
+ query = qq.get()
495
+ futures.append(executor.submit(clsThreadedFetch, query))
496
+
497
+ for future in futures:
498
+ lstOneResult, colName, booError = future.result()
499
+ if spinner:
500
+ spinner.next()
501
+ intErrors += 1 if booError else 0
502
+ if lstOneResult:
503
+ lstResults.append(lstOneResult)
504
+ colNames = colName
505
+
506
+ except Exception:
507
+ LOG.exception("Failed to execute threaded queries")
508
+
509
+ lstResults = [element for sublist in lstResults for element in sublist]
510
+
511
+ return lstResults, colNames, intErrors
512
+
513
+
514
+ def RetrieveDBResultsToList(strCredentialSet, strRunSQL):
515
+ LOG.info("CurrentDB Operation: RetrieveDBResultsToList. Creds: %s", strCredentialSet)
516
+
517
+ with _InitDBConnection(strCredentialSet) as con:
518
+ exQ = con.execute(text(strRunSQL))
519
+ lstResults = exQ.fetchall()
520
+ colNames = exQ.keys()
521
+
522
+ LOG.debug("Last Query='%s'", strRunSQL)
523
+ LOG.debug("%s records retrieved.", exQ.rowcount)
524
+
525
+ return lstResults, colNames
526
+
527
+
528
+ def RetrieveDBResultsToDictList(strCredentialSet, strRunSQL):
529
+ LOG.info("CurrentDB Operation: RetrieveDBResultsToDictList. Creds: %s", strCredentialSet)
530
+ LOG.info("(Processing Query)")
531
+
532
+ with _InitDBConnection(strCredentialSet) as con:
533
+ LOG.debug("Last Query='%s'", strRunSQL)
534
+ exQ = con.execute(text(strRunSQL))
535
+
536
+ # Creates list of dictionaries so records are addressible by column name
537
+ lstResults = [row._mapping for row in exQ]
538
+ LOG.debug("%s records retrieved.", exQ.rowcount)
539
+
540
+ return lstResults
541
+
542
+
543
+ def ExecuteDBQuery(strCredentialSet, strRunSQL):
544
+ LOG.info("CurrentDB Operation: ExecuteDBQuery. Creds: %s", strCredentialSet)
545
+ LOG.info("(Processing Query)")
546
+
547
+ with _InitDBConnection(strCredentialSet) as con:
548
+ LOG.debug("Last Query='%s'", strRunSQL)
549
+ con.execute(text(strRunSQL))
550
+ con.execute("commit")
551
+ LOG.debug("Query ran.")
552
+
553
+
554
+ def RetrieveSingleResultValue(strCredentialSet, strRunSQL):
555
+ LOG.info("CurrentDB Operation: RetrieveSingleResultValue. Creds: %s", strCredentialSet)
556
+
557
+ with _InitDBConnection(strCredentialSet) as con:
558
+ LOG.debug("Last Query='%s'", strRunSQL)
559
+ lstResult = con.execute(text(strRunSQL)).fetchone()
560
+ if lstResult:
561
+ LOG.debug("Single result retrieved.")
562
+ valReturn = lstResult[0]
563
+ return valReturn
564
+ else:
565
+ LOG.debug("Single result NOT retrieved.")
566
+
567
+
568
+ def WriteListToDB(strCredentialSet, lstData, lstColumns, strDBTable):
569
+ LOG.info("CurrentDB Operation: WriteListToDB. Creds: %s", strCredentialSet)
570
+ LOG.debug("(Processing ingestion query: %s records)", lstData)
571
+ # List should have same column names as destination table, though not all columns in table are required
572
+
573
+ # Use COPY for DKTG database, otherwise executemany()
574
+
575
+ con = _InitDBConnection(strCredentialSet, "Y")
576
+ cur = con.cursor()
577
+ if strCredentialSet == "DKTG":
578
+ # Write List to CSV in memory
579
+ sio = StringIO()
580
+ writer = csv.writer(sio, quoting=csv.QUOTE_MINIMAL)
581
+ writer.writerows(lstData)
582
+ sio.seek(0)
583
+
584
+ # Get list of column names for COPY statement
585
+ strColumnNames = ", ".join(lstColumns)
586
+ strCopySQL = f"COPY {strDBTable} ({strColumnNames}) FROM STDIN WITH (FORMAT CSV)"
587
+ LOG.debug("Last Query='%s'", strCopySQL)
588
+
589
+ cur.copy_expert(strCopySQL, sio)
590
+ con.commit()
591
+
592
+ else:
593
+ # Get list of column names and column names formatted as parms
594
+ strColumnNames = ", ".join(lstColumns)
595
+ lstColumnParms = [":" + column_name for column_name in lstColumns]
596
+ strColumnParms = ", ".join(lstColumnParms)
597
+
598
+ # Prep data as list of dictionaries
599
+ lstRowDicts = [dict(row) for row in lstData]
600
+
601
+ strInsertSQL = "INSERT INTO " + strDBTable + "(" + strColumnNames + ")" + " VALUES (" + strColumnParms + ")"
602
+ LOG.debug("Last Query='%s'", strInsertSQL)
603
+
604
+ exQ = con.execute(text(strInsertSQL), lstRowDicts)
605
+ con.commit()
606
+ LOG.debug("%s records saved", exQ.rowcount)
607
+ con.close()
608
+
609
+
610
+ def replace_params(query: str, params_mapping: dict) -> str:
611
+ for key, value in params_mapping.items():
612
+ query = query.replace(f"{{{key}}}", str(value))
613
+ return query
614
+
615
+
616
+ def get_queries_for_command(sub_directory: str, params_mapping: dict, mask: str = r"^.*sql$", path: str | None = None) -> list[str]:
617
+ files = sorted(get_template_files(mask=mask, sub_directory=sub_directory, path=path), key=lambda key: str(key))
618
+
619
+ queries = []
620
+ for file in files:
621
+ query = file.read_text("utf-8")
622
+ template = replace_params(query, params_mapping)
623
+
624
+ queries.append(template)
625
+
626
+ if len(queries) == 0:
627
+ LOG.warning(f"No sql files were found for the mask {mask} in subdirectory {sub_directory}")
628
+
629
+ return queries
File without changes
@@ -0,0 +1,75 @@
1
+ from abc import abstractmethod
2
+
3
+ from testgen import settings
4
+ from testgen.common.encrypt import DecryptText
5
+
6
+
7
+ class FlavorService:
8
+
9
+ url = None
10
+ connect_by_url = None
11
+ username = None
12
+ host = None
13
+ port = None
14
+ dbname = None
15
+ flavor = None
16
+ dbschema = None
17
+ connect_by_key = None
18
+ private_key = None
19
+ private_key_passphrase = None
20
+ catalog = None
21
+
22
+ def init(self, connection_params: dict):
23
+ self.url = connection_params.get("url", None)
24
+ self.connect_by_url = connection_params.get("connect_by_url", False)
25
+ self.username = connection_params.get("user")
26
+ self.host = connection_params.get("host")
27
+ self.port = connection_params.get("port")
28
+ self.dbname = connection_params.get("dbname")
29
+ self.flavor = connection_params.get("flavor")
30
+ self.dbschema = connection_params.get("dbschema", None)
31
+ self.connect_by_key = connection_params.get("connect_by_key", False)
32
+ self.catalog = connection_params.get("catalog", None)
33
+
34
+ private_key = connection_params.get("private_key", None)
35
+ if isinstance(private_key, memoryview):
36
+ private_key = DecryptText(private_key)
37
+ self.private_key = private_key
38
+
39
+ private_key_passphrase = connection_params.get("private_key_passphrase", None)
40
+ if isinstance(private_key_passphrase, memoryview):
41
+ private_key_passphrase = DecryptText(private_key_passphrase)
42
+ self.private_key_passphrase = private_key_passphrase
43
+
44
+ def override_user(self, user_override: str):
45
+ self.username = user_override
46
+
47
+ def get_db_name(self) -> str:
48
+ return self.dbname
49
+
50
+ def is_connect_by_key(self) -> str:
51
+ return self.connect_by_key
52
+
53
+ def get_connect_args(self, is_password_overwritten: bool = False): # NOQA ARG002
54
+ if settings.SKIP_DATABASE_CERTIFICATE_VERIFICATION:
55
+ return {"TrustServerCertificate": "yes"}
56
+ return {}
57
+
58
+ def get_concat_operator(self):
59
+ return "||"
60
+
61
+ def get_connection_string(self, strPW, is_password_overwritten: bool = False):
62
+ if self.connect_by_url:
63
+ header = self.get_connection_string_head(strPW)
64
+ url = header + self.url
65
+ return url
66
+ else:
67
+ return self.get_connection_string_from_fields(strPW, is_password_overwritten)
68
+
69
+ @abstractmethod
70
+ def get_connection_string_from_fields(self, strPW, is_password_overwritten: bool = False):
71
+ raise NotImplementedError("Subclasses must implement this method")
72
+
73
+ @abstractmethod
74
+ def get_connection_string_head(self, strPW):
75
+ raise NotImplementedError("Subclasses must implement this method")