dataops-testgen 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataops_testgen-2.2.0.dist-info/LICENSE +203 -0
- dataops_testgen-2.2.0.dist-info/METADATA +287 -0
- dataops_testgen-2.2.0.dist-info/NOTICE +5 -0
- dataops_testgen-2.2.0.dist-info/RECORD +270 -0
- dataops_testgen-2.2.0.dist-info/WHEEL +5 -0
- dataops_testgen-2.2.0.dist-info/entry_points.txt +2 -0
- dataops_testgen-2.2.0.dist-info/top_level.txt +1 -0
- testgen/__init__.py +0 -0
- testgen/__main__.py +770 -0
- testgen/commands/__init__.py +0 -0
- testgen/commands/queries/__init__.py +0 -0
- testgen/commands/queries/execute_cat_tests_query.py +95 -0
- testgen/commands/queries/execute_tests_query.py +160 -0
- testgen/commands/queries/generate_tests_query.py +94 -0
- testgen/commands/queries/profiling_query.py +366 -0
- testgen/commands/queries/test_parameter_validation_query.py +88 -0
- testgen/commands/run_execute_cat_tests.py +162 -0
- testgen/commands/run_execute_tests.py +168 -0
- testgen/commands/run_generate_tests.py +107 -0
- testgen/commands/run_get_entities.py +122 -0
- testgen/commands/run_launch_db_config.py +84 -0
- testgen/commands/run_observability_exporter.py +330 -0
- testgen/commands/run_profiling_bridge.py +495 -0
- testgen/commands/run_quick_start.py +168 -0
- testgen/commands/run_setup_profiling_tools.py +96 -0
- testgen/commands/run_test_definition.py +146 -0
- testgen/commands/run_test_parameter_validation.py +135 -0
- testgen/commands/run_upgrade_db_config.py +156 -0
- testgen/common/__init__.py +8 -0
- testgen/common/clean_sql.py +53 -0
- testgen/common/credentials.py +25 -0
- testgen/common/database/__init__.py +0 -0
- testgen/common/database/database_service.py +629 -0
- testgen/common/database/flavor/__init__.py +0 -0
- testgen/common/database/flavor/flavor_service.py +75 -0
- testgen/common/database/flavor/mssql_flavor_service.py +34 -0
- testgen/common/database/flavor/postgresql_flavor_service.py +5 -0
- testgen/common/database/flavor/redshift_flavor_service.py +22 -0
- testgen/common/database/flavor/snowflake_flavor_service.py +69 -0
- testgen/common/database/flavor/trino_flavor_service.py +21 -0
- testgen/common/date_service.py +68 -0
- testgen/common/display_service.py +85 -0
- testgen/common/docker_service.py +76 -0
- testgen/common/encrypt.py +55 -0
- testgen/common/get_pipeline_parms.py +57 -0
- testgen/common/logs.py +79 -0
- testgen/common/process_service.py +62 -0
- testgen/common/read_file.py +69 -0
- testgen/settings.py +440 -0
- testgen/template/dbsetup/010_create_base_schema.sql +2 -0
- testgen/template/dbsetup/020_create_standard_functions_sprocs.sql +179 -0
- testgen/template/dbsetup/030_initialize_new_schema_structure.sql +735 -0
- testgen/template/dbsetup/040_populate_new_schema_project.sql +59 -0
- testgen/template/dbsetup/050_populate_new_schema_metadata.sql +1517 -0
- testgen/template/dbsetup/060_create_standard_views.sql +248 -0
- testgen/template/dbsetup/070_create_default_users.sql +17 -0
- testgen/template/dbsetup/075_grant_role_rights.sql +43 -0
- testgen/template/dbsetup/080_set_current_revision.sql +5 -0
- testgen/template/dbupgrade/0100_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0101_incremental_upgrade.sql +15 -0
- testgen/template/dbupgrade/0102_incremental_upgrade.sql +4 -0
- testgen/template/dbupgrade/0103_incremental_upgrade.sql +22 -0
- testgen/template/dbupgrade/0104_incremental_upgrade.sql +44 -0
- testgen/template/dbupgrade/0105_incremental_upgrade.sql +1 -0
- testgen/template/dbupgrade/0106_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0107_incremental_upgrade.sql +3 -0
- testgen/template/dbupgrade_helpers/get_tg_revision.sql +2 -0
- testgen/template/exec_cat_tests/ex_cat_build_agg_table_tests.sql +116 -0
- testgen/template/exec_cat_tests/ex_cat_get_distinct_tables.sql +11 -0
- testgen/template/exec_cat_tests/ex_cat_results_parse.sql +69 -0
- testgen/template/exec_cat_tests/ex_cat_retrieve_agg_test_parms.sql +6 -0
- testgen/template/exec_cat_tests/ex_cat_test_query.sql +8 -0
- testgen/template/execution/ex_finalize_test_run_results.sql +37 -0
- testgen/template/execution/ex_get_tests_non_cat.sql +47 -0
- testgen/template/execution/ex_update_test_record_in_testrun_table.sql +27 -0
- testgen/template/execution/ex_write_test_record_to_testrun_table.sql +6 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_no_drops_generic.sql +48 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_num_incr_generic.sql +34 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_above_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_within_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_same_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_custom_query_generic.sql +39 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_2way_generic.sql +58 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_generic.sql +44 -0
- testgen/template/flavors/generic/exec_query_tests/ex_prior_match_generic.sql +37 -0
- testgen/template/flavors/generic/exec_query_tests/ex_relative_entropy_generic.sql +53 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_no_drops_generic.sql +46 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_same_generic.sql +59 -0
- testgen/template/flavors/generic/profiling/contingency_counts.sql +3 -0
- testgen/template/flavors/generic/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/flavors/mssql/exec_query_tests/ex_relative_entropy_mssql.sql +53 -0
- testgen/template/flavors/mssql/profiling/project_ddf_query_mssql.sql +35 -0
- testgen/template/flavors/mssql/profiling/project_profiling_query_mssql.yaml +246 -0
- testgen/template/flavors/mssql/profiling/project_secondary_profiling_query_mssql.sql +36 -0
- testgen/template/flavors/mssql/setup_profiling_tools/00_drop_existing_functions_mssql.sql +8 -0
- testgen/template/flavors/mssql/setup_profiling_tools/01_create_functions_mssql.sql +12 -0
- testgen/template/flavors/mssql/setup_profiling_tools/02_create_functions_mssql.sql +54 -0
- testgen/template/flavors/mssql/setup_profiling_tools/create_qc_schema_mssql.sql +4 -0
- testgen/template/flavors/mssql/setup_profiling_tools/grant_execute_privileges_mssql.sql +1 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_no_drops_postgresql.sql +46 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_same_postgresql.sql +59 -0
- testgen/template/flavors/postgresql/profiling/project_ddf_query_postgresql.sql +42 -0
- testgen/template/flavors/postgresql/profiling/project_profiling_query_postgresql.yaml +225 -0
- testgen/template/flavors/postgresql/profiling/project_secondary_profiling_query_postgresql.sql +28 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_functions_postgresql.sql +157 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_qc_schema_postgresql.sql +1 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/grant_execute_privileges_postgresql.sql +2 -0
- testgen/template/flavors/redshift/profiling/project_ddf_query_redshift.sql +38 -0
- testgen/template/flavors/redshift/profiling/project_profiling_query_redshift.yaml +221 -0
- testgen/template/flavors/redshift/profiling/project_secondary_profiling_query_redshift.sql +29 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_functions_redshift.sql +115 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_qc_schema_redshift.sql +1 -0
- testgen/template/flavors/redshift/setup_profiling_tools/grant_execute_privileges_redshift.sql +2 -0
- testgen/template/flavors/snowflake/profiling/project_ddf_query_snowflake.sql +38 -0
- testgen/template/flavors/snowflake/profiling/project_profiling_query_snowflake.yaml +220 -0
- testgen/template/flavors/snowflake/profiling/project_secondary_profiling_query_snowflake.sql +29 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_functions_snowflake.sql +69 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_qc_schema_snowflake.sql +1 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/grant_execute_privileges_snowflake.sql +6 -0
- testgen/template/flavors/trino/profiling/project_profiling_query_trino.yaml +219 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_functions_trino.sql +92 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_qc_schema_trino.sql +1 -0
- testgen/template/gen_funny_cat_tests/gen_test_constant.sql +104 -0
- testgen/template/gen_funny_cat_tests/gen_test_distinct_value_ct.sql +98 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct.sql +57 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct_pct.sql +59 -0
- testgen/template/generation/gen_delete_old_tests.sql +5 -0
- testgen/template/generation/gen_insert_test_suite.sql +5 -0
- testgen/template/generation/gen_retrieve_or_insert_test_suite.sql +58 -0
- testgen/template/generation/gen_standard_test_type_list.sql +13 -0
- testgen/template/generation/gen_standard_tests.sql +48 -0
- testgen/template/get_entities/get_connection.sql +21 -0
- testgen/template/get_entities/get_connections_list.sql +9 -0
- testgen/template/get_entities/get_latest.sql +4 -0
- testgen/template/get_entities/get_profile.sql +12 -0
- testgen/template/get_entities/get_profile_info.sql +17 -0
- testgen/template/get_entities/get_profile_list.sql +17 -0
- testgen/template/get_entities/get_profile_screen.sql +275 -0
- testgen/template/get_entities/get_project_list.sql +6 -0
- testgen/template/get_entities/get_table_group_list.sql +10 -0
- testgen/template/get_entities/get_test_generation_list.sql +18 -0
- testgen/template/get_entities/get_test_info.sql +41 -0
- testgen/template/get_entities/get_test_results_for_run_cli.sql +16 -0
- testgen/template/get_entities/get_test_run_list.sql +24 -0
- testgen/template/get_entities/get_test_suite.sql +13 -0
- testgen/template/get_entities/get_test_suite_list.sql +18 -0
- testgen/template/get_entities/list_test_types.sql +4 -0
- testgen/template/observability/get_event_data.sql +23 -0
- testgen/template/observability/get_test_results.sql +41 -0
- testgen/template/observability/update_test_results_exported_to_observability.sql +12 -0
- testgen/template/parms/parms_profiling.sql +34 -0
- testgen/template/parms/parms_test_execution.sql +13 -0
- testgen/template/parms/parms_test_gen.sql +23 -0
- testgen/template/profiling/contingency_columns.sql +7 -0
- testgen/template/profiling/datatype_suggestions.sql +56 -0
- testgen/template/profiling/functional_datatype.sql +523 -0
- testgen/template/profiling/functional_tabletype_stage.sql +48 -0
- testgen/template/profiling/functional_tabletype_update.sql +8 -0
- testgen/template/profiling/pii_flag.sql +133 -0
- testgen/template/profiling/profile_anomalies_screen_column.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_multi_column.sql +58 -0
- testgen/template/profiling/profile_anomalies_screen_table.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_table_dates.sql +30 -0
- testgen/template/profiling/profile_anomalies_screen_variants.sql +40 -0
- testgen/template/profiling/profile_anomaly_types_get.sql +3 -0
- testgen/template/profiling/project_get_table_sample_count.sql +22 -0
- testgen/template/profiling/project_profile_run_record_insert.sql +8 -0
- testgen/template/profiling/project_profile_run_record_update.sql +5 -0
- testgen/template/profiling/project_profile_run_record_update_status.sql +5 -0
- testgen/template/profiling/project_update_profile_results_to_estimates.sql +32 -0
- testgen/template/profiling/refresh_anomalies.sql +33 -0
- testgen/template/profiling/refresh_data_chars_from_profiling.sql +156 -0
- testgen/template/profiling/secondary_profiling_columns.sql +12 -0
- testgen/template/profiling/secondary_profiling_delete.sql +4 -0
- testgen/template/profiling/secondary_profiling_update.sql +18 -0
- testgen/template/quick_start/populate_target_data.sql +1077 -0
- testgen/template/quick_start/recreate_target_data_schema.sql +167 -0
- testgen/template/quick_start/update_target_data.sql +100 -0
- testgen/template/updates/create_tmp_test_definition.sql +19 -0
- testgen/template/updates/get_test_def_parms.sql +38 -0
- testgen/template/updates/populate_stg_test_definitions.sql +184 -0
- testgen/template/validate_tests/ex_disable_tests_test_definitions.sql +5 -0
- testgen/template/validate_tests/ex_flag_tests_test_definitions.sql +64 -0
- testgen/template/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/validate_tests/ex_get_test_column_list_tg.sql +65 -0
- testgen/template/validate_tests/ex_write_test_val_errors.sql +22 -0
- testgen/ui/__init__.py +0 -0
- testgen/ui/app.py +98 -0
- testgen/ui/assets/dk_logo.svg +46 -0
- testgen/ui/assets/question_mark.png +0 -0
- testgen/ui/assets/scripts.js +68 -0
- testgen/ui/assets/style.css +140 -0
- testgen/ui/bootstrap.py +109 -0
- testgen/ui/components/__init__.py +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu4mxK.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 +0 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.css +24 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.woff2 +0 -0
- testgen/ui/components/frontend/css/roboto-font-faces.css +35 -0
- testgen/ui/components/frontend/css/shared.css +36 -0
- testgen/ui/components/frontend/img/dk_logo.svg +46 -0
- testgen/ui/components/frontend/index.html +17 -0
- testgen/ui/components/frontend/js/components/breadcrumbs.js +86 -0
- testgen/ui/components/frontend/js/components/button.js +66 -0
- testgen/ui/components/frontend/js/components/location.js +62 -0
- testgen/ui/components/frontend/js/components/select.js +75 -0
- testgen/ui/components/frontend/js/components/sidebar.js +358 -0
- testgen/ui/components/frontend/js/main.js +99 -0
- testgen/ui/components/frontend/js/streamlit.js +19 -0
- testgen/ui/components/frontend/js/van.min.js +1 -0
- testgen/ui/components/utils/__init__.py +0 -0
- testgen/ui/components/utils/callbacks.py +51 -0
- testgen/ui/components/utils/component.py +13 -0
- testgen/ui/components/widgets/__init__.py +6 -0
- testgen/ui/components/widgets/breadcrumbs.py +32 -0
- testgen/ui/components/widgets/location.py +65 -0
- testgen/ui/components/widgets/modal.py +97 -0
- testgen/ui/components/widgets/sidebar.py +69 -0
- testgen/ui/navigation/__init__.py +0 -0
- testgen/ui/navigation/menu.py +42 -0
- testgen/ui/navigation/page.py +20 -0
- testgen/ui/navigation/router.py +63 -0
- testgen/ui/queries/__init__.py +0 -0
- testgen/ui/queries/authentication_queries.py +47 -0
- testgen/ui/queries/connection_queries.py +121 -0
- testgen/ui/queries/profiling_queries.py +148 -0
- testgen/ui/queries/project_queries.py +9 -0
- testgen/ui/queries/table_group_queries.py +186 -0
- testgen/ui/queries/test_definition_queries.py +270 -0
- testgen/ui/queries/test_run_queries.py +32 -0
- testgen/ui/queries/test_suite_queries.py +145 -0
- testgen/ui/scripts/__init__.py +0 -0
- testgen/ui/scripts/patch_streamlit.py +111 -0
- testgen/ui/services/__init__.py +0 -0
- testgen/ui/services/authentication_service.py +119 -0
- testgen/ui/services/connection_service.py +220 -0
- testgen/ui/services/database_service.py +282 -0
- testgen/ui/services/form_service.py +1008 -0
- testgen/ui/services/javascript_service.py +44 -0
- testgen/ui/services/query_service.py +316 -0
- testgen/ui/services/string_service.py +12 -0
- testgen/ui/services/table_group_service.py +130 -0
- testgen/ui/services/test_definition_service.py +117 -0
- testgen/ui/services/test_run_service.py +13 -0
- testgen/ui/services/test_suite_service.py +76 -0
- testgen/ui/services/toolbar_service.py +77 -0
- testgen/ui/session.py +46 -0
- testgen/ui/views/__init__.py +0 -0
- testgen/ui/views/app_log_modal.py +92 -0
- testgen/ui/views/connections.py +72 -0
- testgen/ui/views/connections_base.py +367 -0
- testgen/ui/views/login.py +40 -0
- testgen/ui/views/not_found.py +16 -0
- testgen/ui/views/overview.py +34 -0
- testgen/ui/views/profiling_anomalies.py +501 -0
- testgen/ui/views/profiling_details.py +335 -0
- testgen/ui/views/profiling_modal.py +40 -0
- testgen/ui/views/profiling_results.py +206 -0
- testgen/ui/views/profiling_summary.py +177 -0
- testgen/ui/views/project_settings.py +74 -0
- testgen/ui/views/table_groups.py +530 -0
- testgen/ui/views/test_definitions.py +1020 -0
- testgen/ui/views/test_results.py +908 -0
- testgen/ui/views/test_runs.py +195 -0
- testgen/ui/views/test_suites.py +545 -0
- testgen/utils/__init__.py +0 -0
- testgen/utils/plugins.py +17 -0
- testgen/utils/singleton.py +14 -0
|
@@ -0,0 +1,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,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
|