You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2020/11/17 22:50:13 UTC
[incubator-superset] branch master updated: feat: API endpoints to
upload dataset/db (#11728)
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new cce716a feat: API endpoints to upload dataset/db (#11728)
cce716a is described below
commit cce716a82165e149988d88c9066de05d8f295bdc
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Tue Nov 17 14:49:33 2020 -0800
feat: API endpoints to upload dataset/db (#11728)
* feat: API endpoints to upload dataset/db
* Fix method call
---
superset/cli.py | 2 +-
superset/constants.py | 1 +
superset/databases/api.py | 56 +++++++++++
.../databases/commands/importers/dispatcher.py | 68 +++++++++++++
superset/datasets/api.py | 56 +++++++++++
superset/datasets/commands/importers/dispatcher.py | 73 ++++++++++++++
superset/datasets/commands/importers/v0.py | 18 +++-
superset/views/base_api.py | 1 +
tests/databases/api_tests.py | 79 ++++++++++++++-
tests/databases/commands_tests.py | 2 +-
tests/datasets/api_tests.py | 106 ++++++++++++++++++++-
11 files changed, 450 insertions(+), 12 deletions(-)
diff --git a/superset/cli.py b/superset/cli.py
index 2c32a2b..adff31f 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -301,7 +301,7 @@ def export_dashboards(dashboard_file: str, print_stdout: bool) -> None:
)
def import_datasources(path: str, sync: str, recursive: bool) -> None:
"""Import datasources from YAML"""
- from superset.datasets.commands.importers.v0 import ImportDatasetsCommand
+ from superset.datasets.commands.importers.dispatcher import ImportDatasetsCommand
sync_array = sync.split(",")
sync_columns = "columns" in sync_array
diff --git a/superset/constants.py b/superset/constants.py
index ea14a38..65ef2b1 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -50,6 +50,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods
# RestModelView specific
EXPORT = "export"
+ IMPORT = "import_"
GET = "get"
GET_LIST = "get_list"
POST = "post"
diff --git a/superset/databases/api.py b/superset/databases/api.py
index dc54c6b..4d8a0d4 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -35,6 +35,7 @@ from sqlalchemy.exc import (
)
from superset import event_logger
+from superset.commands.exceptions import CommandInvalidError
from superset.constants import RouteMethod
from superset.databases.commands.create import CreateDatabaseCommand
from superset.databases.commands.delete import DeleteDatabaseCommand
@@ -49,6 +50,7 @@ from superset.databases.commands.exceptions import (
DatabaseUpdateFailedError,
)
from superset.databases.commands.export import ExportDatabasesCommand
+from superset.databases.commands.importers.dispatcher import ImportDatabasesCommand
from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
from superset.databases.commands.update import UpdateDatabaseCommand
from superset.databases.dao import DatabaseDAO
@@ -80,6 +82,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
RouteMethod.EXPORT,
+ RouteMethod.IMPORT,
"table_metadata",
"select_star",
"schemas",
@@ -722,3 +725,56 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
as_attachment=True,
attachment_filename=filename,
)
+
+ @expose("/import/", methods=["POST"])
+ @protect()
+ @safe
+ @statsd_metrics
+ def import_(self) -> Response:
+ """Import database(s) with associated datasets
+ ---
+ post:
+ requestBody:
+ content:
+ application/zip:
+ schema:
+ type: string
+ format: binary
+ responses:
+ 200:
+ description: Database import result
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ upload = request.files.get("file")
+ if not upload:
+ return self.response_400()
+ with ZipFile(upload) as bundle:
+ contents = {
+ file_name: bundle.read(file_name).decode()
+ for file_name in bundle.namelist()
+ }
+
+ command = ImportDatabasesCommand(contents)
+ try:
+ command.run()
+ return self.response(200, message="OK")
+ except CommandInvalidError as exc:
+ logger.warning("Import database failed")
+ return self.response_422(message=exc.normalized_messages())
+ except Exception as exc: # pylint: disable=broad-except
+ logger.exception("Import database failed")
+ return self.response_500(message=str(exc))
diff --git a/superset/databases/commands/importers/dispatcher.py b/superset/databases/commands/importers/dispatcher.py
new file mode 100644
index 0000000..a29f5ca
--- /dev/null
+++ b/superset/databases/commands/importers/dispatcher.py
@@ -0,0 +1,68 @@
+# 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
+from typing import Any, Dict
+
+from marshmallow.exceptions import ValidationError
+
+from superset.commands.base import BaseCommand
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.databases.commands.importers import v1
+
+logger = logging.getLogger(__name__)
+
+command_versions = [v1.ImportDatabasesCommand]
+
+
+class ImportDatabasesCommand(BaseCommand):
+ """
+ Import databases.
+
+ This command dispatches the import to different versions of the command
+ until it finds one that matches.
+ """
+
+ # pylint: disable=unused-argument
+ def __init__(self, contents: Dict[str, str], *args: Any, **kwargs: Any):
+ self.contents = contents
+
+ def run(self) -> None:
+ # iterate over all commands until we find a version that can
+ # handle the contents
+ for version in command_versions:
+ command = version(self.contents)
+ try:
+ command.run()
+ return
+ except IncorrectVersionError:
+ # file is not handled by this command, skip
+ pass
+ except (CommandInvalidError, ValidationError) as exc:
+ # found right version, but file is invalid
+ logger.info("Command failed validation")
+ raise exc
+ except Exception as exc:
+ # validation succeeded but something went wrong
+ logger.exception("Error running import command")
+ raise exc
+
+ raise CommandInvalidError("Could not find a valid command to import file")
+
+ def validate(self) -> None:
+ pass
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index fbc01a1..1decc70 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -28,6 +28,7 @@ from flask_babel import ngettext
from marshmallow import ValidationError
from superset import is_feature_enabled
+from superset.commands.exceptions import CommandInvalidError
from superset.connectors.sqla.models import SqlaTable
from superset.constants import RouteMethod
from superset.databases.filters import DatabaseFilter
@@ -45,6 +46,7 @@ from superset.datasets.commands.exceptions import (
DatasetUpdateFailedError,
)
from superset.datasets.commands.export import ExportDatasetsCommand
+from superset.datasets.commands.importers.dispatcher import ImportDatasetsCommand
from superset.datasets.commands.refresh import RefreshDatasetCommand
from superset.datasets.commands.update import UpdateDatasetCommand
from superset.datasets.dao import DatasetDAO
@@ -76,6 +78,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
class_permission_name = "TableModelView"
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
RouteMethod.EXPORT,
+ RouteMethod.IMPORT,
RouteMethod.RELATED,
RouteMethod.DISTINCT,
"bulk_delete",
@@ -589,3 +592,56 @@ class DatasetRestApi(BaseSupersetModelRestApi):
return self.response_403()
except DatasetBulkDeleteFailedError as ex:
return self.response_422(message=str(ex))
+
+ @expose("/import/", methods=["POST"])
+ @protect()
+ @safe
+ @statsd_metrics
+ def import_(self) -> Response:
+ """Import dataset (s) with associated databases
+ ---
+ post:
+ requestBody:
+ content:
+ application/zip:
+ schema:
+ type: string
+ format: binary
+ responses:
+ 200:
+ description: Dataset import result
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ upload = request.files.get("file")
+ if not upload:
+ return self.response_400()
+ with ZipFile(upload) as bundle:
+ contents = {
+ file_name: bundle.read(file_name).decode()
+ for file_name in bundle.namelist()
+ }
+
+ command = ImportDatasetsCommand(contents)
+ try:
+ command.run()
+ return self.response(200, message="OK")
+ except CommandInvalidError as exc:
+ logger.warning("Import dataset failed")
+ return self.response_422(message=exc.normalized_messages())
+ except Exception as exc: # pylint: disable=broad-except
+ logger.exception("Import dataset failed")
+ return self.response_500(message=str(exc))
diff --git a/superset/datasets/commands/importers/dispatcher.py b/superset/datasets/commands/importers/dispatcher.py
new file mode 100644
index 0000000..99a4c26
--- /dev/null
+++ b/superset/datasets/commands/importers/dispatcher.py
@@ -0,0 +1,73 @@
+# 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
+from typing import Any, Dict
+
+from marshmallow.exceptions import ValidationError
+
+from superset.commands.base import BaseCommand
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.datasets.commands.importers import v0, v1
+
+logger = logging.getLogger(__name__)
+
+# list of different import formats supported; v0 should be last because
+# the files are not versioned
+command_versions = [
+ v1.ImportDatasetsCommand,
+ v0.ImportDatasetsCommand,
+]
+
+
+class ImportDatasetsCommand(BaseCommand):
+ """
+ Import datasets.
+
+ This command dispatches the import to different versions of the command
+ until it finds one that matches.
+ """
+
+ # pylint: disable=unused-argument
+ def __init__(self, contents: Dict[str, str], *args: Any, **kwargs: Any):
+ self.contents = contents
+
+ def run(self) -> None:
+ # iterate over all commands until we find a version that can
+ # handle the contents
+ for version in command_versions:
+ command = version(self.contents)
+ try:
+ command.run()
+ return
+ except IncorrectVersionError:
+ # file is not handled by command, skip
+ pass
+ except (CommandInvalidError, ValidationError) as exc:
+ # found right version, but file is invalid
+ logger.info("Command failed validation")
+ raise exc
+ except Exception as exc:
+ # validation succeeded but something went wrong
+ logger.exception("Error running import command")
+ raise exc
+
+ raise CommandInvalidError("Could not find a valid command to import file")
+
+ def validate(self) -> None:
+ pass
diff --git a/superset/datasets/commands/importers/v0.py b/superset/datasets/commands/importers/v0.py
index b8c0ab6..d45c58c 100644
--- a/superset/datasets/commands/importers/v0.py
+++ b/superset/datasets/commands/importers/v0.py
@@ -25,6 +25,7 @@ from sqlalchemy.orm.session import make_transient
from superset import db
from superset.commands.base import BaseCommand
+from superset.commands.importers.exceptions import IncorrectVersionError
from superset.connectors.base.models import BaseColumn, BaseDatasource, BaseMetric
from superset.connectors.druid.models import (
DruidCluster,
@@ -289,6 +290,7 @@ class ImportDatasetsCommand(BaseCommand):
sync_metrics: bool = False,
):
self.contents = contents
+ self._configs: Dict[str, Any] = {}
self.sync = []
if sync_columns:
@@ -299,15 +301,21 @@ class ImportDatasetsCommand(BaseCommand):
def run(self) -> None:
self.validate()
- for file_name, content in self.contents.items():
+ for file_name, config in self._configs.items():
logger.info("Importing dataset from file %s", file_name)
- import_from_dict(db.session, yaml.safe_load(content), sync=self.sync)
+ import_from_dict(db.session, config, sync=self.sync)
def validate(self) -> None:
# ensure all files are YAML
- for content in self.contents.values():
+ for file_name, content in self.contents.items():
try:
- yaml.safe_load(content)
+ config = yaml.safe_load(content)
except yaml.parser.ParserError:
logger.exception("Invalid YAML file")
- raise
+ raise IncorrectVersionError(f"{file_name} is not a valid YAML file")
+
+ # check for keys
+ if DATABASES_KEY not in config and DRUID_CLUSTERS_KEY not in config:
+ raise IncorrectVersionError(f"{file_name} has no valid keys")
+
+ self._configs[file_name] = config
diff --git a/superset/views/base_api.py b/superset/views/base_api.py
index a83fcc6..1495a79 100644
--- a/superset/views/base_api.py
+++ b/superset/views/base_api.py
@@ -128,6 +128,7 @@ class BaseSupersetModelRestApi(ModelRestApi):
"delete": "delete",
"distinct": "list",
"export": "mulexport",
+ "import_": "add",
"get": "show",
"get_list": "list",
"info": "list",
diff --git a/tests/databases/api_tests.py b/tests/databases/api_tests.py
index 2b8504d..e38b64a 100644
--- a/tests/databases/api_tests.py
+++ b/tests/databases/api_tests.py
@@ -15,13 +15,15 @@
# specific language governing permissions and limitations
# under the License.
# isort:skip_file
+# pylint: disable=invalid-name, no-self-use, too-many-public-methods, too-many-arguments
"""Unit tests for Superset"""
import json
from io import BytesIO
-from zipfile import is_zipfile
+from zipfile import is_zipfile, ZipFile
import prison
import pytest
+import yaml
from sqlalchemy.sql import func
@@ -31,6 +33,13 @@ from superset.models.core import Database
from superset.utils.core import get_example_database, get_main_database
from tests.base_tests import SupersetTestCase
from tests.fixtures.certificates import ssl_certificate
+from tests.fixtures.importexport import (
+ database_config,
+ dataset_config,
+ database_metadata_config,
+ dataset_metadata_config,
+)
+
from tests.fixtures.unicode_dashboard import load_unicode_dashboard_with_position
from tests.test_app import app
@@ -817,3 +826,71 @@ class TestDatabaseApi(SupersetTestCase):
uri = f"api/v1/database/export/?q={prison.dumps(argument)}"
rv = self.get_assert_metric(uri, "export")
assert rv.status_code == 404
+
+ def test_import_database(self):
+ """
+ Database API: Test import database
+ """
+ self.login(username="admin")
+ uri = "api/v1/database/import/"
+
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("metadata.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_metadata_config).encode())
+ with bundle.open("databases/imported_database.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_config).encode())
+ with bundle.open("datasets/import_dataset.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_config).encode())
+ buf.seek(0)
+
+ form_data = {
+ "file": (buf, "database_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 200
+ assert response == {"message": "OK"}
+
+ database = (
+ db.session.query(Database).filter_by(uuid=database_config["uuid"]).one()
+ )
+ assert database.database_name == "imported_database"
+
+ assert len(database.tables) == 1
+ dataset = database.tables[0]
+ assert dataset.table_name == "imported_dataset"
+ assert str(dataset.uuid) == dataset_config["uuid"]
+
+ db.session.delete(dataset)
+ db.session.delete(database)
+ db.session.commit()
+
+ def test_import_database_invalid(self):
+ """
+ Database API: Test import invalid database
+ """
+ self.login(username="admin")
+ uri = "api/v1/database/import/"
+
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("metadata.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_metadata_config).encode())
+ with bundle.open("databases/imported_database.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_config).encode())
+ with bundle.open("datasets/import_dataset.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_config).encode())
+ buf.seek(0)
+
+ form_data = {
+ "file": (buf, "database_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 422
+ assert response == {
+ "message": {"metadata.yaml": {"type": ["Must be equal to Database."]}}
+ }
diff --git a/tests/databases/commands_tests.py b/tests/databases/commands_tests.py
index a88283f..afaf50f 100644
--- a/tests/databases/commands_tests.py
+++ b/tests/databases/commands_tests.py
@@ -432,7 +432,7 @@ class TestExportDatabasesCommand(SupersetTestCase):
command.run()
assert str(excinfo.value) == "Error importing database"
assert excinfo.value.normalized_messages() == {
- "metadata.yaml": {"type": ["Must be equal to Database."],}
+ "metadata.yaml": {"type": ["Must be equal to Database."]}
}
# must also validate datasets
diff --git a/tests/datasets/api_tests.py b/tests/datasets/api_tests.py
index 757ba77..59854c4 100644
--- a/tests/datasets/api_tests.py
+++ b/tests/datasets/api_tests.py
@@ -14,12 +14,13 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+# pylint: disable=too-many-public-methods, invalid-name
"""Unit tests for Superset"""
import json
from io import BytesIO
from typing import List, Optional
from unittest.mock import patch
-from zipfile import is_zipfile
+from zipfile import is_zipfile, ZipFile
import prison
import pytest
@@ -38,6 +39,12 @@ from superset.utils.core import backend, get_example_database, get_main_database
from superset.utils.dict_import_export import export_to_dict
from tests.base_tests import SupersetTestCase
from tests.conftest import CTAS_SCHEMA_NAME
+from tests.fixtures.importexport import (
+ database_config,
+ database_metadata_config,
+ dataset_config,
+ dataset_metadata_config,
+)
class TestDatasetApi(SupersetTestCase):
@@ -139,7 +146,7 @@ class TestDatasetApi(SupersetTestCase):
arguments = {
"filters": [
{"col": "database", "opr": "rel_o_m", "value": f"{example_db.id}"},
- {"col": "table_name", "opr": "eq", "value": f"birth_names"},
+ {"col": "table_name", "opr": "eq", "value": "birth_names"},
]
}
uri = f"api/v1/dataset/?q={prison.dumps(arguments)}"
@@ -170,7 +177,6 @@ class TestDatasetApi(SupersetTestCase):
"""
Dataset API: Test get dataset list gamma
"""
- example_db = get_example_database()
self.login(username="gamma")
uri = "api/v1/dataset/"
rv = self.get_assert_metric(uri, "get_list")
@@ -423,7 +429,7 @@ class TestDatasetApi(SupersetTestCase):
"table_name": "ab_permission",
"owners": [admin.id, 1000],
}
- uri = f"api/v1/dataset/"
+ uri = "api/v1/dataset/"
rv = self.post_assert_metric(uri, table_data, "post")
assert rv.status_code == 422
data = json.loads(rv.data.decode("utf-8"))
@@ -1169,3 +1175,95 @@ class TestDatasetApi(SupersetTestCase):
data = json.loads(rv.data.decode("utf-8"))
for table_name in self.fixture_tables_names:
assert table_name in [ds["table_name"] for ds in data["result"]]
+
+ def test_import_dataset(self):
+ """
+ Dataset API: Test import dataset
+ """
+ self.login(username="admin")
+ uri = "api/v1/dataset/import/"
+
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("metadata.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_metadata_config).encode())
+ with bundle.open("databases/imported_database.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_config).encode())
+ with bundle.open("datasets/import_dataset.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_config).encode())
+ buf.seek(0)
+
+ form_data = {
+ "file": (buf, "dataset_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 200
+ assert response == {"message": "OK"}
+
+ database = (
+ db.session.query(Database).filter_by(uuid=database_config["uuid"]).one()
+ )
+ assert database.database_name == "imported_database"
+
+ assert len(database.tables) == 1
+ dataset = database.tables[0]
+ assert dataset.table_name == "imported_dataset"
+ assert str(dataset.uuid) == dataset_config["uuid"]
+
+ db.session.delete(dataset)
+ db.session.delete(database)
+ db.session.commit()
+
+ def test_import_dataset_invalid(self):
+ """
+ Dataset API: Test import invalid dataset
+ """
+ self.login(username="admin")
+ uri = "api/v1/dataset/import/"
+
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("metadata.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_metadata_config).encode())
+ with bundle.open("databases/imported_database.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_config).encode())
+ with bundle.open("datasets/import_dataset.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_config).encode())
+ buf.seek(0)
+
+ form_data = {
+ "file": (buf, "dataset_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 422
+ assert response == {
+ "message": {"metadata.yaml": {"type": ["Must be equal to SqlaTable."]}}
+ }
+
+ def test_import_dataset_invalid_v0_validation(self):
+ """
+ Dataset API: Test import invalid dataset
+ """
+ self.login(username="admin")
+ uri = "api/v1/dataset/import/"
+
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("databases/imported_database.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(database_config).encode())
+ with bundle.open("datasets/import_dataset.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(dataset_config).encode())
+ buf.seek(0)
+
+ form_data = {
+ "file": (buf, "dataset_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 422
+ assert response == {"message": "Could not process entity"}