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,99 @@
1
+ /**
2
+ * @typedef Properties
3
+ * @type {object}
4
+ * @property {string} id - id of the specific component to be rendered
5
+ * @property {string} key - user key of the specific component to be rendered
6
+ * @property {object} props - object with the props to pass to the rendered component
7
+ */
8
+ import van from './van.min.js';
9
+ import { Streamlit } from './streamlit.js';
10
+ import { Button } from './components/button.js'
11
+ import { Select } from './components/select.js'
12
+ import { Location } from './components/location.js'
13
+ import { Breadcrumbs } from './components/breadcrumbs.js'
14
+
15
+ let currentWindowVan = van;
16
+ let topWindowVan = window.top.van;
17
+
18
+ const TestGenComponent = (/** @type {string} */ id, /** @type {object} */ props) => {
19
+ const componentById = {
20
+ select: Button,
21
+ button: Select,
22
+ location: Location,
23
+ breadcrumbs: Breadcrumbs,
24
+ sidebar: window.top.testgen.components.Sidebar,
25
+ };
26
+
27
+ if (Object.keys(componentById).includes(id)) {
28
+ return componentById[id](props);
29
+ }
30
+
31
+ return '';
32
+ };
33
+
34
+ window.addEventListener('message', (event) => {
35
+ if (event.data.type === 'streamlit:render') {
36
+ const componentId = event.data.args.id;
37
+ const componentKey = event.data.args.key;
38
+
39
+ let van = currentWindowVan;
40
+ let mountPoint = document.body;
41
+ let componentState = window.testgen.states[componentKey];
42
+ if (shouldRenderOutsideFrame(componentId)) {
43
+ window.frameElement.style.display = 'none';
44
+ componentState = window.top.testgen.states[componentKey];
45
+ mountPoint = window.frameElement.parentElement;
46
+ van = topWindowVan;
47
+ }
48
+
49
+ if (componentId === 'sidebar') {
50
+ window.top.testgen.components.Sidebar.onLogout = logout;
51
+ window.top.testgen.components.Sidebar.onProjectChanged = changeProject;
52
+ window.top.testgen.components.Sidebar.StreamlitInstance = Streamlit;
53
+ }
54
+
55
+ if (componentState === undefined) {
56
+ document.body.dataset.component = event.data.args.id;
57
+
58
+ componentState = {};
59
+ for (const [ key, value ] of Object.entries(event.data.args.props)) {
60
+ componentState[key] = van.state(value);
61
+ }
62
+
63
+ if (shouldRenderOutsideFrame(componentId)) {
64
+ window.top.testgen.states[componentKey] = componentState;
65
+ } else {
66
+ window.testgen.states[componentKey] = componentState;
67
+ }
68
+
69
+ return van.add(mountPoint, TestGenComponent(componentId, componentState));
70
+ }
71
+
72
+ for (const [ key, value ] of Object.entries(event.data.args.props)) {
73
+ if (componentState[key].val !== value) {
74
+ componentState[key].val = value;
75
+ }
76
+ }
77
+ }
78
+ });
79
+
80
+ Streamlit.init();
81
+
82
+ function shouldRenderOutsideFrame(componentId) {
83
+ return 'sidebar' === componentId;
84
+ }
85
+
86
+ function logout(authCookieName) {
87
+ window.parent.postMessage({ type: 'TestgenLogout', cookie: authCookieName }, '*');
88
+ Streamlit.sendData({ logout: true });
89
+ return false;
90
+ }
91
+
92
+ function changeProject(/** @type string */ projectCode) {
93
+ Streamlit.sendData({ change_to_project: projectCode });
94
+ }
95
+
96
+ window.testgen = {
97
+ states: {},
98
+ loadedStylesheets: {},
99
+ };
@@ -0,0 +1,19 @@
1
+ const Streamlit = {
2
+ init: () => {
3
+ sendMessageToStreamlit('streamlit:componentReady', { apiVersion: 1 });
4
+ },
5
+ setFrameHeight: (height) => {
6
+ sendMessageToStreamlit('streamlit:setFrameHeight', { height: height });
7
+ },
8
+ sendData: (data) => {
9
+ sendMessageToStreamlit('streamlit:setComponentValue', { value: data, dataType: 'json' });
10
+ },
11
+ };
12
+
13
+ function sendMessageToStreamlit(type, data) {
14
+ if (window.top) {
15
+ window.top.postMessage(Object.assign({ type: type, isStreamlitMessage: true }, data), '*');
16
+ }
17
+ }
18
+
19
+ export { Streamlit };
@@ -0,0 +1 @@
1
+ let e,t,l,r,o,f=Object,n=f.getPrototypeOf,s=document,a={isConnected:1},i={},d=n(a),u=n(n),_=(e,t,l,r)=>(e??(setTimeout(l,r),new Set)).add(t),h=(e,t,r)=>{let o=l;l=t;try{return e(r)}catch(e){return console.error(e),r}finally{l=o}},c=e=>e.filter(e=>e.t?.isConnected),g=t=>o=_(o,t,()=>{for(let e of o)e.l=c(e.l),e.o=c(e.o);o=e},1e3),w={get val(){return l?.add(this),this.i},get oldVal(){return l?.add(this),this.u},set val(l){let r=this;if(l!==r.i){r.i=l;let o=[...r.o=c(r.o)];for(let t of o)x(t.f,t.s,t.t),t.t=e;r.l.length?t=_(t,r,p):r.u=l}}},v=e=>({__proto__:w,i:e,u:e,l:[],o:[]}),S=e=>n(e??0)===w,y=(e,t)=>{let l=new Set,o={f:e},f=r;r=[];let n=h(e,l,t);n=(n??s).nodeType?n:new Text(n);for(let e of l)g(e),e.l.push(o);for(let e of r)e.t=n;return r=f,o.t=n},x=(e,t=v(),l)=>{let o=new Set,f={f:e,s:t};f.t=l??r?.push(f)??a,t.val=h(e,o);for(let e of o)g(e),e.o.push(f);return t},V=(t,...l)=>{for(let r of l.flat(1/0)){let l=n(r??0),o=l===w?y(()=>r.val):l===u?y(r):r;o!=e&&t.append(o)}return t},b=t=>new Proxy((l,...r)=>{let[o,...a]=n(r[0]??0)===d?r:[{},...r],_=t?s.createElementNS(t,l):s.createElement(l);for(let[t,r]of f.entries(o)){let o=l=>l?f.getOwnPropertyDescriptor(l,t)??o(n(l)):e,s=l+","+t,a=i[s]??(i[s]=o(n(_))?.set??0),d=a?a.bind(_):_.setAttribute.bind(_,t),h=n(r??0);h===w?y(()=>(d(r.val),_)):h!==u||t.startsWith("on")&&!r.h?d(r):y(()=>(d(r()),_))}return V(_,...a)},{get:(t,l)=>t.bind(e,l)}),m=(e,t)=>t?t!==e&&e.replaceWith(t):e.remove(),p=()=>{let l=[...t].filter(e=>e.i!==e.u);t=e;for(let t of new Set(l.flatMap(e=>e.l=c(e.l))))m(t.t,y(t.f,t.t)),t.t=e;for(let e of l)e.u=e.i};export default{add:V,_:e=>(e.h=1,e),tags:b(),tagsNS:b,state:v,val:e=>S(e)?e.val:e,oldVal:e=>S(e)?e.oldVal:e,derive:x,hydrate:(e,t)=>m(e,y(t,e))};
File without changes
@@ -0,0 +1,51 @@
1
+ """
2
+ Streamlit component's callbacks are broken for CustomComponents, this is
3
+ a temporary patch picked up from the internet.
4
+
5
+ https://gist.github.com/okld/1a2b2fd2cb9f85fc8c4e92e26c6597d5
6
+ """
7
+
8
+ import logging
9
+
10
+ from streamlit import session_state
11
+ from streamlit.components.v1 import components
12
+
13
+ LOG = logging.getLogger("testgen")
14
+
15
+
16
+ def _patch_register_widget(register_widget):
17
+ def wrapper_register_widget(*args, **kwargs):
18
+ user_key = kwargs.get("user_key", None)
19
+ callbacks = session_state.get("_components_callbacks", None)
20
+
21
+ # Check if a callback was registered for that user_key.
22
+ if user_key and callbacks and user_key in callbacks:
23
+ callback = callbacks[user_key]
24
+
25
+ # Add callback-specific args for the real register_widget function.
26
+ kwargs["on_change_handler"] = callback[0]
27
+ kwargs["args"] = callback[1]
28
+ kwargs["kwargs"] = callback[2]
29
+
30
+ # Call the original function with updated kwargs.
31
+ return register_widget(*args, **kwargs)
32
+
33
+ return wrapper_register_widget
34
+
35
+
36
+ # Patch function only once.
37
+ if not hasattr(components.register_widget, "__callbacks_patched__"):
38
+ components.register_widget.__callbacks_patched__ = True
39
+ components.register_widget = _patch_register_widget(components.register_widget)
40
+
41
+
42
+ def register_callback(element_key, callback, *callback_args, **callback_kwargs):
43
+ # Initialize callbacks store.
44
+ if "_components_callbacks" not in session_state:
45
+ session_state._components_callbacks = {}
46
+
47
+ # Register a callback for a given element_key.
48
+ try:
49
+ session_state._components_callbacks[element_key] = (callback, callback_args, callback_kwargs)
50
+ except:
51
+ LOG.debug("unexpected error registering component callback", exc_info=False, stack_info=False)
@@ -0,0 +1,13 @@
1
+ import pathlib
2
+
3
+ from streamlit.components import v1 as components
4
+
5
+ components_dir = pathlib.Path(__file__).parent.parent.joinpath("frontend")
6
+ component_function = components.declare_component("testgen", path=components_dir)
7
+
8
+
9
+ def component(*, id_, props, key=None, default=None):
10
+ component_props = props
11
+ if not component_props:
12
+ component_props = {}
13
+ return component_function(id=id_, props=component_props, key=key, default=default)
@@ -0,0 +1,6 @@
1
+ # ruff: noqa: F401
2
+
3
+ from testgen.ui.components.widgets.breadcrumbs import breadcrumbs
4
+ from testgen.ui.components.widgets.location import LocationChanged, location
5
+ from testgen.ui.components.widgets.modal import Modal
6
+ from testgen.ui.components.widgets.sidebar import sidebar
@@ -0,0 +1,32 @@
1
+ import logging
2
+ import typing
3
+
4
+ from testgen.ui.components.utils.component import component
5
+
6
+ LOG = logging.getLogger("testgen")
7
+
8
+
9
+ def breadcrumbs(
10
+ key: str = "testgen:breadcrumbs",
11
+ breadcrumbs: list["Breadcrumb"] | None = None,
12
+ ) -> None:
13
+ """
14
+ Testgen component to display the breadcrumbs with a hash link on
15
+ each page.
16
+
17
+ # Parameters
18
+ :param key: unique key to give the component a persisting state
19
+ :param breadcrumbs: list of dicts with label and path
20
+ """
21
+
22
+ component(
23
+ id_="breadcrumbs",
24
+ key=key,
25
+ default={},
26
+ props={"breadcrumbs": breadcrumbs},
27
+ )
28
+
29
+
30
+ class Breadcrumb(typing.TypedDict):
31
+ path: str | None
32
+ label: str
@@ -0,0 +1,65 @@
1
+ import base64
2
+ import dataclasses
3
+ import logging
4
+ import typing
5
+
6
+ import streamlit as st
7
+
8
+ from testgen.ui.components.utils.callbacks import register_callback
9
+ from testgen.ui.components.utils.component import component
10
+ from testgen.ui.session import session
11
+
12
+ LOG = logging.getLogger("testgen")
13
+
14
+
15
+ def location(
16
+ key: str = "testgen:location",
17
+ on_change: typing.Callable[["LocationChanged"], None] | None = None,
18
+ ) -> None:
19
+ """
20
+ Testgen component to listen for location changes in the url hash.
21
+
22
+ # Parameters
23
+ :param key: unique key to give the component a persisting state
24
+ :param on_change: callback for when the browser location changes
25
+ """
26
+ register_callback(key, _handle_location_change, key, on_change)
27
+
28
+ initialized = bool(session.renders and session.renders > 1)
29
+ current_page_code = _encode_page(session.current_page, session.current_page_args or {})
30
+
31
+ change = component(
32
+ id_="location",
33
+ key=key,
34
+ default={},
35
+ props={"initialized": initialized, "current_page_code": current_page_code},
36
+ )
37
+
38
+ if not initialized and change:
39
+ change = LocationChanged(**change)
40
+ if _encode_page(change.page, change.args) != current_page_code:
41
+ _handle_location_change(key, on_change)
42
+
43
+
44
+ def _handle_location_change(key: str, callback: typing.Callable[["LocationChanged"], None] | None):
45
+ if callback:
46
+ change = st.session_state[key]
47
+ if "page" not in change:
48
+ change["page"] = "overview"
49
+ return callback(LocationChanged(**change))
50
+
51
+
52
+ def _encode_page(page: str, args: dict) -> str | None:
53
+ page_code = None
54
+ if page:
55
+ query_params = "&".join([f"{name}={value}" for name, value in args.items()])
56
+ if query_params:
57
+ page = f"{page}?{query_params}"
58
+ page_code = base64.b64encode(page.encode()).decode()
59
+ return page_code
60
+
61
+
62
+ @dataclasses.dataclass
63
+ class LocationChanged:
64
+ page: str
65
+ args: dict = dataclasses.field(default_factory=dict)
@@ -0,0 +1,97 @@
1
+ from contextlib import contextmanager
2
+
3
+ import streamlit
4
+ import streamlit.components.v1 as components
5
+ from streamlit_modal import Modal as BaseModal
6
+
7
+
8
+ class Modal(BaseModal):
9
+ @contextmanager
10
+ def container(self):
11
+ streamlit.markdown(self._modal_styles(), unsafe_allow_html=True)
12
+ with streamlit.container():
13
+ _container = streamlit.container()
14
+ if self.title:
15
+ _container.markdown(f"<h2>{self.title}</h2>", unsafe_allow_html=True)
16
+
17
+ close_ = streamlit.button("X", key=f"{self.key}-close")
18
+ if close_:
19
+ self.close()
20
+
21
+ if not close_:
22
+ components.html(self._modal_script(), height=0, width=0)
23
+
24
+ with _container:
25
+ yield _container
26
+
27
+ def _modal_styles(self) -> str:
28
+ max_width = f"{self.max_width}px" if self.max_width else "unset"
29
+
30
+ return f"""
31
+ <style>
32
+ div[data-modal-container='true'][key='{self.key}'] {{
33
+ position: fixed;
34
+ width: 100vw !important;
35
+ left: 0;
36
+ z-index: 1001;
37
+ }}
38
+
39
+ div[data-modal-container='true'][key='{self.key}'] > div:first-child {{
40
+ margin: auto;
41
+ }}
42
+
43
+ div[data-modal-container='true'][key='{self.key}'] h1 a {{
44
+ display: none
45
+ }}
46
+
47
+ div[data-modal-container='true'][key='{self.key}']::before {{
48
+ position: fixed;
49
+ content: ' ';
50
+ left: 0;
51
+ right: 0;
52
+ top: 0;
53
+ bottom: 0;
54
+ z-index: 1000;
55
+ background-color: rgba(0, 0, 0, 0.5);
56
+ }}
57
+ div[data-modal-container='true'][key='{self.key}'] > div:first-child {{
58
+ max-width: {max_width};
59
+ }}
60
+
61
+ div[data-modal-container='true'][key='{self.key}'] > div:first-child > div:first-child {{
62
+ width: unset !important;
63
+ background-color: #fff;
64
+ padding: {self.padding}px;
65
+ margin-top: {2*self.padding}px;
66
+ margin-left: -{self.padding}px;
67
+ margin-right: -{self.padding}px;
68
+ margin-bottom: -{2*self.padding}px;
69
+ z-index: 1001;
70
+ border-radius: 5px;
71
+ }}
72
+ div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) {{
73
+ z-index: 1003;
74
+ position: absolute;
75
+ }}
76
+ div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) > div {{
77
+ text-align: right;
78
+ padding-right: {self.padding}px;
79
+ max-width: {max_width};
80
+ }}
81
+
82
+ div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) > div > button {{
83
+ right: 0;
84
+ margin-top: {2*self.padding + 14}px;
85
+ }}
86
+ </style>
87
+ """
88
+
89
+ def _modal_script(self) -> str:
90
+ return f"""
91
+ <script type="text/javascript">
92
+ const modalContainer = window.frameElement.parentElement.parentElement.parentElement;
93
+
94
+ modalContainer.setAttribute('data-modal-container', 'true');
95
+ modalContainer.setAttribute('key', '{self.key}');
96
+ </script>
97
+ """
@@ -0,0 +1,69 @@
1
+ import dataclasses
2
+ import logging
3
+ import typing
4
+
5
+ import streamlit as st
6
+
7
+ from testgen.ui.components.utils.callbacks import register_callback
8
+ from testgen.ui.components.utils.component import component
9
+ from testgen.ui.navigation.menu import Menu
10
+ from testgen.ui.services import authentication_service
11
+
12
+ LOG = logging.getLogger("testgen")
13
+
14
+
15
+ def sidebar(
16
+ key: str = "testgen:sidebar",
17
+ username: str | None = None,
18
+ menu: Menu = None,
19
+ current_page: str | None = None,
20
+ current_project: str | None = None,
21
+ on_logout: typing.Callable[[], None] | None = None,
22
+ ) -> None:
23
+ """
24
+ Testgen custom component to display a styled menu over streamlit's
25
+ sidebar.
26
+
27
+ # Parameters
28
+ :param key: unique key to give the component a persisting state
29
+ :param username: username to display at the bottom of the menu
30
+ :param menu: menu object with all root pages
31
+ :param current_page: page address to highlight the selected item
32
+ :param on_logout: callback for when user clicks logout
33
+ :param on_project_changed: callback for when user switches projects
34
+ """
35
+ register_callback(key, _handle_callbacks, key, on_logout)
36
+
37
+ component(
38
+ id_="sidebar",
39
+ props={
40
+ "username": username,
41
+ "menu": menu.filter_for_current_user().sort_items().asdict(),
42
+ "current_page": current_page,
43
+ "current_project": current_project,
44
+ "auth_cookie_name": authentication_service.AUTH_TOKEN_COOKIE_NAME,
45
+ },
46
+ key=key,
47
+ default={},
48
+ )
49
+
50
+
51
+ def _handle_callbacks(
52
+ key: str,
53
+ on_logout: typing.Callable[[], None] | None = None,
54
+ ):
55
+ action = st.session_state[key]
56
+ action = MenuAction(**action)
57
+
58
+ if action.logout and on_logout:
59
+ return on_logout()
60
+
61
+
62
+ class Project(typing.TypedDict):
63
+ code: str
64
+ name: str
65
+
66
+
67
+ @dataclasses.dataclass
68
+ class MenuAction:
69
+ logout: bool | None = None
File without changes
@@ -0,0 +1,42 @@
1
+ import dataclasses
2
+
3
+ from testgen.ui.services import authentication_service
4
+
5
+
6
+ @dataclasses.dataclass
7
+ class MenuItem:
8
+ icon: str
9
+ label: str
10
+ page: str | None = dataclasses.field(default=None)
11
+ roles: list[authentication_service.RoleType] | None = dataclasses.field(default_factory=list)
12
+ order: int = dataclasses.field(default=0)
13
+
14
+
15
+ @dataclasses.dataclass
16
+ class Version:
17
+ current: str
18
+ latest: str
19
+ schema: str
20
+
21
+
22
+ @dataclasses.dataclass
23
+ class Menu:
24
+ items: list[MenuItem]
25
+ version: Version
26
+
27
+ def filter_for_current_user(self) -> "Menu":
28
+ filtered_items = []
29
+ for menu_item in self.items:
30
+ item_roles = menu_item.roles or []
31
+ if len(item_roles) <= 0 or any(map(authentication_service.current_user_has_role, item_roles)):
32
+ filtered_items.append(menu_item)
33
+ return dataclasses.replace(self, items=filtered_items)
34
+
35
+ def sort_items(self) -> "Menu":
36
+ return dataclasses.replace(self, items=sorted(self.items, key=lambda item: item.order))
37
+
38
+ def update_version(self, version: Version) -> "Menu":
39
+ return dataclasses.replace(self, version=version)
40
+
41
+ def asdict(self):
42
+ return dataclasses.asdict(self)
@@ -0,0 +1,20 @@
1
+ import abc
2
+ import typing
3
+
4
+ import testgen.ui.navigation.router
5
+ from testgen.ui.navigation.menu import MenuItem
6
+
7
+ CanActivateGuard = typing.Callable[[], bool | str]
8
+
9
+
10
+ class Page(abc.ABC):
11
+ path: str
12
+ menu_item: MenuItem | None = None
13
+ can_activate: typing.ClassVar[list[CanActivateGuard] | None] = None
14
+
15
+ def __init__(self, router: testgen.ui.navigation.router.Router) -> None:
16
+ self.router = router
17
+
18
+ @abc.abstractmethod
19
+ def render(self, **kwargs) -> None:
20
+ raise NotImplementedError
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing
5
+
6
+ import streamlit as st
7
+
8
+ import testgen.ui.navigation.page
9
+ from testgen.utils.singleton import Singleton
10
+
11
+ CanActivateGuard = typing.Callable[[], bool | str]
12
+
13
+ LOG = logging.getLogger("testgen")
14
+
15
+
16
+ class Router(Singleton):
17
+ active: testgen.ui.navigation.page.Page | None
18
+ _default: type[testgen.ui.navigation.page.Page] | None
19
+ _routes: dict[str, type[testgen.ui.navigation.page.Page]]
20
+
21
+ def __init__(
22
+ self,
23
+ /,
24
+ routes: list[type[testgen.ui.navigation.page.Page]],
25
+ default: type[testgen.ui.navigation.page.Page] | None = None,
26
+ ) -> None:
27
+ self._routes = {}
28
+
29
+ for route in routes:
30
+ self._routes[route.path] = route
31
+
32
+ self.active = None
33
+ self._default = default
34
+ if self._default:
35
+ self._routes[self._default.path] = self._default
36
+
37
+ def navigate(self, /, to: str, with_args: dict | None = None) -> None:
38
+ try:
39
+ route = self._routes[to]
40
+
41
+ bc_source = route(self).path
42
+
43
+ for guard in route.can_activate or []:
44
+ can_activate = guard()
45
+ if type(can_activate) == str:
46
+ return self.navigate(to=can_activate, with_args={})
47
+
48
+ if not can_activate and self._default:
49
+ return self.navigate(to=self._default.path, with_args=with_args)
50
+
51
+ if not isinstance(self.active, route):
52
+ self.active = route(self)
53
+
54
+ self.active.render(**(with_args or {}))
55
+ except KeyError as k:
56
+ error_message = f"{bc_source}: {k!s}"
57
+ st.error(error_message)
58
+ LOG.exception(error_message)
59
+ return self.navigate(to=self._default.path, with_args=with_args)
60
+ except Exception as e:
61
+ error_message = f"{bc_source}: {e!s}"
62
+ st.error(error_message)
63
+ LOG.exception(error_message)
File without changes
@@ -0,0 +1,47 @@
1
+ import streamlit as st
2
+
3
+ import testgen.ui.services.database_service as db
4
+
5
+
6
+ @st.cache_data(show_spinner=False)
7
+ def get_users(schema):
8
+ sql = f"""SELECT
9
+ id::VARCHAR(50),
10
+ username, email, "name", "password", preauthorized, role
11
+ FROM {schema}.auth_users"""
12
+ return db.retrieve_data(sql)
13
+
14
+
15
+ def delete_users(schema, user_ids):
16
+ if user_ids is None or len(user_ids) == 0:
17
+ raise ValueError("No user is specified.")
18
+
19
+ items = [f"'{item}'" for item in user_ids]
20
+ sql = f"""DELETE FROM {schema}.auth_users WHERE id in ({",".join(items)})"""
21
+ db.execute_sql(sql)
22
+ st.cache_data.clear()
23
+
24
+
25
+ def add_user(schema, user, encrypted_password):
26
+ sql = f"""INSERT INTO {schema}.auth_users
27
+ (username, email, name, password, role)
28
+ SELECT
29
+ '{user["username"]}' as username,
30
+ '{user["email"]}' as email,
31
+ '{user["name"]}' as name,
32
+ '{encrypted_password}' as password,
33
+ '{user["role"]}' as role;"""
34
+ db.execute_sql(sql)
35
+ st.cache_data.clear()
36
+
37
+
38
+ def edit_user(schema, user, encrypted_password):
39
+ sql = f"""UPDATE {schema}.auth_users SET
40
+ username = '{user["username"]}',
41
+ email = '{user["email"]}',
42
+ name = '{user["name"]}',
43
+ password = '{encrypted_password}',
44
+ role = '{user["role"]}'
45
+ WHERE id = '{user["user_id"]}';"""
46
+ db.execute_sql(sql)
47
+ st.cache_data.clear()