airbnb/caravel

View on GitHub
superset/cli/importexport.py

Summary

Maintainability
B
4 hrs
Test Coverage
# 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 logging
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
from zipfile import is_zipfile, ZipFile

import click
import yaml
from flask import g
from flask.cli import with_appcontext

from superset import security_manager
from superset.extensions import db
from superset.utils.core import override_user

logger = logging.getLogger(__name__)


@click.command()
@with_appcontext
@click.argument("directory")
@click.option(
    "--overwrite",
    "-o",
    is_flag=True,
    help="Overwriting existing metadata definitions",
)
@click.option(
    "--force",
    "-f",
    is_flag=True,
    help="Force load data even if table already exists",
)
def import_directory(directory: str, overwrite: bool, force: bool) -> None:
    """Imports configs from a given directory"""
    # pylint: disable=import-outside-toplevel
    from superset.examples.utils import load_configs_from_directory

    load_configs_from_directory(
        root=Path(directory),
        overwrite=overwrite,
        force_data=force,
    )


@click.command()
@with_appcontext
@click.option(
    "--dashboard-file",
    "-f",
    help="Specify the file to export to",
)
def export_dashboards(dashboard_file: Optional[str] = None) -> None:
    """Export dashboards to ZIP file"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dashboard.export import ExportDashboardsCommand
    from superset.models.dashboard import Dashboard

    g.user = security_manager.find_user(username="admin")

    dashboard_ids = [id_ for (id_,) in db.session.query(Dashboard.id).all()]
    timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
    root = f"dashboard_export_{timestamp}"
    dashboard_file = dashboard_file or f"{root}.zip"

    try:
        with ZipFile(dashboard_file, "w") as bundle:
            for file_name, file_content in ExportDashboardsCommand(dashboard_ids).run():
                with bundle.open(f"{root}/{file_name}", "w") as fp:
                    fp.write(file_content().encode())
    except Exception:  # pylint: disable=broad-except
        logger.exception(
            "There was an error when exporting the dashboards, please check "
            "the exception traceback in the log"
        )
        sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--datasource-file",
    "-f",
    help="Specify the file to export to",
)
def export_datasources(datasource_file: Optional[str] = None) -> None:
    """Export datasources to ZIP file"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dataset.export import ExportDatasetsCommand
    from superset.connectors.sqla.models import SqlaTable

    g.user = security_manager.find_user(username="admin")

    dataset_ids = [id_ for (id_,) in db.session.query(SqlaTable.id).all()]
    timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
    root = f"dataset_export_{timestamp}"
    datasource_file = datasource_file or f"{root}.zip"

    try:
        with ZipFile(datasource_file, "w") as bundle:
            for file_name, file_content in ExportDatasetsCommand(dataset_ids).run():
                with bundle.open(f"{root}/{file_name}", "w") as fp:
                    fp.write(file_content().encode())
    except Exception:  # pylint: disable=broad-except
        logger.exception(
            "There was an error when exporting the datasets, please check "
            "the exception traceback in the log"
        )
        sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--path",
    "-p",
    required=True,
    help="Path to a single ZIP file",
)
@click.option(
    "--username",
    "-u",
    required=True,
    help="Specify the user name to assign dashboards to",
)
def import_dashboards(path: str, username: Optional[str]) -> None:
    """Import dashboards from ZIP file"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCommand
    from superset.commands.importers.v1.utils import get_contents_from_bundle

    if username is not None:
        g.user = security_manager.find_user(username=username)
    if is_zipfile(path):
        with ZipFile(path) as bundle:
            contents = get_contents_from_bundle(bundle)
    else:
        with open(path) as file:
            contents = {path: file.read()}
    try:
        ImportDashboardsCommand(contents, overwrite=True).run()
    except Exception:  # pylint: disable=broad-except
        logger.exception(
            "There was an error when importing the dashboards(s), please check "
            "the exception traceback in the log"
        )
        sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--path",
    "-p",
    help="Path to a single ZIP file",
)
@click.option(
    "--username",
    "-u",
    required=False,
    default="admin",
    help="Specify the user name to assign datasources to",
)
def import_datasources(path: str, username: Optional[str] = "admin") -> None:
    """Import datasources from ZIP file"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dataset.importers.dispatcher import ImportDatasetsCommand
    from superset.commands.importers.v1.utils import get_contents_from_bundle

    with override_user(user=security_manager.find_user(username=username)):
        if is_zipfile(path):
            with ZipFile(path) as bundle:
                contents = get_contents_from_bundle(bundle)
        else:
            with open(path) as file:
                contents = {path: file.read()}
        try:
            ImportDatasetsCommand(contents, overwrite=True).run()
        except Exception:  # pylint: disable=broad-except
            logger.exception(
                "There was an error when importing the dataset(s), please check the "
                "exception traceback in the log"
            )
            sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--dashboard-file",
    "-f",
    default=None,
    help="Specify the file to export to",
)
@click.option(
    "--print_stdout",
    "-p",
    is_flag=True,
    default=False,
    help="Print JSON to stdout",
)
def legacy_export_dashboards(
    dashboard_file: Optional[str], print_stdout: bool = False
) -> None:
    """Export dashboards to JSON"""
    # pylint: disable=import-outside-toplevel
    from superset.utils import dashboard_import_export

    data = dashboard_import_export.export_dashboards()
    if print_stdout or not dashboard_file:
        print(data)
    if dashboard_file:
        logger.info("Exporting dashboards to %s", dashboard_file)
        with open(dashboard_file, "w") as data_stream:
            data_stream.write(data)


@click.command()
@with_appcontext
@click.option(
    "--datasource-file",
    "-f",
    default=None,
    help="Specify the file to export to",
)
@click.option(
    "--print_stdout",
    "-p",
    is_flag=True,
    default=False,
    help="Print YAML to stdout",
)
@click.option(
    "--back-references",
    "-b",
    is_flag=True,
    default=False,
    help="Include parent back references",
)
@click.option(
    "--include-defaults",
    "-d",
    is_flag=True,
    default=False,
    help="Include fields containing defaults",
)
def legacy_export_datasources(
    datasource_file: Optional[str],
    print_stdout: bool = False,
    back_references: bool = False,
    include_defaults: bool = False,
) -> None:
    """Export datasources to YAML"""
    # pylint: disable=import-outside-toplevel
    from superset.utils import dict_import_export

    data = dict_import_export.export_to_dict(
        recursive=True,
        back_references=back_references,
        include_defaults=include_defaults,
    )
    if print_stdout or not datasource_file:
        yaml.safe_dump(data, sys.stdout, default_flow_style=False)
    if datasource_file:
        logger.info("Exporting datasources to %s", datasource_file)
        with open(datasource_file, "w") as data_stream:
            yaml.safe_dump(data, data_stream, default_flow_style=False)


@click.command()
@with_appcontext
@click.option(
    "--path",
    "-p",
    help="Path to a single JSON file or path containing multiple JSON "
    "files to import (*.json)",
)
@click.option(
    "--recursive",
    "-r",
    is_flag=True,
    default=False,
    help="recursively search the path for json files",
)
@click.option(
    "--username",
    "-u",
    default=None,
    help="Specify the user name to assign dashboards to",
)
def legacy_import_dashboards(path: str, recursive: bool, username: str) -> None:
    """Import dashboards from JSON file"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand

    path_object = Path(path)
    files: list[Path] = []
    if path_object.is_file():
        files.append(path_object)
    elif path_object.exists() and not recursive:
        files.extend(path_object.glob("*.json"))
    elif path_object.exists() and recursive:
        files.extend(path_object.rglob("*.json"))
    if username is not None:
        g.user = security_manager.find_user(username=username)
    contents = {}
    for path_ in files:
        with open(path_) as file:
            contents[path_.name] = file.read()
    try:
        ImportDashboardsCommand(contents).run()
    except Exception:  # pylint: disable=broad-except
        logger.exception("Error when importing dashboard")
        sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--path",
    "-p",
    help="Path to a single YAML file or path containing multiple YAML "
    "files to import (*.yaml or *.yml)",
)
@click.option(
    "--sync",
    "-s",
    "sync",
    default="",
    help="comma separated list of element types to synchronize "
    'e.g. "metrics,columns" deletes metrics and columns in the DB '
    "that are not specified in the YAML file",
)
@click.option(
    "--recursive",
    "-r",
    is_flag=True,
    default=False,
    help="recursively search the path for yaml files",
)
def legacy_import_datasources(path: str, sync: str, recursive: bool) -> None:
    """Import datasources from YAML"""
    # pylint: disable=import-outside-toplevel
    from superset.commands.dataset.importers.v0 import ImportDatasetsCommand

    sync_array = sync.split(",")
    sync_columns = "columns" in sync_array
    sync_metrics = "metrics" in sync_array

    path_object = Path(path)
    files: list[Path] = []
    if path_object.is_file():
        files.append(path_object)
    elif path_object.exists() and not recursive:
        files.extend(path_object.glob("*.yaml"))
        files.extend(path_object.glob("*.yml"))
    elif path_object.exists() and recursive:
        files.extend(path_object.rglob("*.yaml"))
        files.extend(path_object.rglob("*.yml"))
    contents = {}
    for path_ in files:
        with open(path_) as file:
            contents[path_.name] = file.read()
    try:
        ImportDatasetsCommand(
            contents, sync_columns=sync_columns, sync_metrics=sync_metrics
        ).run()
    except Exception:  # pylint: disable=broad-except
        logger.exception("Error when importing dataset")
        sys.exit(1)


@click.command()
@with_appcontext
@click.option(
    "--back-references",
    "-b",
    is_flag=True,
    default=False,
    help="Include parent back references",
)
def legacy_export_datasource_schema(back_references: bool) -> None:
    """Export datasource YAML schema to stdout"""
    # pylint: disable=import-outside-toplevel
    from superset.utils import dict_import_export

    data = dict_import_export.export_schema_to_dict(back_references=back_references)
    yaml.safe_dump(data, sys.stdout, default_flow_style=False)