From d5bc5088a2fc728d981331462bdae42050fd426e Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 7 May 2024 09:58:57 -0700 Subject: [PATCH 1/2] chore: module scope should not require the app context This is a major issue that's been plaguing the backend for a long time. Currently you can't simply run a simple `from superset import models` without getting an error about the app context missing. This DRAFT PR tries to evaluate what's getting in the way. So far I've identified: - app.config being referenced in module scope, this is mostly easy to fix for common configuration, where we can rely on flask.current_app and avoid module scope - dynamic/configurable model: this seems to be the core issue, where say we let people configure their EncryptedField's type - some reliance on SecurityManager, and SecurityManager.user_model used as a relationship, I think this can be worked around using sqlalchemy's `relationship("use-a-string-reference")` call - ??? --- .gitattributes | 1 + .github/workflows/python-importable.yml | 27 ++ UPDATING.md | 4 + docs/docs/contributing/development.mdx | 2 +- scripts/benchmark_migration.py | 2 +- scripts/erd/erd.py | 2 +- scripts/messages.pot.tmp | 16 + scripts/test_imports.py | 135 ++++++++ superset/__init__.py | 26 +- superset/advanced_data_type/api.py | 7 +- superset/app.py | 1 + superset/async_events/async_query_manager.py | 17 +- superset/available_domains/api.py | 5 +- superset/charts/api.py | 7 +- superset/charts/data/api.py | 7 +- superset/charts/filters.py | 2 +- superset/charts/schemas.py | 36 +-- superset/cli/main.py | 5 +- superset/cli/test.py | 3 +- superset/cli/viz_migrations.py | 2 +- superset/commands/chart/importers/v1/utils.py | 2 +- superset/commands/chart/warm_up_cache.py | 4 +- superset/commands/dashboard/importers/v0.py | 2 +- .../dashboard/importers/v1/__init__.py | 2 +- .../commands/dashboard/importers/v1/utils.py | 2 +- superset/commands/database/create.py | 12 +- .../commands/database/importers/v1/utils.py | 4 +- .../commands/database/ssh_tunnel/delete.py | 4 +- superset/commands/database/test_connection.py | 21 +- superset/commands/database/update.py | 5 +- superset/commands/database/uploaders/base.py | 2 +- superset/commands/dataset/importers/v0.py | 2 +- .../commands/dataset/importers/v1/utils.py | 2 +- superset/commands/importers/v1/__init__.py | 2 +- superset/commands/importers/v1/assets.py | 2 +- superset/commands/importers/v1/examples.py | 2 +- superset/commands/importers/v1/utils.py | 2 +- superset/commands/key_value/create.py | 2 +- superset/commands/key_value/delete.py | 2 +- superset/commands/key_value/delete_expired.py | 2 +- superset/commands/key_value/get.py | 2 +- superset/commands/key_value/update.py | 2 +- superset/commands/key_value/upsert.py | 2 +- superset/commands/query/importers/v1/utils.py | 2 +- superset/commands/report/alert.py | 4 +- superset/commands/report/execute.py | 9 +- superset/commands/report/log_prune.py | 2 +- superset/commands/sql_lab/estimate.py | 11 +- superset/commands/sql_lab/export.py | 8 +- superset/commands/sql_lab/results.py | 10 +- superset/commands/tag/create.py | 2 +- superset/commands/tag/update.py | 2 +- superset/common/query_actions.py | 3 - superset/common/query_context_factory.py | 7 +- superset/common/query_context_processor.py | 252 ++++++--------- superset/common/query_object.py | 2 +- superset/common/utils/query_cache_manager.py | 6 +- superset/common/utils/time_range_utils.py | 3 +- superset/connectors/sqla/__init__.py | 1 - superset/connectors/sqla/models.py | 15 +- superset/connectors/sqla/utils.py | 2 +- superset/connectors/sqla/views.py | 2 +- superset/daos/dashboard.py | 9 +- superset/daos/datasource.py | 2 +- superset/daos/log.py | 2 +- superset/dashboards/api.py | 6 +- superset/dashboards/filters.py | 12 +- superset/databases/api.py | 12 +- superset/databases/filters.py | 6 +- superset/databases/schemas.py | 4 +- superset/databases/ssh_tunnel/models.py | 6 +- superset/datasets/models.py | 5 +- superset/datasource/api.py | 3 +- superset/db_engine_specs/__init__.py | 15 +- superset/db_engine_specs/druid.py | 4 +- superset/db_engine_specs/gsheets.py | 2 +- superset/db_engine_specs/hive.py | 3 +- superset/db_engine_specs/impala.py | 2 +- superset/db_engine_specs/ocient.py | 3 +- superset/db_engine_specs/presto.py | 8 +- superset/db_engine_specs/trino.py | 2 +- superset/embedded/api.py | 5 +- superset/embedded/view.py | 5 +- superset/examples/bart_lines.py | 2 +- superset/examples/birth_names.py | 3 +- superset/examples/country_map.py | 2 +- superset/examples/css_templates.py | 2 +- superset/examples/deck.py | 2 +- superset/examples/energy.py | 2 +- superset/examples/flights.py | 2 +- superset/examples/helpers.py | 4 +- superset/examples/long_lat.py | 2 +- superset/examples/misc_dashboard.py | 2 +- superset/examples/multiformat_time_series.py | 3 +- superset/examples/paris.py | 2 +- superset/examples/random_time_series.py | 3 +- superset/examples/sf_population_polygons.py | 2 +- .../examples/supported_charts_dashboard.py | 2 +- superset/examples/tabbed_dashboard.py | 2 +- superset/examples/world_bank.py | 3 +- superset/extensions/__init__.py | 9 +- superset/extensions/event_logger_manager.py | 39 +++ superset/extensions/metadb.py | 3 +- superset/initialization/__init__.py | 293 +++++++++++++++--- superset/initialization/bootstrap.py | 198 ++++++++++++ superset/key_value/models.py | 5 +- superset/migrations/env.py | 4 +- superset/migrations/shared/catalogs.py | 2 +- superset/migrations/shared/constraints.py | 27 +- .../migrations/shared/migrate_viz/base.py | 6 +- superset/migrations/shared/utils.py | 23 ++ ...4_c3a8f8611885_materializing_permission.py | 2 +- ...3_fix_wrong_constraint_on_table_columns.py | 2 +- ...74f7aad_add_new_field_is_restricted_to_.py | 2 +- ..._08-43_27ae655e4247_make_creator_owners.py | 2 +- ...7_23-50_33d996bcc382_update_slice_model.py | 2 +- ..._10-21_3b626e2a6783_sync_db_with_models.py | 2 +- ...31_eca4694defa7_sqllab_setting_defaults.py | 2 +- ...31_db0c65b146bd_update_slice_model_json.py | 2 +- ...a_rewriting_url_from_shortner_with_new_.py | 2 +- ...8-19_67a6ac9b727b_update_spatial_params.py | 2 +- ...11-06_21e88bc06c02_annotation_migration.py | 2 +- ...8-02-13_08-07_e866bd2d4976_smaller_grid.py | 2 +- ...3_08-19_130915240929_is_sqllab_viz_flow.py | 2 +- ...06ae5eb46_cal_heatmap_metric_to_metrics.py | 2 +- ...6-04_11-12_c5756bec8b47_time_grain_sqla.py | 2 +- ...09-52_afb7730f6a9c_remove_empty_filters.py | 2 +- ...-20_4451805bbaa1_remove_double_percents.py | 2 +- ...-06-13_14-54_bddc498dd179_adhoc_filters.py | 2 +- ...31_80a67c5192fa_single_pie_chart_metric.py | 2 +- ..._migrate_num_period_compare_and_period_.py | 2 +- ...-07-19_23-41_c617da68de7d_form_nullable.py | 2 +- ..._15-31_7f2635b51f5d_update_base_columns.py | 2 +- ..._15-57_e9df189e5c7e_update_base_metrics.py | 2 +- ...f3fed1fe_convert_dashboard_v1_positions.py | 2 +- .../2018-08-01_11-47_7fcdcde0761c_.py | 2 +- ..._migrate_time_range_for_default_filters.py | 2 +- ...12-11_22-03_fb13d49b72f9_better_filters.py | 2 +- ...cd94a4_change_owner_to_m2m_relation_on_.py | 2 +- ...019-03-21_10-22_d94d33dbe938_form_strip.py | 2 +- ...bc82_add_parent_ids_in_dashboard_layout.py | 2 +- .../2019-06-28_13-17_ab8c66efdd01_resample.py | 2 +- ...ique_constraint_on_dashboard_slices_tbl.py | 2 +- ...079866a_serialize_schema_permissions_py.py | 2 +- ...m_strip_leading_and_trailing_whitespace.py | 2 +- ...019-10-10_13-52_1495eb914ad3_time_range.py | 2 +- ...d1d_reconvert_legacy_filters_into_adhoc.py | 2 +- ...3_3325d4caccc8_dashboard_scoped_filters.py | 2 +- ..._f9a30386bd74_cleanup_time_grainularity.py | 2 +- ...-24_620241d1153f_update_time_grain_sqla.py | 12 +- ...5563a02_migrate_iframe_to_dash_markdown.py | 2 +- ...0de1855_add_uuid_column_to_import_mixin.py | 2 +- ...collapse_alerting_models_into_a_single_.py | 2 +- ...b176a0_add_import_mixing_to_saved_query.py | 2 +- ...649a77_migrate_x_dateunit_in_time_range.py | 2 +- ...add_granularity_to_charts_where_missing.py | 2 +- ...2-32_41ce8799acc3_rename_pie_label_type.py | 2 +- ...c1e5a7b_legacy_force_directed_to_echart.py | 2 +- ...13_c501b7c653a3_add_missing_uuid_column.py | 2 +- ...e479899_rename_filter_configuration_in_.py | 2 +- ...5e7_remove_dataset_health_check_message.py | 2 +- ..._country_map_use_lowercase_country_name.py | 2 +- ...ff221_migrate_filter_sets_to_new_format.py | 2 +- ...95_migrate_native_filters_to_new_schema.py | 2 +- ...370a_fix_schemas_allowed_for_csv_upload.py | 2 +- ...migrate_pivot_table_v2_heatmaps_to_new_.py | 2 +- ...2_change_fetch_values_predicate_to_text.py | 2 +- ...add_type_to_native_filter_configuration.py | 2 +- ...e2e_migrate_timeseries_limit_metric_to_.py | 2 +- ...-15_32646df09c64_update_time_grain_sqla.py | 2 +- ...acd_rename_to_schemas_allowed_for_file_.py | 2 +- ..._add_force_screenshot_to_alerts_reports.py | 2 +- ...1_rename_big_viz_total_form_data_fields.py | 2 +- ...move_pivot_table_v2_legacy_order_by_to_.py | 2 +- ...2_fix_query_and_saved_query_null_schema.py | 2 +- ...944_change_adhoc_filter_b_from_none_to_.py | 2 +- ...074e4_deprecate_time_range_endpoints_v2.py | 2 +- ...dbaba_rm_time_range_endpoints_from_qc_3.py | 2 +- ...be71abde154_fix_report_schedule_and_log.py | 2 +- ...b8bca906d2_permalink_rename_filterstate.py | 2 +- ...409c7b420ab0_add_created_by_fk_as_owner.py | 2 +- ...0_c747c78868b6_migrating_legacy_treemap.py | 2 +- ...4-00_06e1e70058c7_migrating_legacy_area.py | 2 +- ...fix_table_chart_conditional_formatting_.py | 2 +- ...c8595_create_ssh_tunnel_credentials_tbl.py | 2 +- ...1d9b25135_remove_filter_bar_orientation.py | 2 +- ...c0a3ea245b61_remove_show_native_filters.py | 2 +- ...b5b83_invert_horizontal_bar_chart_order.py | 2 +- ...24_b5ea9d343307_bar_chart_stack_options.py | 2 +- ..._12-30_7e67aecbf3f1_chart_ds_constraint.py | 2 +- ...1ddfd_convert_key_value_entries_to_json.py | 2 +- ...ea966691069_cross_filter_global_scoping.py | 2 +- ...a2ce3086e5_migrate_pivot_table_v1_to_v2.py | 2 +- ...0-22_4c5da39be729_migrate_treemap_chart.py | 2 +- ...e58e5c_migrate_dual_line_to_mixed_chart.py | 2 +- ...e_obsolete_druid_nosql_slice_parameters.py | 2 +- ...280_cleanup_erroneous_parent_filter_ids.py | 2 +- ...54_ee179a490af9_deckgl_path_width_units.py | 2 +- ...69ef90fddd_fix_schema_perm_for_datasets.py | 2 +- ...676_add_normalize_columns_to_sqla_model.py | 2 +- ...0b4400c_added_time_secondary_column_to_.py | 2 +- ...e8_add_percent_calculation_type_funnel_.py | 2 +- ...-41_a32e0c4d8646_migrate_sunburst_chart.py | 2 +- ..._migrate_filter_boxes_to_native_filters.py | 2 +- ...1b217cd8cd_big_number_kpi_single_metric.py | 2 +- ..._update_charts_with_old_time_comparison.py | 2 +- superset/models/__init__.py | 1 - superset/models/core.py | 39 +-- superset/models/dashboard.py | 17 +- superset/models/helpers.py | 25 +- superset/models/slice.py | 16 +- superset/models/sql_lab.py | 17 +- superset/models/user_attributes.py | 5 +- superset/queries/api.py | 2 +- superset/queries/saved_queries/api.py | 9 +- superset/reports/api.py | 5 +- superset/reports/filters.py | 2 +- superset/reports/logs/api.py | 4 +- superset/reports/models.py | 3 +- superset/reports/notifications/email.py | 2 +- superset/security/manager.py | 20 +- superset/sql_lab.py | 57 ++-- superset/sql_parse.py | 2 +- superset/sql_validators/presto_db.py | 2 - superset/sqllab/api.py | 14 +- superset/sqllab/query_render.py | 4 +- superset/sqllab/sql_json_executer.py | 3 + superset/sqllab/sqllab_execution_context.py | 4 +- superset/sqllab/utils.py | 4 +- superset/tables/models.py | 3 +- superset/tags/models.py | 6 +- superset/tasks/async_queries.py | 9 +- superset/tasks/cache.py | 4 +- superset/tasks/celery_app.py | 12 +- superset/tasks/cron_util.py | 3 +- superset/tasks/scheduler.py | 5 +- superset/tasks/thumbnails.py | 11 +- superset/utils/cache.py | 17 +- superset/utils/dashboard_import_export.py | 2 +- superset/utils/database.py | 4 +- superset/utils/dict_import_export.py | 2 +- superset/utils/encrypt.py | 21 +- superset/utils/log.py | 17 +- superset/utils/mock_data.py | 2 +- superset/utils/oauth2.py | 2 +- superset/utils/screenshots.py | 10 +- superset/utils/webdriver.py | 3 +- superset/views/__init__.py | 26 -- superset/views/alerts.py | 6 +- superset/views/all_entities.py | 32 +- superset/views/api.py | 2 +- superset/views/base.py | 208 +++---------- superset/views/core.py | 47 ++- superset/views/dashboard/views.py | 4 +- superset/views/database/mixins.py | 3 +- superset/views/database/views.py | 5 +- superset/views/datasource/schemas.py | 6 +- superset/views/datasource/utils.py | 3 +- superset/views/datasource/views.py | 2 +- superset/views/dynamic_plugins.py | 4 +- superset/views/health.py | 13 - superset/views/key_value.py | 4 +- superset/views/sql_lab/views.py | 2 +- superset/views/tags.py | 19 +- superset/views/users/api.py | 3 +- superset/views/utils.py | 56 +++- superset/viz.py | 59 ++-- tests/conftest.py | 3 +- .../annotation_layers/api_tests.py | 2 +- .../annotation_layers/fixtures.py | 2 +- tests/integration_tests/base_api_tests.py | 3 +- tests/integration_tests/base_tests.py | 3 +- tests/integration_tests/cache_tests.py | 2 +- tests/integration_tests/celery_tests.py | 3 +- tests/integration_tests/charts/api_tests.py | 2 +- .../charts/commands_tests.py | 3 +- tests/integration_tests/cli_tests.py | 3 +- tests/integration_tests/commands_test.py | 2 +- tests/integration_tests/conftest.py | 4 +- tests/integration_tests/core_tests.py | 9 +- .../css_templates/api_tests.py | 2 +- tests/integration_tests/dashboard_tests.py | 3 +- tests/integration_tests/dashboard_utils.py | 2 +- .../integration_tests/dashboards/api_tests.py | 2 +- .../dashboards/commands_tests.py | 3 +- .../integration_tests/dashboards/dao_tests.py | 3 +- .../dashboards/dashboard_test_utils.py | 2 +- .../dashboards/filter_state/api_tests.py | 5 +- .../dashboards/permalink/api_tests.py | 2 +- .../security/security_dataset_tests.py | 6 +- .../dashboards/superset_factory_util.py | 2 +- .../integration_tests/databases/api_tests.py | 3 +- .../databases/commands/upload_test.py | 3 +- .../databases/commands_tests.py | 3 +- .../datasets/commands_tests.py | 3 +- .../integration_tests/datasource/api_tests.py | 3 +- tests/integration_tests/datasource_tests.py | 3 +- .../dict_import_export_tests.py | 2 +- tests/integration_tests/email_tests.py | 3 +- tests/integration_tests/embedded/api_tests.py | 2 +- tests/integration_tests/embedded/dao_tests.py | 2 +- tests/integration_tests/embedded/test_view.py | 2 +- tests/integration_tests/explore/api_tests.py | 3 +- .../explore/form_data/api_tests.py | 3 +- .../explore/form_data/commands_tests.py | 3 +- .../explore/permalink/api_tests.py | 2 +- .../explore/permalink/commands_tests.py | 3 +- .../fixtures/birth_names_dashboard.py | 2 +- .../fixtures/energy_dashboard.py | 2 +- .../fixtures/tabbed_dashboard.py | 2 +- .../fixtures/unicode_dashboard.py | 2 +- tests/integration_tests/fixtures/users.py | 3 +- .../fixtures/world_bank_dashboard.py | 2 +- .../integration_tests/import_export_tests.py | 3 +- tests/integration_tests/insert_chart_mixin.py | 3 +- tests/integration_tests/log_api_tests.py | 2 +- ...ddd_fix_schema_perm_for_datasets__tests.py | 2 +- ...te_can_view_and_drill_permission__tests.py | 2 +- tests/integration_tests/model_tests.py | 3 +- tests/integration_tests/queries/api_tests.py | 3 +- .../queries/saved_queries/api_tests.py | 2 +- .../queries/saved_queries/commands_tests.py | 3 +- .../integration_tests/query_context_tests.py | 4 +- tests/integration_tests/reports/api_tests.py | 3 +- .../commands/create_dashboard_report_tests.py | 2 +- .../reports/commands_tests.py | 2 +- tests/integration_tests/reports/utils.py | 3 +- tests/integration_tests/security/api_tests.py | 2 +- .../security/guest_token_security_tests.py | 3 +- .../security/row_level_security_tests.py | 3 +- tests/integration_tests/security_tests.py | 22 +- tests/integration_tests/sql_lab/api_tests.py | 3 +- .../sql_lab/commands_tests.py | 4 +- tests/integration_tests/sql_lab/conftest.py | 2 +- .../sql_lab/test_execute_sql_statements.py | 3 +- tests/integration_tests/sqla_models_tests.py | 2 +- tests/integration_tests/sqllab_tests.py | 3 +- tests/integration_tests/strategy_tests.py | 2 +- tests/integration_tests/tags/api_tests.py | 3 +- .../integration_tests/tags/commands_tests.py | 3 +- tests/integration_tests/tags/dao_tests.py | 3 +- tests/integration_tests/thumbnails_tests.py | 18 +- .../integration_tests/utils/get_dashboards.py | 2 +- tests/integration_tests/utils_tests.py | 3 +- tests/integration_tests/viz_tests.py | 2 +- .../unit_tests/charts/test_post_processing.py | 2 +- tests/unit_tests/columns/test_models.py | 2 +- .../commands/dataset/test_update.py | 2 +- .../commands/importers/v1/assets_test.py | 6 +- tests/unit_tests/config_test.py | 2 +- tests/unit_tests/dao/dataset_test.py | 2 +- tests/unit_tests/dao/queries_test.py | 12 +- tests/unit_tests/databases/api_test.py | 2 +- .../commands/importers/v1/import_test.py | 4 +- tests/unit_tests/datasets/api_tests.py | 2 +- .../commands/importers/v1/import_test.py | 2 +- .../unit_tests/extensions/test_sqlalchemy.py | 2 +- tests/unit_tests/feature_flag_test.py | 4 +- tests/unit_tests/jinja_context_test.py | 2 +- tests/unit_tests/models/core_test.py | 2 + tests/unit_tests/sql_lab_test.py | 2 +- tests/unit_tests/tables/test_models.py | 2 +- tests/unit_tests/tags/commands/create_test.py | 2 +- tests/unit_tests/tags/commands/update_test.py | 2 +- tests/unit_tests/thumbnails/test_digest.py | 6 +- tests/unit_tests/utils/test_decorators.py | 2 +- 366 files changed, 1739 insertions(+), 1167 deletions(-) create mode 100644 .github/workflows/python-importable.yml create mode 100644 scripts/messages.pot.tmp create mode 100755 scripts/test_imports.py create mode 100644 superset/extensions/event_logger_manager.py create mode 100644 superset/initialization/bootstrap.py diff --git a/.gitattributes b/.gitattributes index 79f44a6b2637..8fbdfcc2cb48 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ docker/**/*.sh text eol=lf +docs/static/img/erd.svg -diff diff --git a/.github/workflows/python-importable.yml b/.github/workflows/python-importable.yml new file mode 100644 index 000000000000..4dfab0037109 --- /dev/null +++ b/.github/workflows/python-importable.yml @@ -0,0 +1,27 @@ +name: pre-commit checks + +on: + push: + branches: + - "master" + - "[0-9].[0-9]" + pull_request: + types: [synchronize, opened, reopened, ready_for_review] + +# cancel previous workflow jobs for PRs +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + test_python_imports: + runs-on: ubuntu-20.04 + steps: + - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" + uses: actions/checkout@v4 + - name: Setup Python + uses: ./.github/actions/setup-backend/ + - name: Enable brew and helm-docs + run: | + echo "Checking that all python modules are importable" + python scripts/test_imports.py superset diff --git a/UPDATING.md b/UPDATING.md index 67be28ba913b..2327fcfa3fdb 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -47,6 +47,10 @@ assists people when migrating to a new version. more clearly provides access to all databases, as specified in its name. Before it only allowed listing all databases in CRUD-view and dropdown and didn't provide access to data as it seemed the name would imply. +- [28483](https://github.com/apache/superset/pull/28483) Starting with this version we bundle + translations inside the python package. This includes the .mo files needed by pybabel on the + backend, as well as the .json files used by the frontend. If you were doing anything before + as part of your bundling to expose translation packages, it's probably not needed anymore. ### Potential Downtime diff --git a/docs/docs/contributing/development.mdx b/docs/docs/contributing/development.mdx index bf350ffa4f2d..7a7d89cde120 100644 --- a/docs/docs/contributing/development.mdx +++ b/docs/docs/contributing/development.mdx @@ -208,7 +208,7 @@ gunicorn "superset.app:create_app()" -k "geventwebsocket.gunicorn.workers.Gevent You can log anything to the browser console, including objects: ```python -from superset import app +from flask import current_app as app app.logger.error('An exception occurred!') app.logger.info(form_data) ``` diff --git a/scripts/benchmark_migration.py b/scripts/benchmark_migration.py index 90d94853dccb..b4926934f1a0 100644 --- a/scripts/benchmark_migration.py +++ b/scripts/benchmark_migration.py @@ -33,7 +33,7 @@ from sqlalchemy import create_engine, inspect from sqlalchemy.ext.automap import automap_base -from superset import db +from superset.extensions import db from superset.utils.mock_data import add_sample_rows logger = logging.getLogger(__name__) diff --git a/scripts/erd/erd.py b/scripts/erd/erd.py index d49940feb5fc..a5e2568a7c46 100644 --- a/scripts/erd/erd.py +++ b/scripts/erd/erd.py @@ -29,7 +29,7 @@ import click import jinja2 -from superset import db +from superset.extensions import db GROUPINGS: dict[str, Iterable[str]] = { "Core": [ diff --git a/scripts/messages.pot.tmp b/scripts/messages.pot.tmp new file mode 100644 index 000000000000..13a83393a912 --- /dev/null +++ b/scripts/messages.pot.tmp @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/scripts/test_imports.py b/scripts/test_imports.py new file mode 100755 index 000000000000..9a5732b17e91 --- /dev/null +++ b/scripts/test_imports.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Test importability of all modules within a package with parallel +execution support. + +This was implemented to prevent usage of the app's context and configuration +located at app.config within module scope. It may also identify other issues +such as circular imports or anything else that may prevent a module from being +imported independently. +""" + +import argparse +import os +import re +import subprocess +import sys +from concurrent.futures import as_completed, ThreadPoolExecutor # Import as_completed +from typing import List + +EXCLUDE_FILE_PATTERNS: List[str] = [ + r"^superset/migrations/", + r"^tests/integration_tests/migrations/", +] +ROOT_FOLDERS = ["superset", "tests"] + + +def test_module_import(file_path: str) -> str | None: + """Test if a module can be imported independently""" + module_path = file_path.replace(".py", "").replace("/", ".") + splitted = module_path.split(".") + from_part = ".".join(splitted[:-1]) + import_part = splitted[-1] + import_statement = f"from {from_part} import {import_part}" + try: + subprocess.run( + ["python", "-c", import_statement], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return None + except subprocess.CalledProcessError as e: + if e.stderr: + return e.stderr.decode("utf-8").strip() + return str(e) + + +def get_module_paths(package_path: str) -> list[str]: + paths = [] + for root, dirs, files in os.walk(package_path): + for file in files: + filepath = os.path.normpath(os.path.join(package_path, root, file)) + relative_path = os.path.relpath(filepath, package_path) + if file.endswith(".py") and all( + not re.match(pattern, relative_path) + for pattern in EXCLUDE_FILE_PATTERNS + ): + paths.append(relative_path) + return paths + + +def test_import( + path_pattern: str | None = None, max_workers: int | None = None +) -> None: + """Test importability of all modules within a package""" + error_count = 0 + processed_count = 0 + paths = [] + for folder in ROOT_FOLDERS: + paths += get_module_paths(folder) + if path_pattern: + filtered_path = [] + for path in paths: + if re.match(path_pattern, path): + filtered_path.append(path) + paths = filtered_path + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = {executor.submit(test_module_import, path): path for path in paths} + for future in as_completed(futures): # Use as_completed + path = futures[future] + processed_count += 1 + message = f"[{processed_count}/{len(paths)}] {path}" + error = future.result() + if error: + print(f"❌ {message}") + print(error) + error_count += 1 + else: + print(f"✅ {message}") + + print(f"Total errors: {error_count}") + if error_count: + sys.exit(1) + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Test importability of all modules within a package with parallel execution support." + ) + parser.add_argument( + "--workers", + type=int, + default=os.cpu_count(), + help="Number of worker threads for parallel execution (default is number of CPU cores)", + ) + parser.add_argument( + "path", + type=str, + default="*", + help="Path filter", + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_arguments() + test_import(args.path, args.workers) diff --git a/superset/__init__.py b/superset/__init__.py index e68f60ac6acf..c547615f753b 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -16,16 +16,13 @@ # under the License. """Package's main module!""" -from flask import current_app, Flask from werkzeug.local import LocalProxy -from superset.app import create_app # noqa: F401 from superset.extensions import ( appbuilder, # noqa: F401 cache_manager, - db, # noqa: F401 event_logger, # noqa: F401 - feature_flag_manager, + feature_flag_manager, # noqa: F401 manifest_processor, results_backend_manager, security_manager, # noqa: F401 @@ -37,15 +34,26 @@ # to declare "global" dependencies is to define it in extensions.py, # then initialize it in app.create_app(). These fields will be removed # in subsequent PRs as things are migrated towards the factory pattern -app: Flask = current_app cache = cache_manager.cache -conf = LocalProxy(lambda: current_app.config) -get_feature_flags = feature_flag_manager.get_feature_flags +data_cache = LocalProxy(lambda: cache_manager.data_cache) get_manifest_files = manifest_processor.get_manifest_files -is_feature_enabled = feature_flag_manager.is_feature_enabled results_backend = LocalProxy(lambda: results_backend_manager.results_backend) results_backend_use_msgpack = LocalProxy( lambda: results_backend_manager.should_use_msgpack ) -data_cache = LocalProxy(lambda: cache_manager.data_cache) thumbnail_cache = LocalProxy(lambda: cache_manager.thumbnail_cache) + +__all__ = [ + "appbuilder", + "cache", + "cache_manager", + "data_cache", + "event_logger", + "feature_flag_manager", + "get_manifest_files", + "manifest_processor", + "results_backend", + "results_backend_manager", + "results_backend_use_msgpack", + "thumbnail_cache", +] diff --git a/superset/advanced_data_type/api.py b/superset/advanced_data_type/api.py index c96c54157c56..1f261039974b 100644 --- a/superset/advanced_data_type/api.py +++ b/superset/advanced_data_type/api.py @@ -29,9 +29,6 @@ from superset.extensions import event_logger from superset.views.base_api import BaseSupersetApi -config = app.config -ADVANCED_DATA_TYPES = config["ADVANCED_DATA_TYPES"] - class AdvancedDataTypeRestApi(BaseSupersetApi): """ @@ -96,7 +93,7 @@ def get(self, **kwargs: Any) -> Response: item = kwargs["rison"] advanced_data_type = item["type"] values = item["values"] - addon = ADVANCED_DATA_TYPES.get(advanced_data_type) + addon = app.config["ADVANCED_DATA_TYPES"].get(advanced_data_type) if not addon: return self.response( 400, @@ -148,4 +145,4 @@ def get_types(self) -> Response: 500: $ref: '#/components/responses/500' """ - return self.response(200, result=list(ADVANCED_DATA_TYPES.keys())) + return self.response(200, result=list(app.config["ADVANCED_DATA_TYPES"].keys())) diff --git a/superset/app.py b/superset/app.py index a2192b8966d5..010ef3c17d88 100644 --- a/superset/app.py +++ b/superset/app.py @@ -28,6 +28,7 @@ def create_app(superset_config_module: Optional[str] = None) -> Flask: app = SupersetApp(__name__) + print("in CREATE_APP") try: # Allow user to override our config completely diff --git a/superset/async_events/async_query_manager.py b/superset/async_events/async_query_manager.py index 84b20d753ce0..7dc577a8a387 100644 --- a/superset/async_events/async_query_manager.py +++ b/superset/async_events/async_query_manager.py @@ -20,7 +20,7 @@ import jwt import redis -from flask import Flask, Request, request, Response, session +from flask import current_app, Flask, Request, request, Response, session from superset.utils import json from superset.utils.core import get_user_id @@ -121,8 +121,7 @@ def init_app(self, app: Flask) -> None: if config["GLOBAL_ASYNC_QUERIES_REGISTER_REQUEST_HANDLERS"]: self.register_request_handlers(app) - # pylint: disable=import-outside-toplevel - from superset.tasks.async_queries import ( + from superset.tasks.async_queries import ( # pylint: disable=import-outside-toplevel load_chart_data_into_cache, load_explore_json_into_cache, ) @@ -191,8 +190,9 @@ def submit_explore_json_job( force: Optional[bool] = False, user_id: Optional[int] = None, ) -> dict[str, Any]: - # pylint: disable=import-outside-toplevel - from superset import security_manager + from superset.extensions import ( # pylint: disable=import-outside-toplevel + security_manager, + ) job_metadata = self.init_job(channel_id, user_id) self._load_explore_json_into_cache_job.delay( @@ -202,6 +202,7 @@ def submit_explore_json_job( form_data, response_type, force, + soft_time_limit=current_app.config["SQLLAB_ASYNC_TIME_LIMIT_SEC"], ) return job_metadata @@ -211,8 +212,9 @@ def submit_chart_data_job( form_data: dict[str, Any], user_id: Optional[int] = None, ) -> dict[str, Any]: - # pylint: disable=import-outside-toplevel - from superset import security_manager + from superset.extensions import ( # pylint: disable=import-outside-toplevel + security_manager, + ) # if it's guest user, we want to pass the guest token to the celery task # chart data cache key is calculated based on the current user @@ -224,6 +226,7 @@ def submit_chart_data_job( if (guest_user := security_manager.get_current_guest_user_if_guest()) else job_metadata, form_data, + soft_time_limit=current_app.config["SQLLAB_ASYNC_TIME_LIMIT_SEC"], ) return job_metadata diff --git a/superset/available_domains/api.py b/superset/available_domains/api.py index 0b8253d942fe..99d125e84bef 100644 --- a/superset/available_domains/api.py +++ b/superset/available_domains/api.py @@ -16,10 +16,9 @@ # under the License. import logging -from flask import Response +from flask import current_app as app, Response from flask_appbuilder.api import expose, protect, safe -from superset import conf from superset.available_domains.schemas import AvailableDomainsSchema from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.extensions import event_logger @@ -70,6 +69,6 @@ def get(self) -> Response: $ref: '#/components/responses/403' """ result = self.available_domains_schema.dump( - {"domains": conf.get("SUPERSET_WEBSERVER_DOMAINS")} + {"domains": app.conf.get("SUPERSET_WEBSERVER_DOMAINS")} ) return self.response(200, result=result) diff --git a/superset/charts/api.py b/superset/charts/api.py index d32f1f665ae1..bd55a7392e05 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -30,7 +30,7 @@ from werkzeug.wrappers import Response as WerkzeugResponse from werkzeug.wsgi import FileWrapper -from superset import app, is_feature_enabled, thumbnail_cache +from superset import thumbnail_cache from superset.charts.filters import ( ChartAllTextFilter, ChartCertifiedFilter, @@ -76,7 +76,7 @@ from superset.commands.importers.v1.utils import get_contents_from_bundle from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.daos.chart import ChartDAO -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.models.slice import Slice from superset.tasks.thumbnails import cache_chart_thumbnail from superset.tasks.utils import get_current_user @@ -93,7 +93,6 @@ from superset.views.filters import BaseFilterRelatedUsers, FilterRelatedOwners logger = logging.getLogger(__name__) -config = app.config class ChartRestApi(BaseSupersetModelRestApi): @@ -104,7 +103,7 @@ class ChartRestApi(BaseSupersetModelRestApi): @before_request(only=["thumbnail", "screenshot", "cache_screenshot"]) def ensure_thumbnails_enabled(self) -> Optional[Response]: - if not is_feature_enabled("THUMBNAILS"): + if not feature_flag_manager.is_feature_enabled("THUMBNAILS"): return self.response_404() return None diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py index 932a66673875..9fdb24b18417 100644 --- a/superset/charts/data/api.py +++ b/superset/charts/data/api.py @@ -25,7 +25,6 @@ from flask_babel import gettext as _ from marshmallow import ValidationError -from superset import is_feature_enabled, security_manager from superset.async_events.async_query_manager import AsyncQueryTokenException from superset.charts.api import ChartRestApi from superset.charts.data.query_context_cache_loader import QueryContextCacheLoader @@ -43,7 +42,7 @@ from superset.connectors.sqla.models import BaseDatasource from superset.daos.exceptions import DatasourceNotFound from superset.exceptions import QueryObjectValidationError -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager, security_manager from superset.models.sql_lab import Query from superset.utils import json from superset.utils.core import ( @@ -162,7 +161,7 @@ def get_data(self, pk: int) -> Response: # TODO: support CSV, SQL query and other non-JSON types if ( - is_feature_enabled("GLOBAL_ASYNC_QUERIES") + feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES") and query_context.result_format == ChartDataResultFormat.JSON and query_context.result_type == ChartDataResultType.FULL ): @@ -250,7 +249,7 @@ def data(self) -> Response: # TODO: support CSV, SQL query and other non-JSON types if ( - is_feature_enabled("GLOBAL_ASYNC_QUERIES") + feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES") and query_context.result_format == ChartDataResultFormat.JSON and query_context.result_type == ChartDataResultType.FULL ): diff --git a/superset/charts/filters.py b/superset/charts/filters.py index a7543ba284a8..64c6141eea55 100644 --- a/superset/charts/filters.py +++ b/superset/charts/filters.py @@ -21,9 +21,9 @@ from sqlalchemy.orm import aliased from sqlalchemy.orm.query import Query -from superset import db, security_manager from superset.connectors.sqla import models from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db, security_manager from superset.models.core import FavStar from superset.models.slice import Slice from superset.utils.core import get_user_id diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index 89e47a9dcb88..d510a082f73a 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -20,11 +20,11 @@ import inspect from typing import Any, TYPE_CHECKING +from flask import current_app as app from flask_babel import gettext as _ from marshmallow import EXCLUDE, fields, post_load, Schema, validate from marshmallow.validate import Length, Range -from superset import app from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.db_engine_specs.base import builtin_time_grains from superset.utils import pandas_postprocessing, schema as utils @@ -40,11 +40,7 @@ from superset.common.query_context import QueryContext from superset.common.query_context_factory import QueryContextFactory -config = app.config - -# # RISON/JSON schemas for query parameters -# get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}} width_height_schema = { @@ -597,6 +593,14 @@ class ChartDataContributionOptionsSchema(ChartDataPostProcessingOperationOptions ) +def get_time_grain_choices(): # type: ignore + return [ + i + for i in {**builtin_time_grains, **app.config["TIME_GRAIN_ADDONS"]}.keys() + if i + ] + + class ChartDataProphetOptionsSchema(ChartDataPostProcessingOperationOptionsSchema): """ Prophet operation config. @@ -609,13 +613,8 @@ class ChartDataProphetOptionsSchema(ChartDataPostProcessingOperationOptionsSchem "[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.", "example": "P1D", }, - validate=validate.OneOf( - choices=[ - i - for i in {**builtin_time_grains, **config["TIME_GRAIN_ADDONS"]}.keys() - if i - ] - ), + # TODO reinstate at runtime since schemas shouldn't be dynamic + # validate=validate.OneOf(choices=get_time_grain_choices), required=True, ) periods = fields.Integer( @@ -946,14 +945,14 @@ class ChartDataExtrasSchema(Schema): relative_start = fields.String( metadata={ "description": "Start time for relative time deltas. " - 'Default: `config["DEFAULT_RELATIVE_START_TIME"]`' + 'Default: `app.config["DEFAULT_RELATIVE_START_TIME"]`' }, validate=validate.OneOf(choices=("today", "now")), ) relative_end = fields.String( metadata={ "description": "End time for relative time deltas. " - 'Default: `config["DEFAULT_RELATIVE_START_TIME"]`' + 'Default: `app.config["DEFAULT_RELATIVE_START_TIME"]`' }, validate=validate.OneOf(choices=("today", "now")), ) @@ -975,13 +974,8 @@ class ChartDataExtrasSchema(Schema): "[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.", "example": "P1D", }, - validate=validate.OneOf( - choices=[ - i - for i in {**builtin_time_grains, **config["TIME_GRAIN_ADDONS"]}.keys() - if i - ] - ), + # TODO reinstate at runtime since schemas shouldn't be dynamic + # validate=validate.OneOf(choices=[]), allow_none=True, ) instant_time_comparison_range = fields.String( diff --git a/superset/cli/main.py b/superset/cli/main.py index aa7e3068f8b9..ffe068168936 100755 --- a/superset/cli/main.py +++ b/superset/cli/main.py @@ -22,11 +22,12 @@ import click from colorama import Fore, Style +from flask import current_app as app from flask.cli import FlaskGroup, with_appcontext -from superset import app, appbuilder, cli, security_manager +from superset import cli from superset.cli.lib import normalize_token -from superset.extensions import db +from superset.extensions import appbuilder, db, security_manager logger = logging.getLogger(__name__) diff --git a/superset/cli/test.py b/superset/cli/test.py index f175acec470c..1725f34de044 100755 --- a/superset/cli/test.py +++ b/superset/cli/test.py @@ -18,10 +18,11 @@ import click from colorama import Fore +from flask import current_app as app from flask.cli import with_appcontext import superset.utils.database as database_utils -from superset import app, security_manager +from superset import security_manager logger = logging.getLogger(__name__) diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py index 4ddc739cdcfd..2c7bd0292c76 100644 --- a/superset/cli/viz_migrations.py +++ b/superset/cli/viz_migrations.py @@ -20,7 +20,7 @@ from click_option_group import optgroup, RequiredMutuallyExclusiveOptionGroup from flask.cli import with_appcontext -from superset import db +from superset.extensions import db class VizType(str, Enum): diff --git a/superset/commands/chart/importers/v1/utils.py b/superset/commands/chart/importers/v1/utils.py index 39ca49a5d5ff..0fc64e84d852 100644 --- a/superset/commands/chart/importers/v1/utils.py +++ b/superset/commands/chart/importers/v1/utils.py @@ -19,8 +19,8 @@ from inspect import isclass from typing import Any -from superset import db, security_manager from superset.commands.exceptions import ImportFailedError +from superset.extensions import db, security_manager from superset.migrations.shared.migrate_viz import processors from superset.migrations.shared.migrate_viz.base import MigrateViz from superset.models.slice import Slice diff --git a/superset/commands/chart/warm_up_cache.py b/superset/commands/chart/warm_up_cache.py index ea794c2842dd..b2615cae5303 100644 --- a/superset/commands/chart/warm_up_cache.py +++ b/superset/commands/chart/warm_up_cache.py @@ -31,7 +31,7 @@ from superset.utils import json from superset.utils.core import error_msg_from_exception from superset.views.utils import get_dashboard_extra_filters, get_form_data, get_viz -from superset.viz import viz_types +from superset.viz import get_viz_types class ChartWarmUpCacheCommand(BaseCommand): @@ -52,7 +52,7 @@ def run(self) -> dict[str, Any]: try: form_data = get_form_data(chart.id, use_slice_data=True)[0] - if form_data.get("viz_type") in viz_types: + if form_data.get("viz_type") in get_viz_types(): # Legacy visualizations. if not chart.datasource: raise ChartInvalidError("Chart's datasource does not exist") diff --git a/superset/commands/dashboard/importers/v0.py b/superset/commands/dashboard/importers/v0.py index a9ee3e484e1c..4d4765233049 100644 --- a/superset/commands/dashboard/importers/v0.py +++ b/superset/commands/dashboard/importers/v0.py @@ -23,11 +23,11 @@ from flask_babel import lazy_gettext as _ from sqlalchemy.orm import make_transient -from superset import db from superset.commands.base import BaseCommand from superset.commands.dataset.importers.v0 import import_dataset from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.exceptions import DashboardImportException +from superset.extensions import db from superset.migrations.shared.native_filters import migrate_dashboard from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/superset/commands/dashboard/importers/v1/__init__.py b/superset/commands/dashboard/importers/v1/__init__.py index 48b4e93e8cf3..a25b75037c84 100644 --- a/superset/commands/dashboard/importers/v1/__init__.py +++ b/superset/commands/dashboard/importers/v1/__init__.py @@ -21,7 +21,6 @@ from sqlalchemy.orm import Session # noqa: F401 from sqlalchemy.sql import select -from superset import db from superset.charts.schemas import ImportV1ChartSchema from superset.commands.chart.importers.v1.utils import import_chart from superset.commands.dashboard.exceptions import DashboardImportError @@ -38,6 +37,7 @@ from superset.dashboards.schemas import ImportV1DashboardSchema from superset.databases.schemas import ImportV1DatabaseSchema from superset.datasets.schemas import ImportV1DatasetSchema +from superset.extensions import db from superset.migrations.shared.native_filters import migrate_dashboard from superset.models.dashboard import Dashboard, dashboard_slices diff --git a/superset/commands/dashboard/importers/v1/utils.py b/superset/commands/dashboard/importers/v1/utils.py index f10afd12bc9e..3c264b2c0248 100644 --- a/superset/commands/dashboard/importers/v1/utils.py +++ b/superset/commands/dashboard/importers/v1/utils.py @@ -18,8 +18,8 @@ import logging from typing import Any -from superset import db, security_manager from superset.commands.exceptions import ImportFailedError +from superset.extensions import db, security_manager from superset.models.dashboard import Dashboard from superset.utils import json from superset.utils.core import get_user diff --git a/superset/commands/database/create.py b/superset/commands/database/create.py index b45107ca85cc..a815d3c17ee6 100644 --- a/superset/commands/database/create.py +++ b/superset/commands/database/create.py @@ -17,11 +17,10 @@ import logging from typing import Any, Optional -from flask import current_app +from flask import current_app as app from flask_appbuilder.models.sqla import Model from marshmallow import ValidationError -from superset import is_feature_enabled from superset.commands.base import BaseCommand from superset.commands.database.exceptions import ( DatabaseConnectionFailedError, @@ -41,11 +40,10 @@ from superset.daos.database import DatabaseDAO from superset.daos.exceptions import DAOCreateFailedError from superset.exceptions import SupersetErrorsException -from superset.extensions import db, event_logger, security_manager +from superset.extensions import db, event_logger, feature_flag_manager, security_manager from superset.models.core import Database logger = logging.getLogger(__name__) -stats_logger = current_app.config["STATS_LOGGER"] class CreateDatabaseCommand(BaseCommand): @@ -88,7 +86,7 @@ def run(self) -> Model: database = self._create_database() if ssh_tunnel_properties := self._properties.get("ssh_tunnel"): - if not is_feature_enabled("SSH_TUNNELING"): + if not feature_flag_manager.is_feature_enabled("SSH_TUNNELING"): raise SSHTunnelingNotEnabledError() ssh_tunnel = CreateSSHTunnelCommand( @@ -150,12 +148,12 @@ def run(self) -> Model: db.session.rollback() event_logger.log_with_context( action=f"db_creation_failed.{ex.__class__.__name__}", - engine=database.db_engine_spec.__name__, + database=database, ) raise DatabaseCreateFailedError() from ex if ssh_tunnel: - stats_logger.incr("db_creation_success.ssh_tunnel") + app.stats_logger.incr("db_creation_success.ssh_tunnel") return database diff --git a/superset/commands/database/importers/v1/utils.py b/superset/commands/database/importers/v1/utils.py index 56d31b03e199..818a96138c34 100644 --- a/superset/commands/database/importers/v1/utils.py +++ b/superset/commands/database/importers/v1/utils.py @@ -17,11 +17,13 @@ from typing import Any -from superset import app, db, security_manager +from flask import current_app as app + from superset.commands.exceptions import ImportFailedError from superset.databases.ssh_tunnel.models import SSHTunnel from superset.databases.utils import make_url_safe from superset.exceptions import SupersetSecurityException +from superset.extensions import db, security_manager from superset.models.core import Database from superset.security.analytics_db_safety import check_sqlalchemy_uri from superset.utils import json diff --git a/superset/commands/database/ssh_tunnel/delete.py b/superset/commands/database/ssh_tunnel/delete.py index b8919e6d7bae..0de17aa377db 100644 --- a/superset/commands/database/ssh_tunnel/delete.py +++ b/superset/commands/database/ssh_tunnel/delete.py @@ -17,7 +17,6 @@ import logging from typing import Optional -from superset import is_feature_enabled from superset.commands.base import BaseCommand from superset.commands.database.ssh_tunnel.exceptions import ( SSHTunnelDeleteFailedError, @@ -27,6 +26,7 @@ from superset.daos.database import SSHTunnelDAO from superset.daos.exceptions import DAODeleteFailedError from superset.databases.ssh_tunnel.models import SSHTunnel +from superset.extensions import feature_flag_manager logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def __init__(self, model_id: int): self._model: Optional[SSHTunnel] = None def run(self) -> None: - if not is_feature_enabled("SSH_TUNNELING"): + if not feature_flag_manager.is_feature_enabled("SSH_TUNNELING"): raise SSHTunnelingNotEnabledError() self.validate() assert self._model diff --git a/superset/commands/database/test_connection.py b/superset/commands/database/test_connection.py index 6bf69bbb8741..52d96f4908ff 100644 --- a/superset/commands/database/test_connection.py +++ b/superset/commands/database/test_connection.py @@ -25,7 +25,6 @@ from sqlalchemy.engine import Engine from sqlalchemy.exc import DBAPIError, NoSuchModuleError -from superset import is_feature_enabled from superset.commands.base import BaseCommand from superset.commands.database.exceptions import ( DatabaseSecurityUnsafeError, @@ -45,7 +44,7 @@ SupersetSecurityException, SupersetTimeoutException, ) -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.models.core import Database from superset.utils.ssh_tunnel import unmask_password_info @@ -131,7 +130,7 @@ def run(self) -> None: # pylint: disable=too-many-statements event_logger.log_with_context( action=get_log_connection_action("test_connection_attempt", ssh_tunnel), - engine=database.db_engine_spec.__name__, + database=database, ) def ping(engine: Engine) -> bool: @@ -171,7 +170,7 @@ def ping(engine: Engine) -> bool: # Log succesful connection test with engine event_logger.log_with_context( action=get_log_connection_action("test_connection_success", ssh_tunnel), - engine=database.db_engine_spec.__name__, + database=database, ) except (NoSuchModuleError, ModuleNotFoundError) as ex: @@ -179,7 +178,7 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) raise DatabaseTestConnectionDriverError( message=_("Could not load database driver: {}").format( @@ -191,7 +190,7 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) # check for custom errors (wrong username, wrong password, etc) errors = database.db_engine_spec.extract_errors(ex, self._context) @@ -201,7 +200,7 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) raise DatabaseSecurityUnsafeError(message=str(ex)) from ex except SupersetTimeoutException as ex: @@ -209,7 +208,7 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) # bubble up the exception to return a 408 raise ex @@ -218,7 +217,7 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) # bubble up the exception to return a 400 raise ex @@ -227,14 +226,14 @@ def ping(engine: Engine) -> bool: action=get_log_connection_action( "test_connection_error", ssh_tunnel, ex ), - engine=database.db_engine_spec.__name__, + database=database, ) errors = database.db_engine_spec.extract_errors(ex, self._context) raise DatabaseTestConnectionUnexpectedError(errors) from ex def validate(self) -> None: if self._properties.get("ssh_tunnel"): - if not is_feature_enabled("SSH_TUNNELING"): + if not feature_flag_manager.is_feature_enabled("SSH_TUNNELING"): raise SSHTunnelingNotEnabledError() if not self._context.get("port"): raise SSHTunnelDatabasePortError() diff --git a/superset/commands/database/update.py b/superset/commands/database/update.py index c59984238cfe..925ddc1d5f88 100644 --- a/superset/commands/database/update.py +++ b/superset/commands/database/update.py @@ -22,7 +22,6 @@ from flask_appbuilder.models.sqla import Model -from superset import is_feature_enabled, security_manager from superset.commands.base import BaseCommand from superset.commands.database.exceptions import ( DatabaseConnectionFailedError, @@ -42,7 +41,7 @@ from superset.daos.dataset import DatasetDAO from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError from superset.databases.ssh_tunnel.models import SSHTunnel -from superset.extensions import db +from superset.extensions import db, feature_flag_manager, security_manager from superset.models.core import Database logger = logging.getLogger(__name__) @@ -100,7 +99,7 @@ def _handle_ssh_tunnel(self, database: Database) -> SSHTunnel | None: if "ssh_tunnel" not in self._properties: return None - if not is_feature_enabled("SSH_TUNNELING"): + if not feature_flag_manager.is_feature_enabled("SSH_TUNNELING"): db.session.rollback() raise SSHTunnelingNotEnabledError() diff --git a/superset/commands/database/uploaders/base.py b/superset/commands/database/uploaders/base.py index b113e9ebf45d..8c99909ba2e6 100644 --- a/superset/commands/database/uploaders/base.py +++ b/superset/commands/database/uploaders/base.py @@ -23,7 +23,6 @@ from sqlalchemy.exc import SQLAlchemyError from werkzeug.datastructures import FileStorage -from superset import db from superset.commands.base import BaseCommand from superset.commands.database.exceptions import ( DatabaseNotFoundError, @@ -34,6 +33,7 @@ ) from superset.connectors.sqla.models import SqlaTable from superset.daos.database import DatabaseDAO +from superset.extensions import db from superset.models.core import Database from superset.sql_parse import Table from superset.utils.core import get_user diff --git a/superset/commands/dataset/importers/v0.py b/superset/commands/dataset/importers/v0.py index 14be54174a9f..332f6ae2657b 100644 --- a/superset/commands/dataset/importers/v0.py +++ b/superset/commands/dataset/importers/v0.py @@ -21,7 +21,6 @@ from flask_appbuilder import Model from sqlalchemy.orm.session import make_transient -from superset import db from superset.commands.base import BaseCommand from superset.commands.database.exceptions import DatabaseNotFoundError from superset.commands.dataset.exceptions import DatasetInvalidError @@ -32,6 +31,7 @@ SqlMetric, TableColumn, ) +from superset.extensions import db from superset.models.core import Database from superset.utils import json from superset.utils.dict_import_export import DATABASES_KEY diff --git a/superset/commands/dataset/importers/v1/utils.py b/superset/commands/dataset/importers/v1/utils.py index da39be4721c0..dff79db343b7 100644 --- a/superset/commands/dataset/importers/v1/utils.py +++ b/superset/commands/dataset/importers/v1/utils.py @@ -26,10 +26,10 @@ from sqlalchemy.exc import MultipleResultsFound from sqlalchemy.sql.visitors import VisitableType -from superset import db, security_manager from superset.commands.dataset.exceptions import DatasetForbiddenDataURI from superset.commands.exceptions import ImportFailedError from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db, security_manager from superset.models.core import Database from superset.sql_parse import Table from superset.utils import json diff --git a/superset/commands/importers/v1/__init__.py b/superset/commands/importers/v1/__init__.py index 6c86faabeb54..c98ad88720e6 100644 --- a/superset/commands/importers/v1/__init__.py +++ b/superset/commands/importers/v1/__init__.py @@ -20,7 +20,6 @@ from marshmallow.exceptions import ValidationError from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.commands.base import BaseCommand from superset.commands.exceptions import CommandException, CommandInvalidError from superset.commands.importers.v1.utils import ( @@ -31,6 +30,7 @@ validate_metadata_type, ) from superset.daos.base import BaseDAO +from superset.extensions import db from superset.models.core import Database # noqa: F401 diff --git a/superset/commands/importers/v1/assets.py b/superset/commands/importers/v1/assets.py index 29a2dec17908..9b499206f68d 100644 --- a/superset/commands/importers/v1/assets.py +++ b/superset/commands/importers/v1/assets.py @@ -20,7 +20,6 @@ from marshmallow.exceptions import ValidationError from sqlalchemy.sql import delete, insert -from superset import db from superset.charts.schemas import ImportV1ChartSchema from superset.commands.base import BaseCommand from superset.commands.chart.importers.v1.utils import import_chart @@ -41,6 +40,7 @@ from superset.dashboards.schemas import ImportV1DashboardSchema from superset.databases.schemas import ImportV1DatabaseSchema from superset.datasets.schemas import ImportV1DatasetSchema +from superset.extensions import db from superset.migrations.shared.native_filters import migrate_dashboard from superset.models.dashboard import dashboard_slices from superset.queries.saved_queries.schemas import ImportV1SavedQuerySchema diff --git a/superset/commands/importers/v1/examples.py b/superset/commands/importers/v1/examples.py index 6525031ce4f3..e37e8a7a5b45 100644 --- a/superset/commands/importers/v1/examples.py +++ b/superset/commands/importers/v1/examples.py @@ -20,7 +20,6 @@ from sqlalchemy.exc import MultipleResultsFound from sqlalchemy.sql import select -from superset import db from superset.charts.schemas import ImportV1ChartSchema from superset.commands.chart.importers.v1 import ImportChartsCommand from superset.commands.chart.importers.v1.utils import import_chart @@ -40,6 +39,7 @@ from superset.dashboards.schemas import ImportV1DashboardSchema from superset.databases.schemas import ImportV1DatabaseSchema from superset.datasets.schemas import ImportV1DatasetSchema +from superset.extensions import db from superset.models.dashboard import dashboard_slices from superset.utils.core import get_example_default_schema from superset.utils.database import get_example_database diff --git a/superset/commands/importers/v1/utils.py b/superset/commands/importers/v1/utils.py index 912a4d1be5a7..e2bd704038bf 100644 --- a/superset/commands/importers/v1/utils.py +++ b/superset/commands/importers/v1/utils.py @@ -22,9 +22,9 @@ from marshmallow import fields, Schema, validate from marshmallow.exceptions import ValidationError -from superset import db from superset.commands.importers.exceptions import IncorrectVersionError from superset.databases.ssh_tunnel.models import SSHTunnel +from superset.extensions import db from superset.models.core import Database from superset.utils.core import check_is_safe_zip diff --git a/superset/commands/key_value/create.py b/superset/commands/key_value/create.py index a39607b5885d..3afd47979c2b 100644 --- a/superset/commands/key_value/create.py +++ b/superset/commands/key_value/create.py @@ -21,8 +21,8 @@ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand +from superset.extensions import db from superset.key_value.exceptions import KeyValueCreateFailedError from superset.key_value.models import KeyValueEntry from superset.key_value.types import Key, KeyValueCodec, KeyValueResource diff --git a/superset/commands/key_value/delete.py b/superset/commands/key_value/delete.py index 8b9095c09c9b..27310f293c72 100644 --- a/superset/commands/key_value/delete.py +++ b/superset/commands/key_value/delete.py @@ -20,8 +20,8 @@ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand +from superset.extensions import db from superset.key_value.exceptions import KeyValueDeleteFailedError from superset.key_value.models import KeyValueEntry from superset.key_value.types import KeyValueResource diff --git a/superset/commands/key_value/delete_expired.py b/superset/commands/key_value/delete_expired.py index 166a9f6f87ab..55ea49e74807 100644 --- a/superset/commands/key_value/delete_expired.py +++ b/superset/commands/key_value/delete_expired.py @@ -20,8 +20,8 @@ from sqlalchemy import and_ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand +from superset.extensions import db from superset.key_value.exceptions import KeyValueDeleteFailedError from superset.key_value.models import KeyValueEntry from superset.key_value.types import KeyValueResource diff --git a/superset/commands/key_value/get.py b/superset/commands/key_value/get.py index 8a7a250f1c08..f89410dc63f9 100644 --- a/superset/commands/key_value/get.py +++ b/superset/commands/key_value/get.py @@ -22,8 +22,8 @@ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand +from superset.extensions import db from superset.key_value.exceptions import KeyValueGetFailedError from superset.key_value.models import KeyValueEntry from superset.key_value.types import KeyValueCodec, KeyValueResource diff --git a/superset/commands/key_value/update.py b/superset/commands/key_value/update.py index ca940adf6028..6b8d558ac194 100644 --- a/superset/commands/key_value/update.py +++ b/superset/commands/key_value/update.py @@ -22,8 +22,8 @@ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand +from superset.extensions import db from superset.key_value.exceptions import KeyValueUpdateFailedError from superset.key_value.models import KeyValueEntry from superset.key_value.types import Key, KeyValueCodec, KeyValueResource diff --git a/superset/commands/key_value/upsert.py b/superset/commands/key_value/upsert.py index 84f02cb9cd22..21b06cf0c8ff 100644 --- a/superset/commands/key_value/upsert.py +++ b/superset/commands/key_value/upsert.py @@ -22,9 +22,9 @@ from sqlalchemy.exc import SQLAlchemyError -from superset import db from superset.commands.base import BaseCommand from superset.commands.key_value.create import CreateKeyValueCommand +from superset.extensions import db from superset.key_value.exceptions import ( KeyValueCreateFailedError, KeyValueUpsertFailedError, diff --git a/superset/commands/query/importers/v1/utils.py b/superset/commands/query/importers/v1/utils.py index d611aa5e3ac1..2603ca056470 100644 --- a/superset/commands/query/importers/v1/utils.py +++ b/superset/commands/query/importers/v1/utils.py @@ -17,7 +17,7 @@ from typing import Any -from superset import db +from superset.extensions import db from superset.models.sql_lab import SavedQuery diff --git a/superset/commands/report/alert.py b/superset/commands/report/alert.py index 30861bddaa2d..86034cc884b7 100644 --- a/superset/commands/report/alert.py +++ b/superset/commands/report/alert.py @@ -24,9 +24,10 @@ import numpy as np import pandas as pd from celery.exceptions import SoftTimeLimitExceeded +from flask import current_app as app from flask_babel import lazy_gettext as _ -from superset import app, jinja_context, security_manager +from superset import jinja_context from superset.commands.base import BaseCommand from superset.commands.report.exceptions import ( AlertQueryError, @@ -36,6 +37,7 @@ AlertQueryTimeout, AlertValidatorConfigError, ) +from superset.extensions import security_manager from superset.reports.models import ReportSchedule, ReportScheduleValidatorType from superset.tasks.utils import get_executor from superset.utils import json diff --git a/superset/commands/report/execute.py b/superset/commands/report/execute.py index 1540fa70d809..614dc59e6d14 100644 --- a/superset/commands/report/execute.py +++ b/superset/commands/report/execute.py @@ -21,8 +21,8 @@ import pandas as pd from celery.exceptions import SoftTimeLimitExceeded +from flask import current_app as app -from superset import app, db, security_manager from superset.commands.base import BaseCommand from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand from superset.commands.exceptions import CommandException @@ -51,7 +51,12 @@ ) from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetErrorsException, SupersetException -from superset.extensions import feature_flag_manager, machine_auth_provider_factory +from superset.extensions import ( + db, + feature_flag_manager, + machine_auth_provider_factory, + security_manager, +) from superset.reports.models import ( ReportDataFormat, ReportExecutionLog, diff --git a/superset/commands/report/log_prune.py b/superset/commands/report/log_prune.py index f14f7856a1e1..caf3d99a6018 100644 --- a/superset/commands/report/log_prune.py +++ b/superset/commands/report/log_prune.py @@ -17,11 +17,11 @@ import logging from datetime import datetime, timedelta -from superset import db from superset.commands.base import BaseCommand from superset.commands.report.exceptions import ReportSchedulePruneLogError from superset.daos.exceptions import DAODeleteFailedError from superset.daos.report import ReportScheduleDAO +from superset.extensions import db from superset.reports.models import ReportSchedule logger = logging.getLogger(__name__) diff --git a/superset/commands/sql_lab/estimate.py b/superset/commands/sql_lab/estimate.py index d3198815662a..883af71302d8 100644 --- a/superset/commands/sql_lab/estimate.py +++ b/superset/commands/sql_lab/estimate.py @@ -19,20 +19,17 @@ import logging from typing import Any, TypedDict +from flask import current_app as app from flask_babel import gettext as __ -from superset import app, db from superset.commands.base import BaseCommand from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetErrorException, SupersetTimeoutException +from superset.extensions import db from superset.jinja_context import get_template_processor from superset.models.core import Database from superset.utils import core as utils -config = app.config -SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"] -stats_logger = config["STATS_LOGGER"] - logger = logging.getLogger(__name__) @@ -81,7 +78,7 @@ def run( template_processor = get_template_processor(self._database) sql = template_processor.process_template(sql, **self._template_params) - timeout = SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT + timeout = app.config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"] timeout_msg = f"The estimation exceeded the {timeout} seconds timeout." try: with utils.timeout(seconds=timeout, error_message=timeout_msg): @@ -100,7 +97,7 @@ def run( "The query estimation was killed after %(sqllab_timeout)s " "seconds. It might be too complex, or the database might be " "under heavy load.", - sqllab_timeout=SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT, + sqllab_timeout=timeout, ), error_type=SupersetErrorType.SQLLAB_TIMEOUT_ERROR, level=ErrorLevel.ERROR, diff --git a/superset/commands/sql_lab/export.py b/superset/commands/sql_lab/export.py index bfa739054837..95fae367f939 100644 --- a/superset/commands/sql_lab/export.py +++ b/superset/commands/sql_lab/export.py @@ -20,20 +20,20 @@ from typing import Any, cast, TypedDict import pandas as pd +from flask import current_app as app from flask_babel import gettext as __ -from superset import app, db, results_backend, results_backend_use_msgpack +from superset import results_backend, results_backend_use_msgpack from superset.commands.base import BaseCommand from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetErrorException, SupersetSecurityException +from superset.extensions import db from superset.models.sql_lab import Query from superset.sql_parse import ParsedQuery from superset.sqllab.limiting_factor import LimitingFactor from superset.utils import core as utils, csv from superset.views.utils import _deserialize_results_payload -config = app.config - logger = logging.getLogger(__name__) @@ -132,7 +132,7 @@ def run( self._query.schema, )[:limit] - csv_data = csv.df_to_escaped_csv(df, index=False, **config["CSV_EXPORT"]) + csv_data = csv.df_to_escaped_csv(df, index=False, **app.config["CSV_EXPORT"]) return { "query": self._query, diff --git a/superset/commands/sql_lab/results.py b/superset/commands/sql_lab/results.py index 83c8aa8f6a51..2c500def391d 100644 --- a/superset/commands/sql_lab/results.py +++ b/superset/commands/sql_lab/results.py @@ -19,22 +19,20 @@ import logging from typing import Any, cast +from flask import current_app as app from flask_babel import gettext as __ -from superset import app, db, results_backend, results_backend_use_msgpack +from superset import results_backend, results_backend_use_msgpack from superset.commands.base import BaseCommand from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SerializationError, SupersetErrorException +from superset.extensions import db from superset.models.sql_lab import Query from superset.sqllab.utils import apply_display_max_row_configuration_if_require from superset.utils import core as utils from superset.utils.dates import now_as_float from superset.views.utils import _deserialize_results_payload -config = app.config -SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"] -stats_logger = config["STATS_LOGGER"] - logger = logging.getLogger(__name__) @@ -64,7 +62,7 @@ def validate(self) -> None: read_from_results_backend_start = now_as_float() self._blob = results_backend.get(self._key) - stats_logger.timing( + app.stats_logger.timing( "sqllab.query.results_backend_read", now_as_float() - read_from_results_backend_start, ) diff --git a/superset/commands/tag/create.py b/superset/commands/tag/create.py index ea23b8d59da1..4195006390fc 100644 --- a/superset/commands/tag/create.py +++ b/superset/commands/tag/create.py @@ -17,13 +17,13 @@ import logging from typing import Any -from superset import db, security_manager from superset.commands.base import BaseCommand, CreateMixin from superset.commands.tag.exceptions import TagCreateFailedError, TagInvalidError from superset.commands.tag.utils import to_object_model, to_object_type from superset.daos.exceptions import DAOCreateFailedError from superset.daos.tag import TagDAO from superset.exceptions import SupersetSecurityException +from superset.extensions import db, security_manager from superset.tags.models import ObjectType, TagType logger = logging.getLogger(__name__) diff --git a/superset/commands/tag/update.py b/superset/commands/tag/update.py index 431bf93c4de8..e0c203765cd4 100644 --- a/superset/commands/tag/update.py +++ b/superset/commands/tag/update.py @@ -19,11 +19,11 @@ from flask_appbuilder.models.sqla import Model -from superset import db from superset.commands.base import BaseCommand, UpdateMixin from superset.commands.tag.exceptions import TagInvalidError, TagNotFoundError from superset.commands.tag.utils import to_object_type from superset.daos.tag import TagDAO +from superset.extensions import db from superset.tags.models import Tag logger = logging.getLogger(__name__) diff --git a/superset/common/query_actions.py b/superset/common/query_actions.py index bdbccc78dbe2..fb59385f0c0a 100644 --- a/superset/common/query_actions.py +++ b/superset/common/query_actions.py @@ -21,7 +21,6 @@ from flask_babel import _ -from superset import app from superset.common.chart_data import ChartDataResultType from superset.common.db_query_status import QueryStatus from superset.connectors.sqla.models import BaseDatasource @@ -38,8 +37,6 @@ from superset.common.query_context import QueryContext from superset.common.query_object import QueryObject -config = app.config - def _get_datasource( query_context: QueryContext, query_obj: QueryObject diff --git a/superset/common/query_context_factory.py b/superset/common/query_context_factory.py index 5503f3cb0532..a81b2a8cb735 100644 --- a/superset/common/query_context_factory.py +++ b/superset/common/query_context_factory.py @@ -18,7 +18,8 @@ from typing import Any, TYPE_CHECKING -from superset import app +from flask import current_app as app + from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.common.query_context import QueryContext from superset.common.query_object import QueryObject @@ -31,11 +32,9 @@ if TYPE_CHECKING: from superset.connectors.sqla.models import BaseDatasource -config = app.config - def create_query_object_factory() -> QueryObjectFactory: - return QueryObjectFactory(config, DatasourceDAO()) + return QueryObjectFactory(app.config, DatasourceDAO()) class QueryContextFactory: # pylint: disable=too-few-public-methods diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index 75b3e4e976c7..1b639eb10a38 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -19,14 +19,14 @@ import copy import logging import re -from typing import Any, cast, ClassVar, TYPE_CHECKING, TypedDict +from typing import Any, ClassVar, TYPE_CHECKING, TypedDict import numpy as np import pandas as pd +from flask import current_app as app from flask_babel import gettext as _ from pandas import DateOffset -from superset import app from superset.common.chart_data import ChartDataResultFormat from superset.common.db_query_status import QueryStatus from superset.common.query_actions import get_query_results @@ -55,7 +55,6 @@ DateColumn, DTTM_ALIAS, error_msg_from_exception, - FilterOperator, get_base_axis_labels, get_column_names_from_columns, get_column_names_from_metrics, @@ -67,19 +66,16 @@ from superset.utils.date_parser import get_past_or_future, normalize_time_delta from superset.utils.pandas_postprocessing.utils import unescape_separator from superset.views.utils import get_viz -from superset.viz import viz_types +from superset.viz import get_viz_types if TYPE_CHECKING: from superset.common.query_context import QueryContext from superset.common.query_object import QueryObject - from superset.stats_logger import BaseStatsLogger -config = app.config -stats_logger: BaseStatsLogger = config["STATS_LOGGER"] logger = logging.getLogger(__name__) -# Offset join column suffix used for joining offset results -OFFSET_JOIN_COLUMN_SUFFIX = "__offset_join_column_" +# Temporary column used for joining aggregated offset results +AGGREGATED_JOIN_COLUMN = "__aggregated_join_column" # This only includes time grains that may influence # the temporal column used for joining offset results. @@ -340,31 +336,19 @@ def get_time_grain(query_object: QueryObject) -> Any | None: return query_object.extras.get("time_grain_sqla") - # pylint: disable=too-many-arguments - def add_offset_join_column( + def add_aggregated_join_column( self, df: pd.DataFrame, - name: str, time_grain: str, - time_offset: str | None = None, join_column_producer: Any = None, ) -> None: - """ - Adds an offset join column to the provided DataFrame. - - The function modifies the DataFrame in-place. - - :param df: pandas DataFrame to which the offset join column will be added. - :param name: The name of the new column to be added. - :param time_grain: The time grain used to calculate the new column. - :param time_offset: The time offset used to calculate the new column. - :param join_column_producer: A function to generate the join column. - """ if join_column_producer: - df[name] = df.apply(lambda row: join_column_producer(row, 0), axis=1) + df[AGGREGATED_JOIN_COLUMN] = df.apply( + lambda row: join_column_producer(row, 0), axis=1 + ) else: - df[name] = df.apply( - lambda row: self.generate_join_column(row, 0, time_grain, time_offset), + df[AGGREGATED_JOIN_COLUMN] = df.apply( + lambda row: self.get_aggregated_join_column(row, 0, time_grain), axis=1, ) @@ -378,7 +362,7 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme query_object_clone = copy.copy(query_object) queries: list[str] = [] cache_keys: list[str | None] = [] - offset_dfs: dict[str, pd.DataFrame] = {} + offset_dfs: list[pd.DataFrame] = [] outer_from_dttm, outer_to_dttm = get_since_until_from_query_object(query_object) if not outer_from_dttm or not outer_to_dttm: @@ -389,12 +373,28 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme ) ) + columns = df.columns time_grain = self.get_time_grain(query_object) - metric_names = get_metric_names(query_object.metrics) + if not time_grain: + raise QueryObjectValidationError( + _("Time Grain must be specified when using Time Shift.") + ) + + join_column_producer = app.config["TIME_GRAIN_JOIN_COLUMN_PRODUCERS"].get( + time_grain + ) + use_aggregated_join_column = ( + join_column_producer or time_grain in AGGREGATED_JOIN_GRAINS + ) + if use_aggregated_join_column: + self.add_aggregated_join_column(df, time_grain, join_column_producer) + # skips the first column which is the temporal column + # because we'll use the aggregated join columns instead + columns = df.columns[1:] - # use columns that are not metrics as join keys - join_keys = [col for col in df.columns if col not in metric_names] + metric_names = get_metric_names(query_object.metrics) + join_keys = [col for col in columns if col not in metric_names] for offset in query_object.time_offsets: try: @@ -425,28 +425,6 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme query_object_clone.inner_to_dttm = outer_to_dttm query_object_clone.time_offsets = [] query_object_clone.post_processing = [] - # Get time offset index - index = (get_base_axis_labels(query_object.columns) or [DTTM_ALIAS])[0] - # The comparison is not using a temporal column so we need to modify - # the temporal filter so we run the query with the correct time range - if not dataframe_utils.is_datetime_series(df.get(index)): - # Lets find the first temporal filter in the filters array and change - # its val to be the result of get_since_until with the offset - for flt in query_object_clone.filter: - if flt.get( - "op" - ) == FilterOperator.TEMPORAL_RANGE.value and isinstance( - flt.get("val"), str - ): - time_range = cast(str, flt.get("val")) - ( - new_outer_from_dttm, - new_outer_to_dttm, - ) = get_since_until_from_time_range( - time_range=time_range, - time_shift=offset, - ) - flt["val"] = f"{new_outer_from_dttm} : {new_outer_to_dttm}" query_object_clone.filter = [ flt for flt in query_object_clone.filter @@ -462,7 +440,7 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme ) # whether hit on the cache if cache.is_loaded: - offset_dfs[offset] = cache.df + offset_dfs.append(cache.df) queries.append(cache.query) cache_keys.append(cache_key) continue @@ -478,7 +456,7 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme # to the subquery so we prevent data inconsistency due to missing records # in the dataframes when performing the join if query_object.row_limit or query_object.row_offset: - query_object_clone_dct["row_limit"] = config["ROW_LIMIT"] + query_object_clone_dct["row_limit"] = app.config["ROW_LIMIT"] query_object_clone_dct["row_offset"] = 0 if isinstance(self._qc_datasource, Query): @@ -506,6 +484,26 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme # 2. rename extra query columns offset_metrics_df = offset_metrics_df.rename(columns=metrics_mapping) + # 3. set time offset for index + index = (get_base_axis_labels(query_object.columns) or [DTTM_ALIAS])[0] + if not dataframe_utils.is_datetime_series(offset_metrics_df.get(index)): + raise QueryObjectValidationError( + _( + "A time column must be specified " + "when using a Time Comparison." + ) + ) + + # modifies temporal column using offset + offset_metrics_df[index] = offset_metrics_df[index] - DateOffset( + **normalize_time_delta(offset) + ) + + if use_aggregated_join_column: + self.add_aggregated_join_column( + offset_metrics_df, time_grain, join_column_producer + ) + # cache df and query value = { "df": offset_metrics_df, @@ -518,127 +516,51 @@ def processing_time_offsets( # pylint: disable=too-many-locals,too-many-stateme datasource_uid=query_context.datasource.uid, region=CacheRegion.DATA, ) - offset_dfs[offset] = offset_metrics_df + offset_dfs.append(offset_metrics_df) if offset_dfs: - df = self.join_offset_dfs( - df, - offset_dfs, - time_grain, - join_keys, - ) - - return CachedTimeOffset(df=df, queries=queries, cache_keys=cache_keys) - - def join_offset_dfs( - self, - df: pd.DataFrame, - offset_dfs: dict[str, pd.DataFrame], - time_grain: str | None, - join_keys: list[str], - ) -> pd.DataFrame: - """ - Join offset DataFrames with the main DataFrame. - - :param df: The main DataFrame. - :param offset_dfs: A list of offset DataFrames. - :param time_grain: The time grain used to calculate the temporal join key. - :param join_keys: The keys to join on. - """ - join_column_producer = config["TIME_GRAIN_JOIN_COLUMN_PRODUCERS"].get( - time_grain - ) - - if join_column_producer and not time_grain: - raise QueryObjectValidationError( - _("Time Grain must be specified when using Time Shift.") - ) - - # iterate on offset_dfs, left join each with df - for offset, offset_df in offset_dfs.items(): - actual_join_keys = join_keys - - if time_grain: - # defines a column name for the offset join column - column_name = OFFSET_JOIN_COLUMN_SUFFIX + offset - - # add offset join column to df - self.add_offset_join_column( - df, column_name, time_grain, offset, join_column_producer - ) - - # add offset join column to offset_df - self.add_offset_join_column( - offset_df, column_name, time_grain, None, join_column_producer - ) - - # the temporal column is the first column in the join keys - # so we use the join column instead of the temporal column - actual_join_keys = [column_name, *join_keys[1:]] - - if join_keys: + # iterate on offset_dfs, left join each with df + for offset_df in offset_dfs: df = dataframe_utils.left_join_df( left_df=df, right_df=offset_df, - join_keys=actual_join_keys, - rsuffix=R_SUFFIX, - ) - else: - df = dataframe_utils.full_outer_join_df( - left_df=df, - right_df=offset_df, + join_keys=join_keys, rsuffix=R_SUFFIX, ) - if time_grain: - # move the temporal column to the first column in df - col = df.pop(join_keys[0]) - df.insert(0, col.name, col) + # removes columns used for join + df.drop( + list(df.filter(regex=f"{AGGREGATED_JOIN_COLUMN}|{R_SUFFIX}")), + axis=1, + inplace=True, + ) - # removes columns created only for join purposes - df.drop( - list(df.filter(regex=f"{OFFSET_JOIN_COLUMN_SUFFIX}|{R_SUFFIX}")), - axis=1, - inplace=True, - ) - return df + return CachedTimeOffset(df=df, queries=queries, cache_keys=cache_keys) @staticmethod - def generate_join_column( - row: pd.Series, - column_index: int, - time_grain: str, - time_offset: str | None = None, + def get_aggregated_join_column( + row: pd.Series, column_index: int, time_grain: str ) -> str: - value = row[column_index] - - if hasattr(value, "strftime"): - if time_offset: - value = value + DateOffset(**normalize_time_delta(time_offset)) - - if time_grain in ( - TimeGrain.WEEK_STARTING_SUNDAY, - TimeGrain.WEEK_ENDING_SATURDAY, - ): - return value.strftime("%Y-W%U") - - if time_grain in ( - TimeGrain.WEEK, - TimeGrain.WEEK_STARTING_MONDAY, - TimeGrain.WEEK_ENDING_SUNDAY, - ): - return value.strftime("%Y-W%W") + if time_grain in ( + TimeGrain.WEEK_STARTING_SUNDAY, + TimeGrain.WEEK_ENDING_SATURDAY, + ): + return row[column_index].strftime("%Y-W%U") - if time_grain == TimeGrain.MONTH: - return value.strftime("%Y-%m") + if time_grain in ( + TimeGrain.WEEK, + TimeGrain.WEEK_STARTING_MONDAY, + TimeGrain.WEEK_ENDING_SUNDAY, + ): + return row[column_index].strftime("%Y-W%W") - if time_grain == TimeGrain.QUARTER: - return value.strftime("%Y-Q") + str(value.quarter) + if time_grain == TimeGrain.MONTH: + return row[column_index].strftime("%Y-%m") - if time_grain == TimeGrain.YEAR: - return value.strftime("%Y") + if time_grain == TimeGrain.QUARTER: + return row[column_index].strftime("%Y-Q") + str(row[column_index].quarter) - return str(value) + return row[column_index].strftime("%Y") def get_data(self, df: pd.DataFrame) -> str | list[dict[str, Any]]: if self._query_context.result_format in ChartDataResultFormat.table_like(): @@ -651,10 +573,10 @@ def get_data(self, df: pd.DataFrame) -> str | list[dict[str, Any]]: result = None if self._query_context.result_format == ChartDataResultFormat.CSV: result = csv.df_to_escaped_csv( - df, index=include_index, **config["CSV_EXPORT"] + df, index=include_index, **app.config["CSV_EXPORT"] ) elif self._query_context.result_format == ChartDataResultFormat.XLSX: - result = excel.df_to_excel(df, **config["EXCEL_EXPORT"]) + result = excel.df_to_excel(df, **app.config["EXCEL_EXPORT"]) return result or "" return df.to_dict(orient="records") @@ -702,12 +624,12 @@ def get_cache_timeout(self) -> int: if cache_timeout_rv := self._query_context.get_cache_timeout(): return cache_timeout_rv if ( - data_cache_timeout := config["DATA_CACHE_CONFIG"].get( + data_cache_timeout := app.config["DATA_CACHE_CONFIG"].get( "CACHE_DEFAULT_TIMEOUT" ) ) is not None: return data_cache_timeout - return config["CACHE_DEFAULT_TIMEOUT"] + return app.config["CACHE_DEFAULT_TIMEOUT"] def cache_key(self, **extra: Any) -> str: """ @@ -779,7 +701,7 @@ def get_viz_annotation_data( raise QueryObjectValidationError(_("The chart does not exist")) try: - if chart.viz_type in viz_types: + if chart.viz_type in get_viz_types(): if not chart.datasource: raise QueryObjectValidationError( _("The chart datasource does not exist"), diff --git a/superset/common/query_object.py b/superset/common/query_object.py index 209e6f0029d6..2dab0fa3942c 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -26,13 +26,13 @@ from flask_babel import gettext as _ from pandas import DataFrame -from superset import feature_flag_manager from superset.common.chart_data import ChartDataResultType from superset.exceptions import ( InvalidPostProcessingError, QueryClauseValidationException, QueryObjectValidationError, ) +from superset.extensions import feature_flag_manager from superset.sql_parse import sanitize_clause from superset.superset_typing import Column, Metric, OrderBy from superset.utils import json, pandas_postprocessing diff --git a/superset/common/utils/query_cache_manager.py b/superset/common/utils/query_cache_manager.py index d2e6e0743772..f9eafb360912 100644 --- a/superset/common/utils/query_cache_manager.py +++ b/superset/common/utils/query_cache_manager.py @@ -19,10 +19,10 @@ import logging from typing import Any +from flask import current_app as app from flask_caching import Cache from pandas import DataFrame -from superset import app from superset.common.db_query_status import QueryStatus from superset.constants import CacheRegion from superset.exceptions import CacheLoadError @@ -33,8 +33,6 @@ from superset.utils.cache import set_and_log_cache from superset.utils.core import error_msg_from_exception, get_stacktrace -config = app.config -stats_logger: BaseStatsLogger = config["STATS_LOGGER"] logger = logging.getLogger(__name__) _cache: dict[CacheRegion, Cache] = { @@ -107,6 +105,7 @@ def set_query_result( self.sql_rowcount = query_result.sql_rowcount self.annotation_data = {} if annotation_data is None else annotation_data + stats_logger: BaseStatsLogger = app.config["STATS_LOGGER"] if self.status != QueryStatus.FAILED: stats_logger.incr("loaded_from_source") if not force_query: @@ -154,6 +153,7 @@ def get( if cache_value := _cache[region].get(key): logger.debug("Cache key: %s", key) + stats_logger: BaseStatsLogger = app.config["STATS_LOGGER"] stats_logger.incr("loading_from_cache") try: query_cache.df = cache_value["df"] diff --git a/superset/common/utils/time_range_utils.py b/superset/common/utils/time_range_utils.py index 4969988657c0..9ef2acc12980 100644 --- a/superset/common/utils/time_range_utils.py +++ b/superset/common/utils/time_range_utils.py @@ -19,7 +19,8 @@ from datetime import datetime from typing import Any, cast -from superset import app +from flask import current_app as app + from superset.common.query_object import QueryObject from superset.utils.core import FilterOperator from superset.utils.date_parser import get_since_until diff --git a/superset/connectors/sqla/__init__.py b/superset/connectors/sqla/__init__.py index c7619537bca5..13a83393a912 100644 --- a/superset/connectors/sqla/__init__.py +++ b/superset/connectors/sqla/__init__.py @@ -14,4 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from . import models, views # noqa: F401 diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index a566079b0fca..fd13d61ac2d0 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -31,6 +31,7 @@ import numpy as np import pandas as pd import sqlalchemy as sa +from flask import current_app as app from flask_appbuilder import Model from flask_appbuilder.security.sqla.models import User from flask_babel import gettext as __, lazy_gettext as _ @@ -70,7 +71,6 @@ from sqlalchemy.sql.expression import Label, TextAsFrom from sqlalchemy.sql.selectable import Alias, TableClause -from superset import app, db, is_feature_enabled, security_manager from superset.commands.dataset.exceptions import DatasetNotFoundError from superset.common.db_query_status import QueryStatus from superset.connectors.sqla.utils import ( @@ -90,6 +90,7 @@ SupersetGenericDBErrorException, SupersetSecurityException, ) +from superset.extensions import db, feature_flag_manager, security_manager from superset.jinja_context import ( BaseTemplateProcessor, ExtraCache, @@ -120,10 +121,8 @@ from superset.utils.backports import StrEnum from superset.utils.core import GenericDataType, MediumText -config = app.config metadata = Model.metadata # pylint: disable=no-member logger = logging.getLogger(__name__) -ADVANCED_DATA_TYPES = config["ADVANCED_DATA_TYPES"] VIRTUAL_TABLE_ALIAS = "virtual_table" # a non-exhaustive set of additive metrics @@ -1129,7 +1128,7 @@ class SqlaTable( ) metric_class = SqlMetric column_class = TableColumn - owner_class = security_manager.user_model + owner_class = "User" __tablename__ = "tables" @@ -1372,7 +1371,7 @@ def select_star(self) -> str | None: @property def health_check_message(self) -> str | None: - check = config["DATASET_HEALTH_CHECK"] + check = app.config["DATASET_HEALTH_CHECK"] return check(self) if check else None @property @@ -1627,7 +1626,7 @@ def get_sqla_row_level_filters( else: all_filters.append(clause) - if is_feature_enabled("EMBEDDED_SUPERSET"): + if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): for rule in security_manager.get_guest_rls_filters(self): clause = self.text( f"({template_processor.process_template(rule['clause'])})" @@ -1890,7 +1889,7 @@ def fetch_metadata(self, commit: bool = True) -> MetadataResult: self.add_missing_metrics(metrics) # Apply config supplied mutations. - config["SQLA_TABLE_MUTATOR"](self) + app.config["SQLA_TABLE_MUTATOR"](self) db.session.merge(self) if commit: @@ -2119,7 +2118,7 @@ class RowLevelSecurityFilter(Model, AuditMixinNullable): ) group_key = Column(String(255), nullable=True) roles = relationship( - security_manager.role_model, + "Role", secondary=RLSFilterRoles, backref="row_level_security_filters", ) diff --git a/superset/connectors/sqla/utils.py b/superset/connectors/sqla/utils.py index 87b3d5dd3a28..e72d8f52d992 100644 --- a/superset/connectors/sqla/utils.py +++ b/superset/connectors/sqla/utils.py @@ -29,13 +29,13 @@ from sqlalchemy.orm.exc import ObjectDeletedError from sqlalchemy.sql.type_api import TypeEngine -from superset import db from superset.constants import LRU_CACHE_MAX_SIZE from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( SupersetGenericDBErrorException, SupersetSecurityException, ) +from superset.extensions import db from superset.models.core import Database from superset.result_set import SupersetResultSet from superset.sql_parse import ParsedQuery, Table diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index 07a87801503e..dd422a48dcae 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -32,9 +32,9 @@ from markupsafe import Markup from wtforms.validators import DataRequired, Regexp -from superset import db from superset.connectors.sqla import models from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.extensions import db from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( diff --git a/superset/daos/dashboard.py b/superset/daos/dashboard.py index 55288a11a884..94bace7eaa2b 100644 --- a/superset/daos/dashboard.py +++ b/superset/daos/dashboard.py @@ -23,7 +23,6 @@ from flask import g from flask_appbuilder.models.sqla.interface import SQLAInterface -from superset import is_feature_enabled, security_manager from superset.commands.dashboard.exceptions import ( DashboardAccessDeniedError, DashboardForbiddenError, @@ -32,7 +31,7 @@ from superset.daos.base import BaseDAO from superset.dashboards.filters import DashboardAccessFilter, is_uuid from superset.exceptions import SupersetSecurityException -from superset.extensions import db +from superset.extensions import db, feature_flag_manager, security_manager from superset.models.core import FavStar, FavStarClassName from superset.models.dashboard import Dashboard, id_or_slug_filter from superset.models.embedded_dashboard import EmbeddedDashboard @@ -282,9 +281,9 @@ def favorited_ids(dashboards: list[Dashboard]) -> list[FavStar]: def copy_dashboard( cls, original_dash: Dashboard, data: dict[str, Any] ) -> Dashboard: - if is_feature_enabled("DASHBOARD_RBAC") and not security_manager.is_owner( - original_dash - ): + if feature_flag_manager.is_feature_enabled( + "DASHBOARD_RBAC" + ) and not security_manager.is_owner(original_dash): raise DashboardForbiddenError() dash = Dashboard() diff --git a/superset/daos/datasource.py b/superset/daos/datasource.py index 0e6058d6abc8..4aec64564897 100644 --- a/superset/daos/datasource.py +++ b/superset/daos/datasource.py @@ -18,11 +18,11 @@ import logging from typing import Union -from superset import db from superset.connectors.sqla.models import SqlaTable from superset.daos.base import BaseDAO from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError from superset.datasets.models import Dataset +from superset.extensions import db from superset.models.sql_lab import Query, SavedQuery from superset.tables.models import Table from superset.utils.core import DatasourceType diff --git a/superset/daos/log.py b/superset/daos/log.py index 002c3f230725..9206944b7301 100644 --- a/superset/daos/log.py +++ b/superset/daos/log.py @@ -21,8 +21,8 @@ from sqlalchemy import and_, or_ from sqlalchemy.sql import functions as func -from superset import db from superset.daos.base import BaseDAO +from superset.extensions import db from superset.models.core import Log from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 5d3616361f37..8def5dd76c34 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -32,7 +32,7 @@ from werkzeug.wrappers import Response as WerkzeugResponse from werkzeug.wsgi import FileWrapper -from superset import is_feature_enabled, thumbnail_cache +from superset import thumbnail_cache from superset.charts.schemas import ChartEntityResponseSchema from superset.commands.dashboard.create import CreateDashboardCommand from superset.commands.dashboard.delete import DeleteDashboardCommand @@ -78,7 +78,7 @@ openapi_spec_methods_override, thumbnail_query_schema, ) -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.models.dashboard import Dashboard from superset.models.embedded_dashboard import EmbeddedDashboard from superset.tasks.thumbnails import cache_dashboard_thumbnail @@ -128,7 +128,7 @@ class DashboardRestApi(BaseSupersetModelRestApi): @before_request(only=["thumbnail"]) def ensure_thumbnails_enabled(self) -> Optional[Response]: - if not is_feature_enabled("THUMBNAILS"): + if not feature_flag_manager.is_feature_enabled("THUMBNAILS"): return self.response_404() return None diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index 0c7878d50862..bd942305db62 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -22,8 +22,8 @@ from sqlalchemy import and_, or_ from sqlalchemy.orm.query import Query -from superset import db, is_feature_enabled, security_manager from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db, feature_flag_manager, security_manager from superset.models.core import Database from superset.models.dashboard import Dashboard, is_uuid from superset.models.embedded_dashboard import EmbeddedDashboard @@ -106,7 +106,7 @@ def apply(self, query: Query, value: Any) -> Query: is_rbac_disabled_filter = [] dashboard_has_roles = Dashboard.roles.any() - if is_feature_enabled("DASHBOARD_RBAC"): + if feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC"): is_rbac_disabled_filter.append(~dashboard_has_roles) datasource_perm_query = ( @@ -133,7 +133,7 @@ def apply(self, query: Query, value: Any) -> Query: ) feature_flagged_filters = [] - if is_feature_enabled("DASHBOARD_RBAC"): + if feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC"): roles_based_query = ( db.session.query(Dashboard.id) .join(Dashboard.roles) @@ -148,9 +148,9 @@ def apply(self, query: Query, value: Any) -> Query: feature_flagged_filters.append(Dashboard.id.in_(roles_based_query)) - if is_feature_enabled("EMBEDDED_SUPERSET") and security_manager.is_guest_user( - g.user - ): + if feature_flag_manager.is_feature_enabled( + "EMBEDDED_SUPERSET" + ) and security_manager.is_guest_user(g.user): guest_user: GuestUser = g.user embedded_dashboard_ids = [ r["id"] diff --git a/superset/databases/api.py b/superset/databases/api.py index 2c0aff8da03d..8b2cb2dcb334 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -25,13 +25,19 @@ from zipfile import is_zipfile, ZipFile from deprecation import deprecated -from flask import make_response, render_template, request, Response, send_file +from flask import ( + current_app as app, + make_response, + render_template, + request, + Response, + send_file, +) from flask_appbuilder.api import expose, protect, rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import ValidationError from sqlalchemy.exc import NoSuchTableError, OperationalError, SQLAlchemyError -from superset import app, event_logger from superset.commands.database.create import CreateDatabaseCommand from superset.commands.database.delete import DeleteDatabaseCommand from superset.commands.database.exceptions import ( @@ -115,7 +121,7 @@ SupersetSecurityException, TableNotFoundException, ) -from superset.extensions import security_manager +from superset.extensions import event_logger, security_manager from superset.models.core import Database from superset.sql_parse import Table from superset.superset_typing import FlaskResponse diff --git a/superset/databases/filters.py b/superset/databases/filters.py index 420a55fd263b..f65f999d20ba 100644 --- a/superset/databases/filters.py +++ b/superset/databases/filters.py @@ -16,14 +16,14 @@ # under the License. from typing import Any -from flask import current_app, g +from flask import current_app as app, g from flask_babel import lazy_gettext as _ from sqlalchemy import or_ from sqlalchemy.orm import Query from sqlalchemy.sql.expression import cast from sqlalchemy.sql.sqltypes import JSON -from superset import app, security_manager +from superset.extensions import security_manager from superset.models.core import Database from superset.views.base import BaseFilter @@ -50,7 +50,7 @@ def apply(self, query: Query, value: Any) -> Query: filtering. """ - if dynamic_filters := current_app.config["EXTRA_DYNAMIC_QUERY_FILTERS"]: + if dynamic_filters := app.config["EXTRA_DYNAMIC_QUERY_FILTERS"]: if dynamic_databases_filter := dynamic_filters.get("databases"): query = dynamic_databases_filter(query) diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index 7dbaf0243a42..88f0b7e28dcd 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -39,7 +39,6 @@ from sqlalchemy import MetaData from werkzeug.datastructures import FileStorage -from superset import db, is_feature_enabled from superset.commands.database.exceptions import DatabaseInvalidError from superset.commands.database.ssh_tunnel.exceptions import ( SSHTunnelingNotEnabledError, @@ -50,6 +49,7 @@ from superset.databases.utils import make_url_safe from superset.db_engine_specs import get_engine_spec from superset.exceptions import CertificateException, SupersetSecurityException +from superset.extensions import db, feature_flag_manager from superset.models.core import ConfigurationMethod, Database from superset.security.analytics_db_safety import check_sqlalchemy_uri from superset.utils import json @@ -894,7 +894,7 @@ def validate_ssh_tunnel_credentials( # 2. private_key + private_key_password # Based on the data passed we determine what info is required. # You cannot mix the credentials from both methods. - if not is_feature_enabled("SSH_TUNNELING"): + if not feature_flag_manager.is_feature_enabled("SSH_TUNNELING"): # You are trying to import a Database with SSH Tunnel # But the Feature Flag is not enabled. raise SSHTunnelingNotEnabledError() diff --git a/superset/databases/ssh_tunnel/models.py b/superset/databases/ssh_tunnel/models.py index 5c1450cec090..5b6fea63dd32 100644 --- a/superset/databases/ssh_tunnel/models.py +++ b/superset/databases/ssh_tunnel/models.py @@ -18,7 +18,7 @@ from typing import Any import sqlalchemy as sa -from flask import current_app +from flask import current_app as app from flask_appbuilder import Model from sqlalchemy.orm import backref, relationship from sqlalchemy.types import Text @@ -32,7 +32,9 @@ ImportExportMixin, ) -app_config = current_app.config + +def get_key() -> str: + return app.config["SECRET_KEY"] class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model): diff --git a/superset/datasets/models.py b/superset/datasets/models.py index 76e2156e61ff..310c7b2fb7c8 100644 --- a/superset/datasets/models.py +++ b/superset/datasets/models.py @@ -28,7 +28,6 @@ from flask_appbuilder import Model from sqlalchemy.orm import backref, relationship -from superset import security_manager from superset.columns.models import Column from superset.models.core import Database from superset.models.helpers import ( @@ -92,9 +91,7 @@ class Dataset(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model): single_parent=True, backref="datasets", ) - owners = relationship( - security_manager.user_model, secondary=dataset_user_association_table - ) + owners = relationship("User", secondary=dataset_user_association_table) tables: list[Table] = relationship( "Table", secondary=dataset_table_association_table, backref="datasets" ) diff --git a/superset/datasource/api.py b/superset/datasource/api.py index 31e8c503ee0f..903ac7b3d70b 100644 --- a/superset/datasource/api.py +++ b/superset/datasource/api.py @@ -16,12 +16,13 @@ # under the License. import logging +from flask import current_app as app from flask_appbuilder.api import expose, protect, safe -from superset import app, event_logger from superset.daos.datasource import DatasourceDAO from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError from superset.exceptions import SupersetSecurityException +from superset.extensions import event_logger from superset.superset_typing import FlaskResponse from superset.utils.core import apply_max_row_limit, DatasourceType from superset.views.base_api import BaseSupersetApi, statsd_metrics diff --git a/superset/db_engine_specs/__init__.py b/superset/db_engine_specs/__init__.py index 2b840087c67c..d07d44cfe3bc 100644 --- a/superset/db_engine_specs/__init__.py +++ b/superset/db_engine_specs/__init__.py @@ -37,12 +37,13 @@ from typing import Any, Optional import sqlalchemy.dialects +from flask import current_app as app from importlib_metadata import entry_points from sqlalchemy.engine.default import DefaultDialect from sqlalchemy.exc import NoSuchModuleError -from superset import app, feature_flag_manager from superset.db_engine_specs.base import BaseEngineSpec +from superset.utils.feature_flag_manager import FeatureFlagManager logger = logging.getLogger(__name__) @@ -167,6 +168,7 @@ def get_available_engine_specs() -> dict[type[BaseEngineSpec], set[str]]: drivers[backend].add(driver) dbs_denylist = app.config["DBS_AVAILABLE_DENYLIST"] + feature_flag_manager = FeatureFlagManager() if not feature_flag_manager.is_feature_enabled("ENABLE_SUPERSET_META_DB"): dbs_denylist["superset"] = {""} dbs_denylist_engines = dbs_denylist.keys() @@ -192,3 +194,14 @@ def get_available_engine_specs() -> dict[type[BaseEngineSpec], set[str]]: available_engines[engine_spec] = driver return available_engines + + +def get_available_dialects() -> set[str]: + """ + Return available dialects in the current environment. + """ + all_dialects: set[str] = set() + for _, dialects in get_available_engine_specs().values(): + for dialect in dialects: + all_dialects.add(dialect) + return all_dialects diff --git a/superset/db_engine_specs/druid.py b/superset/db_engine_specs/druid.py index 9dc366aa5bef..cafe048c1132 100644 --- a/superset/db_engine_specs/druid.py +++ b/superset/db_engine_specs/druid.py @@ -23,11 +23,11 @@ from sqlalchemy import types -from superset import is_feature_enabled from superset.constants import TimeGrain from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.exceptions import SupersetDBAPIConnectionError from superset.exceptions import SupersetException +from superset.extensions import feature_flag_manager from superset.utils import core as utils, json if TYPE_CHECKING: @@ -42,7 +42,7 @@ class DruidEngineSpec(BaseEngineSpec): engine = "druid" engine_name = "Apache Druid" - allows_joins = is_feature_enabled("DRUID_JOINS") + allows_joins = feature_flag_manager.is_feature_enabled("DRUID_JOINS") allows_subqueries = True _time_grain_expressions = { diff --git a/superset/db_engine_specs/gsheets.py b/superset/db_engine_specs/gsheets.py index e876aca8defd..8ded305ebb0c 100644 --- a/superset/db_engine_specs/gsheets.py +++ b/superset/db_engine_specs/gsheets.py @@ -36,12 +36,12 @@ from sqlalchemy.engine import create_engine from sqlalchemy.engine.url import URL -from superset import db, security_manager from superset.constants import PASSWORD_MASK from superset.databases.schemas import encrypted_field_properties, EncryptedString from superset.db_engine_specs.shillelagh import ShillelaghEngineSpec from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetException +from superset.extensions import db, security_manager from superset.utils import json if TYPE_CHECKING: diff --git a/superset/db_engine_specs/hive.py b/superset/db_engine_specs/hive.py index 80892b59877d..8b543802484c 100644 --- a/superset/db_engine_specs/hive.py +++ b/superset/db_engine_specs/hive.py @@ -36,14 +36,13 @@ from sqlalchemy.engine.url import URL from sqlalchemy.sql.expression import ColumnClause, Select -from superset import db from superset.common.db_query_status import QueryStatus from superset.constants import TimeGrain from superset.databases.utils import make_url_safe from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.presto import PrestoEngineSpec from superset.exceptions import SupersetException -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.models.sql_lab import Query from superset.sql_parse import ParsedQuery, Table from superset.superset_typing import ResultSetColumnType diff --git a/superset/db_engine_specs/impala.py b/superset/db_engine_specs/impala.py index d7d1862aafd9..008122f213d3 100644 --- a/superset/db_engine_specs/impala.py +++ b/superset/db_engine_specs/impala.py @@ -27,9 +27,9 @@ from sqlalchemy import types from sqlalchemy.engine.reflection import Inspector -from superset import db from superset.constants import QUERY_EARLY_CANCEL_KEY, TimeGrain from superset.db_engine_specs.base import BaseEngineSpec +from superset.extensions import db from superset.models.sql_lab import Query if TYPE_CHECKING: diff --git a/superset/db_engine_specs/ocient.py b/superset/db_engine_specs/ocient.py index 02d19add8242..8b83fd4f4486 100644 --- a/superset/db_engine_specs/ocient.py +++ b/superset/db_engine_specs/ocient.py @@ -21,6 +21,7 @@ from re import Pattern from typing import Any, Callable, NamedTuple, Optional +from flask import current_app as app from flask_babel import gettext as __ from sqlalchemy.engine.reflection import Inspector @@ -30,8 +31,6 @@ import pyocient from shapely import wkt - from superset import app - superset_log_level = app.config["LOG_LEVEL"] pyocient.logger.setLevel(superset_log_level) diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index 5e0b433e1e11..239c05ea17cf 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -40,13 +40,13 @@ from sqlalchemy.engine.url import URL from sqlalchemy.sql.expression import ColumnClause, Select -from superset import cache_manager, db, is_feature_enabled from superset.common.db_query_status import QueryStatus from superset.constants import TimeGrain from superset.databases.utils import make_url_safe from superset.db_engine_specs.base import BaseEngineSpec from superset.errors import SupersetErrorType from superset.exceptions import SupersetTemplateException +from superset.extensions import cache_manager, db, feature_flag_manager from superset.models.sql_lab import Query from superset.models.sql_types.presto_sql_types import ( Array, @@ -1017,7 +1017,7 @@ def get_columns( result: list[ResultSetColumnType] = [] for column in columns: # parse column if it is a row or array - if is_feature_enabled("PRESTO_EXPAND_DATA") and ( + if feature_flag_manager.is_feature_enabled("PRESTO_EXPAND_DATA") and ( "array" in column.Type or "row" in column.Type ): structural_column_index = len(result) @@ -1106,7 +1106,7 @@ def select_star( # pylint: disable=too-many-arguments """ cols = cols or [] presto_cols = cols - if is_feature_enabled("PRESTO_EXPAND_DATA") and show_cols: + if feature_flag_manager.is_feature_enabled("PRESTO_EXPAND_DATA") and show_cols: dot_regex = r"\.(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)" presto_cols = [ col @@ -1151,7 +1151,7 @@ def expand_data( # pylint: disable=too-many-locals :return: list of all columns(selected columns and their nested fields), expanded data set, listed of nested fields """ - if not is_feature_enabled("PRESTO_EXPAND_DATA"): + if not feature_flag_manager.is_feature_enabled("PRESTO_EXPAND_DATA"): return columns, data, [] # process each column, unnesting ARRAY types and diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py index eea00877d9fe..a472b91f179c 100644 --- a/superset/db_engine_specs/trino.py +++ b/superset/db_engine_specs/trino.py @@ -27,7 +27,6 @@ from sqlalchemy.engine.url import URL from sqlalchemy.exc import NoSuchTableError -from superset import db from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY, USER_AGENT from superset.databases.utils import make_url_safe from superset.db_engine_specs.base import BaseEngineSpec @@ -38,6 +37,7 @@ SupersetDBAPIProgrammingError, ) from superset.db_engine_specs.presto import PrestoBaseEngineSpec +from superset.extensions import db from superset.models.sql_lab import Query from superset.sql_parse import Table from superset.superset_typing import ResultSetColumnType diff --git a/superset/embedded/api.py b/superset/embedded/api.py index b907422bf516..2894b3f9da4a 100644 --- a/superset/embedded/api.py +++ b/superset/embedded/api.py @@ -22,14 +22,13 @@ from flask_appbuilder.hooks import before_request from flask_appbuilder.models.sqla.interface import SQLAInterface -from superset import is_feature_enabled from superset.commands.dashboard.embedded.exceptions import ( EmbeddedDashboardNotFoundError, ) from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.daos.dashboard import EmbeddedDashboardDAO from superset.dashboards.schemas import EmbeddedDashboardResponseSchema -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.models.embedded_dashboard import EmbeddedDashboard from superset.reports.logs.schemas import openapi_spec_methods_override from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -42,7 +41,7 @@ class EmbeddedDashboardRestApi(BaseSupersetModelRestApi): @before_request def ensure_embedded_enabled(self) -> Optional[Response]: - if not is_feature_enabled("EMBEDDED_SUPERSET"): + if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): return self.response_404() return None diff --git a/superset/embedded/view.py b/superset/embedded/view.py index 05490726f311..162115ebd7f4 100644 --- a/superset/embedded/view.py +++ b/superset/embedded/view.py @@ -21,8 +21,9 @@ from flask_login import AnonymousUserMixin, login_user from flask_wtf.csrf import same_origin -from superset import event_logger, is_feature_enabled +from superset import event_logger from superset.daos.dashboard import EmbeddedDashboardDAO +from superset.extensions import feature_flag_manager from superset.superset_typing import FlaskResponse from superset.utils import json from superset.views.base import BaseSupersetView, common_bootstrap_payload @@ -46,7 +47,7 @@ def embedded( :param add_extra_log_payload: added by `log_this_with_manual_updates`, set a default value to appease pylint """ - if not is_feature_enabled("EMBEDDED_SUPERSET"): + if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): abort(404) embedded = EmbeddedDashboardDAO.find_by_id(uuid) diff --git a/superset/examples/bart_lines.py b/superset/examples/bart_lines.py index 8b3b31522652..3eb3ddd34873 100644 --- a/superset/examples/bart_lines.py +++ b/superset/examples/bart_lines.py @@ -19,7 +19,7 @@ import polyline from sqlalchemy import inspect, String, Text -from superset import db +from superset.extensions import db from superset.sql_parse import Table from superset.utils import json diff --git a/superset/examples/birth_names.py b/superset/examples/birth_names.py index 229734057ceb..cfaa8340e114 100644 --- a/superset/examples/birth_names.py +++ b/superset/examples/birth_names.py @@ -18,11 +18,12 @@ from typing import Union import pandas as pd +from flask import current_app as app from sqlalchemy import DateTime, inspect, String from sqlalchemy.sql import column -from superset import app, db from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/superset/examples/country_map.py b/superset/examples/country_map.py index 1741219470ac..b754c75d6a68 100644 --- a/superset/examples/country_map.py +++ b/superset/examples/country_map.py @@ -21,8 +21,8 @@ from sqlalchemy.sql import column import superset.utils.database as database_utils -from superset import db from superset.connectors.sqla.models import SqlMetric +from superset.extensions import db from superset.models.slice import Slice from superset.sql_parse import Table from superset.utils.core import DatasourceType diff --git a/superset/examples/css_templates.py b/superset/examples/css_templates.py index 2f67d2e1faac..4c5028ee804a 100644 --- a/superset/examples/css_templates.py +++ b/superset/examples/css_templates.py @@ -16,7 +16,7 @@ # under the License. import textwrap -from superset import db +from superset.extensions import db from superset.models.core import CssTemplate diff --git a/superset/examples/deck.py b/superset/examples/deck.py index b0cb65b03fc2..4525c846c077 100644 --- a/superset/examples/deck.py +++ b/superset/examples/deck.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils import json diff --git a/superset/examples/energy.py b/superset/examples/energy.py index 98b444f9db2f..286e3a1bd5a6 100644 --- a/superset/examples/energy.py +++ b/superset/examples/energy.py @@ -23,8 +23,8 @@ from sqlalchemy.sql import column import superset.utils.database as database_utils -from superset import db from superset.connectors.sqla.models import SqlMetric +from superset.extensions import db from superset.models.slice import Slice from superset.sql_parse import Table from superset.utils.core import DatasourceType diff --git a/superset/examples/flights.py b/superset/examples/flights.py index 4db029519fd8..9f58cd6f6852 100644 --- a/superset/examples/flights.py +++ b/superset/examples/flights.py @@ -18,7 +18,7 @@ from sqlalchemy import DateTime, inspect import superset.utils.database as database_utils -from superset import db +from superset.extensions import db from superset.sql_parse import Table from .helpers import get_example_url, get_table_connector_registry diff --git a/superset/examples/helpers.py b/superset/examples/helpers.py index b865e2dfca93..0a3ae1577279 100644 --- a/superset/examples/helpers.py +++ b/superset/examples/helpers.py @@ -19,8 +19,10 @@ import os from typing import Any -from superset import app, db +from flask import current_app as app + from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.slice import Slice from superset.utils import json diff --git a/superset/examples/long_lat.py b/superset/examples/long_lat.py index 4f8de31453c1..14a04da9a1f5 100644 --- a/superset/examples/long_lat.py +++ b/superset/examples/long_lat.py @@ -22,7 +22,7 @@ from sqlalchemy import DateTime, Float, inspect, String import superset.utils.database as database_utils -from superset import db +from superset.extensions import db from superset.models.slice import Slice from superset.sql_parse import Table from superset.utils.core import DatasourceType diff --git a/superset/examples/misc_dashboard.py b/superset/examples/misc_dashboard.py index 825dc6352c8e..73b15c7074df 100644 --- a/superset/examples/misc_dashboard.py +++ b/superset/examples/misc_dashboard.py @@ -16,7 +16,7 @@ # under the License. import textwrap -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.utils import json diff --git a/superset/examples/multiformat_time_series.py b/superset/examples/multiformat_time_series.py index 979be10686f5..f825ac7b5b20 100644 --- a/superset/examples/multiformat_time_series.py +++ b/superset/examples/multiformat_time_series.py @@ -17,9 +17,10 @@ from typing import Optional import pandas as pd +from flask import current_app as app from sqlalchemy import BigInteger, Date, DateTime, inspect, String -from superset import app, db +from superset.extensions import db from superset.models.slice import Slice from superset.sql_parse import Table from superset.utils.core import DatasourceType diff --git a/superset/examples/paris.py b/superset/examples/paris.py index 990aa01ca6c3..362e6c98c16a 100644 --- a/superset/examples/paris.py +++ b/superset/examples/paris.py @@ -19,7 +19,7 @@ from sqlalchemy import inspect, String, Text import superset.utils.database as database_utils -from superset import db +from superset.extensions import db from superset.sql_parse import Table from superset.utils import json diff --git a/superset/examples/random_time_series.py b/superset/examples/random_time_series.py index ec232995fa2e..d4e136e34e45 100644 --- a/superset/examples/random_time_series.py +++ b/superset/examples/random_time_series.py @@ -16,10 +16,11 @@ # under the License. import pandas as pd +from flask import current_app as app from sqlalchemy import DateTime, inspect, String import superset.utils.database as database_utils -from superset import app, db +from superset.extensions import db from superset.models.slice import Slice from superset.sql_parse import Table from superset.utils.core import DatasourceType diff --git a/superset/examples/sf_population_polygons.py b/superset/examples/sf_population_polygons.py index 4fa59db721a6..d359ebd6c278 100644 --- a/superset/examples/sf_population_polygons.py +++ b/superset/examples/sf_population_polygons.py @@ -19,7 +19,7 @@ from sqlalchemy import BigInteger, Float, inspect, Text import superset.utils.database as database_utils -from superset import db +from superset.extensions import db from superset.sql_parse import Table from superset.utils import json diff --git a/superset/examples/supported_charts_dashboard.py b/superset/examples/supported_charts_dashboard.py index 49141eb73cf6..961e66b86622 100644 --- a/superset/examples/supported_charts_dashboard.py +++ b/superset/examples/supported_charts_dashboard.py @@ -21,8 +21,8 @@ from sqlalchemy import inspect -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.sql_parse import Table diff --git a/superset/examples/tabbed_dashboard.py b/superset/examples/tabbed_dashboard.py index bbc11e77306a..ecae492c42f6 100644 --- a/superset/examples/tabbed_dashboard.py +++ b/superset/examples/tabbed_dashboard.py @@ -18,7 +18,7 @@ import textwrap -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.utils import json diff --git a/superset/examples/world_bank.py b/superset/examples/world_bank.py index afbb6a994a83..a807a6374882 100644 --- a/superset/examples/world_bank.py +++ b/superset/examples/world_bank.py @@ -19,11 +19,11 @@ import os import pandas as pd +from flask import current_app as app from sqlalchemy import DateTime, inspect, String from sqlalchemy.sql import column import superset.utils.database -from superset import app, db from superset.connectors.sqla.models import BaseDatasource, SqlMetric from superset.examples.helpers import ( get_example_url, @@ -34,6 +34,7 @@ misc_dash_slices, update_slice_ids, ) +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.sql_parse import Table diff --git a/superset/extensions/__init__.py b/superset/extensions/__init__.py index 65ba7eebc8e0..9fb9040e37d9 100644 --- a/superset/extensions/__init__.py +++ b/superset/extensions/__init__.py @@ -16,7 +16,7 @@ # under the License. import json import os -from typing import Any, Callable, Optional +from typing import Callable, Optional import celery from flask import Flask @@ -29,6 +29,7 @@ from superset.async_events.async_query_manager import AsyncQueryManager from superset.async_events.async_query_manager_factory import AsyncQueryManagerFactory +from superset.extensions.event_logger_manager import EventLoggerManager from superset.extensions.ssh import SSHManagerFactory from superset.extensions.stats_logger import BaseStatsLoggerManager from superset.utils.cache_manager import CacheManager @@ -123,9 +124,11 @@ def init_app(self, app: Flask) -> None: celery_app = celery.Celery() csrf = CSRFProtect() db = SQLA() # pylint: disable=disallowed-name -_event_logger: dict[str, Any] = {} encrypted_field_factory = EncryptedFieldFactory() -event_logger = LocalProxy(lambda: _event_logger.get("event_logger")) + +event_logger_manager = EventLoggerManager() +event_logger = event_logger_manager.get_event_logger() + feature_flag_manager = FeatureFlagManager() machine_auth_provider_factory = MachineAuthProviderFactory() manifest_processor = UIManifestProcessor(APP_DIR) diff --git a/superset/extensions/event_logger_manager.py b/superset/extensions/event_logger_manager.py new file mode 100644 index 000000000000..05012fa7aa5e --- /dev/null +++ b/superset/extensions/event_logger_manager.py @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from superset.utils.log import AbstractEventLogger, StdOutEventLogger + + +class EventLoggerManager: + _instance = None + + def __new__(cls) -> "EventLoggerManager": + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._event_logger = ( + StdOutEventLogger() + ) # Initialize with default logger + return cls._instance + + def get_event_logger(self) -> AbstractEventLogger: + return self._event_logger + + def set_event_logger(self, logger: AbstractEventLogger) -> None: + self._event_logger = logger # pylint: disable=attribute-defined-outside-init + + +__all__ = ["EventLoggerManager"] diff --git a/superset/extensions/metadb.py b/superset/extensions/metadb.py index fd697aea820f..a7b8f5194d49 100644 --- a/superset/extensions/metadb.py +++ b/superset/extensions/metadb.py @@ -67,7 +67,8 @@ from sqlalchemy.exc import NoSuchTableError from sqlalchemy.sql import Select, select -from superset import db, feature_flag_manager, security_manager, sql_parse +from superset import sql_parse +from superset.extensions import db, feature_flag_manager, security_manager # pylint: disable=abstract-method diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 65e518b7c9b1..e1aa7fd42715 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -20,20 +20,34 @@ import logging import os import sys +from importlib.resources import files from typing import Any, Callable, TYPE_CHECKING +import simplejson as json +import sqlalchemy as sqla import wtforms_json from deprecation import deprecated -from flask import Flask, redirect +from flask import ( + Flask, + redirect, + render_template, + request, + Response, + send_file, +) from flask_appbuilder import expose, IndexView from flask_babel import gettext as __ from flask_compress import Compress from flask_session import Session +from flask_wtf.csrf import CSRFError +from werkzeug.exceptions import HTTPException from werkzeug.middleware.proxy_fix import ProxyFix +from superset.commands.exceptions import CommandException, CommandInvalidError from superset.constants import CHANGE_ME_SECRET_KEY +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.exceptions import SupersetErrorException, SupersetErrorsException from superset.extensions import ( - _event_logger, APP_DIR, appbuilder, async_query_manager_factory, @@ -42,19 +56,24 @@ csrf, db, encrypted_field_factory, + event_logger_manager, feature_flag_manager, machine_auth_provider_factory, manifest_processor, migrate, profiling, results_backend_manager, + security_manager, ssh_manager_factory, stats_logger_manager, talisman, ) from superset.security import SupersetSecurityManager from superset.superset_typing import FlaskResponse -from superset.tags.core import register_sqla_event_listeners +from superset.tags.core import ( + register_sqla_event_listeners as register_tag_event_listeners, +) +from superset.utils import core as utils, json as json_utils from superset.utils.core import is_test, pessimistic_connection_handling from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value @@ -64,18 +83,30 @@ logger = logging.getLogger(__name__) +def get_error_level_from_status_code( # pylint: disable=invalid-name + status: int, +) -> ErrorLevel: + if status < 400: + return ErrorLevel.INFO + if status < 500: + return ErrorLevel.WARNING + return ErrorLevel.ERROR + + class SupersetAppInitializer: # pylint: disable=too-many-public-methods def __init__(self, app: SupersetApp) -> None: super().__init__() + self.app = app self.superset_app = app self.config = app.config self.manifest: dict[Any, Any] = {} + self.app.stats_logger = app.config["STATS_LOGGER"] - @deprecated(details="use self.superset_app instead of self.flask_app") # type: ignore + @deprecated(details="use self.app instead of self.flask_app") # type: ignore @property def flask_app(self) -> SupersetApp: - return self.superset_app + return self.app def pre_init(self) -> None: """ @@ -94,7 +125,7 @@ def post_init(self) -> None: def configure_celery(self) -> None: celery_app.config_from_object(self.config["CELERY_CONFIG"]) celery_app.set_default() - superset_app = self.superset_app + app = self.app # Here, we want to ensure that every call into Celery task has an app context # setup properly @@ -106,7 +137,7 @@ class AppContextTask(task_base): # type: ignore # Grab each call into the task and set up an app context def __call__(self, *args: Any, **kwargs: Any) -> Any: - with superset_app.app_context(): + with app.app_context(): return task_base.__call__(self, *args, **kwargs) celery_app.Task = AppContextTask @@ -415,12 +446,174 @@ def init_app_in_ctx(self) -> None: # Hook that provides administrators a handle on the Flask APP # after initialization if flask_app_mutator := self.config["FLASK_APP_MUTATOR"]: - flask_app_mutator(self.superset_app) + flask_app_mutator(self.app) + + self.init_views() + self.register_sqla_event_listeners() + self.register_error_handlers() + self.register_other_views() + + def register_other_views(self) -> None: + @talisman(force_https=False) + @self.app.route("/health") + @self.app.route("/healthcheck") + @self.app.route("/ping") + def health() -> FlaskResponse: + self.app.stats_logger.incr("health") + return "OK" + + def register_sqla_event_listeners(self) -> None: + # pylint: disable=import-outside-toplevel + from superset.models.core import Database + + # TODO move all sqla.event.listen to this method if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): - register_sqla_event_listeners() + register_tag_event_listeners() - self.init_views() + # Using lambdas for security manager to prevent referencing it in module scope + sqla.event.listen( + Database, + "after_insert", + security_manager.database_after_insert, + ) + sqla.event.listen( + Database, + "after_update", + security_manager.database_after_update, + ) + sqla.event.listen( + Database, + "after_delete", + security_manager.database_after_delete, + ) + + def register_error_handlers(self) -> None: + # pylint: disable=import-outside-toplevel + from superset.views.utils import get_error_msg, json_errors_response + + # SIP-40 compatible error responses; make sure APIs raise + # SupersetErrorException or SupersetErrorsException + @self.app.errorhandler(SupersetErrorException) + def show_superset_error(ex: SupersetErrorException) -> FlaskResponse: + logger.warning("SupersetErrorException", exc_info=True) + return json_errors_response(errors=[ex.error], status=ex.status) + + @self.app.errorhandler(SupersetErrorsException) + def show_superset_errors(ex: SupersetErrorsException) -> FlaskResponse: + logger.warning("SupersetErrorsException", exc_info=True) + return json_errors_response(errors=ex.errors, status=ex.status) + + # Redirect to login if the CSRF token is expired + @self.app.errorhandler(CSRFError) + def refresh_csrf_token(ex: CSRFError) -> FlaskResponse: + logger.warning("Refresh CSRF token error", exc_info=True) + + if request.is_json: + return show_http_exception(ex) + + return redirect(appbuilder.get_url_for_login) + + @self.app.errorhandler(HTTPException) + def show_http_exception(ex: HTTPException) -> FlaskResponse: + logger.warning("HTTPException", exc_info=True) + if ( + "text/html" in request.accept_mimetypes + and not self.app.config["DEBUG"] + and ex.code in {404, 500} + ): + path = files("superset") / f"static/assets/{ex.code}.html" + return send_file(path, max_age=0), ex.code + + return json_errors_response( + errors=[ + SupersetError( + message=utils.error_msg_from_exception(ex), + error_type=SupersetErrorType.GENERIC_BACKEND_ERROR, + level=ErrorLevel.ERROR, + ), + ], + status=ex.code or 500, + ) + + @self.app.errorhandler(500) + def show_traceback(self) -> FlaskResponse: # type: ignore # pylint: disable=unused-argument + return ( + render_template("superset/traceback.html", error_msg=get_error_msg()), + 500, + ) + + # Temporary handler for CommandException; if an API raises a + # CommandException it should be fixed to map it to SupersetErrorException + # or SupersetErrorsException, with a specific status code and error type + @self.app.errorhandler(CommandException) + def show_command_errors(ex: CommandException) -> FlaskResponse: + logger.warning("CommandException", exc_info=True) + if "text/html" in request.accept_mimetypes and not self.app.config["DEBUG"]: + path = files("superset") / "static/assets/500.html" + return send_file(path, max_age=0), 500 + + extra = ( + ex.normalized_messages() if isinstance(ex, CommandInvalidError) else {} + ) + return json_errors_response( + errors=[ + SupersetError( + message=ex.message, + error_type=SupersetErrorType.GENERIC_COMMAND_ERROR, + level=get_error_level_from_status_code(ex.status), + extra=extra, + ), + ], + status=ex.status, + ) + + # Catch-all, to ensure all errors from the backend conform to SIP-40 + @self.app.errorhandler(Exception) + def show_unexpected_exception(ex: Exception) -> FlaskResponse: + logger.exception(ex) + if "text/html" in request.accept_mimetypes and not self.app.config["DEBUG"]: + path = files("superset") / "static/assets/500.html" + return send_file(path, max_age=0), 500 + + return json_errors_response( + errors=[ + SupersetError( + message=utils.error_msg_from_exception(ex), + error_type=SupersetErrorType.GENERIC_BACKEND_ERROR, + level=ErrorLevel.ERROR, + ), + ], + ) + + @self.app.context_processor + def get_common_bootstrap_data() -> dict[str, Any]: + from superset.initialization.bootstrap import common_bootstrap_payload + + def serialize_bootstrap_data() -> str: + return json.dumps( + {"common": common_bootstrap_payload()}, + default=json_utils.pessimistic_json_iso_dttm_ser, + ) + + return {"bootstrap_data": serialize_bootstrap_data} + + @self.app.after_request + def apply_http_headers(response: Response) -> Response: + """Applies the configuration's http headers to all responses""" + + # HTTP_HEADERS is deprecated, this provides backwards compatibility + response.headers.extend( + { + **self.app.config["OVERRIDE_HTTP_HEADERS"], + **self.app.config["HTTP_HEADERS"], + } + ) + + for k, v in self.app.config["DEFAULT_HTTP_HEADERS"].items(): + if k not in response.headers: + response.headers[k] = v + return response def check_secret_key(self) -> None: def log_default_secret_key_warning() -> None: @@ -437,11 +630,7 @@ def log_default_secret_key_warning() -> None: logger.warning(bottom_banner) if self.config["SECRET_KEY"] == CHANGE_ME_SECRET_KEY: - if ( - self.superset_app.debug - or self.superset_app.config["TESTING"] - or is_test() - ): + if self.app.debug or self.app.config["TESTING"] or is_test(): logger.warning("Debug mode identified with default secret key") log_default_secret_key_warning() return @@ -451,7 +640,7 @@ def log_default_secret_key_warning() -> None: def configure_session(self) -> None: if self.config["SESSION_SERVER_SIDE"]: - Session(self.superset_app) + Session(self.app) def init_app(self) -> None: """ @@ -477,24 +666,25 @@ def init_app(self) -> None: self.configure_middlewares() self.configure_cache() - with self.superset_app.app_context(): + with self.app.app_context(): self.init_app_in_ctx() self.post_init() def configure_auth_provider(self) -> None: - machine_auth_provider_factory.init_app(self.superset_app) + machine_auth_provider_factory.init_app(self.app) def configure_ssh_manager(self) -> None: - ssh_manager_factory.init_app(self.superset_app) + ssh_manager_factory.init_app(self.app) def configure_stats_manager(self) -> None: - stats_logger_manager.init_app(self.superset_app) + stats_logger_manager.init_app(self.app) def setup_event_logger(self) -> None: - _event_logger["event_logger"] = get_event_logger_from_cfg_value( - self.superset_app.config.get("EVENT_LOGGER", DBEventLogger()) + event_logger = get_event_logger_from_cfg_value( + self.app.config.get("EVENT_LOGGER") or DBEventLogger() ) + event_logger_manager.set_event_logger(event_logger) def configure_data_sources(self) -> None: # Registering sources @@ -507,13 +697,22 @@ def configure_data_sources(self) -> None: __import__(module_name, fromlist=class_names) def configure_cache(self) -> None: - cache_manager.init_app(self.superset_app) - results_backend_manager.init_app(self.superset_app) + cache_manager.init_app(self.app) + results_backend_manager.init_app(self.app) def configure_feature_flags(self) -> None: - feature_flag_manager.init_app(self.superset_app) + feature_flag_manager.init_app(self.app) def configure_fab(self) -> None: + """ + NOTE: somehow appbuilder.init_app will run a `db.create_all` + which creates the foundation db models FAB needs. This needs to happen + before other [superset] models are initialized. For this + reason we tend to do late imports in this module. A simple + `from superset.models import core` in this context would lead to FAB + creating models, and conflict with db migrations happening later in + the installation flows + """ if self.config["SILENCE_FAB"]: logging.getLogger("flask_appbuilder").setLevel(logging.ERROR) @@ -528,7 +727,7 @@ def configure_fab(self) -> None: appbuilder.indexview = SupersetIndexView appbuilder.base_template = "superset/base.html" appbuilder.security_manager_class = custom_sm - appbuilder.init_app(self.superset_app, db.session) + appbuilder.init_app(self.app, db.session) def configure_url_map_converters(self) -> None: # @@ -541,19 +740,19 @@ def configure_url_map_converters(self) -> None: RegexConverter, ) - self.superset_app.url_map.converters["regex"] = RegexConverter - self.superset_app.url_map.converters["object_type"] = ObjectTypeConverter + self.app.url_map.converters["regex"] = RegexConverter + self.app.url_map.converters["object_type"] = ObjectTypeConverter def configure_middlewares(self) -> None: if self.config["ENABLE_CORS"]: # pylint: disable=import-outside-toplevel from flask_cors import CORS - CORS(self.superset_app, **self.config["CORS_OPTIONS"]) + CORS(self.app, **self.config["CORS_OPTIONS"]) if self.config["ENABLE_PROXY_FIX"]: - self.superset_app.wsgi_app = ProxyFix( - self.superset_app.wsgi_app, **self.config["PROXY_FIX_CONFIG"] + self.app.wsgi_app = ProxyFix( + self.app.wsgi_app, **self.config["PROXY_FIX_CONFIG"] ) if self.config["ENABLE_CHUNK_ENCODING"]: @@ -571,33 +770,33 @@ def __call__( environ["wsgi.input_terminated"] = True return self.app(environ, start_response) - self.superset_app.wsgi_app = ChunkedEncodingFix(self.superset_app.wsgi_app) + self.app.wsgi_app = ChunkedEncodingFix(self.app.wsgi_app) if self.config["UPLOAD_FOLDER"]: with contextlib.suppress(OSError): os.makedirs(self.config["UPLOAD_FOLDER"]) for middleware in self.config["ADDITIONAL_MIDDLEWARE"]: - self.superset_app.wsgi_app = middleware(self.superset_app.wsgi_app) + self.app.wsgi_app = middleware(self.app.wsgi_app) # Flask-Compress - Compress(self.superset_app) + Compress(self.app) # Talisman talisman_enabled = self.config["TALISMAN_ENABLED"] talisman_config = ( self.config["TALISMAN_DEV_CONFIG"] - if self.superset_app.debug + if self.app.debug else self.config["TALISMAN_CONFIG"] ) csp_warning = self.config["CONTENT_SECURITY_POLICY_WARNING"] if talisman_enabled: - talisman.init_app(self.superset_app, **talisman_config) + talisman.init_app(self.app, **talisman_config) show_csp_warning = False if ( csp_warning - and not self.superset_app.debug + and not self.app.debug and ( not talisman_enabled or not talisman_config @@ -619,45 +818,45 @@ def __call__( def configure_logging(self) -> None: self.config["LOGGING_CONFIGURATOR"].configure_logging( - self.config, self.superset_app.debug + self.config, self.app.debug ) def configure_db_encrypt(self) -> None: - encrypted_field_factory.init_app(self.superset_app) + encrypted_field_factory.init_app(self.app) def setup_db(self) -> None: - db.init_app(self.superset_app) + db.init_app(self.app) - with self.superset_app.app_context(): + with self.app.app_context(): pessimistic_connection_handling(db.engine) - migrate.init_app(self.superset_app, db=db, directory=APP_DIR + "/migrations") + migrate.init_app(self.app, db=db, directory=APP_DIR + "/migrations") def configure_wtf(self) -> None: if self.config["WTF_CSRF_ENABLED"]: - csrf.init_app(self.superset_app) + csrf.init_app(self.app) csrf_exempt_list = self.config["WTF_CSRF_EXEMPT_LIST"] for ex in csrf_exempt_list: csrf.exempt(ex) def configure_async_queries(self) -> None: if feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES"): - async_query_manager_factory.init_app(self.superset_app) + async_query_manager_factory.init_app(self.app) def register_blueprints(self) -> None: for bp in self.config["BLUEPRINTS"]: try: logger.info("Registering blueprint: %s", bp.name) - self.superset_app.register_blueprint(bp) + self.app.register_blueprint(bp) except Exception: # pylint: disable=broad-except logger.exception("blueprint registration failed") def setup_bundle_manifest(self) -> None: - manifest_processor.init_app(self.superset_app) + manifest_processor.init_app(self.app) def enable_profiling(self) -> None: if self.config["PROFILING"]: - profiling.init_app(self.superset_app) + profiling.init_app(self.app) class SupersetIndexView(IndexView): diff --git a/superset/initialization/bootstrap.py b/superset/initialization/bootstrap.py new file mode 100644 index 000000000000..94b0b9b291cd --- /dev/null +++ b/superset/initialization/bootstrap.py @@ -0,0 +1,198 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import os +from typing import Any, Union + +from babel import Locale +from flask import current_app as app, g, get_flashed_messages, session +from flask_appbuilder.security.sqla.models import User +from flask_babel import get_locale + +from superset.db_engine_specs import get_available_dialects +from superset.extensions import appbuilder, cache_manager, feature_flag_manager +from superset.reports.models import ReportRecipientType +from superset.translations.utils import get_language_pack +from superset.utils import core as utils + +FRONTEND_CONF_KEYS = ( + "SUPERSET_WEBSERVER_TIMEOUT", + "SUPERSET_DASHBOARD_POSITION_DATA_LIMIT", + "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT", + "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE", + "ENABLE_JAVASCRIPT_CONTROLS", + "DEFAULT_SQLLAB_LIMIT", + "DEFAULT_VIZ_TYPE", + "SQL_MAX_ROW", + "SUPERSET_WEBSERVER_DOMAINS", + "SQLLAB_SAVE_WARNING_MESSAGE", + "SQLLAB_DEFAULT_DBID", + "DISPLAY_MAX_ROW", + "GLOBAL_ASYNC_QUERIES_TRANSPORT", + "GLOBAL_ASYNC_QUERIES_POLLING_DELAY", + "SQL_VALIDATORS_BY_ENGINE", + "SQLALCHEMY_DOCS_URL", + "SQLALCHEMY_DISPLAY_TEXT", + "GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL", + "DASHBOARD_AUTO_REFRESH_MODE", + "DASHBOARD_AUTO_REFRESH_INTERVALS", + "DASHBOARD_VIRTUALIZATION", + "SCHEDULED_QUERIES", + "EXCEL_EXTENSIONS", + "CSV_EXTENSIONS", + "COLUMNAR_EXTENSIONS", + "ALLOWED_EXTENSIONS", + "SAMPLES_ROW_LIMIT", + "DEFAULT_TIME_FILTER", + "HTML_SANITIZATION", + "HTML_SANITIZATION_SCHEMA_EXTENSIONS", + "WELCOME_PAGE_LAST_TAB", + "VIZ_TYPE_DENYLIST", + "ALERT_REPORTS_DEFAULT_CRON_VALUE", + "ALERT_REPORTS_DEFAULT_RETENTION", + "ALERT_REPORTS_DEFAULT_WORKING_TIMEOUT", + "NATIVE_FILTER_DEFAULT_ROW_LIMIT", + "PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET", + "JWT_ACCESS_CSRF_COOKIE_NAME", +) + + +def get_environment_tag() -> dict[str, Any]: + # Whether flask is in debug mode (--debug) + debug = app.config["DEBUG"] + + # Getting the configuration option for ENVIRONMENT_TAG_CONFIG + env_tag_config = app.config["ENVIRONMENT_TAG_CONFIG"] + + # These are the predefined templates define in the config + env_tag_templates = env_tag_config.get("values") + + # This is the environment variable name from which to select the template + # default is SUPERSET_ENV (from FLASK_ENV in previous versions) + env_envvar = env_tag_config.get("variable") + + # this is the actual name we want to use + env_name = os.environ.get(env_envvar) + + if not env_name or env_name not in env_tag_templates.keys(): + env_name = "debug" if debug else None + + env_tag = env_tag_templates.get(env_name) + return env_tag or {} + + +def menu_data(user: User) -> dict[str, Any]: + languages = { + lang: {**appbuilder.languages[lang], "url": appbuilder.get_url_for_locale(lang)} + for lang in appbuilder.languages + } + + if callable(brand_text := app.config["LOGO_RIGHT_TEXT"]): + brand_text = brand_text() + + return { + "menu": appbuilder.menu.get_data(), + "brand": { + "path": app.config["LOGO_TARGET_PATH"] or "/superset/welcome/", + "icon": appbuilder.app_icon, + "alt": appbuilder.app_name, + "tooltip": app.config["LOGO_TOOLTIP"], + "text": brand_text, + }, + "environment_tag": get_environment_tag(), + "navbar_right": { + # show the watermark if the default app icon has been overridden + "show_watermark": ("superset-logo-horiz" not in appbuilder.app_icon), + "bug_report_url": app.config["BUG_REPORT_URL"], + "bug_report_icon": app.config["BUG_REPORT_ICON"], + "bug_report_text": app.config["BUG_REPORT_TEXT"], + "documentation_url": app.config["DOCUMENTATION_URL"], + "documentation_icon": app.config["DOCUMENTATION_ICON"], + "documentation_text": app.config["DOCUMENTATION_TEXT"], + "version_string": app.config["VERSION_STRING"], + "version_sha": app.config["VERSION_SHA"], + "build_number": app.config["BUILD_NUMBER"], + "languages": languages, + "show_language_picker": len(languages) > 1, + "user_is_anonymous": user.is_anonymous, + "user_info_url": ( + None + if feature_flag_manager.is_feature_enabled("MENU_HIDE_USER_INFO") + else appbuilder.get_url_for_userinfo + ), + "user_logout_url": appbuilder.get_url_for_logout, + "user_login_url": appbuilder.get_url_for_login, + "locale": session.get("locale", "en"), + }, + } + + +@cache_manager.cache.memoize(timeout=60) +def cached_common_bootstrap_data( # pylint: disable=unused-argument + user_id: Union[int, None], locale: Locale | None +) -> dict[str, Any]: + """Common data always sent to the client + + The function is memoized as the return value only changes when user permissions + or configuration values change. + """ + conf = app.config + + # should not expose API TOKEN to frontend + frontend_config = { + k: (list(conf.get(k)) if isinstance(conf.get(k), set) else conf.get(k)) + for k in FRONTEND_CONF_KEYS + } + + if conf.get("SLACK_API_TOKEN"): + frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [ + ReportRecipientType.EMAIL, + ReportRecipientType.SLACK, + ] + else: + frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [ + ReportRecipientType.EMAIL, + ] + + # TODO maybe we should pass all dialects to define what's visible in the UI (?) + + # verify client has google sheets installed + available_dialects = get_available_dialects() + frontend_config["HAS_GSHEETS_INSTALLED"] = bool("gsheets" in available_dialects) + + language = locale.language if locale else "en" + + bootstrap_data = { + "conf": frontend_config, + "locale": language, + "language_pack": get_language_pack(language), + "d3_format": conf.get("D3_FORMAT"), + "currencies": conf.get("CURRENCIES"), + "feature_flags": feature_flag_manager.get_feature_flags(), + "extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"], + "extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"], + "theme_overrides": conf["THEME_OVERRIDES"], + "menu_data": menu_data(g.user), + } + bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data)) + return bootstrap_data + + +def common_bootstrap_payload() -> dict[str, Any]: + return { + **cached_common_bootstrap_data(utils.get_user_id(), get_locale()), + "flash_messages": get_flashed_messages(with_categories=True), + } diff --git a/superset/key_value/models.py b/superset/key_value/models.py index 7a3f5e52a2f3..86e158aa754c 100644 --- a/superset/key_value/models.py +++ b/superset/key_value/models.py @@ -18,7 +18,6 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, LargeBinary, String from sqlalchemy.orm import relationship -from superset import security_manager from superset.models.helpers import AuditMixinNullable, ImportExportMixin VALUE_MAX_SIZE = 2**24 - 1 @@ -36,5 +35,5 @@ class KeyValueEntry(AuditMixinNullable, ImportExportMixin, Model): changed_on = Column(DateTime, nullable=True) expires_on = Column(DateTime, nullable=True) changed_by_fk = Column(Integer, ForeignKey("ab_user.id"), nullable=True) - created_by = relationship(security_manager.user_model, foreign_keys=[created_by_fk]) - changed_by = relationship(security_manager.user_model, foreign_keys=[changed_by_fk]) + created_by = relationship("User", foreign_keys=[created_by_fk]) + changed_by = relationship("User", foreign_keys=[changed_by_fk]) diff --git a/superset/migrations/env.py b/superset/migrations/env.py index ab9dea78554a..544d285703eb 100755 --- a/superset/migrations/env.py +++ b/superset/migrations/env.py @@ -40,8 +40,8 @@ DATABASE_URI = current_app.config["SQLALCHEMY_DATABASE_URI"] if "sqlite" in DATABASE_URI: logger.warning( - "SQLite Database support for metadata databases will \ - be removed in a future version of Superset." + "SQLite Database support for metadata databases will " + "be removed in a future version of Superset." ) decoded_uri = urllib.parse.unquote(DATABASE_URI) config.set_main_option("sqlalchemy.url", decoded_uri) diff --git a/superset/migrations/shared/catalogs.py b/superset/migrations/shared/catalogs.py index 6c03faec460f..27fb81e3e31b 100644 --- a/superset/migrations/shared/catalogs.py +++ b/superset/migrations/shared/catalogs.py @@ -24,8 +24,8 @@ from alembic import op from sqlalchemy.ext.declarative import declarative_base -from superset import db, security_manager from superset.daos.database import DatabaseDAO +from superset.extensions import db, security_manager from superset.migrations.shared.security_converge import add_pvms, ViewMenu from superset.models.core import Database diff --git a/superset/migrations/shared/constraints.py b/superset/migrations/shared/constraints.py index d3bc140fd0ba..ee341107f745 100644 --- a/superset/migrations/shared/constraints.py +++ b/superset/migrations/shared/constraints.py @@ -18,11 +18,16 @@ from dataclasses import dataclass +import sqlalchemy as sa from alembic import op from sqlalchemy.engine.reflection import Inspector from superset.utils.core import generic_find_fk_constraint_name +NAMING_CONVENTION = { + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", +} + @dataclass(frozen=True) class ForeignKey: @@ -52,9 +57,10 @@ def redefine( bind = op.get_bind() insp = Inspector.from_engine(bind) - conv = {"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s"} - with op.batch_alter_table(foreign_key.table, naming_convention=conv) as batch_op: + with op.batch_alter_table( + foreign_key.table, naming_convention=NAMING_CONVENTION + ) as batch_op: if constraint := generic_find_fk_constraint_name( table=foreign_key.table, columns=set(foreign_key.remote_cols), @@ -71,3 +77,20 @@ def redefine( ondelete=on_delete, onupdate=on_update, ) + + +def delete_fk(table1: str, columns: set[str], table2: str) -> None: + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + with op.batch_alter_table( + "tab_state", naming_convention=NAMING_CONVENTION + ) as batch_op: + table_schema_id_constraint = generic_find_fk_constraint_name( + table1, columns, table2, insp + ) + if table_schema_id_constraint: + batch_op.drop_constraint( + table_schema_id_constraint, + type_="foreignkey", + ) diff --git a/superset/migrations/shared/migrate_viz/base.py b/superset/migrations/shared/migrate_viz/base.py index 1be039165920..11a088144385 100644 --- a/superset/migrations/shared/migrate_viz/base.py +++ b/superset/migrations/shared/migrate_viz/base.py @@ -19,11 +19,11 @@ import copy from typing import Any +from flask import current_app as app from sqlalchemy import and_, Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Session -from superset import conf from superset.constants import TimeGrain from superset.migrations.shared.utils import paginated_update, try_load_json from superset.utils import json @@ -94,7 +94,9 @@ def _post_action(self) -> None: def _migrate_temporal_filter(self, rv_data: dict[str, Any]) -> None: """Adds a temporal filter.""" granularity_sqla = rv_data.pop("granularity_sqla", None) - time_range = rv_data.pop("time_range", None) or conf.get("DEFAULT_TIME_FILTER") + time_range = rv_data.pop("time_range", None) or app.conf.get( + "DEFAULT_TIME_FILTER" + ) if not granularity_sqla: return diff --git a/superset/migrations/shared/utils.py b/superset/migrations/shared/utils.py index 17e5b4128e46..d61a9a145cea 100644 --- a/superset/migrations/shared/utils.py +++ b/superset/migrations/shared/utils.py @@ -21,6 +21,7 @@ from typing import Any, Callable, Optional, Union from uuid import uuid4 +import sqlalchemy as sa from alembic import op from sqlalchemy import inspect from sqlalchemy.dialects.mysql.base import MySQLDialect @@ -168,3 +169,25 @@ def try_load_json(data: Optional[str]) -> dict[str, Any]: except json.JSONDecodeError: print(f"Failed to parse: {data}") return {} + + +def force_add_column(table_name: str, column_object: sa.Column) -> None: + """ + Checks if the column exists in the given table, and drops and recreates it if it does. + + :param table_name: The name of the table. + :param column_object: SQLAlchemy Column object to add. + """ + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + # Check if the column exists + columns = [col["name"] for col in insp.get_columns(table_name)] + if (column_name := column_object.name) in columns: + # Drop the existing column + with op.batch_alter_table(table_name) as batch_op: + batch_op.drop_column(column_name) + + # Add the new column + with op.batch_alter_table(table_name) as batch_op: + batch_op.add_column(column_object) diff --git a/superset/migrations/versions/2016-04-25_08-54_c3a8f8611885_materializing_permission.py b/superset/migrations/versions/2016-04-25_08-54_c3a8f8611885_materializing_permission.py index 0231fb1e2e69..ea388847d8f1 100644 --- a/superset/migrations/versions/2016-04-25_08-54_c3a8f8611885_materializing_permission.py +++ b/superset/migrations/versions/2016-04-25_08-54_c3a8f8611885_materializing_permission.py @@ -27,7 +27,7 @@ from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "c3a8f8611885" diff --git a/superset/migrations/versions/2016-05-27_15-03_1226819ee0e3_fix_wrong_constraint_on_table_columns.py b/superset/migrations/versions/2016-05-27_15-03_1226819ee0e3_fix_wrong_constraint_on_table_columns.py index 9eda0bf8989d..cf3c8b65b871 100644 --- a/superset/migrations/versions/2016-05-27_15-03_1226819ee0e3_fix_wrong_constraint_on_table_columns.py +++ b/superset/migrations/versions/2016-05-27_15-03_1226819ee0e3_fix_wrong_constraint_on_table_columns.py @@ -26,7 +26,7 @@ from alembic import op -from superset import db +from superset.extensions import db from superset.utils.core import generic_find_constraint_name # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2016-06-07_12-33_d8bc074f7aad_add_new_field_is_restricted_to_.py b/superset/migrations/versions/2016-06-07_12-33_d8bc074f7aad_add_new_field_is_restricted_to_.py index def84c9a9094..5546790352d9 100644 --- a/superset/migrations/versions/2016-06-07_12-33_d8bc074f7aad_add_new_field_is_restricted_to_.py +++ b/superset/migrations/versions/2016-06-07_12-33_d8bc074f7aad_add_new_field_is_restricted_to_.py @@ -31,7 +31,7 @@ from sqlalchemy import Boolean, Column, Integer # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2016-06-27_08-43_27ae655e4247_make_creator_owners.py b/superset/migrations/versions/2016-06-27_08-43_27ae655e4247_make_creator_owners.py index 76a823041521..35f04f0df3e3 100644 --- a/superset/migrations/versions/2016-06-27_08-43_27ae655e4247_make_creator_owners.py +++ b/superset/migrations/versions/2016-06-27_08-43_27ae655e4247_make_creator_owners.py @@ -31,7 +31,7 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr # noqa: E402 from sqlalchemy.orm import relationship # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils.core import get_user_id # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2016-09-07_23-50_33d996bcc382_update_slice_model.py b/superset/migrations/versions/2016-09-07_23-50_33d996bcc382_update_slice_model.py index 01907e958a3f..c502eebe9b20 100644 --- a/superset/migrations/versions/2016-09-07_23-50_33d996bcc382_update_slice_model.py +++ b/superset/migrations/versions/2016-09-07_23-50_33d996bcc382_update_slice_model.py @@ -27,7 +27,7 @@ from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "33d996bcc382" diff --git a/superset/migrations/versions/2016-09-22_10-21_3b626e2a6783_sync_db_with_models.py b/superset/migrations/versions/2016-09-22_10-21_3b626e2a6783_sync_db_with_models.py index 8ba6110071f2..ce161da777f7 100644 --- a/superset/migrations/versions/2016-09-22_10-21_3b626e2a6783_sync_db_with_models.py +++ b/superset/migrations/versions/2016-09-22_10-21_3b626e2a6783_sync_db_with_models.py @@ -31,7 +31,7 @@ from alembic import op from sqlalchemy.dialects import mysql -from superset import db +from superset.extensions import db from superset.utils.core import generic_find_constraint_name # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2016-09-22_11-31_eca4694defa7_sqllab_setting_defaults.py b/superset/migrations/versions/2016-09-22_11-31_eca4694defa7_sqllab_setting_defaults.py index b63ebef5d143..3401d850baff 100644 --- a/superset/migrations/versions/2016-09-22_11-31_eca4694defa7_sqllab_setting_defaults.py +++ b/superset/migrations/versions/2016-09-22_11-31_eca4694defa7_sqllab_setting_defaults.py @@ -26,7 +26,7 @@ from sqlalchemy import Boolean, Column, Integer from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "eca4694defa7" diff --git a/superset/migrations/versions/2017-01-24_12-31_db0c65b146bd_update_slice_model_json.py b/superset/migrations/versions/2017-01-24_12-31_db0c65b146bd_update_slice_model_json.py index 6de97648fa67..2cecaa636869 100644 --- a/superset/migrations/versions/2017-01-24_12-31_db0c65b146bd_update_slice_model_json.py +++ b/superset/migrations/versions/2017-01-24_12-31_db0c65b146bd_update_slice_model_json.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2017-02-08_14-16_a99f2f7c195a_rewriting_url_from_shortner_with_new_.py b/superset/migrations/versions/2017-02-08_14-16_a99f2f7c195a_rewriting_url_from_shortner_with_new_.py index 00182f5eebfe..4335a142440f 100644 --- a/superset/migrations/versions/2017-02-08_14-16_a99f2f7c195a_rewriting_url_from_shortner_with_new_.py +++ b/superset/migrations/versions/2017-02-08_14-16_a99f2f7c195a_rewriting_url_from_shortner_with_new_.py @@ -32,7 +32,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2017-12-08_08-19_67a6ac9b727b_update_spatial_params.py b/superset/migrations/versions/2017-12-08_08-19_67a6ac9b727b_update_spatial_params.py index ec2fc2009e85..fb8df3079858 100644 --- a/superset/migrations/versions/2017-12-08_08-19_67a6ac9b727b_update_spatial_params.py +++ b/superset/migrations/versions/2017-12-08_08-19_67a6ac9b727b_update_spatial_params.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2017-12-17_11-06_21e88bc06c02_annotation_migration.py b/superset/migrations/versions/2017-12-17_11-06_21e88bc06c02_annotation_migration.py index 7cf379c468ef..a7bb3cb5ceee 100644 --- a/superset/migrations/versions/2017-12-17_11-06_21e88bc06c02_annotation_migration.py +++ b/superset/migrations/versions/2017-12-17_11-06_21e88bc06c02_annotation_migration.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, or_, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2018-02-13_08-07_e866bd2d4976_smaller_grid.py b/superset/migrations/versions/2018-02-13_08-07_e866bd2d4976_smaller_grid.py index e97bfea6391c..a8754b60efb7 100644 --- a/superset/migrations/versions/2018-02-13_08-07_e866bd2d4976_smaller_grid.py +++ b/superset/migrations/versions/2018-02-13_08-07_e866bd2d4976_smaller_grid.py @@ -25,7 +25,7 @@ from alembic import op from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json revision = "e866bd2d4976" diff --git a/superset/migrations/versions/2018-04-03_08-19_130915240929_is_sqllab_viz_flow.py b/superset/migrations/versions/2018-04-03_08-19_130915240929_is_sqllab_viz_flow.py index 63e0d1ff26d7..981aa1fe1e97 100644 --- a/superset/migrations/versions/2018-04-03_08-19_130915240929_is_sqllab_viz_flow.py +++ b/superset/migrations/versions/2018-04-03_08-19_130915240929_is_sqllab_viz_flow.py @@ -26,7 +26,7 @@ from alembic import op from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "130915240929" diff --git a/superset/migrations/versions/2018-04-10_11-19_bf706ae5eb46_cal_heatmap_metric_to_metrics.py b/superset/migrations/versions/2018-04-10_11-19_bf706ae5eb46_cal_heatmap_metric_to_metrics.py index 30cb7e4bb220..c4b9ff973dc8 100644 --- a/superset/migrations/versions/2018-04-10_11-19_bf706ae5eb46_cal_heatmap_metric_to_metrics.py +++ b/superset/migrations/versions/2018-04-10_11-19_bf706ae5eb46_cal_heatmap_metric_to_metrics.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json Base = declarative_base() diff --git a/superset/migrations/versions/2018-06-04_11-12_c5756bec8b47_time_grain_sqla.py b/superset/migrations/versions/2018-06-04_11-12_c5756bec8b47_time_grain_sqla.py index 36ad22526d27..b956503e65d3 100644 --- a/superset/migrations/versions/2018-06-04_11-12_c5756bec8b47_time_grain_sqla.py +++ b/superset/migrations/versions/2018-06-04_11-12_c5756bec8b47_time_grain_sqla.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-06-07_09-52_afb7730f6a9c_remove_empty_filters.py b/superset/migrations/versions/2018-06-07_09-52_afb7730f6a9c_remove_empty_filters.py index c9ea7b9b349e..175654c8b6a6 100644 --- a/superset/migrations/versions/2018-06-07_09-52_afb7730f6a9c_remove_empty_filters.py +++ b/superset/migrations/versions/2018-06-07_09-52_afb7730f6a9c_remove_empty_filters.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-06-13_10-20_4451805bbaa1_remove_double_percents.py b/superset/migrations/versions/2018-06-13_10-20_4451805bbaa1_remove_double_percents.py index 1786ae37e60f..047a4f62e3df 100644 --- a/superset/migrations/versions/2018-06-13_10-20_4451805bbaa1_remove_double_percents.py +++ b/superset/migrations/versions/2018-06-13_10-20_4451805bbaa1_remove_double_percents.py @@ -38,7 +38,7 @@ ) from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-06-13_14-54_bddc498dd179_adhoc_filters.py b/superset/migrations/versions/2018-06-13_14-54_bddc498dd179_adhoc_filters.py index 8ebcc104e8aa..e69a3029b7a9 100644 --- a/superset/migrations/versions/2018-06-13_14-54_bddc498dd179_adhoc_filters.py +++ b/superset/migrations/versions/2018-06-13_14-54_bddc498dd179_adhoc_filters.py @@ -31,7 +31,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 from superset.utils.core import ( # noqa: E402 convert_legacy_filters_into_adhoc, diff --git a/superset/migrations/versions/2018-06-14_14-31_80a67c5192fa_single_pie_chart_metric.py b/superset/migrations/versions/2018-06-14_14-31_80a67c5192fa_single_pie_chart_metric.py index 302dda2a51e6..1fe064ed2cf7 100644 --- a/superset/migrations/versions/2018-06-14_14-31_80a67c5192fa_single_pie_chart_metric.py +++ b/superset/migrations/versions/2018-06-14_14-31_80a67c5192fa_single_pie_chart_metric.py @@ -31,7 +31,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-07-05_15-19_3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/2018-07-05_15-19_3dda56f1c4c6_migrate_num_period_compare_and_period_.py index 685c45c63131..74091faff49a 100644 --- a/superset/migrations/versions/2018-07-05_15-19_3dda56f1c4c6_migrate_num_period_compare_and_period_.py +++ b/superset/migrations/versions/2018-07-05_15-19_3dda56f1c4c6_migrate_num_period_compare_and_period_.py @@ -31,7 +31,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json from superset.utils.date_parser import parse_human_timedelta diff --git a/superset/migrations/versions/2018-07-19_23-41_c617da68de7d_form_nullable.py b/superset/migrations/versions/2018-07-19_23-41_c617da68de7d_form_nullable.py index 8521845db49e..fa490c021bee 100644 --- a/superset/migrations/versions/2018-07-19_23-41_c617da68de7d_form_nullable.py +++ b/superset/migrations/versions/2018-07-19_23-41_c617da68de7d_form_nullable.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils.core import MediumText # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-07-20_15-31_7f2635b51f5d_update_base_columns.py b/superset/migrations/versions/2018-07-20_15-31_7f2635b51f5d_update_base_columns.py index 4f9fdec10a3b..cefc01b4028b 100644 --- a/superset/migrations/versions/2018-07-20_15-31_7f2635b51f5d_update_base_columns.py +++ b/superset/migrations/versions/2018-07-20_15-31_7f2635b51f5d_update_base_columns.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, engine, Integer, String # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils.core import generic_find_uq_constraint_name # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-07-20_15-57_e9df189e5c7e_update_base_metrics.py b/superset/migrations/versions/2018-07-20_15-57_e9df189e5c7e_update_base_metrics.py index 1291fb13d964..43b44f3fefb5 100644 --- a/superset/migrations/versions/2018-07-20_15-57_e9df189e5c7e_update_base_metrics.py +++ b/superset/migrations/versions/2018-07-20_15-57_e9df189e5c7e_update_base_metrics.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, engine, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils.core import generic_find_uq_constraint_name # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py b/superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py index b57f4f3e9f17..2d6c4968698d 100644 --- a/superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py +++ b/superset/migrations/versions/2018-07-22_11-59_bebcf3fed1fe_convert_dashboard_v1_positions.py @@ -33,7 +33,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from superset import db +from superset.extensions import db from superset.utils import json revision = "bebcf3fed1fe" diff --git a/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py b/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py index 6f5220197d54..17b619d2c2eb 100644 --- a/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py +++ b/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py @@ -29,7 +29,7 @@ from alembic import op from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json revision = "7fcdcde0761c" diff --git a/superset/migrations/versions/2018-11-12_13-31_4ce8df208545_migrate_time_range_for_default_filters.py b/superset/migrations/versions/2018-11-12_13-31_4ce8df208545_migrate_time_range_for_default_filters.py index d1f68979cc46..0caa61f2741b 100644 --- a/superset/migrations/versions/2018-11-12_13-31_4ce8df208545_migrate_time_range_for_default_filters.py +++ b/superset/migrations/versions/2018-11-12_13-31_4ce8df208545_migrate_time_range_for_default_filters.py @@ -27,7 +27,7 @@ from sqlalchemy import Column, Integer, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json revision = "4ce8df208545" diff --git a/superset/migrations/versions/2018-12-11_22-03_fb13d49b72f9_better_filters.py b/superset/migrations/versions/2018-12-11_22-03_fb13d49b72f9_better_filters.py index c2af161f73c6..3fc2b407c787 100644 --- a/superset/migrations/versions/2018-12-11_22-03_fb13d49b72f9_better_filters.py +++ b/superset/migrations/versions/2018-12-11_22-03_fb13d49b72f9_better_filters.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py b/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py index 332fe88e535f..82b16533f62c 100644 --- a/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py +++ b/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py @@ -26,7 +26,7 @@ from alembic import op # revision identifiers, used by Alembic. -from superset import db +from superset.extensions import db from superset.utils.core import generic_find_fk_constraint_name revision = "3e1b21cd94a4" diff --git a/superset/migrations/versions/2019-03-21_10-22_d94d33dbe938_form_strip.py b/superset/migrations/versions/2019-03-21_10-22_d94d33dbe938_form_strip.py index 7796ad107b77..aeb4129c75c8 100644 --- a/superset/migrations/versions/2019-03-21_10-22_d94d33dbe938_form_strip.py +++ b/superset/migrations/versions/2019-03-21_10-22_d94d33dbe938_form_strip.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils.core import MediumText # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2019-04-09_16-27_80aa3f04bc82_add_parent_ids_in_dashboard_layout.py b/superset/migrations/versions/2019-04-09_16-27_80aa3f04bc82_add_parent_ids_in_dashboard_layout.py index b78af79e3463..e4c0e7cdf730 100644 --- a/superset/migrations/versions/2019-04-09_16-27_80aa3f04bc82_add_parent_ids_in_dashboard_layout.py +++ b/superset/migrations/versions/2019-04-09_16-27_80aa3f04bc82_add_parent_ids_in_dashboard_layout.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2019-06-28_13-17_ab8c66efdd01_resample.py b/superset/migrations/versions/2019-06-28_13-17_ab8c66efdd01_resample.py index 7ca0d5eacb8d..6b9434d8c6b8 100644 --- a/superset/migrations/versions/2019-06-28_13-17_ab8c66efdd01_resample.py +++ b/superset/migrations/versions/2019-06-28_13-17_ab8c66efdd01_resample.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2019-07-15_12-00_190188938582_adding_unique_constraint_on_dashboard_slices_tbl.py b/superset/migrations/versions/2019-07-15_12-00_190188938582_adding_unique_constraint_on_dashboard_slices_tbl.py index 011a72902b1b..1708abdf931b 100644 --- a/superset/migrations/versions/2019-07-15_12-00_190188938582_adding_unique_constraint_on_dashboard_slices_tbl.py +++ b/superset/migrations/versions/2019-07-15_12-00_190188938582_adding_unique_constraint_on_dashboard_slices_tbl.py @@ -28,7 +28,7 @@ from sqlalchemy import and_, Column, ForeignKey, Integer from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "190188938582" diff --git a/superset/migrations/versions/2019-09-11_21-49_5afa9079866a_serialize_schema_permissions_py.py b/superset/migrations/versions/2019-09-11_21-49_5afa9079866a_serialize_schema_permissions_py.py index 1cf60f10566a..08b082552e9d 100644 --- a/superset/migrations/versions/2019-09-11_21-49_5afa9079866a_serialize_schema_permissions_py.py +++ b/superset/migrations/versions/2019-09-11_21-49_5afa9079866a_serialize_schema_permissions_py.py @@ -28,7 +28,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from superset import db +from superset.extensions import db revision = "5afa9079866a" down_revision = "db4b49eb0782" diff --git a/superset/migrations/versions/2019-09-19_13-40_258b5280a45e_form_strip_leading_and_trailing_whitespace.py b/superset/migrations/versions/2019-09-19_13-40_258b5280a45e_form_strip_leading_and_trailing_whitespace.py index b5d80877a131..20fd39cb3a32 100644 --- a/superset/migrations/versions/2019-09-19_13-40_258b5280a45e_form_strip_leading_and_trailing_whitespace.py +++ b/superset/migrations/versions/2019-09-19_13-40_258b5280a45e_form_strip_leading_and_trailing_whitespace.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils.core import MediumText Base = declarative_base() diff --git a/superset/migrations/versions/2019-10-10_13-52_1495eb914ad3_time_range.py b/superset/migrations/versions/2019-10-10_13-52_1495eb914ad3_time_range.py index a1b0fca3df6e..5247c985767a 100644 --- a/superset/migrations/versions/2019-10-10_13-52_1495eb914ad3_time_range.py +++ b/superset/migrations/versions/2019-10-10_13-52_1495eb914ad3_time_range.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.legacy import update_time_range from superset.utils import json diff --git a/superset/migrations/versions/2019-11-06_15-23_78ee127d0d1d_reconvert_legacy_filters_into_adhoc.py b/superset/migrations/versions/2019-11-06_15-23_78ee127d0d1d_reconvert_legacy_filters_into_adhoc.py index a969bc9ba15b..4c6938adcb7f 100644 --- a/superset/migrations/versions/2019-11-06_15-23_78ee127d0d1d_reconvert_legacy_filters_into_adhoc.py +++ b/superset/migrations/versions/2019-11-06_15-23_78ee127d0d1d_reconvert_legacy_filters_into_adhoc.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 from superset.utils.core import ( # noqa: E402 convert_legacy_filters_into_adhoc, diff --git a/superset/migrations/versions/2020-02-07_14-13_3325d4caccc8_dashboard_scoped_filters.py b/superset/migrations/versions/2020-02-07_14-13_3325d4caccc8_dashboard_scoped_filters.py index f37aa3e87200..6d1d18afc07f 100644 --- a/superset/migrations/versions/2020-02-07_14-13_3325d4caccc8_dashboard_scoped_filters.py +++ b/superset/migrations/versions/2020-02-07_14-13_3325d4caccc8_dashboard_scoped_filters.py @@ -30,7 +30,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from superset import db +from superset.extensions import db from superset.utils import json from superset.utils.dashboard_filter_scopes_converter import convert_filter_scopes diff --git a/superset/migrations/versions/2020-03-25_10-42_f9a30386bd74_cleanup_time_grainularity.py b/superset/migrations/versions/2020-03-25_10-42_f9a30386bd74_cleanup_time_grainularity.py index f85cf5db9c1f..e4d97323f2cd 100644 --- a/superset/migrations/versions/2020-03-25_10-42_f9a30386bd74_cleanup_time_grainularity.py +++ b/superset/migrations/versions/2020-03-25_10-42_f9a30386bd74_cleanup_time_grainularity.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2020-04-29_09-24_620241d1153f_update_time_grain_sqla.py b/superset/migrations/versions/2020-04-29_09-24_620241d1153f_update_time_grain_sqla.py index 2f9e7cc56614..0ba09043c468 100644 --- a/superset/migrations/versions/2020-04-29_09-24_620241d1153f_update_time_grain_sqla.py +++ b/superset/migrations/versions/2020-04-29_09-24_620241d1153f_update_time_grain_sqla.py @@ -22,18 +22,20 @@ """ -# revision identifiers, used by Alembic. -revision = "620241d1153f" -down_revision = "f9a30386bd74" - from alembic import op # noqa: E402 from sqlalchemy import Column, ForeignKey, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db, db_engine_specs # noqa: E402 +from superset import db_engine_specs from superset.databases.utils import make_url_safe # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 +# revision identifiers, used by Alembic. +revision = "620241d1153f" +down_revision = "f9a30386bd74" + + Base = declarative_base() diff --git a/superset/migrations/versions/2020-08-12_00-24_978245563a02_migrate_iframe_to_dash_markdown.py b/superset/migrations/versions/2020-08-12_00-24_978245563a02_migrate_iframe_to_dash_markdown.py index 404d8b3511c8..286a31286091 100644 --- a/superset/migrations/versions/2020-08-12_00-24_978245563a02_migrate_iframe_to_dash_markdown.py +++ b/superset/migrations/versions/2020-08-12_00-24_978245563a02_migrate_iframe_to_dash_markdown.py @@ -31,7 +31,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2020-09-28_17-57_b56500de1855_add_uuid_column_to_import_mixin.py b/superset/migrations/versions/2020-09-28_17-57_b56500de1855_add_uuid_column_to_import_mixin.py index 57c9917ce46e..e5ded8e9cf38 100644 --- a/superset/migrations/versions/2020-09-28_17-57_b56500de1855_add_uuid_column_to_import_mixin.py +++ b/superset/migrations/versions/2020-09-28_17-57_b56500de1855_add_uuid_column_to_import_mixin.py @@ -31,7 +31,7 @@ from sqlalchemy.orm import load_only from sqlalchemy_utils import UUIDType -from superset import db +from superset.extensions import db from superset.migrations.shared.utils import assign_uuids from superset.utils import core as utils, json diff --git a/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py b/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py index 9502a66f4db3..489d4c19afac 100644 --- a/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py +++ b/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py @@ -28,7 +28,7 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import backref, relationship, RelationshipProperty -from superset import db +from superset.extensions import db from superset.utils.core import generic_find_fk_constraint_name revision = "af30ca79208f" diff --git a/superset/migrations/versions/2020-10-21_21-09_96e99fb176a0_add_import_mixing_to_saved_query.py b/superset/migrations/versions/2020-10-21_21-09_96e99fb176a0_add_import_mixing_to_saved_query.py index b3f57cf35805..ed73c492c503 100644 --- a/superset/migrations/versions/2020-10-21_21-09_96e99fb176a0_add_import_mixing_to_saved_query.py +++ b/superset/migrations/versions/2020-10-21_21-09_96e99fb176a0_add_import_mixing_to_saved_query.py @@ -31,7 +31,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_utils import UUIDType -from superset import db +from superset.extensions import db from superset.migrations.shared.utils import assign_uuids # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2021-01-23_16-25_260bf0649a77_migrate_x_dateunit_in_time_range.py b/superset/migrations/versions/2021-01-23_16-25_260bf0649a77_migrate_x_dateunit_in_time_range.py index 5718458fc7b7..86d864db9f7d 100644 --- a/superset/migrations/versions/2021-01-23_16-25_260bf0649a77_migrate_x_dateunit_in_time_range.py +++ b/superset/migrations/versions/2021-01-23_16-25_260bf0649a77_migrate_x_dateunit_in_time_range.py @@ -36,7 +36,7 @@ from sqlalchemy.exc import OperationalError # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 from superset.utils.date_parser import DateRangeMigration # noqa: E402 diff --git a/superset/migrations/versions/2021-02-04_09-34_070c043f2fdb_add_granularity_to_charts_where_missing.py b/superset/migrations/versions/2021-02-04_09-34_070c043f2fdb_add_granularity_to_charts_where_missing.py index cda2a9b41120..1d3e7a1f14c6 100644 --- a/superset/migrations/versions/2021-02-04_09-34_070c043f2fdb_add_granularity_to_charts_where_missing.py +++ b/superset/migrations/versions/2021-02-04_09-34_070c043f2fdb_add_granularity_to_charts_where_missing.py @@ -30,7 +30,7 @@ from sqlalchemy import and_, Boolean, Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-02-10_12-32_41ce8799acc3_rename_pie_label_type.py b/superset/migrations/versions/2021-02-10_12-32_41ce8799acc3_rename_pie_label_type.py index f5bdaf9725c5..7d8511c094ea 100644 --- a/superset/migrations/versions/2021-02-10_12-32_41ce8799acc3_rename_pie_label_type.py +++ b/superset/migrations/versions/2021-02-10_12-32_41ce8799acc3_rename_pie_label_type.py @@ -30,7 +30,7 @@ from sqlalchemy import and_, Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-02-14_11-46_1412ec1e5a7b_legacy_force_directed_to_echart.py b/superset/migrations/versions/2021-02-14_11-46_1412ec1e5a7b_legacy_force_directed_to_echart.py index 5a8dbb934e53..a7caa1bda868 100644 --- a/superset/migrations/versions/2021-02-14_11-46_1412ec1e5a7b_legacy_force_directed_to_echart.py +++ b/superset/migrations/versions/2021-02-14_11-46_1412ec1e5a7b_legacy_force_directed_to_echart.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2021-02-18_09-13_c501b7c653a3_add_missing_uuid_column.py b/superset/migrations/versions/2021-02-18_09-13_c501b7c653a3_add_missing_uuid_column.py index be921e3ac7b1..24557a923974 100644 --- a/superset/migrations/versions/2021-02-18_09-13_c501b7c653a3_add_missing_uuid_column.py +++ b/superset/migrations/versions/2021-02-18_09-13_c501b7c653a3_add_missing_uuid_column.py @@ -36,7 +36,7 @@ from sqlalchemy.orm import load_only # noqa: E402 from sqlalchemy_utils import UUIDType # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 add_uuid_column_to_import_mixin = import_module( "superset.migrations.versions." diff --git a/superset/migrations/versions/2021-03-24_09-47_989bbe479899_rename_filter_configuration_in_.py b/superset/migrations/versions/2021-03-24_09-47_989bbe479899_rename_filter_configuration_in_.py index 4610545458c7..127da089d610 100644 --- a/superset/migrations/versions/2021-03-24_09-47_989bbe479899_rename_filter_configuration_in_.py +++ b/superset/migrations/versions/2021-03-24_09-47_989bbe479899_rename_filter_configuration_in_.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-04-07_07-21_134cea61c5e7_remove_dataset_health_check_message.py b/superset/migrations/versions/2021-04-07_07-21_134cea61c5e7_remove_dataset_health_check_message.py index ceb7c49408ae..0fc7c3d8ba92 100644 --- a/superset/migrations/versions/2021-04-07_07-21_134cea61c5e7_remove_dataset_health_check_message.py +++ b/superset/migrations/versions/2021-04-07_07-21_134cea61c5e7_remove_dataset_health_check_message.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-04-09_16-14_085f06488938_country_map_use_lowercase_country_name.py b/superset/migrations/versions/2021-04-09_16-14_085f06488938_country_map_use_lowercase_country_name.py index a16caedeb9b0..29656be0aeab 100644 --- a/superset/migrations/versions/2021-04-09_16-14_085f06488938_country_map_use_lowercase_country_name.py +++ b/superset/migrations/versions/2021-04-09_16-14_085f06488938_country_map_use_lowercase_country_name.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2021-04-12_12-38_fc3a3a8ff221_migrate_filter_sets_to_new_format.py b/superset/migrations/versions/2021-04-12_12-38_fc3a3a8ff221_migrate_filter_sets_to_new_format.py index a7f85e510680..fb8bb02b1225 100644 --- a/superset/migrations/versions/2021-04-12_12-38_fc3a3a8ff221_migrate_filter_sets_to_new_format.py +++ b/superset/migrations/versions/2021-04-12_12-38_fc3a3a8ff221_migrate_filter_sets_to_new_format.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-04-29_15-32_f1410ed7ec95_migrate_native_filters_to_new_schema.py b/superset/migrations/versions/2021-04-29_15-32_f1410ed7ec95_migrate_native_filters_to_new_schema.py index ab60f9d00c1f..681739062f93 100644 --- a/superset/migrations/versions/2021-04-29_15-32_f1410ed7ec95_migrate_native_filters_to_new_schema.py +++ b/superset/migrations/versions/2021-04-29_15-32_f1410ed7ec95_migrate_native_filters_to_new_schema.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-08-02_16-39_e323605f370a_fix_schemas_allowed_for_csv_upload.py b/superset/migrations/versions/2021-08-02_16-39_e323605f370a_fix_schemas_allowed_for_csv_upload.py index 92f63d2f8eb4..8130fde1358e 100644 --- a/superset/migrations/versions/2021-08-02_16-39_e323605f370a_fix_schemas_allowed_for_csv_upload.py +++ b/superset/migrations/versions/2021-08-02_16-39_e323605f370a_fix_schemas_allowed_for_csv_upload.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2021-08-03_15-36_143b6f2815da_migrate_pivot_table_v2_heatmaps_to_new_.py b/superset/migrations/versions/2021-08-03_15-36_143b6f2815da_migrate_pivot_table_v2_heatmaps_to_new_.py index f49a37a10515..895145519dc0 100644 --- a/superset/migrations/versions/2021-08-03_15-36_143b6f2815da_migrate_pivot_table_v2_heatmaps_to_new_.py +++ b/superset/migrations/versions/2021-08-03_15-36_143b6f2815da_migrate_pivot_table_v2_heatmaps_to_new_.py @@ -30,7 +30,7 @@ from sqlalchemy import and_, Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-08-09_17-32_07071313dd52_change_fetch_values_predicate_to_text.py b/superset/migrations/versions/2021-08-09_17-32_07071313dd52_change_fetch_values_predicate_to_text.py index b5d9715daaad..5244cc989210 100644 --- a/superset/migrations/versions/2021-08-09_17-32_07071313dd52_change_fetch_values_predicate_to_text.py +++ b/superset/migrations/versions/2021-08-09_17-32_07071313dd52_change_fetch_values_predicate_to_text.py @@ -32,8 +32,8 @@ from alembic import op # noqa: E402 from sqlalchemy import func # noqa: E402 -from superset import db # noqa: E402 from superset.connectors.sqla.models import SqlaTable # noqa: E402 +from superset.extensions import db # noqa: E402 def upgrade(): diff --git a/superset/migrations/versions/2021-08-31_11-37_021b81fe4fbb_add_type_to_native_filter_configuration.py b/superset/migrations/versions/2021-08-31_11-37_021b81fe4fbb_add_type_to_native_filter_configuration.py index 92371482840b..cba97e5e24e8 100644 --- a/superset/migrations/versions/2021-08-31_11-37_021b81fe4fbb_add_type_to_native_filter_configuration.py +++ b/superset/migrations/versions/2021-08-31_11-37_021b81fe4fbb_add_type_to_native_filter_configuration.py @@ -32,7 +32,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-09-27_11-31_60dc453f4e2e_migrate_timeseries_limit_metric_to_.py b/superset/migrations/versions/2021-09-27_11-31_60dc453f4e2e_migrate_timeseries_limit_metric_to_.py index 537d73c85856..49f75e742b20 100644 --- a/superset/migrations/versions/2021-09-27_11-31_60dc453f4e2e_migrate_timeseries_limit_metric_to_.py +++ b/superset/migrations/versions/2021-09-27_11-31_60dc453f4e2e_migrate_timeseries_limit_metric_to_.py @@ -30,7 +30,7 @@ from sqlalchemy import and_, Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-10-12_11-15_32646df09c64_update_time_grain_sqla.py b/superset/migrations/versions/2021-10-12_11-15_32646df09c64_update_time_grain_sqla.py index aa87f30369ba..7e5ba9a282b6 100644 --- a/superset/migrations/versions/2021-10-12_11-15_32646df09c64_update_time_grain_sqla.py +++ b/superset/migrations/versions/2021-10-12_11-15_32646df09c64_update_time_grain_sqla.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-11-11_04-18_0ca9e5f1dacd_rename_to_schemas_allowed_for_file_.py b/superset/migrations/versions/2021-11-11_04-18_0ca9e5f1dacd_rename_to_schemas_allowed_for_file_.py index d017fac53922..ee5dd2098192 100644 --- a/superset/migrations/versions/2021-11-11_04-18_0ca9e5f1dacd_rename_to_schemas_allowed_for_file_.py +++ b/superset/migrations/versions/2021-11-11_04-18_0ca9e5f1dacd_rename_to_schemas_allowed_for_file_.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-12-10_19-25_bb38f40aa3ff_add_force_screenshot_to_alerts_reports.py b/superset/migrations/versions/2021-12-10_19-25_bb38f40aa3ff_add_force_screenshot_to_alerts_reports.py index 38d8275d9f05..649e5d658c3a 100644 --- a/superset/migrations/versions/2021-12-10_19-25_bb38f40aa3ff_add_force_screenshot_to_alerts_reports.py +++ b/superset/migrations/versions/2021-12-10_19-25_bb38f40aa3ff_add_force_screenshot_to_alerts_reports.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-12-13_14-06_fe23025b9441_rename_big_viz_total_form_data_fields.py b/superset/migrations/versions/2021-12-13_14-06_fe23025b9441_rename_big_viz_total_form_data_fields.py index f2e8407add4f..d1c0601d3517 100644 --- a/superset/migrations/versions/2021-12-13_14-06_fe23025b9441_rename_big_viz_total_form_data_fields.py +++ b/superset/migrations/versions/2021-12-13_14-06_fe23025b9441_rename_big_viz_total_form_data_fields.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2021-12-17_16-56_31bb738bd1d2_move_pivot_table_v2_legacy_order_by_to_.py b/superset/migrations/versions/2021-12-17_16-56_31bb738bd1d2_move_pivot_table_v2_legacy_order_by_to_.py index f9ee00b7052b..7b7805e767ea 100644 --- a/superset/migrations/versions/2021-12-17_16-56_31bb738bd1d2_move_pivot_table_v2_legacy_order_by_to_.py +++ b/superset/migrations/versions/2021-12-17_16-56_31bb738bd1d2_move_pivot_table_v2_legacy_order_by_to_.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2022-03-02_09-20_b5a422d8e252_fix_query_and_saved_query_null_schema.py b/superset/migrations/versions/2022-03-02_09-20_b5a422d8e252_fix_query_and_saved_query_null_schema.py index 061b32096934..510943ddcfa4 100644 --- a/superset/migrations/versions/2022-03-02_09-20_b5a422d8e252_fix_query_and_saved_query_null_schema.py +++ b/superset/migrations/versions/2022-03-02_09-20_b5a422d8e252_fix_query_and_saved_query_null_schema.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db # revision identifiers, used by Alembic. revision = "b5a422d8e252" diff --git a/superset/migrations/versions/2022-03-02_16-41_7293b0ca7944_change_adhoc_filter_b_from_none_to_.py b/superset/migrations/versions/2022-03-02_16-41_7293b0ca7944_change_adhoc_filter_b_from_none_to_.py index 9085ea05d300..173a5bd9e2d0 100644 --- a/superset/migrations/versions/2022-03-02_16-41_7293b0ca7944_change_adhoc_filter_b_from_none_to_.py +++ b/superset/migrations/versions/2022-03-02_16-41_7293b0ca7944_change_adhoc_filter_b_from_none_to_.py @@ -31,7 +31,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2022-04-04_15-04_b0d0249074e4_deprecate_time_range_endpoints_v2.py b/superset/migrations/versions/2022-04-04_15-04_b0d0249074e4_deprecate_time_range_endpoints_v2.py index 9b9a67f3feb7..dc3e3a7910fe 100644 --- a/superset/migrations/versions/2022-04-04_15-04_b0d0249074e4_deprecate_time_range_endpoints_v2.py +++ b/superset/migrations/versions/2022-04-04_15-04_b0d0249074e4_deprecate_time_range_endpoints_v2.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2022-04-18_11-20_ad07e4fdbaba_rm_time_range_endpoints_from_qc_3.py b/superset/migrations/versions/2022-04-18_11-20_ad07e4fdbaba_rm_time_range_endpoints_from_qc_3.py index 7b8709838429..16395b48b7bb 100644 --- a/superset/migrations/versions/2022-04-18_11-20_ad07e4fdbaba_rm_time_range_endpoints_from_qc_3.py +++ b/superset/migrations/versions/2022-04-18_11-20_ad07e4fdbaba_rm_time_range_endpoints_from_qc_3.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2022-05-03_19-39_cbe71abde154_fix_report_schedule_and_log.py b/superset/migrations/versions/2022-05-03_19-39_cbe71abde154_fix_report_schedule_and_log.py index 89f9dc3279f6..d15d0a2c2c3b 100644 --- a/superset/migrations/versions/2022-05-03_19-39_cbe71abde154_fix_report_schedule_and_log.py +++ b/superset/migrations/versions/2022-05-03_19-39_cbe71abde154_fix_report_schedule_and_log.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Float, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.reports.models import ReportState # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py b/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py index 194760d2fcf1..b31146814d24 100644 --- a/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py +++ b/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py @@ -33,7 +33,7 @@ from sqlalchemy.ext.declarative import declarative_base # noqa: E402 from sqlalchemy.orm import Session # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2022-07-05_15-48_409c7b420ab0_add_created_by_fk_as_owner.py b/superset/migrations/versions/2022-07-05_15-48_409c7b420ab0_add_created_by_fk_as_owner.py index 57c5af4f2199..7403f252bcce 100644 --- a/superset/migrations/versions/2022-07-05_15-48_409c7b420ab0_add_created_by_fk_as_owner.py +++ b/superset/migrations/versions/2022-07-05_15-48_409c7b420ab0_add_created_by_fk_as_owner.py @@ -27,7 +27,7 @@ from sqlalchemy.ext.declarative import declarative_base # revision identifiers, used by Alembic. -from superset import db +from superset.extensions import db revision = "409c7b420ab0" down_revision = "a39867932713" diff --git a/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py b/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py index ff67fc5f2b9b..672ad66b4a8d 100644 --- a/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py +++ b/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py @@ -25,7 +25,7 @@ from alembic import op from sqlalchemy.dialects.mysql.base import MySQLDialect -from superset import db +from superset.extensions import db from superset.migrations.shared.migrate_viz import MigrateTreeMap # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2022-07-07_14-00_06e1e70058c7_migrating_legacy_area.py b/superset/migrations/versions/2022-07-07_14-00_06e1e70058c7_migrating_legacy_area.py index adeaabac0caf..9fb2c4838a2c 100644 --- a/superset/migrations/versions/2022-07-07_14-00_06e1e70058c7_migrating_legacy_area.py +++ b/superset/migrations/versions/2022-07-07_14-00_06e1e70058c7_migrating_legacy_area.py @@ -24,7 +24,7 @@ from alembic import op -from superset import db +from superset.extensions import db from superset.migrations.shared.migrate_viz import MigrateAreaChart # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2022-08-16_15-23_6d3c6f9d665d_fix_table_chart_conditional_formatting_.py b/superset/migrations/versions/2022-08-16_15-23_6d3c6f9d665d_fix_table_chart_conditional_formatting_.py index dc86babffb9b..cc5cfe6c8a8e 100644 --- a/superset/migrations/versions/2022-08-16_15-23_6d3c6f9d665d_fix_table_chart_conditional_formatting_.py +++ b/superset/migrations/versions/2022-08-16_15-23_6d3c6f9d665d_fix_table_chart_conditional_formatting_.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py b/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py index 147ad8d372d8..9d1e14596003 100644 --- a/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py +++ b/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py @@ -30,9 +30,9 @@ import sqlalchemy as sa # noqa: E402 from alembic import op # noqa: E402 +from flask import current_app as app # noqa: E402 from sqlalchemy_utils import UUIDType # noqa: E402 -from superset import app # noqa: E402 from superset.extensions import encrypted_field_factory # noqa: E402 app_config = app.config diff --git a/superset/migrations/versions/2022-11-28_17-51_4ce1d9b25135_remove_filter_bar_orientation.py b/superset/migrations/versions/2022-11-28_17-51_4ce1d9b25135_remove_filter_bar_orientation.py index 6b4dc27b85cd..04ce61a3c790 100644 --- a/superset/migrations/versions/2022-11-28_17-51_4ce1d9b25135_remove_filter_bar_orientation.py +++ b/superset/migrations/versions/2022-11-28_17-51_4ce1d9b25135_remove_filter_bar_orientation.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-02-28_14-46_c0a3ea245b61_remove_show_native_filters.py b/superset/migrations/versions/2023-02-28_14-46_c0a3ea245b61_remove_show_native_filters.py index 7be7162d2963..249f2da48341 100644 --- a/superset/migrations/versions/2023-02-28_14-46_c0a3ea245b61_remove_show_native_filters.py +++ b/superset/migrations/versions/2023-02-28_14-46_c0a3ea245b61_remove_show_native_filters.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-03-05_10-06_d0ac08bb5b83_invert_horizontal_bar_chart_order.py b/superset/migrations/versions/2023-03-05_10-06_d0ac08bb5b83_invert_horizontal_bar_chart_order.py index f060a596dc89..b29dd007e37b 100644 --- a/superset/migrations/versions/2023-03-05_10-06_d0ac08bb5b83_invert_horizontal_bar_chart_order.py +++ b/superset/migrations/versions/2023-03-05_10-06_d0ac08bb5b83_invert_horizontal_bar_chart_order.py @@ -30,7 +30,7 @@ from sqlalchemy import and_, Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py b/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py index c5bce59ce65f..c0f9f8d8a998 100644 --- a/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py +++ b/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py @@ -30,7 +30,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-03-27_12-30_7e67aecbf3f1_chart_ds_constraint.py b/superset/migrations/versions/2023-03-27_12-30_7e67aecbf3f1_chart_ds_constraint.py index 4507ea7368d5..0b93786fa140 100644 --- a/superset/migrations/versions/2023-03-27_12-30_7e67aecbf3f1_chart_ds_constraint.py +++ b/superset/migrations/versions/2023-03-27_12-30_7e67aecbf3f1_chart_ds_constraint.py @@ -32,7 +32,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-05-01_12-03_9c2a5681ddfd_convert_key_value_entries_to_json.py b/superset/migrations/versions/2023-05-01_12-03_9c2a5681ddfd_convert_key_value_entries_to_json.py index ccc199df5e82..122250763f31 100644 --- a/superset/migrations/versions/2023-05-01_12-03_9c2a5681ddfd_convert_key_value_entries_to_json.py +++ b/superset/migrations/versions/2023-05-01_12-03_9c2a5681ddfd_convert_key_value_entries_to_json.py @@ -34,7 +34,7 @@ from sqlalchemy.ext.declarative import declarative_base # noqa: E402 from sqlalchemy.orm import Session # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 from superset.utils import json # noqa: E402 diff --git a/superset/migrations/versions/2023-05-11_12-41_4ea966691069_cross_filter_global_scoping.py b/superset/migrations/versions/2023-05-11_12-41_4ea966691069_cross_filter_global_scoping.py index 3eb7af7bb9c6..f844e39cd6ca 100644 --- a/superset/migrations/versions/2023-05-11_12-41_4ea966691069_cross_filter_global_scoping.py +++ b/superset/migrations/versions/2023-05-11_12-41_4ea966691069_cross_filter_global_scoping.py @@ -33,7 +33,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 from superset.utils import json # noqa: E402 diff --git a/superset/migrations/versions/2023-06-08_09-02_9ba2ce3086e5_migrate_pivot_table_v1_to_v2.py b/superset/migrations/versions/2023-06-08_09-02_9ba2ce3086e5_migrate_pivot_table_v1_to_v2.py index f7c7a63ae682..894c45e73ce4 100644 --- a/superset/migrations/versions/2023-06-08_09-02_9ba2ce3086e5_migrate_pivot_table_v1_to_v2.py +++ b/superset/migrations/versions/2023-06-08_09-02_9ba2ce3086e5_migrate_pivot_table_v1_to_v2.py @@ -24,7 +24,7 @@ from alembic import op -from superset import db +from superset.extensions import db from superset.migrations.shared.migrate_viz import MigratePivotTable # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py b/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py index e80102b02d77..a1bdf68c7b73 100644 --- a/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py +++ b/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py @@ -25,7 +25,7 @@ from alembic import op from sqlalchemy.dialects.mysql.base import MySQLDialect -from superset import db +from superset.extensions import db from superset.migrations.shared.migrate_viz import MigrateTreeMap # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2023-06-08_11-34_ae58e1e58e5c_migrate_dual_line_to_mixed_chart.py b/superset/migrations/versions/2023-06-08_11-34_ae58e1e58e5c_migrate_dual_line_to_mixed_chart.py index dc8e068af9c2..0cdcd0392a86 100644 --- a/superset/migrations/versions/2023-06-08_11-34_ae58e1e58e5c_migrate_dual_line_to_mixed_chart.py +++ b/superset/migrations/versions/2023-06-08_11-34_ae58e1e58e5c_migrate_dual_line_to_mixed_chart.py @@ -24,7 +24,7 @@ from alembic import op -from superset import db +from superset.extensions import db from superset.migrations.shared.migrate_viz.processors import ( MigrateDualLine, # noqa: E402 ) diff --git a/superset/migrations/versions/2023-07-18_15-30_863adcf72773_delete_obsolete_druid_nosql_slice_parameters.py b/superset/migrations/versions/2023-07-18_15-30_863adcf72773_delete_obsolete_druid_nosql_slice_parameters.py index 1c6e9c2419f3..cf7d592f5329 100644 --- a/superset/migrations/versions/2023-07-18_15-30_863adcf72773_delete_obsolete_druid_nosql_slice_parameters.py +++ b/superset/migrations/versions/2023-07-18_15-30_863adcf72773_delete_obsolete_druid_nosql_slice_parameters.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py b/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py index f728e2067921..70e329edd7a2 100644 --- a/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py +++ b/superset/migrations/versions/2023-07-19_16-48_a23c6f8b1280_cleanup_erroneous_parent_filter_ids.py @@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.utils import json # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-07-19_17-54_ee179a490af9_deckgl_path_width_units.py b/superset/migrations/versions/2023-07-19_17-54_ee179a490af9_deckgl_path_width_units.py index 25042dd6826d..021b0f18569e 100644 --- a/superset/migrations/versions/2023-07-19_17-54_ee179a490af9_deckgl_path_width_units.py +++ b/superset/migrations/versions/2023-07-19_17-54_ee179a490af9_deckgl_path_width_units.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Integer, or_, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.utils import json # revision identifiers, used by Alembic. diff --git a/superset/migrations/versions/2023-08-02_15-23_0769ef90fddd_fix_schema_perm_for_datasets.py b/superset/migrations/versions/2023-08-02_15-23_0769ef90fddd_fix_schema_perm_for_datasets.py index 428677b372ff..ef90d8883667 100644 --- a/superset/migrations/versions/2023-08-02_15-23_0769ef90fddd_fix_schema_perm_for_datasets.py +++ b/superset/migrations/versions/2023-08-02_15-23_0769ef90fddd_fix_schema_perm_for_datasets.py @@ -31,7 +31,7 @@ from sqlalchemy.dialects.sqlite.base import SQLiteDialect # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-08-14_09-38_9f4a086c2676_add_normalize_columns_to_sqla_model.py b/superset/migrations/versions/2023-08-14_09-38_9f4a086c2676_add_normalize_columns_to_sqla_model.py index b79b032a054e..5cc340e092aa 100644 --- a/superset/migrations/versions/2023-08-14_09-38_9f4a086c2676_add_normalize_columns_to_sqla_model.py +++ b/superset/migrations/versions/2023-08-14_09-38_9f4a086c2676_add_normalize_columns_to_sqla_model.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 Base = declarative_base() diff --git a/superset/migrations/versions/2023-09-06_13-18_317970b4400c_added_time_secondary_column_to_.py b/superset/migrations/versions/2023-09-06_13-18_317970b4400c_added_time_secondary_column_to_.py index 5d1012169061..41309e4f7e7e 100755 --- a/superset/migrations/versions/2023-09-06_13-18_317970b4400c_added_time_secondary_column_to_.py +++ b/superset/migrations/versions/2023-09-06_13-18_317970b4400c_added_time_secondary_column_to_.py @@ -30,7 +30,7 @@ from alembic import op # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import ( # noqa: E402 paginated_update, table_has_column, diff --git a/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py b/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py index 7f89235cf2ea..56aa33eec349 100644 --- a/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py +++ b/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py @@ -26,7 +26,7 @@ from sqlalchemy import Column, Integer, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.migrations.shared.utils import paginated_update from superset.utils import json diff --git a/superset/migrations/versions/2024-01-18_14-41_a32e0c4d8646_migrate_sunburst_chart.py b/superset/migrations/versions/2024-01-18_14-41_a32e0c4d8646_migrate_sunburst_chart.py index 1ed8d4662121..f0c8c4b7ff6c 100644 --- a/superset/migrations/versions/2024-01-18_14-41_a32e0c4d8646_migrate_sunburst_chart.py +++ b/superset/migrations/versions/2024-01-18_14-41_a32e0c4d8646_migrate_sunburst_chart.py @@ -28,7 +28,7 @@ from alembic import op # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.migrate_viz import MigrateSunburst # noqa: E402 diff --git a/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py b/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py index 830905e0e014..a7a59c1224cb 100644 --- a/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py +++ b/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py @@ -31,7 +31,7 @@ from sqlalchemy.ext.declarative import declarative_base # noqa: E402 from sqlalchemy.orm import relationship # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.native_filters import migrate_dashboard # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 diff --git a/superset/migrations/versions/2024-03-01_10-47_be1b217cd8cd_big_number_kpi_single_metric.py b/superset/migrations/versions/2024-03-01_10-47_be1b217cd8cd_big_number_kpi_single_metric.py index e09e6fa647f5..810db680a4cb 100644 --- a/superset/migrations/versions/2024-03-01_10-47_be1b217cd8cd_big_number_kpi_single_metric.py +++ b/superset/migrations/versions/2024-03-01_10-47_be1b217cd8cd_big_number_kpi_single_metric.py @@ -31,7 +31,7 @@ from sqlalchemy import Column, Integer, String, Text # noqa: E402 from sqlalchemy.ext.declarative import declarative_base # noqa: E402 -from superset import db # noqa: E402 +from superset.extensions import db # noqa: E402 from superset.migrations.shared.utils import paginated_update # noqa: E402 from superset.utils import json # noqa: E402 diff --git a/superset/migrations/versions/2024-05-10_18-02_f84fde59123a_update_charts_with_old_time_comparison.py b/superset/migrations/versions/2024-05-10_18-02_f84fde59123a_update_charts_with_old_time_comparison.py index dcdc3f0b4487..dc67ecc6e48e 100644 --- a/superset/migrations/versions/2024-05-10_18-02_f84fde59123a_update_charts_with_old_time_comparison.py +++ b/superset/migrations/versions/2024-05-10_18-02_f84fde59123a_update_charts_with_old_time_comparison.py @@ -32,7 +32,7 @@ from sqlalchemy import Column, Integer, or_, String, Text from sqlalchemy.ext.declarative import declarative_base -from superset import db +from superset.extensions import db from superset.migrations.shared.utils import paginated_update from superset.utils import json from superset.utils.date_parser import get_since_until diff --git a/superset/models/__init__.py b/superset/models/__init__.py index 750c61ccc23d..13a83393a912 100644 --- a/superset/models/__init__.py +++ b/superset/models/__init__.py @@ -14,4 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from . import core, dynamic_plugins, sql_lab, user_attributes # noqa: F401 diff --git a/superset/models/core.py b/superset/models/core.py index b933c1694faa..244e99c07c62 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -35,7 +35,7 @@ import pandas as pd import sqlalchemy as sqla import sshtunnel -from flask import g, request +from flask import current_app as app, g, request from flask_appbuilder import Model from sqlalchemy import ( Boolean, @@ -59,7 +59,7 @@ from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql import ColumnElement, expression, Select -from superset import app, db_engine_specs +from superset import db_engine_specs from superset.commands.database.exceptions import DatabaseInvalidError from superset.constants import LRU_CACHE_MAX_SIZE, PASSWORD_MASK from superset.databases.utils import make_url_safe @@ -80,10 +80,6 @@ from superset.utils.core import DatasourceName, get_username from superset.utils.oauth2 import get_oauth2_access_token -config = app.config -custom_password_store = config["SQLALCHEMY_CUSTOM_PASSWORD_STORE"] -stats_logger = config["STATS_LOGGER"] -log_query = config["QUERY_LOGGER"] metadata = Model.metadata # pylint: disable=no-member logger = logging.getLogger(__name__) @@ -91,8 +87,6 @@ from superset.databases.ssh_tunnel.models import SSHTunnel from superset.models.sql_lab import Query -DB_CONNECTION_MUTATOR = config["DB_CONNECTION_MUTATOR"] - class KeyValue(Model): # pylint: disable=too-few-public-methods """Used for any type of key-value store""" @@ -373,6 +367,7 @@ def get_password_masked_url(cls, masked_url: URL) -> URL: def set_sqlalchemy_uri(self, uri: str) -> None: conn = make_url_safe(uri.strip()) + custom_password_store = app.config["SQLALCHEMY_CUSTOM_PASSWORD_STORE"] if conn.password != PASSWORD_MASK and not custom_password_store: # do not over-write the password with the password mask self.password = conn.password @@ -450,7 +445,7 @@ def get_sqla_engine( # pylint: disable=too-many-arguments sqlalchemy_uri=sqlalchemy_uri, ) - def _get_sqla_engine( + def _get_sqla_engine( # pylint: disable=too-many-locals self, catalog: str | None = None, schema: str | None = None, @@ -514,7 +509,7 @@ def _get_sqla_engine( self.update_params_from_encrypted_extra(params) - if DB_CONNECTION_MUTATOR: + if db_conn_mutator := app.config["DB_CONNECTION_MUTATOR"]: if not source and request and request.referrer: if "/superset/dashboard/" in request.referrer: source = utils.QuerySource.DASHBOARD @@ -523,7 +518,7 @@ def _get_sqla_engine( elif "/sqllab/" in request.referrer: source = utils.QuerySource.SQL_LAB - sqlalchemy_url, params = DB_CONNECTION_MUTATOR( + sqlalchemy_url, params = db_conn_mutator( sqlalchemy_url, params, effective_username, @@ -622,8 +617,8 @@ def mutate_sql_based_on_config(self, sql_: str, is_split: bool = False) -> str: on the group of queries as a whole. Here the called passes the context as to whether the SQL is split or already. """ - sql_mutator = config["SQL_QUERY_MUTATOR"] - if sql_mutator and (is_split == config["MUTATE_AFTER_SPLIT"]): + sql_mutator = app.config["SQL_QUERY_MUTATOR"] + if sql_mutator and (is_split == app.config["MUTATE_AFTER_SPLIT"]): return sql_mutator( sql_, security_manager=security_manager, @@ -642,6 +637,8 @@ def get_df( # pylint: disable=too-many-locals with self.get_sqla_engine(catalog=catalog, schema=schema) as engine: engine_url = engine.url + log_query = app.config["QUERY_LOGGER"] + def _log_query(sql: str) -> None: if log_query: log_query( @@ -978,7 +975,7 @@ def get_schema_access_for_file_upload( # pylint: disable=invalid-name allowed_databases = literal_eval(allowed_databases) if hasattr(g, "user"): - extra_allowed_databases = config["ALLOWED_USER_CSV_SCHEMA_FUNC"]( + extra_allowed_databases = app.config["ALLOWED_USER_CSV_SCHEMA_FUNC"]( self, g.user ) allowed_databases += extra_allowed_databases @@ -992,7 +989,8 @@ def sqlalchemy_uri_decrypted(self) -> str: # if the URI is invalid, ignore and return a placeholder url # (so users see 500 less often) return "dialect://invalid_uri" - if custom_password_store: + + if custom_password_store := app.config["SQLALCHEMY_CUSTOM_PASSWORD_STORE"]: conn = conn.set(password=custom_password_store(conn)) else: conn = conn.set(password=self.password) @@ -1075,11 +1073,6 @@ def get_oauth2_config(self) -> OAuth2ClientConfig | None: return self.db_engine_spec.get_oauth2_config() -sqla.event.listen(Database, "after_insert", security_manager.database_after_insert) -sqla.event.listen(Database, "after_update", security_manager.database_after_update) -sqla.event.listen(Database, "after_delete", security_manager.database_after_delete) - - class DatabaseUserOAuth2Tokens(Model, AuditMixinNullable): """ Store OAuth2 tokens, for authenticating to DBs using user personal tokens. @@ -1095,7 +1088,7 @@ class DatabaseUserOAuth2Tokens(Model, AuditMixinNullable): ForeignKey("ab_user.id", ondelete="CASCADE"), nullable=False, ) - user = relationship(security_manager.user_model, foreign_keys=[user_id]) + user = relationship("User", foreign_keys=[user_id]) database_id = Column( Integer, @@ -1120,9 +1113,7 @@ class Log(Model): # pylint: disable=too-few-public-methods dashboard_id = Column(Integer) slice_id = Column(Integer) json = Column(utils.MediumText()) - user = relationship( - security_manager.user_model, backref="logs", foreign_keys=[user_id] - ) + user = relationship("User", backref="logs", foreign_keys=[user_id]) dttm = Column(DateTime, default=datetime.utcnow) duration_ms = Column(Integer) referrer = Column(String(1024)) diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 6e6989bf9e95..b0f15df6d090 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -22,6 +22,7 @@ from typing import Any, Callable import sqlalchemy as sqla +from flask import current_app as app from flask_appbuilder import Model from flask_appbuilder.models.decorators import renders from flask_appbuilder.security.sqla.models import User @@ -41,9 +42,14 @@ from sqlalchemy.orm.mapper import Mapper from sqlalchemy.sql.elements import BinaryExpression -from superset import app, db, is_feature_enabled, security_manager from superset.connectors.sqla.models import BaseDatasource, SqlaTable from superset.daos.datasource import DatasourceDAO +from superset.extensions import db, feature_flag_manager, security_manager + +# Import required for sqlalchemy's order of operations +from superset.models.embedded_dashboard import ( # pylint: disable=unused-import # noqa + EmbeddedDashboard, +) from superset.models.helpers import AuditMixinNullable, ImportExportMixin from superset.models.slice import Slice from superset.models.user_attributes import UserAttribute @@ -53,12 +59,11 @@ from superset.utils import core as utils, json metadata = Model.metadata # pylint: disable=no-member -config = app.config logger = logging.getLogger(__name__) def copy_dashboard(_mapper: Mapper, _connection: Connection, target: Dashboard) -> None: - dashboard_id = config["DASHBOARD_TEMPLATE_ID"] + dashboard_id = app.config["DASHBOARD_TEMPLATE_ID"] if dashboard_id is None: return @@ -144,7 +149,7 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model): Slice, secondary=dashboard_slices, backref="dashboards" ) owners = relationship( - security_manager.user_model, + "User", secondary=dashboard_user, passive_deletes=True, ) @@ -159,7 +164,7 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model): published = Column(Boolean, default=False) is_managed_externally = Column(Boolean, nullable=False, default=False) external_url = Column(Text, nullable=True) - roles = relationship(security_manager.role_model, secondary=DashboardRoles) + roles = relationship("Role", secondary=DashboardRoles) embedded = relationship( "EmbeddedDashboard", back_populates="dashboard", @@ -416,7 +421,7 @@ def id_or_slug_filter(id_or_slug: int | str) -> BinaryExpression: OnDashboardChange = Callable[[Mapper, Connection, Dashboard], Any] -if is_feature_enabled("THUMBNAILS_SQLA_LISTENERS"): +if feature_flag_manager.is_feature_enabled("THUMBNAILS_SQLA_LISTENERS"): update_thumbnail: OnDashboardChange = lambda _, __, dash: dash.update_thumbnail() # noqa: E731 sqla.event.listen(Dashboard, "after_insert", update_thumbnail) sqla.event.listen(Dashboard, "after_update", update_thumbnail) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index dffb285663d4..84b5fe89e296 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -35,7 +35,7 @@ import sqlalchemy as sa import sqlparse import yaml -from flask import g +from flask import current_app as app, g from flask_appbuilder import Model from flask_appbuilder.models.decorators import renders from flask_appbuilder.models.mixins import AuditMixin @@ -52,7 +52,6 @@ from sqlalchemy.sql.selectable import Alias, TableClause from sqlalchemy_utils import UUIDType -from superset import app, db, is_feature_enabled, security_manager from superset.advanced_data_type.types import AdvancedDataTypeResponse from superset.common.db_query_status import QueryStatus from superset.common.utils.time_range_utils import get_since_until_from_time_range @@ -67,7 +66,7 @@ SupersetParseError, SupersetSecurityException, ) -from superset.extensions import feature_flag_manager +from superset.extensions import db, feature_flag_manager, security_manager from superset.jinja_context import BaseTemplateProcessor from superset.sql_parse import ( has_table_query, @@ -103,12 +102,10 @@ from superset.models.core import Database -config = app.config logger = logging.getLogger(__name__) VIRTUAL_TABLE_ALIAS = "virtual_table" SERIES_LIMIT_SUBQ_ALIAS = "series_limit" -ADVANCED_DATA_TYPES = config["ADVANCED_DATA_TYPES"] def validate_adhoc_subquery( @@ -129,7 +126,7 @@ def validate_adhoc_subquery( statements = [] for statement in sqlparse.parse(sql): if has_table_query(statement): - if not is_feature_enabled("ALLOW_ADHOC_SUBQUERY"): + if not feature_flag_manager.is_feature_enabled("ALLOW_ADHOC_SUBQUERY"): raise SupersetSecurityException( SupersetError( error_type=SupersetErrorType.ADHOC_SUBQUERY_NOT_ALLOWED_ERROR, @@ -827,7 +824,7 @@ def get_sqla_row_level_filters( else: all_filters.append(clause) - if is_feature_enabled("EMBEDDED_SUPERSET"): + if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): for rule in security_manager.get_guest_rls_filters(self): clause = self.text( f"({template_processor.process_template(rule['clause'])})" @@ -1820,12 +1817,12 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma and feature_flag_manager.is_feature_enabled( "ENABLE_ADVANCED_DATA_TYPES" ) - and col_advanced_data_type in ADVANCED_DATA_TYPES + and col_advanced_data_type in app.config["ADVANCED_DATA_TYPES"] ): values = eq if is_list_target else [eq] # type: ignore - bus_resp: AdvancedDataTypeResponse = ADVANCED_DATA_TYPES[ - col_advanced_data_type - ].translate_type( + bus_resp: AdvancedDataTypeResponse = app.config[ + "ADVANCED_DATA_TYPES" + ][col_advanced_data_type].translate_type( { "type": col_advanced_data_type, "values": values, @@ -1837,9 +1834,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma ) where_clause_and.append( - ADVANCED_DATA_TYPES[col_advanced_data_type].translate_filter( - sqla_col, op, bus_resp["values"] - ) + app.config["ADVANCED_DATA_TYPES"][ + col_advanced_data_type + ].translate_filter(sqla_col, op, bus_resp["values"]) ) elif is_list_target: assert isinstance(eq, (tuple, list)) diff --git a/superset/models/slice.py b/superset/models/slice.py index bc89b5b7c468..858910e017d6 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -38,14 +38,14 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.mapper import Mapper -from superset import db, is_feature_enabled, security_manager +from superset.extensions import db, feature_flag_manager from superset.legacy import update_time_range from superset.models.helpers import AuditMixinNullable, ImportExportMixin from superset.tasks.thumbnails import cache_chart_thumbnail from superset.tasks.utils import get_current_user from superset.thumbnails.digest import get_chart_digest from superset.utils import core as utils, json -from superset.viz import BaseViz, viz_types +from superset.viz import BaseViz, get_viz_types if TYPE_CHECKING: from superset.common.query_context import QueryContext @@ -92,11 +92,9 @@ class Slice( # pylint: disable=too-many-public-methods certification_details = Column(Text) is_managed_externally = Column(Boolean, nullable=False, default=False) external_url = Column(Text, nullable=True) - last_saved_by = relationship( - security_manager.user_model, foreign_keys=[last_saved_by_fk] - ) + last_saved_by = relationship("User", foreign_keys=[last_saved_by_fk]) owners = relationship( - security_manager.user_model, + "User", secondary=slice_user, passive_deletes=True, ) @@ -203,7 +201,7 @@ def datasource_edit_url(self) -> str | None: @property def viz(self) -> BaseViz | None: form_data = json.loads(self.params) - viz_class = viz_types.get(self.viz_type) + viz_class = get_viz_types().get(self.viz_type) datasource = self.datasource if viz_class and datasource: return viz_class(datasource=datasource, form_data=form_data) @@ -384,6 +382,8 @@ def event_after_chart_changed( sqla.event.listen(Slice, "before_insert", set_related_perm) sqla.event.listen(Slice, "before_update", set_related_perm) -if is_feature_enabled("THUMBNAILS_SQLA_LISTENERS"): +thumbs_on: bool = feature_flag_manager.is_feature_enabled("THUMBNAILS_SQLA_LISTENERS") + +if thumbs_on: sqla.event.listen(Slice, "after_insert", event_after_chart_changed) sqla.event.listen(Slice, "after_update", event_after_chart_changed) diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index 3c26c6c12fb8..ca02ee173ac9 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -16,7 +16,7 @@ # under the License. """A collection of ORM sqlalchemy models for SQL Lab""" -import builtins +import builtins # noqa: I001 import inspect import logging import re @@ -47,8 +47,8 @@ from sqlalchemy.orm import backref, relationship from sqlalchemy.sql.elements import ColumnElement, literal_column -from superset import security_manager from superset.exceptions import SupersetSecurityException +from superset.extensions import security_manager from superset.jinja_context import BaseTemplateProcessor, get_template_processor from superset.models.helpers import ( AuditMixinNullable, @@ -67,6 +67,9 @@ user_label, ) +# Import required for sqlalchemy's order of operations +from superset.tags.models import TaggedObject # pylint: disable=unused-import # noqa + if TYPE_CHECKING: from superset.connectors.sqla.models import TableColumn from superset.db_engine_specs import BaseEngineSpec @@ -103,7 +106,7 @@ class Query( __tablename__ = "query" type = "query" id = Column(Integer, primary_key=True) - client_id = Column(String(11), unique=True, nullable=False) + client_id = Column(Integer, unique=True, nullable=False) query_language = "sql" database_id = Column(Integer, ForeignKey("dbs.id"), nullable=False) @@ -154,7 +157,7 @@ class Query( foreign_keys=[database_id], backref=backref("queries", cascade="all, delete-orphan"), ) - user = relationship(security_manager.user_model, foreign_keys=[user_id]) + user = relationship("User", foreign_keys=[user_id]) __table_args__ = (sqla.Index("ti_user_id_changed_on", user_id, changed_on),) @@ -401,7 +404,7 @@ class SavedQuery( sql = Column(MediumText()) template_parameters = Column(Text) user = relationship( - security_manager.user_model, + "User", backref=backref("saved_queries", cascade="all, delete-orphan"), foreign_keys=[user_id], ) @@ -503,7 +506,9 @@ class TabState(AuditMixinNullable, ExtraJSONMixin, Model): latest_query_id = Column( Integer, ForeignKey("query.client_id", ondelete="SET NULL") ) - latest_query = relationship("Query") + latest_query = relationship( + "Query", primaryjoin="TabState.latest_query_id == Query.client_id" + ) # other properties autorun = Column(Boolean, default=False) diff --git a/superset/models/user_attributes.py b/superset/models/user_attributes.py index 512270c89ce8..f5438ccc6ec8 100644 --- a/superset/models/user_attributes.py +++ b/superset/models/user_attributes.py @@ -19,7 +19,6 @@ from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship -from superset import security_manager from superset.models.helpers import AuditMixinNullable @@ -36,9 +35,7 @@ class UserAttribute(Model, AuditMixinNullable): __tablename__ = "user_attribute" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("ab_user.id")) - user = relationship( - security_manager.user_model, backref="extra_attributes", foreign_keys=[user_id] - ) + user = relationship("User", backref="extra_attributes", foreign_keys=[user_id]) welcome_dashboard_id = Column(Integer, ForeignKey("dashboards.id")) welcome_dashboard = relationship("Dashboard") avatar_url = Column(String(100)) diff --git a/superset/queries/api.py b/superset/queries/api.py index 0695946fe07f..028c197865b2 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -21,11 +21,11 @@ from flask_appbuilder.api import expose, protect, request, rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface -from superset import db, event_logger from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.daos.query import QueryDAO from superset.databases.filters import DatabaseFilter from superset.exceptions import SupersetException +from superset.extensions import db, event_logger from superset.models.sql_lab import Query from superset.queries.filters import QueryFilter from superset.queries.schemas import ( diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py index cd7b04193ff8..113f36724a2a 100644 --- a/superset/queries/saved_queries/api.py +++ b/superset/queries/saved_queries/api.py @@ -25,7 +25,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import ngettext -from superset import is_feature_enabled from superset.commands.importers.exceptions import ( IncorrectFormatError, NoValidFilesFoundError, @@ -40,7 +39,7 @@ from superset.commands.query.importers.dispatcher import ImportSavedQueriesCommand from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.databases.filters import DatabaseFilter -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.models.sql_lab import SavedQuery from superset.queries.saved_queries.filters import ( SavedQueryAllTextFilter, @@ -125,7 +124,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi): "sql", "sql_tables", ] - if is_feature_enabled("TAGGING_SYSTEM"): + if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): list_columns += ["tags.id", "tags.name", "tags.type"] list_select_columns = list_columns + ["changed_by_fk", "changed_on"] add_columns = [ @@ -162,13 +161,13 @@ class SavedQueryRestApi(BaseSupersetModelRestApi): "created_by", "changed_by", ] - if is_feature_enabled("TAGGING_SYSTEM"): + if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): search_columns += ["tags"] search_filters = { "id": [SavedQueryFavoriteFilter], "label": [SavedQueryAllTextFilter], } - if is_feature_enabled("TAGGING_SYSTEM"): + if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): search_filters["tags"] = [SavedQueryTagFilter] apispec_parameter_schemas = { diff --git a/superset/reports/api.py b/superset/reports/api.py index 4a298b564dd8..a7772bda9f81 100644 --- a/superset/reports/api.py +++ b/superset/reports/api.py @@ -24,7 +24,6 @@ from flask_babel import ngettext from marshmallow import ValidationError -from superset import is_feature_enabled from superset.charts.filters import ChartFilter from superset.commands.report.create import CreateReportScheduleCommand from superset.commands.report.delete import DeleteReportScheduleCommand @@ -40,7 +39,7 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.dashboards.filters import DashboardAccessFilter from superset.databases.filters import DatabaseFilter -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.reports.filters import ReportScheduleAllTextFilter, ReportScheduleFilter from superset.reports.models import ReportSchedule from superset.reports.schemas import ( @@ -65,7 +64,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi): @before_request def ensure_alert_reports_enabled(self) -> Optional[Response]: - if not is_feature_enabled("ALERT_REPORTS"): + if not feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): return self.response_404() return None diff --git a/superset/reports/filters.py b/superset/reports/filters.py index a03238b64084..3c06052a0b61 100644 --- a/superset/reports/filters.py +++ b/superset/reports/filters.py @@ -20,7 +20,7 @@ from sqlalchemy import or_ from sqlalchemy.orm.query import Query -from superset import db, security_manager +from superset.extensions import db, security_manager from superset.reports.models import ReportSchedule from superset.views.base import BaseFilter diff --git a/superset/reports/logs/api.py b/superset/reports/logs/api.py index db307039bfb5..3565305dd703 100644 --- a/superset/reports/logs/api.py +++ b/superset/reports/logs/api.py @@ -23,8 +23,8 @@ from flask_appbuilder.hooks import before_request from flask_appbuilder.models.sqla.interface import SQLAInterface -from superset import is_feature_enabled from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.extensions import feature_flag_manager from superset.reports.logs.schemas import openapi_spec_methods_override from superset.reports.models import ReportExecutionLog from superset.views.base_api import BaseSupersetModelRestApi @@ -37,7 +37,7 @@ class ReportExecutionLogRestApi(BaseSupersetModelRestApi): @before_request def ensure_alert_reports_enabled(self) -> Optional[Response]: - if not is_feature_enabled("ALERT_REPORTS"): + if not feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): return self.response_404() return None diff --git a/superset/reports/models.py b/superset/reports/models.py index 3627a2ebf46e..a4bb048e8937 100644 --- a/superset/reports/models.py +++ b/superset/reports/models.py @@ -35,7 +35,6 @@ from sqlalchemy.schema import UniqueConstraint from sqlalchemy_utils import UUIDType -from superset.extensions import security_manager from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.helpers import AuditMixinNullable, ExtraJSONMixin @@ -143,7 +142,7 @@ class ReportSchedule(AuditMixinNullable, ExtraJSONMixin, Model): database_id = Column(Integer, ForeignKey("dbs.id"), nullable=True) database = relationship(Database, foreign_keys=[database_id]) owners = relationship( - security_manager.user_model, + "User", secondary=report_schedule_user, passive_deletes=True, ) diff --git a/superset/reports/notifications/email.py b/superset/reports/notifications/email.py index d9ac6dca22a2..6f46536f8074 100644 --- a/superset/reports/notifications/email.py +++ b/superset/reports/notifications/email.py @@ -21,9 +21,9 @@ from typing import Any, Optional import nh3 +from flask import current_app as app from flask_babel import gettext as __ -from superset import app from superset.exceptions import SupersetErrorsException from superset.reports.models import ReportRecipientType from superset.reports.notifications.base import BaseNotification diff --git a/superset/security/manager.py b/superset/security/manager.py index 722ac363a072..a8ca93a1db64 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -58,6 +58,7 @@ DatasetInvalidPermissionEvaluationException, SupersetSecurityException, ) +from superset.extensions import feature_flag_manager from superset.security.guest_token import ( GuestToken, GuestTokenResources, @@ -340,7 +341,6 @@ def create_login_manager(self, app: Flask) -> LoginManager: def request_loader(self, request: Request) -> Optional[User]: # pylint: disable=import-outside-toplevel - from superset.extensions import feature_flag_manager if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): return self.get_guest_user_from_request(request) @@ -2157,7 +2157,6 @@ def raise_for_access( """ # pylint: disable=import-outside-toplevel - from superset import is_feature_enabled from superset.connectors.sqla.models import SqlaTable from superset.models.dashboard import Dashboard from superset.models.slice import Slice @@ -2290,9 +2289,12 @@ def raise_for_access( .one_or_none() ) and ( - (is_feature_enabled("DASHBOARD_RBAC") and dashboard_.roles) + ( + feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC") + and dashboard_.roles + ) or ( - is_feature_enabled("EMBEDDED_SUPERSET") + feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET") and self.is_guest_user() ) ) @@ -2352,7 +2354,10 @@ def raise_for_access( # DASHBOARD_RBAC logic - Manage dashboard access through roles. # Only applicable in case the dashboard has roles set. - if is_feature_enabled("DASHBOARD_RBAC") and dashboard.roles: + if ( + feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC") + and dashboard.roles + ): if dashboard.published and {role.id for role in dashboard.roles} & { role.id for role in self.get_user_roles() }: @@ -2628,9 +2633,8 @@ def parse_jwt_guest_token(self, raw_token: str) -> dict[str, Any]: @staticmethod def is_guest_user(user: Optional[Any] = None) -> bool: # pylint: disable=import-outside-toplevel - from superset import is_feature_enabled - if not is_feature_enabled("EMBEDDED_SUPERSET"): + if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): return False if not user: user = g.user @@ -2674,7 +2678,7 @@ def raise_for_ownership(self, resource: Model) -> None: """ # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db if self.is_admin(): return diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 0e77c5a90231..f4026f6b4c05 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -25,12 +25,10 @@ import backoff import msgpack from celery.exceptions import SoftTimeLimitExceeded +from flask import current_app as app from flask_babel import gettext as __ from superset import ( - app, - db, - is_feature_enabled, results_backend, results_backend_use_msgpack, security_manager, @@ -45,7 +43,7 @@ SupersetErrorException, SupersetErrorsException, ) -from superset.extensions import celery_app, event_logger +from superset.extensions import celery_app, db, event_logger, feature_flag_manager from superset.models.core import Database from superset.models.sql_lab import Query from superset.result_set import SupersetResultSet @@ -67,13 +65,6 @@ from superset.utils.dates import now_as_float from superset.utils.decorators import stats_timing -config = app.config -stats_logger = config["STATS_LOGGER"] -SQLLAB_TIMEOUT = config["SQLLAB_ASYNC_TIME_LIMIT_SEC"] -SQLLAB_HARD_TIMEOUT = SQLLAB_TIMEOUT + 60 -SQL_MAX_ROW = config["SQL_MAX_ROW"] -SQLLAB_CTAS_NO_LIMIT = config["SQLLAB_CTAS_NO_LIMIT"] -log_query = config["QUERY_LOGGER"] logger = logging.getLogger(__name__) @@ -121,7 +112,7 @@ def handle_query_error( db.session.commit() payload.update({"status": query.status, "error": msg, "errors": errors_payload}) - if troubleshooting_link := config["TROUBLESHOOTING_LINK"]: + if troubleshooting_link := app.config["TROUBLESHOOTING_LINK"]: payload["link"] = troubleshooting_link return payload @@ -131,14 +122,14 @@ def get_query_backoff_handler(details: dict[Any, Any]) -> None: logger.error( "Query with id `%s` could not be retrieved", str(query_id), exc_info=True ) - stats_logger.incr(f"error_attempting_orm_query_{details['tries'] - 1}") + app.stats_logger.incr(f"error_attempting_orm_query_{details['tries'] - 1}") logger.error( "Query %s: Sleeping for a sec before retrying...", str(query_id), exc_info=True ) def get_query_giveup_handler(_: Any) -> None: - stats_logger.incr("error_failed_at_getting_orm_query") + app.stats_logger.incr("error_failed_at_getting_orm_query") @backoff.on_exception( @@ -157,11 +148,7 @@ def get_query(query_id: int) -> Query: raise SqlLabException("Failed at getting query") from ex -@celery_app.task( - name="sql_lab.get_sql_results", - time_limit=SQLLAB_HARD_TIMEOUT, - soft_time_limit=SQLLAB_TIMEOUT, -) +@celery_app.task(name="sql_lab.get_sql_results") def get_sql_results( # pylint: disable=too-many-arguments query_id: int, rendered_query: str, @@ -186,12 +173,12 @@ def get_sql_results( # pylint: disable=too-many-arguments ) except Exception as ex: # pylint: disable=broad-except logger.debug("Query %d: %s", query_id, ex) - stats_logger.incr("error_sqllab_unhandled") + app.stats_logger.incr("error_sqllab_unhandled") query = get_query(query_id) return handle_query_error(ex, query) -def execute_sql_statement( # pylint: disable=too-many-statements +def execute_sql_statement( # pylint: disable=too-many-statements,too-many-locals sql_statement: str, query: Query, cursor: Any, @@ -203,7 +190,7 @@ def execute_sql_statement( # pylint: disable=too-many-statements db_engine_spec = database.db_engine_spec parsed_query = ParsedQuery(sql_statement, engine=db_engine_spec.engine) - if is_feature_enabled("RLS_IN_SQLLAB"): + if feature_flag_manager.is_feature_enabled("RLS_IN_SQLLAB"): # There are two ways to insert RLS: either replacing the table with a subquery # that has the RLS, or appending the RLS to the ``WHERE`` clause. The former is # safer, but not supported in all databases. @@ -256,16 +243,18 @@ def execute_sql_statement( # pylint: disable=too-many-statements # Do not apply limit to the CTA queries when SQLLAB_CTAS_NO_LIMIT is set to true if db_engine_spec.is_select_query(parsed_query) and not ( - query.select_as_cta_used and SQLLAB_CTAS_NO_LIMIT + query.select_as_cta_used and app.config["SQLLAB_CTAS_NO_LIMIT"] ): - if SQL_MAX_ROW and (not query.limit or query.limit > SQL_MAX_ROW): - query.limit = SQL_MAX_ROW + max_row = app.config["SQL_MAX_ROW"] + if max_row and (not query.limit or query.limit > max_row): + query.limit = max_row sql = apply_limit_if_exists(database, increased_limit, query, sql) # Hook to allow environment-specific mutation (usually comments) to the SQL sql = database.mutate_sql_based_on_config(sql) try: query.executed_sql = sql + log_query = app.config["QUERY_LOGGER"] if log_query: log_query( query.database.sqlalchemy_uri, @@ -281,10 +270,10 @@ def execute_sql_statement( # pylint: disable=too-many-statements database=database, object_ref=__name__, ): - with stats_timing("sqllab.query.time_executing_query", stats_logger): + with stats_timing("sqllab.query.time_executing_query", app.stats_logger): db_engine_spec.execute_with_cursor(cursor, sql, query) - with stats_timing("sqllab.query.time_fetching_results", stats_logger): + with stats_timing("sqllab.query.time_fetching_results", app.stats_logger): logger.debug( "Query %d: Fetching data for query object: %s", query.id, @@ -306,7 +295,7 @@ def execute_sql_statement( # pylint: disable=too-many-statements message=__( "The query was killed after %(sqllab_timeout)s seconds. It might " "be too complex, or the database might be under heavy load.", - sqllab_timeout=SQLLAB_TIMEOUT, + sqllab_timeout=app.config["SQLLAB_ASYNC_TIME_LIMIT_SEC"], ), error_type=SupersetErrorType.SQLLAB_TIMEOUT_ERROR, level=ErrorLevel.ERROR, @@ -365,7 +354,7 @@ def _serialize_and_expand_data( if use_msgpack: with stats_timing( - "sqllab.query.results_backend_pa_serialization", stats_logger + "sqllab.query.results_backend_pa_serialization", app.stats_logger ): data = write_ipc_buffer(result_set.pa_table).to_pybytes() @@ -399,7 +388,9 @@ def execute_sql_statements( """Executes the sql query returns the results.""" if store_results and start_time: # only asynchronous queries - stats_logger.timing("sqllab.query.time_pending", now_as_float() - start_time) + app.stats_logger.timing( + "sqllab.query.time_pending", now_as_float() - start_time + ) query = get_query(query_id) payload: dict[str, Any] = {"query_id": query_id} @@ -574,16 +565,16 @@ def execute_sql_statements( logger.info( "Query %s: Storing results in results backend, key: %s", str(query_id), key ) - with stats_timing("sqllab.query.results_backend_write", stats_logger): + with stats_timing("sqllab.query.results_backend_write", app.stats_logger): with stats_timing( - "sqllab.query.results_backend_write_serialization", stats_logger + "sqllab.query.results_backend_write_serialization", app.stats_logger ): serialized_payload = _serialize_payload( payload, cast(bool, results_backend_use_msgpack) ) cache_timeout = database.cache_timeout if cache_timeout is None: - cache_timeout = config["CACHE_DEFAULT_TIMEOUT"] + cache_timeout = app.config["CACHE_DEFAULT_TIMEOUT"] compressed = zlib_compress(serialized_payload) logger.debug( diff --git a/superset/sql_parse.py b/superset/sql_parse.py index f32647042b0a..ddedc03e3455 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -1203,8 +1203,8 @@ def get_rls_for_table( Given a table name, return any associated RLS predicates. """ # pylint: disable=import-outside-toplevel - from superset import db from superset.connectors.sqla.models import SqlaTable + from superset.extensions import db if not isinstance(candidate, Identifier): candidate = Identifier([Token(Name, candidate.value)]) diff --git a/superset/sql_validators/presto_db.py b/superset/sql_validators/presto_db.py index 06bee217cf22..7bb65d2b0f9d 100644 --- a/superset/sql_validators/presto_db.py +++ b/superset/sql_validators/presto_db.py @@ -22,7 +22,6 @@ from contextlib import closing from typing import Any -from superset import app from superset.models.core import Database from superset.sql_parse import ParsedQuery from superset.sql_validators.base import BaseSQLValidator, SQLValidationAnnotation @@ -30,7 +29,6 @@ MAX_ERROR_ROWS = 10 -config = app.config logger = logging.getLogger(__name__) diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py index cdb331c19bc3..f2a7751c122f 100644 --- a/superset/sqllab/api.py +++ b/superset/sqllab/api.py @@ -18,13 +18,12 @@ from typing import Any, cast, Optional from urllib import parse -from flask import request, Response +from flask import current_app as app, request, Response from flask_appbuilder import permission_name from flask_appbuilder.api import expose, protect, rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import ValidationError -from superset import app, is_feature_enabled from superset.commands.sql_lab.estimate import QueryEstimationCommand from superset.commands.sql_lab.execute import CommandResult, ExecuteSqlCommand from superset.commands.sql_lab.export import SqlResultExportCommand @@ -32,7 +31,7 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.daos.database import DatabaseDAO from superset.daos.query import QueryDAO -from superset.extensions import event_logger +from superset.extensions import event_logger, feature_flag_manager from superset.jinja_context import get_template_processor from superset.models.sql_lab import Query from superset.sql_lab import get_sql_results @@ -65,7 +64,6 @@ from superset.views.base import CsvResponse, generate_download_headers, json_success from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics -config = app.config logger = logging.getLogger(__name__) @@ -432,7 +430,7 @@ def _create_sql_json_command( ) execution_context_convertor = ExecutionContextConvertor() execution_context_convertor.set_max_row_in_display( - int(config.get("DISPLAY_MAX_ROW")) + int(app.config.get("DISPLAY_MAX_ROW")) ) return ExecuteSqlCommand( execution_context, @@ -442,7 +440,7 @@ def _create_sql_json_command( SqlQueryRenderImpl(get_template_processor), sql_json_executor, execution_context_convertor, - config["SQLLAB_CTAS_NO_LIMIT"], + app.config["SQLLAB_CTAS_NO_LIMIT"], log_params, ) @@ -457,7 +455,7 @@ def _create_sql_json_executor( sql_json_executor = SynchronousSqlJsonExecutor( query_dao, get_sql_results, - config.get("SQLLAB_TIMEOUT"), - is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"), + app.config.get("SQLLAB_TIMEOUT"), + feature_flag_manager.is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"), ) return sql_json_executor diff --git a/superset/sqllab/query_render.py b/superset/sqllab/query_render.py index caf9a3cb2b20..6f51e6af5331 100644 --- a/superset/sqllab/query_render.py +++ b/superset/sqllab/query_render.py @@ -23,9 +23,9 @@ from jinja2 import TemplateError from jinja2.meta import find_undeclared_variables -from superset import is_feature_enabled from superset.commands.sql_lab.execute import SqlQueryRender from superset.errors import SupersetErrorType +from superset.extensions import feature_flag_manager from superset.sql_parse import ParsedQuery from superset.sqllab.exceptions import SqlLabException from superset.utils import core as utils @@ -78,7 +78,7 @@ def _validate( rendered_query: str, sql_template_processor: BaseTemplateProcessor, ) -> None: - if is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"): + if feature_flag_manager.is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"): syntax_tree = sql_template_processor.env.parse(rendered_query) undefined_parameters = find_undeclared_variables(syntax_tree) if undefined_parameters: diff --git a/superset/sqllab/sql_json_executer.py b/superset/sqllab/sql_json_executer.py index 246154f47b77..adb00dbd5859 100644 --- a/superset/sqllab/sql_json_executer.py +++ b/superset/sqllab/sql_json_executer.py @@ -22,6 +22,7 @@ from abc import ABC from typing import Any, Callable, TYPE_CHECKING +from flask import current_app as app from flask_babel import gettext as __ from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -176,6 +177,8 @@ def execute( start_time=now_as_float(), expand_data=execution_context.expand_data, log_params=log_params, + time_limit=app.config["SQLLAB_ASYNC_TIME_LIMIT_SEC"] + 60, + soft_time_limit=app.config["SQLLAB_ASYNC_TIME_LIMIT_SEC"], ) try: task.forget() diff --git a/superset/sqllab/sqllab_execution_context.py b/superset/sqllab/sqllab_execution_context.py index f67e2bf5a99a..65d35b6565ef 100644 --- a/superset/sqllab/sqllab_execution_context.py +++ b/superset/sqllab/sqllab_execution_context.py @@ -24,7 +24,7 @@ from flask import g from sqlalchemy.orm.exc import DetachedInstanceError -from superset import is_feature_enabled +from superset.extensions import feature_flag_manager from superset.models.sql_lab import Query from superset.sql_parse import CtasMethod from superset.utils import core as utils, json @@ -87,7 +87,7 @@ def _init_from_query_params(self, query_params: dict[str, Any]) -> None: self.tab_name = cast(str, query_params.get("tab")) self.expand_data: bool = cast( bool, - is_feature_enabled("PRESTO_EXPAND_DATA") + feature_flag_manager.is_feature_enabled("PRESTO_EXPAND_DATA") and query_params.get("expand_data"), ) diff --git a/superset/sqllab/utils.py b/superset/sqllab/utils.py index 65b87bbf6e04..d15c07603f7d 100644 --- a/superset/sqllab/utils.py +++ b/superset/sqllab/utils.py @@ -20,9 +20,9 @@ import pyarrow as pa -from superset import db, is_feature_enabled from superset.common.db_query_status import QueryStatus from superset.daos.database import DatabaseDAO +from superset.extensions import db, feature_flag_manager from superset.models.sql_lab import TabState DATABASE_KEYS = [ @@ -91,7 +91,7 @@ def bootstrap_sqllab_data(user_id: int | None) -> dict[str, Any]: databases[database.id]["backend"] = database.backend # These are unnecessary if sqllab backend persistence is disabled - if is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"): + if feature_flag_manager.is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"): # send list of tab state ids tabs_state = ( db.session.query(TabState.id, TabState.label) diff --git a/superset/tables/models.py b/superset/tables/models.py index 2616aaf90f6b..a6bb0d7e0573 100644 --- a/superset/tables/models.py +++ b/superset/tables/models.py @@ -31,7 +31,6 @@ from flask_appbuilder import Model from sqlalchemy import inspect from sqlalchemy.orm import backref, relationship, Session -from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql import and_, or_ from superset.columns.models import Column @@ -75,7 +74,7 @@ class Table(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model): # not exist in the migrations. The reason it does not physically exist is MySQL, # PostgreSQL, etc. have a different interpretation of uniqueness when it comes to NULL # which is problematic given the catalog and schema are optional. - __table_args__ = (UniqueConstraint("database_id", "catalog", "schema", "name"),) + # __table_args__ = (UniqueConstraint("database_id", "catalog", "schema", "name"),) id = sa.Column(sa.Integer, primary_key=True) database_id = sa.Column(sa.Integer, sa.ForeignKey("dbs.id"), nullable=False) diff --git a/superset/tags/models.py b/superset/tags/models.py index ba859f519bf4..0472aecc658e 100644 --- a/superset/tags/models.py +++ b/superset/tags/models.py @@ -37,7 +37,6 @@ from sqlalchemy.orm.mapper import Mapper from sqlalchemy.schema import UniqueConstraint -from superset import security_manager from superset.models.helpers import AuditMixinNullable if TYPE_CHECKING: @@ -54,6 +53,7 @@ Model.metadata, # pylint: disable=no-member Column("user_id", Integer, ForeignKey("ab_user.id")), Column("tag_id", Integer, ForeignKey("tag.id")), + extend_existing=True, ) @@ -99,9 +99,7 @@ class Tag(Model, AuditMixinNullable): "TaggedObject", back_populates="tag", overlaps="objects,tags" ) - users_favorited = relationship( - security_manager.user_model, secondary=user_favorite_tag_table - ) + users_favorited = relationship("User", secondary=user_favorite_tag_table) class TaggedObject(Model, AuditMixinNullable): diff --git a/superset/tasks/async_queries.py b/superset/tasks/async_queries.py index b804847cd84e..dcd01a32a50c 100644 --- a/superset/tasks/async_queries.py +++ b/superset/tasks/async_queries.py @@ -21,7 +21,7 @@ from typing import Any, cast, TYPE_CHECKING from celery.exceptions import SoftTimeLimitExceeded -from flask import current_app, g +from flask import g from flask_appbuilder.security.sqla.models import User from marshmallow import ValidationError @@ -41,9 +41,6 @@ from superset.common.query_context import QueryContext logger = logging.getLogger(__name__) -query_timeout = current_app.config[ - "SQLLAB_ASYNC_TIME_LIMIT_SEC" -] # TODO: new config key def set_form_data(form_data: dict[str, Any]) -> None: @@ -73,7 +70,7 @@ def _load_user_from_job_metadata(job_metadata: dict[str, Any]) -> User: return user -@celery_app.task(name="load_chart_data_into_cache", soft_time_limit=query_timeout) +@celery_app.task(name="load_chart_data_into_cache") def load_chart_data_into_cache( job_metadata: dict[str, Any], form_data: dict[str, Any], @@ -107,7 +104,7 @@ def load_chart_data_into_cache( raise ex -@celery_app.task(name="load_explore_json_into_cache", soft_time_limit=query_timeout) +@celery_app.task(name="load_explore_json_into_cache") def load_explore_json_into_cache( # pylint: disable=too-many-locals job_metadata: dict[str, Any], form_data: dict[str, Any], diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py index d1051c8fcb89..46ed7c62df98 100644 --- a/superset/tasks/cache.py +++ b/superset/tasks/cache.py @@ -21,10 +21,10 @@ from celery.beat import SchedulingError from celery.utils.log import get_task_logger +from flask import current_app as app from sqlalchemy import and_, func -from superset import app, db, security_manager -from superset.extensions import celery_app +from superset.extensions import celery_app, db, security_manager from superset.models.core import Log from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/superset/tasks/celery_app.py b/superset/tasks/celery_app.py index 4d36917be0bb..bd0f30a200e9 100644 --- a/superset/tasks/celery_app.py +++ b/superset/tasks/celery_app.py @@ -25,20 +25,15 @@ from celery.signals import task_postrun, worker_process_init # Superset framework imports -from superset import create_app -from superset.extensions import celery_app, db +from superset.app import create_app +from superset.extensions import db # Init the Flask app / configure everything -flask_app = create_app() - -# Need to import late, as the celery_app will have been setup by "create_app()" - -# Export the celery app globally for Celery (as run on the cmd line) to find -app = celery_app @worker_process_init.connect def reset_db_connection_pool(**kwargs: Any) -> None: # pylint: disable=unused-argument + flask_app = create_app() with flask_app.app_context(): # https://docs.sqlalchemy.org/en/14/core/connections.html#engine-disposal db.engine.dispose() @@ -59,6 +54,7 @@ def teardown( # pylint: disable=unused-argument :see: https://docs.celeryq.dev/en/stable/userguide/signals.html#task-postrun :see: https://gist.github.com/twolfson/a1b329e9353f9b575131 """ + flask_app = create_app() if flask_app.config.get("SQLALCHEMY_COMMIT_ON_TEARDOWN"): if not isinstance(retval, Exception): diff --git a/superset/tasks/cron_util.py b/superset/tasks/cron_util.py index 329937fb8285..508caab2339b 100644 --- a/superset/tasks/cron_util.py +++ b/superset/tasks/cron_util.py @@ -20,10 +20,9 @@ from datetime import datetime, timedelta from croniter import croniter +from flask import current_app as app from pytz import timezone as pytz_timezone, UnknownTimeZoneError -from superset import app - logger = logging.getLogger(__name__) diff --git a/superset/tasks/scheduler.py b/superset/tasks/scheduler.py index cb55dc9f69ad..09b880ad8971 100644 --- a/superset/tasks/scheduler.py +++ b/superset/tasks/scheduler.py @@ -19,8 +19,9 @@ from celery import Celery from celery.exceptions import SoftTimeLimitExceeded +from flask import current_app as app -from superset import app, is_feature_enabled +from superset import feature_flag_manager from superset.commands.exceptions import CommandException from superset.commands.report.exceptions import ReportScheduleUnexpectedError from superset.commands.report.execute import AsyncExecuteReportScheduleCommand @@ -43,7 +44,7 @@ def scheduler() -> None: stats_logger: BaseStatsLogger = app.config["STATS_LOGGER"] stats_logger.incr("reports.scheduler") - if not is_feature_enabled("ALERT_REPORTS"): + if not feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): return active_schedules = ReportScheduleDAO.find_active() triggered_at = ( diff --git a/superset/tasks/thumbnails.py b/superset/tasks/thumbnails.py index 03b3999dce87..06daed63a0b9 100644 --- a/superset/tasks/thumbnails.py +++ b/superset/tasks/thumbnails.py @@ -22,8 +22,7 @@ from flask import current_app -from superset import security_manager, thumbnail_cache -from superset.extensions import celery_app +from superset.extensions import cache_manager, celery_app, security_manager from superset.tasks.utils import get_executor from superset.utils.core import override_user from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot @@ -44,7 +43,7 @@ def cache_chart_thumbnail( # pylint: disable=import-outside-toplevel from superset.models.slice import Slice - if not thumbnail_cache: + if not cache_manager.thumbnail_cache: logger.warning("No cache set, refusing to compute") return None chart = cast(Slice, Slice.get(chart_id)) @@ -63,7 +62,7 @@ def cache_chart_thumbnail( screenshot = ChartScreenshot(url, chart.digest) screenshot.compute_and_cache( user=user, - cache=thumbnail_cache, + cache=cache_manager.thumbnail_cache, force=force, window_size=window_size, thumb_size=thumb_size, @@ -81,7 +80,7 @@ def cache_dashboard_thumbnail( # pylint: disable=import-outside-toplevel from superset.models.dashboard import Dashboard - if not thumbnail_cache: + if not cache_manager.thumbnail_cache: logging.warning("No cache set, refusing to compute") return dashboard = Dashboard.get(dashboard_id) @@ -98,7 +97,7 @@ def cache_dashboard_thumbnail( screenshot = DashboardScreenshot(url, dashboard.digest) screenshot.compute_and_cache( user=user, - cache=thumbnail_cache, + cache=cache_manager.thumbnail_cache, force=force, thumb_size=thumb_size, ) diff --git a/superset/utils/cache.py b/superset/utils/cache.py index 15f334e128ae..e43465b195a7 100644 --- a/superset/utils/cache.py +++ b/superset/utils/cache.py @@ -20,24 +20,18 @@ import logging from datetime import datetime, timedelta from functools import wraps -from typing import Any, Callable, TYPE_CHECKING +from typing import Any, Callable from flask import current_app as app, request from flask_caching import Cache from flask_caching.backends import NullCache from werkzeug.wrappers import Response -from superset import db -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.models.cache import CacheKey from superset.utils.hashing import md5_sha_from_dict from superset.utils.json import json_int_dttm_ser -if TYPE_CHECKING: - from superset.stats_logger import BaseStatsLogger - -config = app.config -stats_logger: BaseStatsLogger = config["STATS_LOGGER"] logger = logging.getLogger(__name__) @@ -65,9 +59,9 @@ def set_and_log_cache( dttm = datetime.utcnow().isoformat().split(".")[0] value = {**cache_value, "dttm": dttm} cache_instance.set(cache_key, value, timeout=timeout) - stats_logger.incr("set_cache_key") + app.stats_logger.incr("set_cache_key") - if datasource_uid and config["STORE_CACHE_KEYS_IN_METADATA_DB"]: + if datasource_uid and app.config["STORE_CACHE_KEYS_IN_METADATA_DB"]: ck = CacheKey( cache_key=cache_key, cache_timeout=cache_timeout, @@ -147,7 +141,7 @@ def wrapped_f(*args: Any, **kwargs: Any) -> Any: def etag_cache( cache: Cache = cache_manager.cache, get_last_modified: Callable[..., datetime] | None = None, - max_age: int | float = app.config["CACHE_DEFAULT_TIMEOUT"], + max_age: int | float = 0, raise_for_access: Callable[..., Any] | None = None, skip: Callable[..., bool] | None = None, ) -> Callable[..., Any]: @@ -167,6 +161,7 @@ def etag_cache( def decorator(f: Callable[..., Any]) -> Callable[..., Any]: @wraps(f) def wrapper(*args: Any, **kwargs: Any) -> Response: + max_age = app.config["CACHE_DEFAULT_TIMEOUT"] or 0 # Check if the user can access the resource if raise_for_access: try: diff --git a/superset/utils/dashboard_import_export.py b/superset/utils/dashboard_import_export.py index c21761dadbd2..e437d63673fa 100644 --- a/superset/utils/dashboard_import_export.py +++ b/superset/utils/dashboard_import_export.py @@ -16,7 +16,7 @@ # under the License. import logging -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard logger = logging.getLogger(__name__) diff --git a/superset/utils/database.py b/superset/utils/database.py index 073e58ffda6f..c614cdc0c01b 100644 --- a/superset/utils/database.py +++ b/superset/utils/database.py @@ -35,7 +35,7 @@ def get_or_create_db( database_name: str, sqlalchemy_uri: str, always_create: bool | None = True ) -> Database: # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db from superset.models import core as models database = ( @@ -77,7 +77,7 @@ def get_main_database() -> Database: # with above function... think of how to refactor it def remove_database(database: Database) -> None: # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db db.session.delete(database) db.session.commit() diff --git a/superset/utils/dict_import_export.py b/superset/utils/dict_import_export.py index 7b3d995249f3..30eb301e11e0 100644 --- a/superset/utils/dict_import_export.py +++ b/superset/utils/dict_import_export.py @@ -17,7 +17,7 @@ import logging from typing import Any -from superset import db +from superset.extensions import db from superset.models.core import Database EXPORT_VERSION = "1.0.0" diff --git a/superset/utils/encrypt.py b/superset/utils/encrypt.py index 899b3391afa9..cbc552e56954 100644 --- a/superset/utils/encrypt.py +++ b/superset/utils/encrypt.py @@ -18,7 +18,7 @@ from abc import ABC, abstractmethod from typing import Any, Optional -from flask import Flask +from flask import current_app, Flask from flask_babel import lazy_gettext as _ from sqlalchemy import text, TypeDecorator from sqlalchemy.engine import Connection, Dialect, Row @@ -28,6 +28,10 @@ logger = logging.getLogger(__name__) +def get_secret_key() -> str: + return current_app.config["SECRET_KEY"] + + class AbstractEncryptedFieldAdapter(ABC): # pylint: disable=too-few-public-methods @abstractmethod def create( @@ -39,6 +43,9 @@ def create( pass +# pylint: disable-all + + class SQLAlchemyUtilsAdapter( # pylint: disable=too-few-public-methods AbstractEncryptedFieldAdapter ): @@ -48,12 +55,16 @@ def create( *args: list[Any], **kwargs: Optional[dict[str, Any]], ) -> TypeDecorator: + kwargs["key"] = get_secret_key # type: ignore + return EncryptedType(*args, **kwargs) + """ if app_config: - return EncryptedType(*args, app_config["SECRET_KEY"], **kwargs) + return EncryptedType(*args, secret_key=get_secret_key, **kwargs) raise Exception( # pylint: disable=broad-exception-raised "Missing app_config kwarg" ) + """ class EncryptedFieldFactory: @@ -70,6 +81,9 @@ def init_app(self, app: Flask) -> None: def create( self, *args: list[Any], **kwargs: Optional[dict[str, Any]] ) -> TypeDecorator: + return SQLAlchemyUtilsAdapter().create(self._config, *args, **kwargs) + + """ if self._concrete_type_adapter: adapter = self._concrete_type_adapter.create(self._config, *args, **kwargs) setattr(adapter, ENC_ADAPTER_TAG_ATTR_NAME, True) @@ -78,6 +92,7 @@ def create( raise Exception( # pylint: disable=broad-exception-raised "App not initialized yet. Please call init_app first" ) + """ @staticmethod def created_by_enc_field_factory(field: TypeDecorator) -> bool: @@ -86,7 +101,7 @@ def created_by_enc_field_factory(field: TypeDecorator) -> bool: class SecretsMigrator: def __init__(self, previous_secret_key: str) -> None: - from superset import db # pylint: disable=import-outside-toplevel + from superset.extensions import db # pylint: disable=import-outside-toplevel self._db = db self._previous_secret_key = previous_secret_key diff --git a/superset/utils/log.py b/superset/utils/log.py index 4b9ebb50b989..b26ec0e96568 100644 --- a/superset/utils/log.py +++ b/superset/utils/log.py @@ -24,19 +24,15 @@ from collections.abc import Iterator from contextlib import contextmanager from datetime import datetime, timedelta -from typing import Any, Callable, cast, Literal, TYPE_CHECKING +from typing import Any, Callable, cast, Literal -from flask import g, request +from flask import current_app as app, g, request from flask_appbuilder.const import API_URI_RIS_KEY from sqlalchemy.exc import SQLAlchemyError -from superset.extensions import stats_logger_manager from superset.utils import json from superset.utils.core import get_user_id, LoggerLevel, to_int -if TYPE_CHECKING: - pass - logger = logging.getLogger(__name__) @@ -181,7 +177,7 @@ def log_with_context( # pylint: disable=too-many-locals,too-many-arguments **payload_override: dict[str, Any] | None, ) -> None: # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db from superset.views.core import get_form_data referrer = request.referrer[:1000] if request and request.referrer else None @@ -228,7 +224,10 @@ def log_with_context( # pylint: disable=too-many-locals,too-many-arguments slice_id = to_int(slice_id) if log_to_statsd: - stats_logger_manager.instance.incr(action) + print("ACTION", action) + print("OBJ", app.stats_logger) + app.stats_logger.incr(action) + print("AFtER") try: # bulk insert @@ -380,7 +379,7 @@ def log( # pylint: disable=too-many-arguments,too-many-locals **kwargs: Any, ) -> None: # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db from superset.models.core import Log records = kwargs.get("records", []) diff --git a/superset/utils/mock_data.py b/superset/utils/mock_data.py index cffa89719d49..610cc93c97bd 100644 --- a/superset/utils/mock_data.py +++ b/superset/utils/mock_data.py @@ -33,7 +33,7 @@ from sqlalchemy.sql import func from sqlalchemy.sql.visitors import VisitableType -from superset import db +from superset.extensions import db from superset.sql_parse import Table from superset.utils import json diff --git a/superset/utils/oauth2.py b/superset/utils/oauth2.py index 9cc58a0b7ffc..13884dde832d 100644 --- a/superset/utils/oauth2.py +++ b/superset/utils/oauth2.py @@ -25,8 +25,8 @@ from flask import current_app from marshmallow import EXCLUDE, fields, post_load, Schema -from superset import db from superset.exceptions import CreateKeyValueDistributedLockFailedException +from superset.extensions import db from superset.superset_typing import OAuth2ClientConfig, OAuth2State from superset.utils.lock import KeyValueDistributedLock diff --git a/superset/utils/screenshots.py b/superset/utils/screenshots.py index bf6ed0f9e849..71a1d47b97b9 100644 --- a/superset/utils/screenshots.py +++ b/superset/utils/screenshots.py @@ -20,9 +20,9 @@ from io import BytesIO from typing import TYPE_CHECKING -from flask import current_app +from flask import current_app as app -from superset import feature_flag_manager +from superset.extensions import feature_flag_manager from superset.utils.hashing import md5_sha_from_dict from superset.utils.urls import modify_url_query from superset.utils.webdriver import ( @@ -53,7 +53,6 @@ class BaseScreenshot: - driver_type = current_app.config["WEBDRIVER_TYPE"] thumbnail_type: str = "" element: str = "" window_size: WindowSize = DEFAULT_SCREENSHOT_WINDOW_SIZE @@ -66,9 +65,10 @@ def __init__(self, url: str, digest: str): def driver(self, window_size: WindowSize | None = None) -> WebDriver: window_size = window_size or self.window_size + driver_type = app.config["WEBDRIVER_TYPE"] if feature_flag_manager.is_feature_enabled("PLAYWRIGHT_REPORTS_AND_THUMBNAILS"): - return WebDriverPlaywright(self.driver_type, window_size) - return WebDriverSelenium(self.driver_type, window_size) + return WebDriverPlaywright(driver_type, window_size) + return WebDriverSelenium(driver_type, window_size) def cache_key( self, diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index fc7f18aca930..ed6c7321b626 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -35,8 +35,7 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait -from superset import feature_flag_manager -from superset.extensions import machine_auth_provider_factory +from superset.extensions import feature_flag_manager, machine_auth_provider_factory from superset.utils.retries import retry_call WindowSize = tuple[int, int] diff --git a/superset/views/__init__.py b/superset/views/__init__.py index 0ef34edd7456..13a83393a912 100644 --- a/superset/views/__init__.py +++ b/superset/views/__init__.py @@ -14,29 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from . import ( - alerts, - api, - base, - core, - css_templates, - dynamic_plugins, - health, - sql_lab, - tags, -) -from .log import api as log_api, views - -__all__ = [ - "alerts", - "api", - "base", - "core", - "css_templates", - "dynamic_plugins", - "health", - "log_api", - "views", - "sql_lab", - "tags", -] diff --git a/superset/views/alerts.py b/superset/views/alerts.py index ae870d1c52cb..8534fbd55468 100644 --- a/superset/views/alerts.py +++ b/superset/views/alerts.py @@ -19,7 +19,7 @@ from flask_appbuilder.api import expose from flask_appbuilder.security.decorators import has_access -from superset import is_feature_enabled +from superset.extensions import feature_flag_manager from superset.superset_typing import FlaskResponse from .base import BaseSupersetView @@ -35,7 +35,7 @@ class BaseAlertReportView(BaseSupersetView): @has_access @permission_name("read") def list(self) -> FlaskResponse: - if not is_feature_enabled("ALERT_REPORTS"): + if not feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): return abort(404) return super().render_app_template() @@ -43,7 +43,7 @@ def list(self) -> FlaskResponse: @has_access @permission_name("read") def log(self, pk: int) -> FlaskResponse: # pylint: disable=unused-argument - if not is_feature_enabled("ALERT_REPORTS"): + if not feature_flag_manager.is_feature_enabled("ALERT_REPORTS"): return abort(404) return super().render_app_template() diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py index 5ca7e41e1d75..4a6c1198c5ce 100644 --- a/superset/views/all_entities.py +++ b/superset/views/all_entities.py @@ -18,27 +18,53 @@ import logging from flask_appbuilder import expose +from flask_appbuilder.hooks import before_request from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access +from jinja2.sandbox import SandboxedEnvironment +from werkzeug.exceptions import NotFound -from superset import is_feature_enabled +from superset.extensions import feature_flag_manager +from superset.jinja_context import ExtraCache from superset.superset_typing import FlaskResponse from superset.tags.models import Tag from superset.views.base import SupersetModelView +from .base import BaseSupersetView + logger = logging.getLogger(__name__) +def process_template(content: str) -> str: + env = SandboxedEnvironment() + template = env.from_string(content) + context = { + "current_user_id": ExtraCache.current_user_id, + "current_username": ExtraCache.current_username, + } + return template.render(context) + + class TaggedObjectsModelView(SupersetModelView): route_base = "/superset/all_entities" datamodel = SQLAInterface(Tag) class_permission_name = "Tags" - include_route_methods = {"list"} @has_access @expose("/") def list(self) -> FlaskResponse: - if not is_feature_enabled("TAGGING_SYSTEM"): + if not feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): return super().list() return super().render_app_template() + + +class TaggedObjectView(BaseSupersetView): + @staticmethod + def is_enabled() -> bool: + return feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM") + + @before_request + def ensure_enabled(self) -> None: + if not self.is_enabled(): + raise NotFound() diff --git a/superset/views/api.py b/superset/views/api.py index 8ef59cb4829a..bf963b49ad2b 100644 --- a/superset/views/api.py +++ b/superset/views/api.py @@ -24,11 +24,11 @@ from flask_appbuilder.security.decorators import has_access_api from flask_babel import lazy_gettext as _ -from superset import db, event_logger from superset.commands.chart.exceptions import ( TimeRangeAmbiguousError, TimeRangeParseFailError, ) +from superset.extensions import db, event_logger from superset.legacy import update_time_range from superset.models.slice import Slice from superset.superset_typing import FlaskResponse diff --git a/superset/views/base.py b/superset/views/base.py index be3af99147e8..f4cd78b8722f 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -22,20 +22,18 @@ import os import traceback from datetime import datetime -from importlib.resources import files from typing import Any, Callable, cast import yaml from babel import Locale from flask import ( abort, + current_app as app, flash, g, get_flashed_messages, redirect, - request, Response, - send_file, session, ) from flask_appbuilder import BaseView, expose, Model, ModelView @@ -50,35 +48,30 @@ ) from flask_appbuilder.security.sqla.models import User from flask_appbuilder.widgets import ListWidget -from flask_babel import get_locale, gettext as __ +from flask_babel import get_locale, gettext as __, lazy_gettext as _ from flask_jwt_extended.exceptions import NoAuthorizationError -from flask_wtf.csrf import CSRFError from flask_wtf.form import FlaskForm from sqlalchemy import exc from sqlalchemy.orm import Query from werkzeug.exceptions import HTTPException +from wtforms import Form from wtforms.fields.core import Field, UnboundField from superset import ( - app as superset_app, appbuilder, - conf, - get_feature_flags, - is_feature_enabled, security_manager, ) -from superset.commands.exceptions import CommandException, CommandInvalidError from superset.connectors.sqla import models from superset.db_engine_specs import get_available_engine_specs from superset.db_engine_specs.gsheets import GSheetsEngineSpec -from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.errors import ErrorLevel, SupersetError from superset.exceptions import ( SupersetErrorException, SupersetErrorsException, SupersetException, SupersetSecurityException, ) -from superset.extensions import cache_manager +from superset.extensions import cache_manager, feature_flag_manager from superset.models.helpers import ImportExportMixin from superset.reports.models import ReportRecipientType from superset.superset_typing import FlaskResponse @@ -130,11 +123,10 @@ ) logger = logging.getLogger(__name__) -config = superset_app.config def get_error_msg() -> str: - if conf.get("SHOW_STACKTRACE"): + if app.config.get("SHOW_STACKTRACE"): error_msg = traceback.format_exc() else: error_msg = "FATAL ERROR \n" @@ -314,10 +306,10 @@ def render_app_template( def get_environment_tag() -> dict[str, Any]: # Whether flask is in debug mode (--debug) - debug = appbuilder.app.config["DEBUG"] + debug = app.config["DEBUG"] # Getting the configuration option for ENVIRONMENT_TAG_CONFIG - env_tag_config = appbuilder.app.config["ENVIRONMENT_TAG_CONFIG"] + env_tag_config = app.config["ENVIRONMENT_TAG_CONFIG"] # These are the predefined templates define in the config env_tag_templates = env_tag_config.get("values") @@ -342,37 +334,37 @@ def menu_data(user: User) -> dict[str, Any]: for lang in appbuilder.languages } - if callable(brand_text := appbuilder.app.config["LOGO_RIGHT_TEXT"]): + if callable(brand_text := app.config["LOGO_RIGHT_TEXT"]): brand_text = brand_text() return { "menu": appbuilder.menu.get_data(), "brand": { - "path": appbuilder.app.config["LOGO_TARGET_PATH"] or "/superset/welcome/", + "path": app.config["LOGO_TARGET_PATH"] or "/superset/welcome/", "icon": appbuilder.app_icon, "alt": appbuilder.app_name, - "tooltip": appbuilder.app.config["LOGO_TOOLTIP"], + "tooltip": app.config["LOGO_TOOLTIP"], "text": brand_text, }, "environment_tag": get_environment_tag(), "navbar_right": { # show the watermark if the default app icon has been overridden "show_watermark": ("superset-logo-horiz" not in appbuilder.app_icon), - "bug_report_url": appbuilder.app.config["BUG_REPORT_URL"], - "bug_report_icon": appbuilder.app.config["BUG_REPORT_ICON"], - "bug_report_text": appbuilder.app.config["BUG_REPORT_TEXT"], - "documentation_url": appbuilder.app.config["DOCUMENTATION_URL"], - "documentation_icon": appbuilder.app.config["DOCUMENTATION_ICON"], - "documentation_text": appbuilder.app.config["DOCUMENTATION_TEXT"], - "version_string": appbuilder.app.config["VERSION_STRING"], - "version_sha": appbuilder.app.config["VERSION_SHA"], - "build_number": appbuilder.app.config["BUILD_NUMBER"], + "bug_report_url": app.config["BUG_REPORT_URL"], + "bug_report_icon": app.config["BUG_REPORT_ICON"], + "bug_report_text": app.config["BUG_REPORT_TEXT"], + "documentation_url": app.config["DOCUMENTATION_URL"], + "documentation_icon": app.config["DOCUMENTATION_ICON"], + "documentation_text": app.config["DOCUMENTATION_TEXT"], + "version_string": app.config["VERSION_STRING"], + "version_sha": app.config["VERSION_SHA"], + "build_number": app.config["BUILD_NUMBER"], "languages": languages, "show_language_picker": len(languages) > 1, "user_is_anonymous": user.is_anonymous, "user_info_url": ( None - if is_feature_enabled("MENU_HIDE_USER_INFO") + if feature_flag_manager.is_feature_enabled("MENU_HIDE_USER_INFO") else appbuilder.get_url_for_userinfo ), "user_logout_url": appbuilder.get_url_for_logout, @@ -394,11 +386,15 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument # should not expose API TOKEN to frontend frontend_config = { - k: (list(conf.get(k)) if isinstance(conf.get(k), set) else conf.get(k)) + k: ( + list(app.config.get(k)) + if isinstance(app.config.get(k), set) + else app.config.get(k) + ) for k in FRONTEND_CONF_KEYS } - if conf.get("SLACK_API_TOKEN"): + if app.config.get("SLACK_API_TOKEN"): frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [ ReportRecipientType.EMAIL, ReportRecipientType.SLACK, @@ -418,15 +414,17 @@ def cached_common_bootstrap_data( # pylint: disable=unused-argument "conf": frontend_config, "locale": language, "language_pack": get_language_pack(language), - "d3_format": conf.get("D3_FORMAT"), - "currencies": conf.get("CURRENCIES"), - "feature_flags": get_feature_flags(), - "extra_sequential_color_schemes": conf["EXTRA_SEQUENTIAL_COLOR_SCHEMES"], - "extra_categorical_color_schemes": conf["EXTRA_CATEGORICAL_COLOR_SCHEMES"], - "theme_overrides": conf["THEME_OVERRIDES"], + "d3_format": app.config.get("D3_FORMAT"), + "currencies": app.config.get("CURRENCIES"), + "feature_flags": feature_flag_manager.get_feature_flags(), + "extra_sequential_color_schemes": app.config["EXTRA_SEQUENTIAL_COLOR_SCHEMES"], + "extra_categorical_color_schemes": app.config[ + "EXTRA_CATEGORICAL_COLOR_SCHEMES" + ], + "theme_overrides": app.config["THEME_OVERRIDES"], "menu_data": menu_data(g.user), } - bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data)) + bootstrap_data.update(app.config["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data)) return bootstrap_data @@ -447,108 +445,6 @@ def get_error_level_from_status_code( # pylint: disable=invalid-name return ErrorLevel.ERROR -# SIP-40 compatible error responses; make sure APIs raise -# SupersetErrorException or SupersetErrorsException -@superset_app.errorhandler(SupersetErrorException) -def show_superset_error(ex: SupersetErrorException) -> FlaskResponse: - logger.warning("SupersetErrorException", exc_info=True) - return json_errors_response(errors=[ex.error], status=ex.status) - - -@superset_app.errorhandler(SupersetErrorsException) -def show_superset_errors(ex: SupersetErrorsException) -> FlaskResponse: - logger.warning("SupersetErrorsException", exc_info=True) - return json_errors_response(errors=ex.errors, status=ex.status) - - -# Redirect to login if the CSRF token is expired -@superset_app.errorhandler(CSRFError) -def refresh_csrf_token(ex: CSRFError) -> FlaskResponse: - logger.warning("Refresh CSRF token error", exc_info=True) - - if request.is_json: - return show_http_exception(ex) - - return redirect(appbuilder.get_url_for_login) - - -@superset_app.errorhandler(HTTPException) -def show_http_exception(ex: HTTPException) -> FlaskResponse: - logger.warning("HTTPException", exc_info=True) - if ( - "text/html" in request.accept_mimetypes - and not config["DEBUG"] - and ex.code in {404, 500} - ): - path = files("superset") / f"static/assets/{ex.code}.html" - return send_file(path, max_age=0), ex.code - - return json_errors_response( - errors=[ - SupersetError( - message=utils.error_msg_from_exception(ex), - error_type=SupersetErrorType.GENERIC_BACKEND_ERROR, - level=ErrorLevel.ERROR, - ), - ], - status=ex.code or 500, - ) - - -# Temporary handler for CommandException; if an API raises a -# CommandException it should be fixed to map it to SupersetErrorException -# or SupersetErrorsException, with a specific status code and error type -@superset_app.errorhandler(CommandException) -def show_command_errors(ex: CommandException) -> FlaskResponse: - logger.warning("CommandException", exc_info=True) - if "text/html" in request.accept_mimetypes and not config["DEBUG"]: - path = files("superset") / "static/assets/500.html" - return send_file(path, max_age=0), 500 - - extra = ex.normalized_messages() if isinstance(ex, CommandInvalidError) else {} - return json_errors_response( - errors=[ - SupersetError( - message=ex.message, - error_type=SupersetErrorType.GENERIC_COMMAND_ERROR, - level=get_error_level_from_status_code(ex.status), - extra=extra, - ), - ], - status=ex.status, - ) - - -# Catch-all, to ensure all errors from the backend conform to SIP-40 -@superset_app.errorhandler(Exception) -def show_unexpected_exception(ex: Exception) -> FlaskResponse: - logger.exception(ex) - if "text/html" in request.accept_mimetypes and not config["DEBUG"]: - path = files("superset") / "static/assets/500.html" - return send_file(path, max_age=0), 500 - - return json_errors_response( - errors=[ - SupersetError( - message=utils.error_msg_from_exception(ex), - error_type=SupersetErrorType.GENERIC_BACKEND_ERROR, - level=ErrorLevel.ERROR, - ), - ], - ) - - -@superset_app.context_processor -def get_common_bootstrap_data() -> dict[str, Any]: - def serialize_bootstrap_data() -> str: - return json.dumps( - {"common": common_bootstrap_payload()}, - default=json.pessimistic_json_iso_dttm_ser, - ) - - return {"bootstrap_data": serialize_bootstrap_data} - - class SupersetListWidget(ListWidget): # pylint: disable=too-few-public-methods template = "superset/fab_overrides/list.html" @@ -638,6 +534,16 @@ class ListWidgetWithCheckboxes(ListWidget): # pylint: disable=too-few-public-me template = "superset/fab_overrides/list_with_checkboxes.html" +def validate_json(form: Form, field: Field) -> None: # pylint: disable=unused-argument + try: + json.loads(field.data) + except Exception as ex: + logger.exception(ex) + raise Exception( # pylint: disable=broad-exception-raised + _("json isn't valid") + ) from ex + + class YamlExportMixin: # pylint: disable=too-few-public-methods """ Override this if you want a dict response instead, with a certain key. @@ -735,8 +641,11 @@ class CsvResponse(Response): Override Response to take into account csv encoding from config.py """ - charset = conf["CSV_EXPORT"].get("encoding", "utf-8") - default_mimetype = "text/csv" + default_mimetype: str = "text/csv" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.charset: str = app.config["CSV_EXPORT"].get("encoding", "utf-8") + super().__init__(*args, **kwargs) class XlsxResponse(Response): @@ -768,18 +677,3 @@ def bind_field( FlaskForm.Meta.bind_field = bind_field - - -@superset_app.after_request -def apply_http_headers(response: Response) -> Response: - """Applies the configuration's http headers to all responses""" - - # HTTP_HEADERS is deprecated, this provides backwards compatibility - response.headers.extend( - {**config["OVERRIDE_HTTP_HEADERS"], **config["HTTP_HEADERS"]} - ) - - for k, v in config["DEFAULT_HTTP_HEADERS"].items(): - if k not in response.headers: - response.headers[k] = v - return response diff --git a/superset/views/core.py b/superset/views/core.py index 5f76b05a7881..c459b73ac7b1 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -23,7 +23,15 @@ from typing import Any, Callable, cast from urllib import parse -from flask import abort, flash, g, redirect, render_template, request, Response +from flask import ( + abort, + current_app as app, + flash, + g, + redirect, + request, + Response, +) from flask_appbuilder import expose from flask_appbuilder.security.decorators import ( has_access, @@ -33,15 +41,6 @@ from flask_babel import gettext as __, lazy_gettext as _ from sqlalchemy.exc import SQLAlchemyError -from superset import ( - app, - appbuilder, - conf, - db, - event_logger, - is_feature_enabled, - security_manager, -) from superset.async_events.async_query_manager import AsyncQueryTokenException from superset.commands.chart.exceptions import ChartNotFoundError from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand @@ -63,7 +62,15 @@ SupersetSecurityException, ) from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError -from superset.extensions import async_query_manager, cache_manager +from superset.extensions import ( + appbuilder, + async_query_manager, + cache_manager, + db, + event_logger, + feature_flag_manager, + security_manager, +) from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice @@ -85,7 +92,6 @@ data_payload_response, deprecated, generate_download_headers, - get_error_msg, handle_api_exception, json_error_response, json_success, @@ -104,9 +110,6 @@ ) from superset.viz import BaseViz -config = app.config -SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"] -stats_logger = config["STATS_LOGGER"] logger = logging.getLogger(__name__) DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted") @@ -181,9 +184,10 @@ def generate_json( self, viz_obj: BaseViz, response_type: str | None = None ) -> FlaskResponse: if response_type == ChartDataResultFormat.CSV: - return CsvResponse( + csv_resp: CsvResponse = CsvResponse( viz_obj.get_csv(), headers=generate_download_headers("csv") ) + return csv_resp if response_type == ChartDataResultType.QUERY: return self.get_query_string_response(viz_obj) @@ -300,7 +304,7 @@ def explore_json( # TODO: support CSV, SQL query and other non-JSON types if ( - is_feature_enabled("GLOBAL_ASYNC_QUERIES") + feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES") and response_type == ChartDataResultFormat.JSON ): # First, look for the chart query results in the cache. @@ -886,19 +890,12 @@ def fetch_datasource_metadata(self) -> FlaskResponse: datasource.raise_for_access() return json_success(json.dumps(sanitize_datasource_data(datasource.data))) - @app.errorhandler(500) - def show_traceback(self) -> FlaskResponse: - return ( - render_template("superset/traceback.html", error_msg=get_error_msg()), - 500, - ) - @event_logger.log_this @expose("/welcome/") def welcome(self) -> FlaskResponse: """Personalized welcome page""" if not g.user or not get_user_id(): - if conf["PUBLIC_ROLE_LIKE"]: + if app.config["PUBLIC_ROLE_LIKE"]: return self.render_template("superset/public_welcome.html") return redirect(appbuilder.get_url_for_login) diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py index 2e88b4acd02b..baf58a21fa83 100644 --- a/superset/views/dashboard/views.py +++ b/superset/views/dashboard/views.py @@ -30,8 +30,8 @@ from flask_babel import gettext as __, lazy_gettext as _ from flask_login import AnonymousUserMixin, login_user -from superset import db, event_logger, is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.extensions import db, event_logger, feature_flag_manager from superset.models.dashboard import Dashboard as DashboardModel from superset.superset_typing import FlaskResponse from superset.utils import json @@ -138,7 +138,7 @@ def embedded( :param add_extra_log_payload: added by `log_this_with_manual_updates`, set a default value to appease pylint """ - if not is_feature_enabled("EMBEDDED_SUPERSET"): + if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): return Response(status=404) # Log in as an anonymous user, just for this view. diff --git a/superset/views/database/mixins.py b/superset/views/database/mixins.py index 21c664fa1f4e..5437f768c5ae 100644 --- a/superset/views/database/mixins.py +++ b/superset/views/database/mixins.py @@ -16,14 +16,15 @@ # under the License. import inspect +from flask import current_app as app from flask_babel import lazy_gettext as _ from markupsafe import Markup from sqlalchemy import MetaData -from superset import app, security_manager from superset.databases.filters import DatabaseFilter from superset.databases.utils import make_url_safe from superset.exceptions import SupersetException +from superset.extensions import security_manager from superset.models.core import Database from superset.security.analytics_db_safety import check_sqlalchemy_uri from superset.utils import core as utils diff --git a/superset/views/database/views.py b/superset/views/database/views.py index 019dc1138bd1..b1180182fa79 100644 --- a/superset/views/database/views.py +++ b/superset/views/database/views.py @@ -16,6 +16,7 @@ # under the License. from typing import TYPE_CHECKING +from flask import current_app as app from flask_appbuilder import expose from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access @@ -24,7 +25,6 @@ from wtforms.validators import ValidationError import superset.models.core as models -from superset import app from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.exceptions import CertificateException from superset.superset_typing import FlaskResponse @@ -42,9 +42,6 @@ if TYPE_CHECKING: from werkzeug.datastructures import FileStorage -config = app.config -stats_logger = config["STATS_LOGGER"] - def sqlalchemy_uri_form_validator(_: _, field: StringField) -> None: """ diff --git a/superset/views/datasource/schemas.py b/superset/views/datasource/schemas.py index 8ae28f9c7a21..cbc810d6d222 100644 --- a/superset/views/datasource/schemas.py +++ b/superset/views/datasource/schemas.py @@ -18,7 +18,6 @@ from marshmallow import fields, post_load, pre_load, Schema, validate -from superset import app from superset.charts.schemas import ChartDataExtrasSchema, ChartDataFilterSchema from superset.utils.core import DatasourceType @@ -99,7 +98,4 @@ class SamplesRequestSchema(Schema): datasource_id = fields.Integer(required=True) force = fields.Boolean(load_default=False) page = fields.Integer(load_default=1) - per_page = fields.Integer( - validate=validate.Range(min=1, max=app.config.get("SAMPLES_ROW_LIMIT", 1000)), - load_default=app.config.get("SAMPLES_ROW_LIMIT", 1000), - ) + per_page = fields.Integer() diff --git a/superset/views/datasource/utils.py b/superset/views/datasource/utils.py index 6bad2370c875..75c9eb7b0d3a 100644 --- a/superset/views/datasource/utils.py +++ b/superset/views/datasource/utils.py @@ -16,7 +16,8 @@ # under the License. from typing import Any, Optional -from superset import app +from flask import current_app as app + from superset.commands.dataset.exceptions import DatasetSamplesFailedError from superset.common.chart_data import ChartDataResultType from superset.common.query_context_factory import QueryContextFactory diff --git a/superset/views/datasource/views.py b/superset/views/datasource/views.py index 89907df000fa..eff4b50f3aee 100644 --- a/superset/views/datasource/views.py +++ b/superset/views/datasource/views.py @@ -25,7 +25,6 @@ from marshmallow import ValidationError from sqlalchemy.exc import NoResultFound, NoSuchTableError -from superset import db, event_logger, security_manager from superset.commands.dataset.exceptions import ( DatasetForbiddenError, DatasetNotFoundError, @@ -35,6 +34,7 @@ from superset.connectors.sqla.utils import get_physical_table_metadata from superset.daos.datasource import DatasourceDAO from superset.exceptions import SupersetException, SupersetSecurityException +from superset.extensions import db, event_logger, security_manager from superset.models.core import Database from superset.sql_parse import Table from superset.superset_typing import FlaskResponse diff --git a/superset/views/dynamic_plugins.py b/superset/views/dynamic_plugins.py index 3b4246cd720a..3619eb5eeb11 100644 --- a/superset/views/dynamic_plugins.py +++ b/superset/views/dynamic_plugins.py @@ -22,8 +22,8 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import lazy_gettext as _ -from superset import is_feature_enabled from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP +from superset.extensions import feature_flag_manager from superset.models.dynamic_plugins import DynamicPlugin @@ -62,6 +62,6 @@ class DynamicPluginsView(ModelView): @before_request def ensure_dynamic_plugins_enabled(self) -> Optional[Response]: - if not is_feature_enabled("DYNAMIC_PLUGINS"): + if not feature_flag_manager.is_feature_enabled("DYNAMIC_PLUGINS"): return make_response("Not found", 404) return None diff --git a/superset/views/health.py b/superset/views/health.py index 8b082ff88ffe..13a83393a912 100644 --- a/superset/views/health.py +++ b/superset/views/health.py @@ -14,16 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from superset import app, talisman -from superset.stats_logger import BaseStatsLogger -from superset.superset_typing import FlaskResponse - - -@talisman(force_https=False) -@app.route("/health") -@app.route("/healthcheck") -@app.route("/ping") -def health() -> FlaskResponse: - stats_logger: BaseStatsLogger = app.config["STATS_LOGGER"] - stats_logger.incr("health") - return "OK" diff --git a/superset/views/key_value.py b/superset/views/key_value.py index 3ba53073c704..b3a54e34e79d 100644 --- a/superset/views/key_value.py +++ b/superset/views/key_value.py @@ -20,7 +20,7 @@ from flask_appbuilder.security.decorators import has_access_api from werkzeug.exceptions import NotFound -from superset import db, event_logger, is_feature_enabled +from superset.extensions import db, event_logger, feature_flag_manager from superset.models import core as models from superset.superset_typing import FlaskResponse from superset.utils import core as utils, json @@ -32,7 +32,7 @@ class KV(BaseSupersetView): @staticmethod def is_enabled() -> bool: - return is_feature_enabled("KV_STORE") + return feature_flag_manager.is_feature_enabled("KV_STORE") @before_request def ensure_enabled(self) -> None: diff --git a/superset/views/sql_lab/views.py b/superset/views/sql_lab/views.py index 693299118d08..40bfb6de932c 100644 --- a/superset/views/sql_lab/views.py +++ b/superset/views/sql_lab/views.py @@ -23,8 +23,8 @@ from flask_babel import gettext as __ from sqlalchemy import and_ -from superset import db from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.extensions import db from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState from superset.superset_typing import FlaskResponse from superset.utils import json diff --git a/superset/views/tags.py b/superset/views/tags.py index 0a161e823f98..36eed28b2c6f 100644 --- a/superset/views/tags.py +++ b/superset/views/tags.py @@ -21,9 +21,11 @@ from flask_appbuilder.hooks import before_request from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access, has_access_api +from jinja2.sandbox import SandboxedEnvironment from werkzeug.exceptions import NotFound -from superset import db, is_feature_enabled +from superset.extensions import db, feature_flag_manager +from superset.jinja_context import ExtraCache from superset.superset_typing import FlaskResponse from superset.tags.models import Tag from superset.utils import json @@ -34,16 +36,25 @@ logger = logging.getLogger(__name__) +def process_template(content: str) -> str: + env = SandboxedEnvironment() + template = env.from_string(content) + context = { + "current_user_id": ExtraCache.current_user_id, + "current_username": ExtraCache.current_username, + } + return template.render(context) + + class TagModelView(SupersetModelView): route_base = "/superset/tags" datamodel = SQLAInterface(Tag) class_permission_name = "Tags" - include_route_methods = {"list"} @has_access @expose("/") def list(self) -> FlaskResponse: - if not is_feature_enabled("TAGGING_SYSTEM"): + if not feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"): return super().list() return super().render_app_template() @@ -52,7 +63,7 @@ def list(self) -> FlaskResponse: class TagView(BaseSupersetView): @staticmethod def is_enabled() -> bool: - return is_feature_enabled("TAGGING_SYSTEM") + return feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM") @before_request def ensure_enabled(self) -> None: diff --git a/superset/views/users/api.py b/superset/views/users/api.py index a7000b6b96c0..918280475fed 100644 --- a/superset/views/users/api.py +++ b/superset/views/users/api.py @@ -14,12 +14,11 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from flask import g, redirect, Response +from flask import current_app as app, g, redirect, Response from flask_appbuilder.api import expose, safe from flask_jwt_extended.exceptions import NoAuthorizationError from sqlalchemy.orm.exc import NoResultFound -from superset import app from superset.daos.user import UserDAO from superset.utils.slack import get_user_avatar, SlackClientError from superset.views.base_api import BaseSupersetApi diff --git a/superset/views/utils.py b/superset/views/utils.py index 8c45b774aa61..8e8a33cc65f0 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -15,21 +15,23 @@ # specific language governing permissions and limitations # under the License. import contextlib +import dataclasses import logging +import traceback from collections import defaultdict from functools import wraps from typing import Any, Callable, DefaultDict, Optional, Union import msgpack import pyarrow as pa -from flask import flash, g, has_request_context, redirect, request +from flask import current_app as app, flash, g, has_request_context, redirect, request from flask_appbuilder.security.sqla import models as ab_models from flask_appbuilder.security.sqla.models import User from flask_babel import _ from sqlalchemy.exc import NoResultFound from werkzeug.wrappers.response import Response -from superset import app, dataframe, db, result_set, viz +from superset import dataframe, result_set, viz from superset.common.db_query_status import QueryStatus from superset.daos.datasource import DatasourceDAO from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -39,20 +41,23 @@ SupersetException, SupersetSecurityException, ) -from superset.extensions import cache_manager, feature_flag_manager, security_manager +from superset.extensions import ( + cache_manager, + db, + feature_flag_manager, + security_manager, +) from superset.legacy import update_time_range from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.models.sql_lab import Query -from superset.superset_typing import FormData -from superset.utils import json +from superset.superset_typing import FlaskResponse, FormData +from superset.utils import json, json as json_utils from superset.utils.core import DatasourceType from superset.utils.decorators import stats_timing -from superset.viz import BaseViz logger = logging.getLogger(__name__) -stats_logger = app.config["STATS_LOGGER"] REJECTED_FORM_DATA_KEYS: list[str] = [] if not feature_flag_manager.is_feature_enabled("ENABLE_JAVASCRIPT_CONTROLS"): @@ -124,13 +129,14 @@ def get_viz( datasource_id: int, force: bool = False, force_cached: bool = False, -) -> BaseViz: +) -> viz.BaseViz: viz_type = form_data.get("viz_type", "table") datasource = DatasourceDAO.get_datasource( DatasourceType(datasource_type), datasource_id, ) - viz_obj = viz.viz_types[viz_type]( + viz_types = viz.get_viz_types() + viz_obj = viz_types[viz_type]( datasource, form_data=form_data, force=force, force_cached=force_cached ) return viz_obj @@ -143,6 +149,18 @@ def loads_request_json(request_json_data: str) -> dict[Any, Any]: return {} +def get_error_msg() -> str: + if app.conf.get("SHOW_STACKTRACE"): + error_msg = traceback.format_exc() + else: + error_msg = "FATAL ERROR \n" + error_msg += ( + "Stacktrace is hidden. Change the SHOW_STACKTRACE " + "configuration setting to enable it" + ) + return error_msg + + def get_form_data( slice_id: Optional[int] = None, use_slice_data: bool = False, @@ -502,6 +520,7 @@ def _deserialize_results_payload( payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False ) -> dict[str, Any]: logger.debug("Deserializing from msgpack: %r", use_msgpack) + stats_logger = app.config["STATS_LOGGER"] if use_msgpack: with stats_timing( "sqllab.query.results_backend_msgpack_deserialize", stats_logger @@ -550,3 +569,22 @@ def get_cta_schema_name( def redirect_with_flash(url: str, message: str, category: str) -> Response: flash(message=message, category=category) return redirect(url) + + +def json_errors_response( + errors: list[SupersetError], + status: int = 500, + payload: Union[dict[str, Any], None] = None, +) -> FlaskResponse: + payload = payload or {} + + payload["errors"] = [dataclasses.asdict(error) for error in errors] + return Response( + json.dumps(payload, default=json_utils.json_iso_dttm_ser, ignore_nan=True), + status=status, + mimetype="application/json; charset=utf-8", + ) + + +def json_success(json_msg: str, status: int = 200) -> FlaskResponse: + return Response(json_msg, status=status, mimetype="application/json") diff --git a/superset/viz.py b/superset/viz.py index 43b85a29f20d..0d34869d69af 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -39,12 +39,11 @@ import polyline from dateutil import relativedelta as rdelta from deprecation import deprecated -from flask import request +from flask import current_app as app, request from flask_babel import lazy_gettext as _ from geopy.point import Point from pandas.tseries.frequencies import to_offset -from superset import app from superset.common.db_query_status import QueryStatus from superset.constants import NULL_STRING from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -85,10 +84,6 @@ if TYPE_CHECKING: from superset.connectors.sqla.models import BaseDatasource -config = app.config -stats_logger = config["STATS_LOGGER"] -relative_start = config["DEFAULT_RELATIVE_START_TIME"] -relative_end = config["DEFAULT_RELATIVE_END_TIME"] logger = logging.getLogger(__name__) METRIC_KEYS = [ @@ -131,6 +126,7 @@ def __init__( self.query = "" self.token = utils.get_form_data_token(form_data) + self.stats_logger = app.config["STATS_LOGGER"] self.groupby: list[Column] = self.form_data.get("groupby") or [] self.time_shift = timedelta() @@ -249,7 +245,7 @@ def get_samples(self) -> dict[str, Any]: "groupby": [], "metrics": [], "orderby": [], - "row_limit": config["SAMPLES_ROW_LIMIT"], + "row_limit": app.config["SAMPLES_ROW_LIMIT"], "columns": [o.column_name for o in self.datasource.columns], "from_dttm": None, "to_dttm": None, @@ -361,7 +357,7 @@ def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals timeseries_limit_metric = self.form_data.get("timeseries_limit_metric") # apply row limit to query - row_limit = int(self.form_data.get("row_limit") or config["ROW_LIMIT"]) + row_limit = int(self.form_data.get("row_limit") or app.config["ROW_LIMIT"]) row_limit = apply_max_row_limit(row_limit) # default order direction @@ -369,8 +365,8 @@ def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals try: since, until = get_since_until( - relative_start=relative_start, - relative_end=relative_end, + relative_start=app.config["DEFAULT_RELATIVE_START_TIME"], + relative_end=app.config["DEFAULT_RELATIVE_END_TIME"], time_range=self.form_data.get("time_range"), since=self.form_data.get("since"), until=self.form_data.get("until"), @@ -433,9 +429,9 @@ def cache_timeout(self) -> int: and self.datasource.database.cache_timeout ) is not None: return self.datasource.database.cache_timeout - if config["DATA_CACHE_CONFIG"].get("CACHE_DEFAULT_TIMEOUT") is not None: - return config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] - return config["CACHE_DEFAULT_TIMEOUT"] + if app.config["DATA_CACHE_CONFIG"].get("CACHE_DEFAULT_TIMEOUT") is not None: + return app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] + return app.config["CACHE_DEFAULT_TIMEOUT"] @deprecated(deprecated_in="3.0") def get_json(self) -> str: @@ -531,7 +527,7 @@ def get_df_payload( # pylint: disable=too-many-statements if cache_key and cache_manager.data_cache and not force: cache_value = cache_manager.data_cache.get(cache_key) if cache_value: - stats_logger.incr("loading_from_cache") + self.stats_logger.incr("loading_from_cache") try: df = cache_value["df"] self.query = cache_value["query"] @@ -543,7 +539,7 @@ def get_df_payload( # pylint: disable=too-many-statements ) self.status = QueryStatus.SUCCESS is_loaded = True - stats_logger.incr("loaded_from_cache") + self.stats_logger.incr("loaded_from_cache") except Exception as ex: # pylint: disable=broad-except logger.exception(ex) logger.error( @@ -581,9 +577,9 @@ def get_df_payload( # pylint: disable=too-many-statements ) df = self.get_df(query_obj) if self.status != QueryStatus.FAILED: - stats_logger.incr("loaded_from_source") + self.stats_logger.incr("loaded_from_source") if not self.force: - stats_logger.incr("loaded_from_source_without_force") + self.stats_logger.incr("loaded_from_source_without_force") is_loaded = True except QueryObjectValidationError as ex: error = dataclasses.asdict( @@ -676,7 +672,9 @@ def data(self) -> dict[str, Any]: def get_csv(self) -> str | None: df = self.get_df_payload()["df"] # leverage caching logic include_index = not isinstance(df.index, pd.RangeIndex) - return csv.df_to_escaped_csv(df, index=include_index, **config["CSV_EXPORT"]) + return csv.df_to_escaped_csv( + df, index=include_index, **app.config["CSV_EXPORT"] + ) @deprecated(deprecated_in="3.0") def get_data(self, df: pd.DataFrame) -> VizData: @@ -771,8 +769,8 @@ def get_data(self, df: pd.DataFrame) -> VizData: # pylint: disable=too-many-loc try: start, end = get_since_until( - relative_start=relative_start, - relative_end=relative_end, + relative_start=app.config["DEFAULT_RELATIVE_START_TIME"], + relative_end=app.config["DEFAULT_RELATIVE_END_TIME"], time_range=form_data.get("time_range"), since=form_data.get("since"), until=form_data.get("until"), @@ -1793,7 +1791,7 @@ def get_data(self, df: pd.DataFrame) -> VizData: return { "geoJSON": geo_json, "hasCustomMetric": has_custom_metric, - "mapboxApiKey": config["MAPBOX_API_KEY"], + "mapboxApiKey": app.config["MAPBOX_API_KEY"], "mapStyle": self.form_data.get("mapbox_style"), "aggregatorName": self.form_data.get("pandas_aggfunc"), "clusteringRadius": self.form_data.get("clustering_radius"), @@ -1823,13 +1821,13 @@ def query_obj(self) -> QueryObjectDict: def get_data(self, df: pd.DataFrame) -> VizData: # Late imports to avoid circular import issues # pylint: disable=import-outside-toplevel - from superset import db + from superset.extensions import db from superset.models.slice import Slice slice_ids = self.form_data.get("deck_slices") slices = db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all() return { - "mapboxApiKey": config["MAPBOX_API_KEY"], + "mapboxApiKey": app.config["MAPBOX_API_KEY"], "slices": [slc.data for slc in slices], } @@ -2001,7 +1999,7 @@ def get_data(self, df: pd.DataFrame) -> VizData: return { "features": features, - "mapboxApiKey": config["MAPBOX_API_KEY"], + "mapboxApiKey": app.config["MAPBOX_API_KEY"], "metricLabels": self.metric_labels, } @@ -2324,7 +2322,7 @@ def get_data(self, df: pd.DataFrame) -> VizData: return { "features": super().get_data(df)["features"], - "mapboxApiKey": config["MAPBOX_API_KEY"], + "mapboxApiKey": app.config["MAPBOX_API_KEY"], } @@ -2659,8 +2657,9 @@ def get_subclasses(cls: type[BaseViz]) -> set[type[BaseViz]]: ) -viz_types = { - o.viz_type: o - for o in get_subclasses(BaseViz) - if o.viz_type not in config["VIZ_TYPE_DENYLIST"] -} +def get_viz_types() -> dict[str, type[BaseViz]]: + return { + o.viz_type: o + for o in get_subclasses(BaseViz) + if o.viz_type not in app.config["VIZ_TYPE_DENYLIST"] + } diff --git a/tests/conftest.py b/tests/conftest.py index 3a712ec5827f..61b87595cfa0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,8 @@ def pandas_loader_configuration( @fixture(scope="session") def support_datetime_type(example_db_provider: Callable[[], Database]) -> bool: - return example_db_provider().backend != PRESTO + with app.app_context(): + return example_db_provider().backend != PRESTO @fixture(scope="session") diff --git a/tests/integration_tests/annotation_layers/api_tests.py b/tests/integration_tests/annotation_layers/api_tests.py index 49c88ae2920f..a732016db2f8 100644 --- a/tests/integration_tests/annotation_layers/api_tests.py +++ b/tests/integration_tests/annotation_layers/api_tests.py @@ -22,7 +22,7 @@ from sqlalchemy.sql import func import tests.integration_tests.test_app # noqa: F401 -from superset import db +from superset.extensions import db from superset.utils import json from superset.models.annotations import Annotation, AnnotationLayer diff --git a/tests/integration_tests/annotation_layers/fixtures.py b/tests/integration_tests/annotation_layers/fixtures.py index a3fb6e1842cb..706decf2f8d6 100644 --- a/tests/integration_tests/annotation_layers/fixtures.py +++ b/tests/integration_tests/annotation_layers/fixtures.py @@ -20,7 +20,7 @@ import pytest from flask.ctx import AppContext -from superset import db +from superset.extensions import db from superset.models.annotations import Annotation, AnnotationLayer ANNOTATION_LAYERS_COUNT = 10 diff --git a/tests/integration_tests/base_api_tests.py b/tests/integration_tests/base_api_tests.py index de003ff945b6..b39b9ed4bfd4 100644 --- a/tests/integration_tests/base_api_tests.py +++ b/tests/integration_tests/base_api_tests.py @@ -27,7 +27,8 @@ import prison import tests.integration_tests.test_app # noqa: F401 -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.extensions import appbuilder from superset.models.dashboard import Dashboard from superset.views.base_api import BaseSupersetModelRestApi, requires_json # noqa: F401 diff --git a/tests/integration_tests/base_tests.py b/tests/integration_tests/base_tests.py index 77633d65642e..a6ce79777106 100644 --- a/tests/integration_tests/base_tests.py +++ b/tests/integration_tests/base_tests.py @@ -35,7 +35,8 @@ from tests.integration_tests.test_app import app, login from superset.sql_parse import CtasMethod -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.connectors.sqla.models import BaseDatasource, SqlaTable from superset.models import core as models from superset.models.slice import Slice diff --git a/tests/integration_tests/cache_tests.py b/tests/integration_tests/cache_tests.py index 88b20282f40a..8d1cab525ade 100644 --- a/tests/integration_tests/cache_tests.py +++ b/tests/integration_tests/cache_tests.py @@ -18,7 +18,6 @@ import pytest -from superset import app, db # noqa: F401 from superset.common.db_query_status import QueryStatus from superset.extensions import cache_manager from superset.utils import json @@ -28,6 +27,7 @@ load_birth_names_dashboard_with_slices, # noqa: F401 load_birth_names_data, # noqa: F401 ) +from tests.integration_tests.test_app import app class TestCache(SupersetTestCase): diff --git a/tests/integration_tests/celery_tests.py b/tests/integration_tests/celery_tests.py index 3bd82211e5da..027159a3041c 100644 --- a/tests/integration_tests/celery_tests.py +++ b/tests/integration_tests/celery_tests.py @@ -33,7 +33,8 @@ import flask # noqa: F401 from flask import current_app, has_app_context # noqa: F401 -from superset import db, sql_lab +from superset.extensions import db +from superset import sql_lab from superset.common.db_query_status import QueryStatus from superset.result_set import SupersetResultSet from superset.db_engine_specs.base import BaseEngineSpec diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index 42df108de96e..ab2be2404761 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -40,7 +40,7 @@ from superset.utils import json from superset.utils.core import get_example_default_schema from superset.utils.database import get_example_database # noqa: F401 -from superset.viz import viz_types # noqa: F401 +from superset.viz import get_viz_types # noqa: F401 from tests.integration_tests.base_api_tests import ApiOwnersTestCaseMixin from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.conftest import with_feature_flags # noqa: F401 diff --git a/tests/integration_tests/charts/commands_tests.py b/tests/integration_tests/charts/commands_tests.py index 1ee4658b88ff..a1099f67a029 100644 --- a/tests/integration_tests/charts/commands_tests.py +++ b/tests/integration_tests/charts/commands_tests.py @@ -20,7 +20,7 @@ import yaml from flask import g # noqa: F401 -from superset import db, security_manager +from superset import security_manager from superset.commands.chart.create import CreateChartCommand from superset.commands.chart.exceptions import ( ChartNotFoundError, @@ -33,6 +33,7 @@ from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.exceptions import IncorrectVersionError from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.slice import Slice from superset.utils import json diff --git a/tests/integration_tests/cli_tests.py b/tests/integration_tests/cli_tests.py index 048612a08a7b..83ab138dcfb0 100644 --- a/tests/integration_tests/cli_tests.py +++ b/tests/integration_tests/cli_tests.py @@ -23,11 +23,12 @@ import pytest import yaml # noqa: F401 +from flask import current_app as app from freezegun import freeze_time import superset.cli.importexport import superset.cli.thumbnails -from superset import app, db +from superset.extensions import db from superset.models.dashboard import Dashboard from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, # noqa: F401 diff --git a/tests/integration_tests/commands_test.py b/tests/integration_tests/commands_test.py index 83409fd02280..010f04d786bc 100644 --- a/tests/integration_tests/commands_test.py +++ b/tests/integration_tests/commands_test.py @@ -19,10 +19,10 @@ import yaml from flask import g -from superset import db from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.v1.assets import ImportAssetsCommand from superset.commands.importers.v1.utils import is_valid_config +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils import json diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 84c579310556..613abd8d684d 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -27,8 +27,8 @@ from flask_appbuilder.security.sqla import models as ab_models from sqlalchemy.engine import Engine -from superset import db, security_manager -from superset.extensions import feature_flag_manager +from superset import security_manager +from superset.extensions import db, feature_flag_manager from superset.utils.database import get_example_database, remove_database from superset.utils.json import json_dumps_w_dates from tests.integration_tests.test_app import app, login diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 923fe4a6b947..20d88b5a0a97 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -34,7 +34,7 @@ import superset.utils.database import superset.views.utils -from superset import dataframe, db, security_manager, sql_lab +from superset import dataframe, sql_lab from superset.commands.chart.data.get_data_command import ChartDataCommand from superset.commands.chart.exceptions import ChartDataQueryFailedError from superset.common.db_query_status import QueryStatus @@ -42,7 +42,12 @@ from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.mssql import MssqlEngineSpec from superset.exceptions import SupersetException -from superset.extensions import async_query_manager_factory, cache_manager +from superset.extensions import ( + async_query_manager_factory, + cache_manager, + db, + security_manager, +) from superset.models import core as models from superset.models.cache import CacheKey from superset.models.dashboard import Dashboard diff --git a/tests/integration_tests/css_templates/api_tests.py b/tests/integration_tests/css_templates/api_tests.py index 27754afd30f0..84878860e3f9 100644 --- a/tests/integration_tests/css_templates/api_tests.py +++ b/tests/integration_tests/css_templates/api_tests.py @@ -24,7 +24,7 @@ from sqlalchemy.sql import func import tests.integration_tests.test_app # noqa: F401 -from superset import db +from superset.extensions import db from superset.models.core import CssTemplate from superset.utils.database import get_example_database # noqa: F401 from superset.utils import json diff --git a/tests/integration_tests/dashboard_tests.py b/tests/integration_tests/dashboard_tests.py index 1852adba48af..1055d052dc4d 100644 --- a/tests/integration_tests/dashboard_tests.py +++ b/tests/integration_tests/dashboard_tests.py @@ -25,7 +25,8 @@ from flask import Response, escape, url_for from sqlalchemy import func -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.connectors.sqla.models import SqlaTable from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/dashboard_utils.py b/tests/integration_tests/dashboard_utils.py index 1b900ecbcc8a..2d256cf44281 100644 --- a/tests/integration_tests/dashboard_utils.py +++ b/tests/integration_tests/dashboard_utils.py @@ -20,8 +20,8 @@ from pandas import DataFrame # noqa: F401 -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index bd7e230dbe5b..2913a346aaef 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -30,7 +30,7 @@ from freezegun import freeze_time from sqlalchemy import and_ -from superset import app, db, security_manager # noqa: F401 +from superset.extensions import db, security_manager from superset.models.dashboard import Dashboard from superset.models.core import FavStar, FavStarClassName from superset.reports.models import ReportSchedule, ReportScheduleType diff --git a/tests/integration_tests/dashboards/commands_tests.py b/tests/integration_tests/dashboards/commands_tests.py index 06edd6c6d0f1..531f89a86de2 100644 --- a/tests/integration_tests/dashboards/commands_tests.py +++ b/tests/integration_tests/dashboards/commands_tests.py @@ -21,7 +21,7 @@ import yaml from werkzeug.utils import secure_filename -from superset import db, security_manager +from superset import security_manager from superset.commands.dashboard.exceptions import DashboardNotFoundError from superset.commands.dashboard.export import ( append_charts, @@ -32,6 +32,7 @@ from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.exceptions import IncorrectVersionError from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/dashboards/dao_tests.py b/tests/integration_tests/dashboards/dao_tests.py index 83ef02730b0a..6cd5ba9b0cd4 100644 --- a/tests/integration_tests/dashboards/dao_tests.py +++ b/tests/integration_tests/dashboards/dao_tests.py @@ -21,8 +21,9 @@ import pytest import tests.integration_tests.test_app # pylint: disable=unused-import # noqa: F401 -from superset import db, security_manager from superset.utils import json +from superset.extensions import db +from superset import security_manager from superset.daos.dashboard import DashboardDAO from superset.models.dashboard import Dashboard from tests.integration_tests.base_tests import SupersetTestCase diff --git a/tests/integration_tests/dashboards/dashboard_test_utils.py b/tests/integration_tests/dashboards/dashboard_test_utils.py index 39bce02caa37..2a37f4e91417 100644 --- a/tests/integration_tests/dashboards/dashboard_test_utils.py +++ b/tests/integration_tests/dashboards/dashboard_test_utils.py @@ -21,8 +21,8 @@ from sqlalchemy import func -from superset import appbuilder, db, security_manager from superset.connectors.sqla.models import SqlaTable +from superset.extensions import appbuilder, db, security_manager from superset.models.dashboard import Dashboard from superset.models.slice import Slice from tests.integration_tests.dashboards.consts import DEFAULT_DASHBOARD_SLUG_TO_TEST diff --git a/tests/integration_tests/dashboards/filter_state/api_tests.py b/tests/integration_tests/dashboards/filter_state/api_tests.py index b050a326d5ee..43dc330c528d 100644 --- a/tests/integration_tests/dashboards/filter_state/api_tests.py +++ b/tests/integration_tests/dashboards/filter_state/api_tests.py @@ -21,12 +21,11 @@ from flask_appbuilder.security.sqla.models import User from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.commands.dashboard.exceptions import ( DashboardAccessDeniedError, # noqa: F401 ) from superset.commands.temporary_cache.entry import Entry -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.models.dashboard import Dashboard from superset.temporary_cache.utils import cache_key from superset.utils import json @@ -54,7 +53,7 @@ def admin_id(app_context: AppContext) -> int: @pytest.fixture(autouse=True) -def cache(dashboard_id, admin_id): +def cache(dashboard_id: int, admin_id: int) -> None: entry: Entry = {"owner": admin_id, "value": INITIAL_VALUE} cache_manager.filter_state_cache.set(cache_key(dashboard_id, KEY), entry) diff --git a/tests/integration_tests/dashboards/permalink/api_tests.py b/tests/integration_tests/dashboards/permalink/api_tests.py index 8e0dcaaa420d..0c31d8d8abc8 100644 --- a/tests/integration_tests/dashboards/permalink/api_tests.py +++ b/tests/integration_tests/dashboards/permalink/api_tests.py @@ -22,10 +22,10 @@ from flask_appbuilder.security.sqla.models import User # noqa: F401 from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.commands.dashboard.exceptions import ( DashboardAccessDeniedError, # noqa: F401 ) +from superset.extensions import db from superset.key_value.models import KeyValueEntry from superset.key_value.types import KeyValueResource from superset.key_value.utils import decode_permalink_id diff --git a/tests/integration_tests/dashboards/security/security_dataset_tests.py b/tests/integration_tests/dashboards/security/security_dataset_tests.py index 17a5c477e6b8..2b33b2aeecbb 100644 --- a/tests/integration_tests/dashboards/security/security_dataset_tests.py +++ b/tests/integration_tests/dashboards/security/security_dataset_tests.py @@ -18,9 +18,11 @@ import prison import pytest -from flask import escape # noqa: F401 +from flask import ( + current_app as app, + escape, # noqa: F401 +) -from superset import app from superset.daos.dashboard import DashboardDAO from superset.utils import json from tests.integration_tests.constants import ADMIN_USERNAME, GAMMA_USERNAME diff --git a/tests/integration_tests/dashboards/superset_factory_util.py b/tests/integration_tests/dashboards/superset_factory_util.py index aeae6171dfcd..7facd2b7cd72 100644 --- a/tests/integration_tests/dashboards/superset_factory_util.py +++ b/tests/integration_tests/dashboards/superset_factory_util.py @@ -20,8 +20,8 @@ from flask_appbuilder import Model from flask_appbuilder.security.sqla.models import User -from superset import db from superset.connectors.sqla.models import SqlaTable, sqlatable_user +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import ( Dashboard, diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 9f9882c99bde..1b17a49d75d0 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -34,7 +34,8 @@ from sqlalchemy.exc import DBAPIError from sqlalchemy.sql import func -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.connectors.sqla.models import SqlaTable from superset.databases.ssh_tunnel.models import SSHTunnel from superset.databases.utils import make_url_safe # noqa: F401 diff --git a/tests/integration_tests/databases/commands/upload_test.py b/tests/integration_tests/databases/commands/upload_test.py index 3e0eb55b77b1..ba680d955f84 100644 --- a/tests/integration_tests/databases/commands/upload_test.py +++ b/tests/integration_tests/databases/commands/upload_test.py @@ -20,7 +20,7 @@ import pytest from flask.ctx import AppContext -from superset import db, security_manager +from superset import security_manager from superset.commands.database.exceptions import ( DatabaseNotFoundError, DatabaseSchemaUploadNotAllowed, @@ -29,6 +29,7 @@ from superset.commands.database.uploaders.base import UploadCommand from superset.commands.database.uploaders.csv_reader import CSVReader from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.utils import json from superset.utils.core import override_user diff --git a/tests/integration_tests/databases/commands_tests.py b/tests/integration_tests/databases/commands_tests.py index 8979b91c4724..d51e5947bf0b 100644 --- a/tests/integration_tests/databases/commands_tests.py +++ b/tests/integration_tests/databases/commands_tests.py @@ -22,7 +22,7 @@ from func_timeout import FunctionTimedOut from sqlalchemy.exc import DBAPIError -from superset import db, event_logger, security_manager # noqa: F401 +from superset import event_logger, security_manager # noqa: F401 from superset.commands.database.create import CreateDatabaseCommand from superset.commands.database.exceptions import ( DatabaseInvalidError, @@ -49,6 +49,7 @@ SupersetSecurityException, SupersetTimeoutException, ) +from superset.extensions import db from superset.models.core import Database from superset.utils.core import backend from superset.utils.database import get_example_database diff --git a/tests/integration_tests/datasets/commands_tests.py b/tests/integration_tests/datasets/commands_tests.py index 53bd7fa051aa..e813dbb36c38 100644 --- a/tests/integration_tests/datasets/commands_tests.py +++ b/tests/integration_tests/datasets/commands_tests.py @@ -22,7 +22,7 @@ import yaml from sqlalchemy.exc import SQLAlchemyError -from superset import db, security_manager +from superset import security_manager from superset.commands.database.importers.v1 import ImportDatabasesCommand from superset.commands.dataset.create import CreateDatasetCommand from superset.commands.dataset.exceptions import ( @@ -36,6 +36,7 @@ from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.exceptions import IncorrectVersionError from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.slice import Slice from superset.utils.core import get_example_default_schema, override_user diff --git a/tests/integration_tests/datasource/api_tests.py b/tests/integration_tests/datasource/api_tests.py index 5cf29d5755f5..397fa73a4ab7 100644 --- a/tests/integration_tests/datasource/api_tests.py +++ b/tests/integration_tests/datasource/api_tests.py @@ -18,9 +18,10 @@ import pytest -from superset import db, security_manager +from superset import security_manager from superset.connectors.sqla.models import SqlaTable from superset.daos.exceptions import DatasourceTypeNotSupportedError +from superset.extensions import db from superset.utils import json from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.constants import ADMIN_USERNAME, GAMMA_USERNAME diff --git a/tests/integration_tests/datasource_tests.py b/tests/integration_tests/datasource_tests.py index 1b7fcb733b5a..18c65a2d48c9 100644 --- a/tests/integration_tests/datasource_tests.py +++ b/tests/integration_tests/datasource_tests.py @@ -22,8 +22,8 @@ import prison import pytest +from flask import current_app as app -from superset import app, db from superset.commands.dataset.exceptions import DatasetNotFoundError from superset.common.utils.query_cache_manager import QueryCacheManager from superset.connectors.sqla.models import ( # noqa: F401 @@ -34,6 +34,7 @@ from superset.constants import CacheRegion from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError from superset.exceptions import SupersetGenericDBErrorException +from superset.extensions import db from superset.models.core import Database from superset.utils import json from superset.utils.core import backend, get_example_default_schema # noqa: F401 diff --git a/tests/integration_tests/dict_import_export_tests.py b/tests/integration_tests/dict_import_export_tests.py index 2db17a77b50b..b5c6da60daaf 100644 --- a/tests/integration_tests/dict_import_export_tests.py +++ b/tests/integration_tests/dict_import_export_tests.py @@ -23,7 +23,7 @@ import yaml from tests.integration_tests.test_app import app -from superset import db +from superset.extensions import db from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.utils.database import get_example_database diff --git a/tests/integration_tests/email_tests.py b/tests/integration_tests/email_tests.py index c8bc137062af..8673a886c897 100644 --- a/tests/integration_tests/email_tests.py +++ b/tests/integration_tests/email_tests.py @@ -25,7 +25,8 @@ from email.mime.multipart import MIMEMultipart from unittest import mock -from superset import app +from flask import current_app as app + from superset.utils import core as utils from tests.integration_tests.base_tests import SupersetTestCase diff --git a/tests/integration_tests/embedded/api_tests.py b/tests/integration_tests/embedded/api_tests.py index 533f1311d3d6..6727e5c4e619 100644 --- a/tests/integration_tests/embedded/api_tests.py +++ b/tests/integration_tests/embedded/api_tests.py @@ -21,7 +21,7 @@ import pytest -from superset import db +from superset.extensions import db from superset.daos.dashboard import EmbeddedDashboardDAO from superset.models.dashboard import Dashboard from tests.integration_tests.base_tests import SupersetTestCase diff --git a/tests/integration_tests/embedded/dao_tests.py b/tests/integration_tests/embedded/dao_tests.py index e1f72feb89db..c4b354806dda 100644 --- a/tests/integration_tests/embedded/dao_tests.py +++ b/tests/integration_tests/embedded/dao_tests.py @@ -18,7 +18,7 @@ import pytest import tests.integration_tests.test_app # pylint: disable=unused-import # noqa: F401 -from superset import db +from superset.extensions import db from superset.daos.dashboard import EmbeddedDashboardDAO from superset.models.dashboard import Dashboard from tests.integration_tests.base_tests import SupersetTestCase diff --git a/tests/integration_tests/embedded/test_view.py b/tests/integration_tests/embedded/test_view.py index 7fcfcdba9ff0..248326621d5a 100644 --- a/tests/integration_tests/embedded/test_view.py +++ b/tests/integration_tests/embedded/test_view.py @@ -21,8 +21,8 @@ import pytest -from superset import db from superset.daos.dashboard import EmbeddedDashboardDAO +from superset.extensions import db from superset.models.dashboard import Dashboard from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, # noqa: F401 diff --git a/tests/integration_tests/explore/api_tests.py b/tests/integration_tests/explore/api_tests.py index 730e013f8271..2cf66a5af74b 100644 --- a/tests/integration_tests/explore/api_tests.py +++ b/tests/integration_tests/explore/api_tests.py @@ -20,11 +20,10 @@ from flask_appbuilder.security.sqla.models import User from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.commands.explore.form_data.state import TemporaryExploreState from superset.connectors.sqla.models import SqlaTable from superset.explore.exceptions import DatasetAccessDeniedError -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.models.slice import Slice from superset.utils import json from tests.integration_tests.fixtures.world_bank_dashboard import ( diff --git a/tests/integration_tests/explore/form_data/api_tests.py b/tests/integration_tests/explore/form_data/api_tests.py index 6057bb271f9b..20a305e3ca76 100644 --- a/tests/integration_tests/explore/form_data/api_tests.py +++ b/tests/integration_tests/explore/form_data/api_tests.py @@ -20,11 +20,10 @@ from flask_appbuilder.security.sqla.models import User from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.commands.dataset.exceptions import DatasetAccessDeniedError from superset.commands.explore.form_data.state import TemporaryExploreState from superset.connectors.sqla.models import SqlaTable -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.models.slice import Slice from superset.utils import json from superset.utils.core import DatasourceType diff --git a/tests/integration_tests/explore/form_data/commands_tests.py b/tests/integration_tests/explore/form_data/commands_tests.py index 979d0f2cf0b2..276f8d7ba343 100644 --- a/tests/integration_tests/explore/form_data/commands_tests.py +++ b/tests/integration_tests/explore/form_data/commands_tests.py @@ -18,8 +18,8 @@ from unittest.mock import patch import pytest +from flask import current_app as app -from superset import app, db, security_manager from superset.commands.exceptions import DatasourceTypeInvalidError from superset.commands.explore.form_data.create import CreateFormDataCommand from superset.commands.explore.form_data.delete import DeleteFormDataCommand @@ -27,6 +27,7 @@ from superset.commands.explore.form_data.parameters import CommandParameters from superset.commands.explore.form_data.update import UpdateFormDataCommand from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db, security_manager from superset.models.slice import Slice from superset.models.sql_lab import Query from superset.utils import json diff --git a/tests/integration_tests/explore/permalink/api_tests.py b/tests/integration_tests/explore/permalink/api_tests.py index a2410306121f..d811ac6da50d 100644 --- a/tests/integration_tests/explore/permalink/api_tests.py +++ b/tests/integration_tests/explore/permalink/api_tests.py @@ -21,8 +21,8 @@ import pytest from sqlalchemy.orm import Session # noqa: F401 -from superset import db from superset.explore.permalink.schemas import ExplorePermalinkSchema +from superset.extensions import db from superset.key_value.models import KeyValueEntry from superset.key_value.types import KeyValueResource, MarshmallowKeyValueCodec from superset.key_value.utils import decode_permalink_id, encode_permalink_key diff --git a/tests/integration_tests/explore/permalink/commands_tests.py b/tests/integration_tests/explore/permalink/commands_tests.py index 4993e33f1895..e59fe2c31e46 100644 --- a/tests/integration_tests/explore/permalink/commands_tests.py +++ b/tests/integration_tests/explore/permalink/commands_tests.py @@ -18,11 +18,12 @@ from unittest.mock import patch import pytest +from flask import current_app as app -from superset import app, db, security_manager from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand from superset.commands.explore.permalink.get import GetExplorePermalinkCommand from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db, security_manager from superset.models.slice import Slice from superset.models.sql_lab import Query from superset.utils.core import DatasourceType, get_example_default_schema diff --git a/tests/integration_tests/fixtures/birth_names_dashboard.py b/tests/integration_tests/fixtures/birth_names_dashboard.py index 513a9f84a24e..4dfd4b23dbff 100644 --- a/tests/integration_tests/fixtures/birth_names_dashboard.py +++ b/tests/integration_tests/fixtures/birth_names_dashboard.py @@ -18,8 +18,8 @@ import pytest -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/fixtures/energy_dashboard.py b/tests/integration_tests/fixtures/energy_dashboard.py index 5d938e05416c..c49ccf8d0918 100644 --- a/tests/integration_tests/fixtures/energy_dashboard.py +++ b/tests/integration_tests/fixtures/energy_dashboard.py @@ -20,8 +20,8 @@ import pytest from sqlalchemy import column, Float, String -from superset import db from superset.connectors.sqla.models import SqlaTable, SqlMetric +from superset.extensions import db from superset.models.slice import Slice from superset.utils.core import get_example_default_schema from superset.utils.database import get_example_database diff --git a/tests/integration_tests/fixtures/tabbed_dashboard.py b/tests/integration_tests/fixtures/tabbed_dashboard.py index d4ddff579634..707c1dd93979 100644 --- a/tests/integration_tests/fixtures/tabbed_dashboard.py +++ b/tests/integration_tests/fixtures/tabbed_dashboard.py @@ -17,7 +17,7 @@ import pytest -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.utils import json from superset.utils.core import shortid diff --git a/tests/integration_tests/fixtures/unicode_dashboard.py b/tests/integration_tests/fixtures/unicode_dashboard.py index e68e8f079944..04b5da39a47b 100644 --- a/tests/integration_tests/fixtures/unicode_dashboard.py +++ b/tests/integration_tests/fixtures/unicode_dashboard.py @@ -18,8 +18,8 @@ import pytest from sqlalchemy import String -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils.core import get_example_default_schema diff --git a/tests/integration_tests/fixtures/users.py b/tests/integration_tests/fixtures/users.py index dd3730df9649..b11c0a0d7b26 100644 --- a/tests/integration_tests/fixtures/users.py +++ b/tests/integration_tests/fixtures/users.py @@ -18,7 +18,8 @@ from flask.ctx import AppContext from flask_appbuilder.security.sqla.models import Role, User -from superset import db, security_manager +from superset import security_manager +from superset.extensions import db from tests.integration_tests.constants import GAMMA_SQLLAB_NO_DATA_USERNAME diff --git a/tests/integration_tests/fixtures/world_bank_dashboard.py b/tests/integration_tests/fixtures/world_bank_dashboard.py index 34c718c200c2..e68aeeb90d05 100644 --- a/tests/integration_tests/fixtures/world_bank_dashboard.py +++ b/tests/integration_tests/fixtures/world_bank_dashboard.py @@ -23,8 +23,8 @@ from pandas import DataFrame from sqlalchemy import DateTime, String -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/import_export_tests.py b/tests/integration_tests/import_export_tests.py index e4c9bff51e4c..445112ad999f 100644 --- a/tests/integration_tests/import_export_tests.py +++ b/tests/integration_tests/import_export_tests.py @@ -33,7 +33,8 @@ ) from tests.integration_tests.test_app import app from superset.commands.dashboard.importers.v0 import decode_dashboards -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.commands.dashboard.importers.v0 import import_chart, import_dashboard diff --git a/tests/integration_tests/insert_chart_mixin.py b/tests/integration_tests/insert_chart_mixin.py index 722e387a543a..261481f9926a 100644 --- a/tests/integration_tests/insert_chart_mixin.py +++ b/tests/integration_tests/insert_chart_mixin.py @@ -16,8 +16,9 @@ # under the License. from typing import Optional -from superset import db, security_manager +from superset import security_manager from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.slice import Slice diff --git a/tests/integration_tests/log_api_tests.py b/tests/integration_tests/log_api_tests.py index fae09754aa9b..70110d890b90 100644 --- a/tests/integration_tests/log_api_tests.py +++ b/tests/integration_tests/log_api_tests.py @@ -25,7 +25,7 @@ import prison from unittest.mock import patch -from superset import db +from superset.extensions import db from superset.models.core import Log from superset.views.log.api import LogRestApi from superset.utils import json diff --git a/tests/integration_tests/migrations/0769ef90fddd_fix_schema_perm_for_datasets__tests.py b/tests/integration_tests/migrations/0769ef90fddd_fix_schema_perm_for_datasets__tests.py index 1e15e87115ce..bb8ae9dcf791 100644 --- a/tests/integration_tests/migrations/0769ef90fddd_fix_schema_perm_for_datasets__tests.py +++ b/tests/integration_tests/migrations/0769ef90fddd_fix_schema_perm_for_datasets__tests.py @@ -18,8 +18,8 @@ import pytest -from superset import db from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.slice import Slice from superset.utils.core import backend, get_example_default_schema from tests.integration_tests.fixtures.birth_names_dashboard import ( diff --git a/tests/integration_tests/migrations/87d38ad83218_migrate_can_view_and_drill_permission__tests.py b/tests/integration_tests/migrations/87d38ad83218_migrate_can_view_and_drill_permission__tests.py index 789b80db1f37..a98a81e29f6f 100644 --- a/tests/integration_tests/migrations/87d38ad83218_migrate_can_view_and_drill_permission__tests.py +++ b/tests/integration_tests/migrations/87d38ad83218_migrate_can_view_and_drill_permission__tests.py @@ -18,7 +18,7 @@ import pytest -from superset import db +from superset.extensions import db from superset.migrations.shared.security_converge import ( _find_pvm, Permission, diff --git a/tests/integration_tests/model_tests.py b/tests/integration_tests/model_tests.py index 458168009be1..1ca142709f2d 100644 --- a/tests/integration_tests/model_tests.py +++ b/tests/integration_tests/model_tests.py @@ -29,12 +29,13 @@ load_birth_names_data, # noqa: F401 ) +from flask import current_app as app import pytest from sqlalchemy.engine.url import make_url from sqlalchemy.types import DateTime # noqa: F401 import tests.integration_tests.test_app # noqa: F401 -from superset import app, db as metadata_db +from superset.extensions import db as metadata_db from superset.db_engine_specs.postgres import PostgresEngineSpec # noqa: F401 from superset.common.db_query_status import QueryStatus from superset.models.core import Database diff --git a/tests/integration_tests/queries/api_tests.py b/tests/integration_tests/queries/api_tests.py index 2819c23b4141..bc1186679ae2 100644 --- a/tests/integration_tests/queries/api_tests.py +++ b/tests/integration_tests/queries/api_tests.py @@ -27,7 +27,8 @@ from sqlalchemy.sql import func import tests.integration_tests.test_app # noqa: F401 -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.common.db_query_status import QueryStatus from superset.models.core import Database from superset.utils.database import get_example_database, get_main_database diff --git a/tests/integration_tests/queries/saved_queries/api_tests.py b/tests/integration_tests/queries/saved_queries/api_tests.py index da203a7139b2..684b18c9dc99 100644 --- a/tests/integration_tests/queries/saved_queries/api_tests.py +++ b/tests/integration_tests/queries/saved_queries/api_tests.py @@ -28,7 +28,7 @@ from freezegun import freeze_time from sqlalchemy.sql import func, and_ -from superset import db +from superset.extensions import db from superset.models.core import Database from superset.models.core import FavStar from superset.models.sql_lab import SavedQuery diff --git a/tests/integration_tests/queries/saved_queries/commands_tests.py b/tests/integration_tests/queries/saved_queries/commands_tests.py index 8babd7efb9db..a3201861987d 100644 --- a/tests/integration_tests/queries/saved_queries/commands_tests.py +++ b/tests/integration_tests/queries/saved_queries/commands_tests.py @@ -20,12 +20,13 @@ import pytest import yaml -from superset import db, security_manager +from superset import security_manager from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.exceptions import IncorrectVersionError from superset.commands.query.exceptions import SavedQueryNotFoundError from superset.commands.query.export import ExportSavedQueriesCommand from superset.commands.query.importers.v1 import ImportSavedQueriesCommand +from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import SavedQuery from superset.utils.database import get_example_database diff --git a/tests/integration_tests/query_context_tests.py b/tests/integration_tests/query_context_tests.py index 9c18b5e07c9d..5ec954a2963f 100644 --- a/tests/integration_tests/query_context_tests.py +++ b/tests/integration_tests/query_context_tests.py @@ -22,9 +22,9 @@ import numpy as np import pandas as pd import pytest +from flask import current_app as app from pandas import DateOffset -from superset import app, db from superset.charts.schemas import ChartDataQueryContextSchema from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.common.query_context import QueryContext @@ -32,7 +32,7 @@ from superset.common.query_object import QueryObject from superset.connectors.sqla.models import SqlMetric from superset.daos.datasource import DatasourceDAO -from superset.extensions import cache_manager +from superset.extensions import cache_manager, db from superset.superset_typing import AdhocColumn from superset.utils.core import ( AdhocMetricExpressionType, diff --git a/tests/integration_tests/reports/api_tests.py b/tests/integration_tests/reports/api_tests.py index 180ac81e2a5f..e55b4e8fca30 100644 --- a/tests/integration_tests/reports/api_tests.py +++ b/tests/integration_tests/reports/api_tests.py @@ -27,7 +27,8 @@ from parameterized import parameterized from sqlalchemy.sql import func -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.models.core import Database from superset.models.slice import Slice from superset.models.dashboard import Dashboard diff --git a/tests/integration_tests/reports/commands/create_dashboard_report_tests.py b/tests/integration_tests/reports/commands/create_dashboard_report_tests.py index 7f92ad85b0ce..dbb8829b4e14 100644 --- a/tests/integration_tests/reports/commands/create_dashboard_report_tests.py +++ b/tests/integration_tests/reports/commands/create_dashboard_report_tests.py @@ -17,9 +17,9 @@ import pytest -from superset import db from superset.commands.report.create import CreateReportScheduleCommand from superset.commands.report.exceptions import ReportScheduleInvalidError +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.reports.models import ( ReportCreationMethod, diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py index 30d183485238..304c4c57342c 100644 --- a/tests/integration_tests/reports/commands_tests.py +++ b/tests/integration_tests/reports/commands_tests.py @@ -38,7 +38,6 @@ ) from sqlalchemy.sql import func -from superset import db from superset.commands.report.exceptions import ( AlertQueryError, AlertQueryInvalidTypeError, @@ -60,6 +59,7 @@ ) from superset.commands.report.log_prune import AsyncPruneReportScheduleLogCommand from superset.exceptions import SupersetException +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/reports/utils.py b/tests/integration_tests/reports/utils.py index c963efc840a3..d0fe84f8b6f9 100644 --- a/tests/integration_tests/reports/utils.py +++ b/tests/integration_tests/reports/utils.py @@ -21,7 +21,8 @@ from flask_appbuilder.security.sqla.models import User -from superset import db, security_manager +from superset import security_manager +from superset.extensions import db from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice diff --git a/tests/integration_tests/security/api_tests.py b/tests/integration_tests/security/api_tests.py index 1fff519bd430..05402980814d 100644 --- a/tests/integration_tests/security/api_tests.py +++ b/tests/integration_tests/security/api_tests.py @@ -21,7 +21,7 @@ import pytest from flask_wtf.csrf import generate_csrf -from superset import db +from superset.extensions import db from superset.daos.dashboard import EmbeddedDashboardDAO from superset.models.dashboard import Dashboard from superset.utils.urls import get_url_host diff --git a/tests/integration_tests/security/guest_token_security_tests.py b/tests/integration_tests/security/guest_token_security_tests.py index f4a515bb75c2..b2f77ca16358 100644 --- a/tests/integration_tests/security/guest_token_security_tests.py +++ b/tests/integration_tests/security/guest_token_security_tests.py @@ -21,10 +21,11 @@ import pytest from flask import g -from superset import db, security_manager +from superset import security_manager from superset.connectors.sqla.models import SqlaTable from superset.daos.dashboard import EmbeddedDashboardDAO from superset.exceptions import SupersetSecurityException +from superset.extensions import db from superset.models.dashboard import Dashboard from superset.security.guest_token import GuestTokenResourceType # noqa: F401 from superset.sql_parse import Table # noqa: F401 diff --git a/tests/integration_tests/security/row_level_security_tests.py b/tests/integration_tests/security/row_level_security_tests.py index 2c8a13a71f4d..a112892f0c33 100644 --- a/tests/integration_tests/security/row_level_security_tests.py +++ b/tests/integration_tests/security/row_level_security_tests.py @@ -23,7 +23,8 @@ from flask import g import prison -from superset import db, security_manager, app # noqa: F401 +from superset.extensions import db +from superset import security_manager, app # noqa: F401 from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable from superset.security.guest_token import ( GuestTokenResourceType, diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index 5b8e4f2ae00e..3c6e2225c34a 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -27,14 +27,16 @@ import prison import pytest -from flask import current_app, g +from flask import current_app as app, g from flask_appbuilder.security.sqla.models import Role + +from superset import viz from superset.daos.datasource import DatasourceDAO # noqa: F401 from superset.models.dashboard import Dashboard -from superset import app, appbuilder, db, security_manager, viz from superset.connectors.sqla.models import SqlaTable from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException +from superset.extensions import appbuilder, db, security_manager from superset.models.core import Database from superset.models.slice import Slice from superset.sql_parse import Table @@ -1253,7 +1255,7 @@ def test_public_sync_role_builtin_perms(self): [permission.view_menu.name, permission.permission.name] for permission in public_role.permissions ] - for pvm in current_app.config["FAB_ROLES"]["TestRole"]: + for pvm in app.config["FAB_ROLES"]["TestRole"]: assert pvm in public_role_resource_names @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") @@ -2021,7 +2023,7 @@ def test_create_guest_access_token(self, get_time_mock): def test_get_guest_user(self): token = self.create_guest_token() fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token guest_user = security_manager.get_guest_user_from_request(fake_request) @@ -2031,7 +2033,7 @@ def test_get_guest_user(self): def test_get_guest_user_with_request_form(self): token = self.create_guest_token() fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = None + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = None fake_request.form["guest_token"] = token guest_user = security_manager.get_guest_user_from_request(fake_request) @@ -2047,7 +2049,7 @@ def test_get_guest_user_expired_token(self, get_time_mock): ) token = self.create_guest_token() fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token guest_user = security_manager.get_guest_user_from_request(fake_request) @@ -2059,7 +2061,7 @@ def test_get_guest_user_no_user(self): rls = {} token = security_manager.create_guest_access_token(user, resources, rls) fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token guest_user = security_manager.get_guest_user_from_request(fake_request) self.assertIsNone(guest_user) @@ -2071,7 +2073,7 @@ def test_get_guest_user_no_resource(self): rls = {} token = security_manager.create_guest_access_token(user, resources, rls) fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token security_manager.get_guest_user_from_request(fake_request) self.assertRaisesRegex( @@ -2099,7 +2101,7 @@ def test_get_guest_user_not_guest_type(self): algorithm=self.app.config["GUEST_TOKEN_JWT_ALGO"], ) fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token guest_user = security_manager.get_guest_user_from_request(fake_request) self.assertIsNone(guest_user) @@ -2126,7 +2128,7 @@ def test_get_guest_user_bad_audience(self): algorithm=self.app.config["GUEST_TOKEN_JWT_ALGO"], ) fake_request = FakeRequest() - fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token + fake_request.headers[app.config["GUEST_TOKEN_HEADER_NAME"]] = token guest_user = security_manager.get_guest_user_from_request(fake_request) self.assertRaisesRegex(jwt.exceptions.InvalidAudienceError, "Invalid audience") diff --git a/tests/integration_tests/sql_lab/api_tests.py b/tests/integration_tests/sql_lab/api_tests.py index 282a0c993e26..f57709dcf87c 100644 --- a/tests/integration_tests/sql_lab/api_tests.py +++ b/tests/integration_tests/sql_lab/api_tests.py @@ -30,7 +30,8 @@ from flask_appbuilder.security.sqla.models import Role from tests.integration_tests.test_app import app -from superset import db, sql_lab +from superset.extensions import db +from superset import sql_lab from superset.common.db_query_status import QueryStatus from superset.models.core import Database # noqa: F401 from superset.utils.database import get_example_database, get_main_database # noqa: F401 diff --git a/tests/integration_tests/sql_lab/commands_tests.py b/tests/integration_tests/sql_lab/commands_tests.py index d18c7dbad9d0..295b08fde93e 100644 --- a/tests/integration_tests/sql_lab/commands_tests.py +++ b/tests/integration_tests/sql_lab/commands_tests.py @@ -19,9 +19,10 @@ import pandas as pd import pytest +from flask import current_app as app from flask_babel import gettext as __ -from superset import app, db, sql_lab +from superset import sql_lab from superset.commands.sql_lab import estimate, export, results from superset.common.db_query_status import QueryStatus from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -31,6 +32,7 @@ SupersetSecurityException, SupersetTimeoutException, ) +from superset.extensions import db from superset.models.core import Database # noqa: F401 from superset.models.sql_lab import Query from superset.sqllab.limiting_factor import LimitingFactor diff --git a/tests/integration_tests/sql_lab/conftest.py b/tests/integration_tests/sql_lab/conftest.py index c8d00131732b..522bf923d555 100644 --- a/tests/integration_tests/sql_lab/conftest.py +++ b/tests/integration_tests/sql_lab/conftest.py @@ -20,7 +20,7 @@ import pytest from flask_appbuilder.security.sqla import models as ab_models -from superset import db +from superset.extensions import db from superset.models.sql_lab import Query from superset.utils.core import shortid from superset.utils.database import get_example_database diff --git a/tests/integration_tests/sql_lab/test_execute_sql_statements.py b/tests/integration_tests/sql_lab/test_execute_sql_statements.py index 7a08f35d3ddc..757379f1c4bf 100644 --- a/tests/integration_tests/sql_lab/test_execute_sql_statements.py +++ b/tests/integration_tests/sql_lab/test_execute_sql_statements.py @@ -14,7 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from superset import app +from flask import current_app as app + from superset.common.db_query_status import QueryStatus from superset.models.core import Database from superset.models.sql_lab import Query diff --git a/tests/integration_tests/sqla_models_tests.py b/tests/integration_tests/sqla_models_tests.py index b6131f32bb37..89f5bfe8efe6 100644 --- a/tests/integration_tests/sqla_models_tests.py +++ b/tests/integration_tests/sqla_models_tests.py @@ -29,7 +29,7 @@ from sqlalchemy.sql import text from sqlalchemy.sql.elements import TextClause -from superset import db +from superset.extensions import db from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric from superset.constants import EMPTY_STRING, NULL_STRING from superset.db_engine_specs.bigquery import BigQueryEngineSpec diff --git a/tests/integration_tests/sqllab_tests.py b/tests/integration_tests/sqllab_tests.py index 3602097b2026..cc4f74e6ebf5 100644 --- a/tests/integration_tests/sqllab_tests.py +++ b/tests/integration_tests/sqllab_tests.py @@ -27,7 +27,8 @@ import prison from freezegun import freeze_time -from superset import db, security_manager +from superset.extensions import db +from superset import security_manager from superset.connectors.sqla.models import SqlaTable # noqa: F401 from superset.db_engine_specs import BaseEngineSpec from superset.db_engine_specs.hive import HiveEngineSpec diff --git a/tests/integration_tests/strategy_tests.py b/tests/integration_tests/strategy_tests.py index 07aa7f5b7a78..a50396dbd7aa 100644 --- a/tests/integration_tests/strategy_tests.py +++ b/tests/integration_tests/strategy_tests.py @@ -31,7 +31,7 @@ from superset.models.slice import Slice # noqa: F401 from superset.utils.database import get_example_database # noqa: F401 -from superset import db +from superset.extensions import db from superset.models.core import Log from superset.tags.models import get_tag, ObjectType, TaggedObject, TagType diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py index 3f6e499449d5..c1c882ab4919 100644 --- a/tests/integration_tests/tags/api_tests.py +++ b/tests/integration_tests/tags/api_tests.py @@ -35,7 +35,8 @@ import tests.integration_tests.test_app # noqa: F401 -from superset import db, security_manager # noqa: F401 +from superset.extensions import db +from superset import security_manager # noqa: F401 from superset.common.db_query_status import QueryStatus # noqa: F401 from superset.models.core import Database # noqa: F401 from superset.utils.database import get_example_database, get_main_database # noqa: F401 diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py index cf340c5b110f..1e83330f3c26 100644 --- a/tests/integration_tests/tags/commands_tests.py +++ b/tests/integration_tests/tags/commands_tests.py @@ -21,7 +21,7 @@ import yaml # noqa: F401 from werkzeug.utils import secure_filename # noqa: F401 -from superset import db, security_manager # noqa: F401 +from superset import security_manager # noqa: F401 from superset.commands.dashboard.exceptions import DashboardNotFoundError # noqa: F401 from superset.commands.dashboard.export import ( append_charts, # noqa: F401 @@ -34,6 +34,7 @@ from superset.commands.tag.create import CreateCustomTagCommand from superset.commands.tag.delete import DeleteTaggedObjectCommand, DeleteTagsCommand from superset.connectors.sqla.models import SqlaTable # noqa: F401 +from superset.extensions import db from superset.models.core import Database # noqa: F401 from superset.models.dashboard import Dashboard from superset.models.slice import Slice # noqa: F401 diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py index b06e22054ec6..4a52e773686f 100644 --- a/tests/integration_tests/tags/dao_tests.py +++ b/tests/integration_tests/tags/dao_tests.py @@ -27,7 +27,8 @@ from tests.integration_tests.tags.api_tests import TAGS_FIXTURE_COUNT import tests.integration_tests.test_app # pylint: disable=unused-import # noqa: F401 -from superset import db, security_manager # noqa: F401 +from superset.extensions import db +from superset import security_manager # noqa: F401 from superset.daos.dashboard import DashboardDAO # noqa: F401 from superset.models.dashboard import Dashboard from tests.integration_tests.base_tests import SupersetTestCase diff --git a/tests/integration_tests/thumbnails_tests.py b/tests/integration_tests/thumbnails_tests.py index cbab3f84f92b..b164f04f875d 100644 --- a/tests/integration_tests/thumbnails_tests.py +++ b/tests/integration_tests/thumbnails_tests.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# from superset import db +# from superset.extensions import db # from superset.models.dashboard import Dashboard import urllib.request @@ -26,8 +26,12 @@ from flask_testing import LiveServerTestCase from sqlalchemy.sql import func -from superset import db, is_feature_enabled, security_manager -from superset.extensions import machine_auth_provider_factory +from superset.extensions import ( + db, + feature_flag_manager, + machine_auth_provider_factory, + security_manager, +) from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.tasks.types import ExecutorType @@ -59,7 +63,9 @@ def url_open_auth(self, username: str, url: str): opener.addheaders.append(("Cookie", f"session={cookies['session']}")) return opener.open(f"{self.get_server_url()}/{url}") - @skipUnless((is_feature_enabled("THUMBNAILS")), "Thumbnails feature") + @skipUnless( + (feature_flag_manager.is_feature_enabled("THUMBNAILS")), "Thumbnails feature" + ) def test_get_async_dashboard_screenshot(self): """ Thumbnails: Simple get async dashboard screenshot @@ -298,7 +304,9 @@ def test_get_async_dashboard_notfound(self): self.assertEqual(rv.status_code, 404) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") - @skipUnless((is_feature_enabled("THUMBNAILS")), "Thumbnails feature") + @skipUnless( + (feature_flag_manager.is_feature_enabled("THUMBNAILS")), "Thumbnails feature" + ) def test_get_async_dashboard_not_allowed(self): """ Thumbnails: Simple get async dashboard not allowed diff --git a/tests/integration_tests/utils/get_dashboards.py b/tests/integration_tests/utils/get_dashboards.py index b23b372310c0..3658293fa65d 100644 --- a/tests/integration_tests/utils/get_dashboards.py +++ b/tests/integration_tests/utils/get_dashboards.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from superset import db +from superset.extensions import db from superset.models.dashboard import Dashboard diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py index cbdea3f60b62..f7f27d17d7d8 100644 --- a/tests/integration_tests/utils_tests.py +++ b/tests/integration_tests/utils_tests.py @@ -29,6 +29,7 @@ load_birth_names_data, # noqa: F401 ) +from flask import current_app as app import numpy as np import pandas as pd import pytest @@ -37,7 +38,7 @@ from sqlalchemy.exc import ArgumentError # noqa: F401 import tests.integration_tests.test_app # noqa: F401 -from superset import app, db, security_manager +from superset.extensions import db, security_manager from superset.constants import NO_TIME_RANGE from superset.exceptions import CertificateException, SupersetException # noqa: F401 from superset.models.core import Database, Log diff --git a/tests/integration_tests/viz_tests.py b/tests/integration_tests/viz_tests.py index 86f3853a47de..7b6ed67deedd 100644 --- a/tests/integration_tests/viz_tests.py +++ b/tests/integration_tests/viz_tests.py @@ -26,7 +26,7 @@ import tests.integration_tests.test_app # noqa: F401 import superset.viz as viz -from superset import app +from flask import current_app as app from superset.constants import NULL_STRING from superset.exceptions import QueryObjectValidationError, SpatialException from superset.utils.core import DTTM_ALIAS diff --git a/tests/unit_tests/charts/test_post_processing.py b/tests/unit_tests/charts/test_post_processing.py index 790c49451634..e1f0dc10a0e0 100644 --- a/tests/unit_tests/charts/test_post_processing.py +++ b/tests/unit_tests/charts/test_post_processing.py @@ -1964,8 +1964,8 @@ def test_apply_post_process_json_format_data_is_none(): def test_apply_post_process_verbose_map(session: Session): - from superset import db from superset.connectors.sqla.models import SqlaTable, SqlMetric + from superset.extensions import db from superset.models.core import Database engine = db.session.get_bind() diff --git a/tests/unit_tests/columns/test_models.py b/tests/unit_tests/columns/test_models.py index 0ea230da1779..bcd433eefbf7 100644 --- a/tests/unit_tests/columns/test_models.py +++ b/tests/unit_tests/columns/test_models.py @@ -24,8 +24,8 @@ def test_column_model(session: Session) -> None: """ Test basic attributes of a ``Column``. """ - from superset import db from superset.columns.models import Column + from superset.extensions import db engine = db.session.get_bind() Column.metadata.create_all(engine) # pylint: disable=no-member diff --git a/tests/unit_tests/commands/dataset/test_update.py b/tests/unit_tests/commands/dataset/test_update.py index 59d43de6fdda..26a408fd3f86 100644 --- a/tests/unit_tests/commands/dataset/test_update.py +++ b/tests/unit_tests/commands/dataset/test_update.py @@ -19,10 +19,10 @@ import pytest from pytest_mock import MockFixture -from superset import db from superset.commands.dataset.exceptions import DatasetInvalidError from superset.commands.dataset.update import UpdateDatasetCommand from superset.connectors.sqla.models import SqlaTable +from superset.extensions import db from superset.models.core import Database diff --git a/tests/unit_tests/commands/importers/v1/assets_test.py b/tests/unit_tests/commands/importers/v1/assets_test.py index 9609b0b45cf8..15308e4d56e5 100644 --- a/tests/unit_tests/commands/importers/v1/assets_test.py +++ b/tests/unit_tests/commands/importers/v1/assets_test.py @@ -35,8 +35,8 @@ def test_import_new_assets(mocker: MockFixture, session: Session) -> None: """ Test that all new assets are imported correctly. """ - from superset import db, security_manager from superset.commands.importers.v1.assets import ImportAssetsCommand + from superset.extensions import db, security_manager from superset.models.dashboard import dashboard_slices from superset.models.slice import Slice @@ -67,8 +67,8 @@ def test_import_adds_dashboard_charts(mocker: MockFixture, session: Session) -> """ Test that existing dashboards are updated with new charts. """ - from superset import db, security_manager from superset.commands.importers.v1.assets import ImportAssetsCommand + from superset.extensions import db, security_manager from superset.models.dashboard import dashboard_slices from superset.models.slice import Slice @@ -106,8 +106,8 @@ def test_import_removes_dashboard_charts(mocker: MockFixture, session: Session) """ Test that existing dashboards are updated without old charts. """ - from superset import db, security_manager from superset.commands.importers.v1.assets import ImportAssetsCommand + from superset.extensions import db, security_manager from superset.models.dashboard import dashboard_slices from superset.models.slice import Slice diff --git a/tests/unit_tests/config_test.py b/tests/unit_tests/config_test.py index 837c53ec074b..e67433a0e1d7 100644 --- a/tests/unit_tests/config_test.py +++ b/tests/unit_tests/config_test.py @@ -23,7 +23,7 @@ from pytest_mock import MockerFixture from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db if TYPE_CHECKING: from superset.connectors.sqla.models import SqlaTable diff --git a/tests/unit_tests/dao/dataset_test.py b/tests/unit_tests/dao/dataset_test.py index 473d1e27b766..25c870a1f73a 100644 --- a/tests/unit_tests/dao/dataset_test.py +++ b/tests/unit_tests/dao/dataset_test.py @@ -28,8 +28,8 @@ def test_validate_update_uniqueness(session: Session) -> None: In particular, allow datasets with the same name in the same database as long as they are in different schemas """ - from superset import db from superset.connectors.sqla.models import SqlaTable + from superset.extensions import db from superset.models.core import Database SqlaTable.metadata.create_all(session.get_bind()) diff --git a/tests/unit_tests/dao/queries_test.py b/tests/unit_tests/dao/queries_test.py index eb84b288fdc8..e4fa2f610e55 100644 --- a/tests/unit_tests/dao/queries_test.py +++ b/tests/unit_tests/dao/queries_test.py @@ -25,7 +25,7 @@ def test_query_dao_save_metadata(session: Session) -> None: - from superset import db + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query @@ -60,7 +60,7 @@ def test_query_dao_save_metadata(session: Session) -> None: def test_query_dao_get_queries_changed_after(session: Session) -> None: - from superset import db + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query @@ -118,8 +118,8 @@ def test_query_dao_get_queries_changed_after(session: Session) -> None: def test_query_dao_stop_query_not_found( mocker: MockFixture, app: Any, session: Session ) -> None: - from superset import db from superset.common.db_query_status import QueryStatus + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query @@ -161,8 +161,8 @@ def test_query_dao_stop_query_not_found( def test_query_dao_stop_query_not_running( mocker: MockFixture, app: Any, session: Session ) -> None: - from superset import db from superset.common.db_query_status import QueryStatus + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query @@ -200,8 +200,8 @@ def test_query_dao_stop_query_not_running( def test_query_dao_stop_query_failed( mocker: MockFixture, app: Any, session: Session ) -> None: - from superset import db from superset.common.db_query_status import QueryStatus + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query @@ -241,8 +241,8 @@ def test_query_dao_stop_query_failed( def test_query_dao_stop_query(mocker: MockFixture, app: Any, session: Session) -> None: - from superset import db from superset.common.db_query_status import QueryStatus + from superset.extensions import db from superset.models.core import Database from superset.models.sql_lab import Query diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py index 43bc085405df..330e410611be 100644 --- a/tests/unit_tests/databases/api_test.py +++ b/tests/unit_tests/databases/api_test.py @@ -31,7 +31,6 @@ from pytest_mock import MockFixture from sqlalchemy.orm.session import Session -from superset import db from superset.commands.database.uploaders.base import UploadCommand from superset.commands.database.uploaders.columnar_reader import ColumnarReader from superset.commands.database.uploaders.csv_reader import CSVReader @@ -39,6 +38,7 @@ from superset.db_engine_specs.sqlite import SqliteEngineSpec from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException +from superset.extensions import db from superset.sql_parse import Table from superset.utils import json from tests.unit_tests.fixtures.common import ( diff --git a/tests/unit_tests/databases/commands/importers/v1/import_test.py b/tests/unit_tests/databases/commands/importers/v1/import_test.py index bf1237cd9e41..8b90280b4c7c 100644 --- a/tests/unit_tests/databases/commands/importers/v1/import_test.py +++ b/tests/unit_tests/databases/commands/importers/v1/import_test.py @@ -19,11 +19,12 @@ import copy import pytest +from flask import current_app as app from pytest_mock import MockFixture from sqlalchemy.orm.session import Session -from superset import db from superset.commands.exceptions import ImportFailedError +from superset.extensions import db, security_manager from superset.utils import json @@ -71,7 +72,6 @@ def test_import_database_sqlite_invalid(mocker: MockFixture, session: Session) - """ Test importing a database. """ - from superset import app, security_manager from superset.commands.database.importers.v1.utils import import_database from superset.models.core import Database from tests.integration_tests.fixtures.importexport import database_config_sqlite diff --git a/tests/unit_tests/datasets/api_tests.py b/tests/unit_tests/datasets/api_tests.py index e0786afaa3ba..06846d611f67 100644 --- a/tests/unit_tests/datasets/api_tests.py +++ b/tests/unit_tests/datasets/api_tests.py @@ -19,7 +19,7 @@ from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db def test_put_invalid_dataset( diff --git a/tests/unit_tests/datasets/commands/importers/v1/import_test.py b/tests/unit_tests/datasets/commands/importers/v1/import_test.py index 97fef4866fc5..8c1cdad39943 100644 --- a/tests/unit_tests/datasets/commands/importers/v1/import_test.py +++ b/tests/unit_tests/datasets/commands/importers/v1/import_test.py @@ -27,11 +27,11 @@ from pytest_mock import MockFixture from sqlalchemy.orm.session import Session -from superset import db from superset.commands.dataset.exceptions import ( DatasetForbiddenDataURI, ) from superset.commands.dataset.importers.v1.utils import validate_data_uri +from superset.extensions import db from superset.utils import json diff --git a/tests/unit_tests/extensions/test_sqlalchemy.py b/tests/unit_tests/extensions/test_sqlalchemy.py index 24c849f55aba..3e41f29adbba 100644 --- a/tests/unit_tests/extensions/test_sqlalchemy.py +++ b/tests/unit_tests/extensions/test_sqlalchemy.py @@ -26,9 +26,9 @@ from sqlalchemy.exc import ProgrammingError from sqlalchemy.orm.session import Session -from superset import db from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException +from superset.extensions import db from tests.unit_tests.conftest import with_feature_flags if TYPE_CHECKING: diff --git a/tests/unit_tests/feature_flag_test.py b/tests/unit_tests/feature_flag_test.py index 43d142e7f67d..6cb961f85a0e 100644 --- a/tests/unit_tests/feature_flag_test.py +++ b/tests/unit_tests/feature_flag_test.py @@ -16,7 +16,9 @@ # under the License. from pytest_mock import MockFixture -from superset import is_feature_enabled +from superset.extensions import feature_flag_manager + +is_feature_enabled = feature_flag_manager.is_feature_enabled def dummy_is_feature_enabled(feature_flag_name: str, default: bool = True) -> bool: diff --git a/tests/unit_tests/jinja_context_test.py b/tests/unit_tests/jinja_context_test.py index b53b3537eac8..76f1041b16fd 100644 --- a/tests/unit_tests/jinja_context_test.py +++ b/tests/unit_tests/jinja_context_test.py @@ -19,11 +19,11 @@ from typing import Any import pytest +from flask import current_app as app from pytest_mock import MockFixture from sqlalchemy.dialects import mysql from sqlalchemy.dialects.postgresql import dialect -from superset import app from superset.commands.dataset.exceptions import DatasetNotFoundError from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.exceptions import SupersetTemplateException diff --git a/tests/unit_tests/models/core_test.py b/tests/unit_tests/models/core_test.py index e653eee71616..b8ad8b92e959 100644 --- a/tests/unit_tests/models/core_test.py +++ b/tests/unit_tests/models/core_test.py @@ -24,7 +24,9 @@ from superset.connectors.sqla.models import SqlaTable, TableColumn from superset.models.core import Database +from superset.models.dashboard import Dashboard # noqa from superset.sql_parse import Table +from superset.tags import models # noqa from superset.utils import json diff --git a/tests/unit_tests/sql_lab_test.py b/tests/unit_tests/sql_lab_test.py index 3b2e7690e141..d3f388029639 100644 --- a/tests/unit_tests/sql_lab_test.py +++ b/tests/unit_tests/sql_lab_test.py @@ -20,7 +20,7 @@ from pytest_mock import MockerFixture from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db from superset.utils.core import override_user diff --git a/tests/unit_tests/tables/test_models.py b/tests/unit_tests/tables/test_models.py index 926e059261cd..75f59c3ce12a 100644 --- a/tests/unit_tests/tables/test_models.py +++ b/tests/unit_tests/tables/test_models.py @@ -17,7 +17,7 @@ # pylint: disable=import-outside-toplevel, unused-argument from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db def test_table_model(session: Session) -> None: diff --git a/tests/unit_tests/tags/commands/create_test.py b/tests/unit_tests/tags/commands/create_test.py index 2697e74aec62..974683957f08 100644 --- a/tests/unit_tests/tags/commands/create_test.py +++ b/tests/unit_tests/tags/commands/create_test.py @@ -18,7 +18,7 @@ from pytest_mock import MockFixture from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db from superset.utils.core import DatasourceType diff --git a/tests/unit_tests/tags/commands/update_test.py b/tests/unit_tests/tags/commands/update_test.py index d3786b23953b..636ea02d814a 100644 --- a/tests/unit_tests/tags/commands/update_test.py +++ b/tests/unit_tests/tags/commands/update_test.py @@ -18,7 +18,7 @@ from pytest_mock import MockFixture from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db from superset.utils.core import DatasourceType diff --git a/tests/unit_tests/thumbnails/test_digest.py b/tests/unit_tests/thumbnails/test_digest.py index 987488ffe7d5..0f0781011230 100644 --- a/tests/unit_tests/thumbnails/test_digest.py +++ b/tests/unit_tests/thumbnails/test_digest.py @@ -156,7 +156,8 @@ def test_dashboard_digest( use_custom_digest: bool, expected_result: str | Exception, ) -> None: - from superset import app + from flask import current_app as app + from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.thumbnails.digest import get_dashboard_digest @@ -231,7 +232,8 @@ def test_chart_digest( use_custom_digest: bool, expected_result: str | Exception, ) -> None: - from superset import app + from flask import current_app as app + from superset.models.slice import Slice from superset.thumbnails.digest import get_chart_digest diff --git a/tests/unit_tests/utils/test_decorators.py b/tests/unit_tests/utils/test_decorators.py index 0a622f4bce97..84a7cd94904b 100644 --- a/tests/unit_tests/utils/test_decorators.py +++ b/tests/unit_tests/utils/test_decorators.py @@ -24,8 +24,8 @@ from unittest.mock import call, Mock, patch import pytest +from flask import current_app as app -from superset import app from superset.utils import decorators from superset.utils.backports import StrEnum From e6eb823391c93692e017a666ea70ed0c63a1afe3 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 28 May 2024 18:24:54 -0700 Subject: [PATCH 2/2] minor fix --- tests/unit_tests/datasets/commands/export_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/datasets/commands/export_test.py b/tests/unit_tests/datasets/commands/export_test.py index 0dc01ed3e92a..3d726a6fe63e 100644 --- a/tests/unit_tests/datasets/commands/export_test.py +++ b/tests/unit_tests/datasets/commands/export_test.py @@ -18,7 +18,7 @@ from sqlalchemy.orm.session import Session -from superset import db +from superset.extensions import db from superset.utils import json