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,34 @@
1
+ from urllib.parse import quote_plus
2
+
3
+ from testgen.common.database.flavor.flavor_service import FlavorService
4
+
5
+
6
+ class MssqlFlavorService(FlavorService):
7
+ def get_connection_string_head(self, strPW):
8
+ username = self.username
9
+ password = quote_plus(strPW)
10
+
11
+ strConnect = f"mssql+pyodbc://{username}:{password}@"
12
+
13
+ return strConnect
14
+
15
+ def get_connection_string_from_fields(self, strPW, is_password_overwritten: bool = False): # NOQA ARG002
16
+ password = quote_plus(strPW)
17
+
18
+ strConnect = (
19
+ f"mssql+pyodbc://{self.username}:{password}@{self.host}:{self.port}/{self.dbname}?driver=ODBC+Driver+18+for+SQL+Server"
20
+ )
21
+
22
+ if "synapse" in self.host:
23
+ strConnect += "&autocommit=True"
24
+
25
+ return strConnect
26
+
27
+ def get_pre_connection_queries(self): # ARG002
28
+ return [
29
+ "SET ANSI_DEFAULTS ON;",
30
+ "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;",
31
+ ]
32
+
33
+ def get_concat_operator(self):
34
+ return "+"
@@ -0,0 +1,5 @@
1
+ from testgen.common.database.flavor.redshift_flavor_service import RedshiftFlavorService
2
+
3
+
4
+ class PostgresqlFlavorService(RedshiftFlavorService):
5
+ pass
@@ -0,0 +1,22 @@
1
+ from urllib.parse import quote_plus
2
+
3
+ from testgen.common.database.flavor.flavor_service import FlavorService
4
+
5
+
6
+ class RedshiftFlavorService(FlavorService):
7
+ def get_connection_string_head(self, strPW):
8
+ strConnect = f"{self.flavor}://{self.username}:{quote_plus(strPW)}@"
9
+ return strConnect
10
+
11
+ def get_connection_string_from_fields(self, strPW, is_password_overwritten: bool = False): # NOQA ARG002
12
+ # STANDARD FORMAT: strConnect = 'flavor://username:password@host:port/database'
13
+ strConnect = f"{self.flavor}://{self.username}:{quote_plus(strPW)}@{self.host}:{self.port}/{self.dbname}"
14
+ return strConnect
15
+
16
+ def get_pre_connection_queries(self):
17
+ return [
18
+ "SET SEARCH_PATH = '" + self.dbschema + "'",
19
+ ]
20
+
21
+ def get_connect_args(self, is_password_overwritten: bool = False): # NOQA ARG002
22
+ return {}
@@ -0,0 +1,69 @@
1
+ from urllib.parse import quote_plus
2
+
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import serialization
5
+
6
+ from testgen.common.database.flavor.flavor_service import FlavorService
7
+
8
+
9
+ class SnowflakeFlavorService(FlavorService):
10
+
11
+ def get_connect_args(self, is_password_overwritten: bool = False):
12
+ connect_args = super().get_connect_args(is_password_overwritten)
13
+
14
+ if self.connect_by_key and not is_password_overwritten:
15
+ # https://docs.snowflake.com/en/developer-guide/python-connector/sqlalchemy#key-pair-authentication-support
16
+ private_key_passphrase = self.private_key_passphrase.encode() if self.private_key_passphrase else None
17
+ private_key = serialization.load_pem_private_key(
18
+ self.private_key.encode(),
19
+ password=private_key_passphrase,
20
+ backend=default_backend(),
21
+ )
22
+
23
+ private_key_bytes = private_key.private_bytes(
24
+ encoding=serialization.Encoding.DER,
25
+ format=serialization.PrivateFormat.PKCS8,
26
+ encryption_algorithm=serialization.NoEncryption(),
27
+ )
28
+
29
+ connect_args.update({"private_key": private_key_bytes})
30
+ return connect_args
31
+
32
+ def get_connection_string_head(self, strPW):
33
+ if self.connect_by_key and not strPW:
34
+ strConnect = f"snowflake://{self.username}@"
35
+ else:
36
+ strConnect = f"snowflake://{self.username}:{quote_plus(strPW)}@"
37
+ return strConnect
38
+
39
+ def get_connection_string_from_fields(self, strPW, is_password_overwritten: bool = False):
40
+ # SNOWFLAKE FORMAT: strConnect = 'flavor://username:password@host/database'
41
+ # optionally + '/[schema]' + '?warehouse=xxx'
42
+ # NOTE: Snowflake host should NOT include ".snowflakecomputing.com"
43
+
44
+ def get_raw_host_name(host):
45
+ endings = [
46
+ ".snowflakecomputing.com",
47
+ ]
48
+ for ending in endings:
49
+ if host.endswith(ending):
50
+ i = host.index(ending)
51
+ return host[0:i]
52
+ return host
53
+
54
+ raw_host = get_raw_host_name(self.host)
55
+ host = raw_host
56
+ if self.port != "443":
57
+ host += ":" + self.port
58
+
59
+ if self.connect_by_key and not is_password_overwritten:
60
+ strConnect = f"snowflake://{self.username}@{host}/{self.dbname}/{self.dbschema}"
61
+ else:
62
+ strConnect = f"snowflake://{self.username}:{quote_plus(strPW)}@{host}/{self.dbname}/{self.dbschema}"
63
+ return strConnect
64
+
65
+ def get_pre_connection_queries(self): # ARG002
66
+ return [
67
+ "ALTER SESSION SET MULTI_STATEMENT_COUNT = 0;",
68
+ "ALTER SESSION SET WEEK_START = 7;",
69
+ ]
@@ -0,0 +1,21 @@
1
+ from urllib.parse import quote_plus
2
+
3
+ from testgen.common.database.flavor.flavor_service import FlavorService
4
+
5
+
6
+ class TrinoFlavorService(FlavorService):
7
+ def get_connection_string_head(self, strPW):
8
+ strConnect = f"{self.flavor}://{self.username}:{quote_plus(strPW)}@"
9
+ return strConnect
10
+
11
+ def get_connection_string_from_fields(self, strPW, is_password_overwritten: bool = False): # NOQA ARG002
12
+ # STANDARD FORMAT: strConnect = 'flavor://username:password@host:port/catalog'
13
+ return f"{self.flavor}://{self.username}:{quote_plus(strPW)}@{self.host}:{self.port}/{self.catalog}"
14
+
15
+ def get_pre_connection_queries(self):
16
+ return [
17
+ "USE " + self.catalog + "." + self.dbschema,
18
+ ]
19
+
20
+ def get_connect_args(self, is_password_overwritten: bool = False): # NOQA ARG002
21
+ return {}
@@ -0,0 +1,68 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import pandas as pd
4
+
5
+
6
+ def get_today_as_string():
7
+ return datetime.utcnow().strftime("%Y-%m-%d")
8
+
9
+
10
+ def get_now_as_string():
11
+ return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
12
+
13
+
14
+ def get_now_as_string_with_offset(minutes_offset):
15
+ ret = datetime.utcnow()
16
+ if minutes_offset > 0:
17
+ ret = ret + timedelta(minutes=minutes_offset)
18
+ return ret.strftime("%Y-%m-%d %H:%M:%S")
19
+
20
+
21
+ def get_now_as_iso_timestamp():
22
+ return as_iso_timestamp(datetime.utcnow())
23
+
24
+
25
+ def as_iso_timestamp(date: datetime) -> str | None:
26
+ if date is None:
27
+ return None
28
+ return date.strftime("%Y-%m-%dT%H:%M:%SZ")
29
+
30
+
31
+ def accommodate_dataframe_to_timezone(df, streamlit_session, time_columns=None):
32
+ if time_columns is None:
33
+ time_columns = []
34
+ for column_name in df.columns:
35
+ if df[column_name].dtype == "datetime64[ns]":
36
+ time_columns.append(column_name)
37
+
38
+ if time_columns and "browser_timezone" in streamlit_session:
39
+ timezone = streamlit_session["browser_timezone"]
40
+ for time_column in time_columns:
41
+ df[time_column] = pd.to_datetime(df[time_column], errors="coerce")
42
+ df[time_column] = df[time_column].dt.tz_localize("UTC")
43
+ df[time_column] = df[time_column].dt.tz_convert(timezone)
44
+ df[time_column] = df[time_column].dt.strftime("%Y-%m-%d %H:%M:%S")
45
+
46
+
47
+ def create_timezoned_column_in_dataframe(streamlit_session, df, new_column_name, existing_column_name):
48
+ if new_column_name and existing_column_name and "browser_timezone" in streamlit_session:
49
+ timezone = streamlit_session["browser_timezone"]
50
+ df[new_column_name] = (
51
+ df[existing_column_name].dt.tz_localize("UTC").dt.tz_convert(timezone).dt.strftime("%Y-%m-%d %H:%M:%S")
52
+ )
53
+
54
+
55
+ def get_timezoned_timestamp(streamlit_session, value):
56
+ ret = None
57
+ if value and "browser_timezone" in streamlit_session:
58
+ data = {"value": [value]}
59
+ df = pd.DataFrame(data)
60
+ timezone = streamlit_session["browser_timezone"]
61
+ df["value"] = df["value"].dt.tz_localize("UTC").dt.tz_convert(timezone).dt.strftime("%Y-%m-%d %H:%M:%S")
62
+ ret = df.iloc[0, 0]
63
+ return ret
64
+
65
+
66
+ def get_timezoned_now(streamlit_session):
67
+ value = datetime.utcnow()
68
+ return get_timezoned_timestamp(streamlit_session, value)
@@ -0,0 +1,85 @@
1
+ import csv
2
+ import logging
3
+ import os
4
+
5
+ import click
6
+ import yaml
7
+ from prettytable import PrettyTable
8
+
9
+ LOG = logging.getLogger("testgen")
10
+
11
+
12
+ def print_table(rows: list[dict], column_names: list[str]):
13
+ table = PrettyTable(column_names)
14
+ table.max_width = 80
15
+ table.align = "l"
16
+
17
+ for row in rows:
18
+ table.add_row(row)
19
+ click.echo(table)
20
+
21
+
22
+ def to_csv(file_name: str, rows: list[dict], column_names: list[str]):
23
+ _, file_out_path = get_in_out_paths()
24
+ full_path = os.path.join(file_out_path, file_name)
25
+ with open(full_path, "w", newline="") as file:
26
+ writer = csv.writer(file)
27
+ writer.writerow(column_names)
28
+ for row in rows:
29
+ writer.writerow(row)
30
+ echo(f"Output written to: ~/testgen/file-out/{file_name}")
31
+
32
+
33
+ def get_in_out_paths():
34
+ # create the paths if not exist
35
+ home = os.path.expanduser("~")
36
+ file_in_path = os.path.join(home, "testgen", "file-in")
37
+ file_out_path = os.path.join(home, "testgen", "file-out")
38
+ os.makedirs(file_in_path, exist_ok=True)
39
+ os.makedirs(file_out_path, exist_ok=True)
40
+ return file_in_path, file_out_path
41
+
42
+
43
+ def write_to_file(full_path_and_name: str, file_content: str):
44
+ with open(full_path_and_name, "w") as file:
45
+ file.write(file_content)
46
+
47
+
48
+ def to_yaml(file_name: str, yaml_dict: dict, display: bool):
49
+ yaml_content = yaml.dump(yaml_dict, sort_keys=False)
50
+ yaml_content.replace("None", "null")
51
+
52
+ _, file_out_path = get_in_out_paths()
53
+ full_path = os.path.join(file_out_path, file_name)
54
+ with open(full_path, "w", newline="") as file:
55
+ file.write(yaml_content)
56
+
57
+ if display:
58
+ echo(yaml_content + "\n")
59
+
60
+ echo(f"Output written to: ~/testgen/file-out/{file_name}")
61
+
62
+
63
+ def echo(message: str):
64
+ click.echo(message)
65
+
66
+
67
+ def from_yaml(file_name: str, display: bool):
68
+ echo(f"Attempting to read from : ~/testgen/file-in/{file_name}")
69
+ file_in_path, _ = get_in_out_paths()
70
+ full_path = os.path.join(file_in_path, file_name)
71
+ with open(full_path, newline="") as file:
72
+ yaml_content = yaml.safe_load(file)
73
+
74
+ if display:
75
+ data = yaml.dump(yaml_content, sort_keys=False)
76
+ echo(data)
77
+
78
+ return yaml_content
79
+
80
+
81
+ def check_config_file_presence(file_name: str) -> None:
82
+ file_in_path, _ = get_in_out_paths()
83
+ full_path = os.path.join(file_in_path, file_name)
84
+ if not os.path.exists(full_path):
85
+ echo(click.style(f"Warning: File ~/testgen/file-in/{file_name} is not present.", fg="yellow"))
@@ -0,0 +1,76 @@
1
+ import logging
2
+
3
+ import requests
4
+
5
+ from testgen import settings
6
+ from testgen.common import get_tg_db, get_tg_host, get_tg_password, get_tg_schema, get_tg_username
7
+
8
+ LOG = logging.getLogger("testgen")
9
+
10
+
11
+
12
+ def check_for_new_docker_release() -> str:
13
+ if not settings.CHECK_FOR_LATEST_VERSION:
14
+ return "unknown"
15
+
16
+ try:
17
+ tags = get_docker_tags()
18
+
19
+ if len(tags) == 0:
20
+ LOG.debug("docker_service: No tags to parse, skipping check.")
21
+ return "unknown"
22
+
23
+ ordered_tags = sorted(tags, key=lambda item: item[1], reverse=True)
24
+ latest_tag = ordered_tags[0][0]
25
+
26
+ if latest_tag != settings.VERSION:
27
+ LOG.warning(
28
+ f"A new TestGen upgrade is available. Please update to version {latest_tag} for new features and improvements."
29
+ )
30
+
31
+ return latest_tag # noqa: TRY300
32
+ except Exception:
33
+ LOG.warning("Unable to check for latest release", exc_info=True, stack_info=True)
34
+
35
+
36
+ def get_docker_tags(url: str = "https://hub.docker.com/v2/repositories/datakitchen/dataops-testgen/tags/"):
37
+ params = {"page_size": 25, "page": 1, "ordering": "last_updated"}
38
+ response = requests.get(url, params=params, timeout=3)
39
+
40
+ tags_to_return = []
41
+ if not response.status_code == 200:
42
+ LOG.warning(f"docker_service: Failed to fetch docker tags. Status code: {response.status_code}")
43
+ return tags_to_return
44
+
45
+ tags_data = response.json()
46
+ results = tags_data.get("results", [])
47
+ for result in results:
48
+ tag_name = result["name"]
49
+ last_pushed = result["tag_last_pushed"]
50
+ if tag_name.count(".") >= 2 and "experimental" not in tag_name:
51
+ tags_to_return.append((tag_name, last_pushed))
52
+
53
+ return tags_to_return
54
+
55
+
56
+ def check_basic_configuration():
57
+ ret = True
58
+ message = ""
59
+
60
+ configs = [
61
+ ("host", get_tg_host),
62
+ ("username", get_tg_username),
63
+ ("password", get_tg_password),
64
+ ("schema", get_tg_schema),
65
+ ("db", get_tg_db),
66
+ ]
67
+
68
+ for config in configs:
69
+ if not config[1]():
70
+ ret = False
71
+ message += f"\n{config[0]} configuration is missing."
72
+
73
+ if message:
74
+ message = "The system is not properly configured. Please check. Details: \n" + message
75
+
76
+ return ret, message
@@ -0,0 +1,55 @@
1
+ import base64
2
+
3
+ import streamlit_authenticator as stauth
4
+ from Crypto.Cipher import AES
5
+ from Crypto.Protocol.KDF import PBKDF2
6
+ from Crypto.Random import get_random_bytes
7
+
8
+ from testgen import settings
9
+
10
+
11
+ def EncryptText(strText):
12
+ block_size = 16
13
+
14
+ def pad(s):
15
+ return s + (block_size - len(s) % block_size) * chr(block_size - len(s) % block_size)
16
+
17
+ # Generate a random salt
18
+ salt = settings.APP_ENCRYPTION_SALT.encode("ascii")
19
+ strPassword = settings.APP_ENCRYPTION_SECRET.encode("ascii")
20
+
21
+ # Derive the key using PBKDF2
22
+ kdf = PBKDF2(strPassword, salt, 64, 1000)
23
+ private_key = kdf[:32]
24
+
25
+ # Initialize the cipher
26
+ strText = pad(strText)
27
+ strText = bytes(strText, "utf-8")
28
+ iv = get_random_bytes(AES.block_size)
29
+ cipher = AES.new(private_key, AES.MODE_CBC, iv)
30
+
31
+ # Perform encryption
32
+ encrypted_text = base64.b64encode(iv + cipher.encrypt(strText))
33
+ return encrypted_text.decode("UTF-8")
34
+
35
+
36
+ def DecryptText(baEncrypted):
37
+ def unpad(s):
38
+ return s[: -ord(s[len(s) - 1 :])]
39
+
40
+ # Calc Private Key from Password
41
+ salt = settings.APP_ENCRYPTION_SALT.encode("ascii")
42
+ strPassword = settings.APP_ENCRYPTION_SECRET.encode("ascii")
43
+ kdf = PBKDF2(strPassword, salt, 64, 1000)
44
+ private_key = kdf[:32]
45
+
46
+ baEncrypted = base64.b64decode(baEncrypted)
47
+ iv = baEncrypted[:16]
48
+ cipher = AES.new(private_key, AES.MODE_CBC, iv)
49
+
50
+ return bytes.decode(unpad(cipher.decrypt(baEncrypted[16:])))
51
+
52
+
53
+ def encrypt_ui_password(plain_password):
54
+ hashed_passwords = stauth.Hasher([plain_password]).generate()
55
+ return hashed_passwords.pop()
@@ -0,0 +1,57 @@
1
+ from testgen.common.database.database_service import RetrieveDBResultsToDictList
2
+ from testgen.common.read_file import read_template_sql_file
3
+
4
+
5
+ def RetrieveProfilingParms(strTableGroupsID):
6
+ strSQL = read_template_sql_file("parms_profiling.sql", "parms")
7
+ # Replace Parameters
8
+ strSQL = strSQL.replace("{TABLE_GROUPS_ID}", strTableGroupsID)
9
+
10
+ # Execute Query
11
+ lstParms = RetrieveDBResultsToDictList("DKTG", strSQL)
12
+
13
+ if lstParms is None:
14
+ raise ValueError("Project Connection Parameters not found")
15
+ elif (
16
+ lstParms[0]["project_code"] == ""
17
+ or lstParms[0]["connection_id"] == ""
18
+ or lstParms[0]["sql_flavor"] == ""
19
+ or lstParms[0]["project_user"] == ""
20
+ or lstParms[0]["profile_use_sampling"] == ""
21
+ or lstParms[0]["profile_sample_percent"] == ""
22
+ or lstParms[0]["profile_sample_min_count"] == ""
23
+ or lstParms[0]["project_qc_schema"] == ""
24
+ or lstParms[0]["table_group_schema"] == ""
25
+ ):
26
+ raise ValueError("Project Connection parameters not correctly set")
27
+ else:
28
+ return lstParms[0]
29
+
30
+
31
+ def RetrieveTestGenParms(strTableGroupsID, strTestSuite):
32
+ strSQL = read_template_sql_file("parms_test_gen.sql", "parms")
33
+ # Replace Parameters
34
+ strSQL = strSQL.replace("{TABLE_GROUPS_ID}", strTableGroupsID)
35
+ strSQL = strSQL.replace("{TEST_SUITE}", strTestSuite)
36
+
37
+ # Execute Query
38
+ lstParms = RetrieveDBResultsToDictList("DKTG", strSQL)
39
+ if len(lstParms) == 0:
40
+ raise ValueError("SQL retrieved 0 records")
41
+ return lstParms[0]
42
+
43
+
44
+ def RetrieveTestExecParms(strProjectCode, strTestSuite):
45
+ strSQL = read_template_sql_file("parms_test_execution.sql", "parms")
46
+ # Replace Parameters
47
+ strSQL = strSQL.replace("{PROJECT_CODE}", strProjectCode)
48
+ strSQL = strSQL.replace("{TEST_SUITE}", strTestSuite)
49
+
50
+ # Execute Query
51
+ lstParms = RetrieveDBResultsToDictList("DKTG", strSQL)
52
+ if len(lstParms) == 0:
53
+ raise ValueError("Test Execution parameters could not be retrieved")
54
+ elif len(lstParms) > 1:
55
+ raise ValueError("Test Execution parameters returned too many records")
56
+
57
+ return lstParms[0]
testgen/common/logs.py ADDED
@@ -0,0 +1,79 @@
1
+ __all__ = ["configure_logging"]
2
+
3
+ import io
4
+ import logging
5
+ import logging.handlers
6
+ import os
7
+ import sys
8
+ import threading
9
+
10
+ from concurrent_log_handler import ConcurrentTimedRotatingFileHandler
11
+
12
+ from testgen import settings
13
+
14
+
15
+ def configure_logging(
16
+ level: int = logging.DEBUG,
17
+ log_format: str = "[PID: %(process)s] %(asctime)s - %(levelname)s - %(message)s",
18
+ ) -> None:
19
+ """
20
+ Configures the testgen logger.
21
+ """
22
+ logger = logging.getLogger("testgen")
23
+ logger.setLevel(level)
24
+
25
+ formatter = logging.Formatter(log_format)
26
+
27
+ console_out_handler = logging.StreamHandler(stream=sys.stdout)
28
+ if settings.IS_DEBUG:
29
+ console_out_handler.setLevel(level)
30
+ else:
31
+ console_out_handler.setLevel(logging.WARNING)
32
+ console_out_handler.setFormatter(formatter)
33
+
34
+ console_err_handler = logging.StreamHandler(stream=sys.stderr)
35
+ console_err_handler.setLevel(logging.WARNING)
36
+ console_err_handler.setFormatter(formatter)
37
+
38
+ logger.addHandler(console_out_handler)
39
+ logger.addHandler(console_err_handler)
40
+
41
+ if settings.LOG_TO_FILE:
42
+ os.makedirs(settings.LOG_FILE_PATH, exist_ok=True)
43
+
44
+ file_handler = ConcurrentTimedRotatingFileHandler(
45
+ get_log_full_path(),
46
+ when="D",
47
+ interval=1,
48
+ backupCount=int(settings.LOG_FILE_MAX_QTY),
49
+ )
50
+ file_handler.setLevel(level)
51
+ file_handler.setFormatter(formatter)
52
+
53
+ logger.addHandler(file_handler)
54
+
55
+
56
+ def get_log_full_path() -> str:
57
+ return os.path.join(settings.LOG_FILE_PATH, "app.log")
58
+
59
+
60
+ class LogPipe(threading.Thread, io.TextIOBase):
61
+ def __init__(self, logger: logging.Logger, log_level: int) -> None:
62
+ threading.Thread.__init__(self)
63
+
64
+ self.daemon = False
65
+ self.logger = logger
66
+ self.level = log_level
67
+ self.readDescriptor, self.writeDescriptor = os.pipe()
68
+ self.start()
69
+
70
+ def run(self) -> None:
71
+ with os.fdopen(self.readDescriptor) as reader:
72
+ for line in iter(reader.readline, ""):
73
+ self.logger.log(self.level, line.strip("\n"))
74
+
75
+ def fileno(self) -> int:
76
+ return self.writeDescriptor
77
+
78
+ def close(self) -> None:
79
+ os.close(self.writeDescriptor)
@@ -0,0 +1,62 @@
1
+ import logging
2
+ import os
3
+
4
+ import psutil
5
+
6
+ from testgen import settings
7
+
8
+ LOG = logging.getLogger("testgen")
9
+
10
+
11
+ def get_current_process_id():
12
+ return os.getpid()
13
+
14
+
15
+ def kill_profile_run(process_id):
16
+ keywords = ["run-profile"]
17
+ status, message = kill_process(process_id, keywords)
18
+ return status, message
19
+
20
+
21
+ def kill_test_run(process_id):
22
+ keywords = ["run-tests"]
23
+ status, message = kill_process(process_id, keywords)
24
+ return status, message
25
+
26
+
27
+ def kill_process(process_id, keywords=None):
28
+ if settings.IS_DEBUG:
29
+ msg = "Cannot kill processes in debug mode (threads are used instead of new process)"
30
+ LOG.warn(msg)
31
+ return False, msg
32
+ try:
33
+ process = psutil.Process(process_id)
34
+ if process.name().lower() != "testgen":
35
+ message = f"The process was not killed because the process_id {process_id} is not a testgen process. Details: {process.name()}"
36
+ LOG.error(f"kill_process: {message}")
37
+ return False, message
38
+
39
+ if keywords:
40
+ for keyword in keywords:
41
+ if keyword.lower() not in process.cmdline():
42
+ message = f"The process was not killed because the keyword {keyword} was not found. Details: {process.cmdline()}"
43
+ LOG.error(f"kill_process: {message}")
44
+ return False, message
45
+
46
+ process.terminate()
47
+ process.wait(timeout=10)
48
+ message = f"Process {process_id} has been terminated."
49
+ except psutil.NoSuchProcess:
50
+ message = f"No such process with PID {process_id}."
51
+ LOG.exception(f"kill_process: {message}")
52
+ return False, message
53
+ except psutil.AccessDenied:
54
+ message = f"Access denied when trying to terminate process {process_id}."
55
+ LOG.exception(f"kill_process: {message}")
56
+ return False, message
57
+ except psutil.TimeoutExpired:
58
+ message = f"Process {process_id} did not terminate within the timeout period."
59
+ LOG.exception(f"kill_process: {message}")
60
+ return False, message
61
+ LOG.info(f"kill_process: Success. {message}")
62
+ return True, message