From a698587e8c867f25f4cace6ec02259cccaa5986c Mon Sep 17 00:00:00 2001
From: koushik-rout-samsung
<146946876+koushik-rout-samsung@users.noreply.github.com>
Date: Thu, 9 Nov 2023 12:42:49 +0530
Subject: [PATCH 001/119] docs: Remove Python 3.8 from CONTRIBUTING.md (#25885)
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9e480ee95..343356b5a9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -424,7 +424,7 @@ Commits to `master` trigger a rebuild and redeploy of the documentation site. Su
Make sure your machine meets the [OS dependencies](https://superset.apache.org/docs/installation/installing-superset-from-scratch#os-dependencies) before following these steps.
You also need to install MySQL or [MariaDB](https://mariadb.com/downloads).
-Ensure that you are using Python version 3.8, 3.9, 3.10 or 3.11, then proceed with:
+Ensure that you are using Python version 3.9, 3.10 or 3.11, then proceed with:
```bash
# Create a virtual environment and activate it (recommended)
From 10205d0b508a803e32c6c742a69c0019d5899678 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Thu, 9 Nov 2023 08:22:08 -0800
Subject: [PATCH 002/119] chore: Singularize tag models (#25819)
---
superset/charts/schemas.py | 4 +--
superset/common/tags.py | 26 +++++++--------
superset/daos/tag.py | 30 ++++++++---------
superset/dashboards/schemas.py | 4 +--
...26_11-10_c82ee8a39623_add_implicit_tags.py | 6 ++--
superset/tags/api.py | 6 ++--
superset/tags/commands/create.py | 6 ++--
superset/tags/commands/delete.py | 4 +--
superset/tags/commands/utils.py | 16 +++++-----
superset/tags/models.py | 32 +++++++++----------
superset/utils/url_map_converters.py | 4 +--
tests/integration_tests/strategy_tests.py | 12 +++----
tests/integration_tests/tagging_tests.py | 16 +++++-----
tests/integration_tests/tags/api_tests.py | 24 +++++++-------
.../integration_tests/tags/commands_tests.py | 18 +++++------
tests/integration_tests/tags/dao_tests.py | 24 +++++++-------
tests/unit_tests/dao/tag_test.py | 8 ++---
tests/unit_tests/tags/commands/create_test.py | 16 +++++-----
tests/unit_tests/tags/commands/update_test.py | 14 ++++----
19 files changed, 134 insertions(+), 136 deletions(-)
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 0ad68ceb49..48e0cbb318 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -27,7 +27,7 @@ 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.tags.models import TagTypes
+from superset.tags.models import TagType
from superset.utils import pandas_postprocessing, schema as utils
from superset.utils.core import (
AnnotationType,
@@ -146,7 +146,7 @@ openapi_spec_methods_override = {
class TagSchema(Schema):
id = fields.Int()
name = fields.String()
- type = fields.Enum(TagTypes, by_value=True)
+ type = fields.Enum(TagType, by_value=True)
class ChartEntityResponseSchema(Schema):
diff --git a/superset/common/tags.py b/superset/common/tags.py
index c7b06bdd4b..ce5c5ab195 100644
--- a/superset/common/tags.py
+++ b/superset/common/tags.py
@@ -22,7 +22,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import and_, func, join, literal, select
from superset.extensions import db
-from superset.tags.models import ObjectTypes, TagTypes
+from superset.tags.models import ObjectType, TagType
def add_types_to_charts(
@@ -35,7 +35,7 @@ def add_types_to_charts(
[
tag.c.id.label("tag_id"),
slices.c.id.label("object_id"),
- literal(ObjectTypes.chart.name).label("object_type"),
+ literal(ObjectType.chart.name).label("object_type"),
]
)
.select_from(
@@ -67,7 +67,7 @@ def add_types_to_dashboards(
[
tag.c.id.label("tag_id"),
dashboard_table.c.id.label("object_id"),
- literal(ObjectTypes.dashboard.name).label("object_type"),
+ literal(ObjectType.dashboard.name).label("object_type"),
]
)
.select_from(
@@ -99,7 +99,7 @@ def add_types_to_saved_queries(
[
tag.c.id.label("tag_id"),
saved_query.c.id.label("object_id"),
- literal(ObjectTypes.query.name).label("object_type"),
+ literal(ObjectType.query.name).label("object_type"),
]
)
.select_from(
@@ -131,7 +131,7 @@ def add_types_to_datasets(
[
tag.c.id.label("tag_id"),
tables.c.id.label("object_id"),
- literal(ObjectTypes.dataset.name).label("object_type"),
+ literal(ObjectType.dataset.name).label("object_type"),
]
)
.select_from(
@@ -221,9 +221,9 @@ def add_types(metadata: MetaData) -> None:
# add a tag for each object type
insert = tag.insert()
- for type_ in ObjectTypes.__members__:
+ for type_ in ObjectType.__members__:
with contextlib.suppress(IntegrityError): # already exists
- db.session.execute(insert, name=f"type:{type_}", type=TagTypes.type)
+ db.session.execute(insert, name=f"type:{type_}", type=TagType.type)
add_types_to_charts(metadata, tag, tagged_object, columns)
add_types_to_dashboards(metadata, tag, tagged_object, columns)
@@ -241,7 +241,7 @@ def add_owners_to_charts(
[
tag.c.id.label("tag_id"),
slices.c.id.label("object_id"),
- literal(ObjectTypes.chart.name).label("object_type"),
+ literal(ObjectType.chart.name).label("object_type"),
]
)
.select_from(
@@ -277,7 +277,7 @@ def add_owners_to_dashboards(
[
tag.c.id.label("tag_id"),
dashboard_table.c.id.label("object_id"),
- literal(ObjectTypes.dashboard.name).label("object_type"),
+ literal(ObjectType.dashboard.name).label("object_type"),
]
)
.select_from(
@@ -313,7 +313,7 @@ def add_owners_to_saved_queries(
[
tag.c.id.label("tag_id"),
saved_query.c.id.label("object_id"),
- literal(ObjectTypes.query.name).label("object_type"),
+ literal(ObjectType.query.name).label("object_type"),
]
)
.select_from(
@@ -349,7 +349,7 @@ def add_owners_to_datasets(
[
tag.c.id.label("tag_id"),
tables.c.id.label("object_id"),
- literal(ObjectTypes.dataset.name).label("object_type"),
+ literal(ObjectType.dataset.name).label("object_type"),
]
)
.select_from(
@@ -444,7 +444,7 @@ def add_owners(metadata: MetaData) -> None:
insert = tag.insert()
for (id_,) in db.session.execute(ids):
with contextlib.suppress(IntegrityError): # already exists
- db.session.execute(insert, name=f"owner:{id_}", type=TagTypes.owner)
+ db.session.execute(insert, name=f"owner:{id_}", type=TagType.owner)
add_owners_to_charts(metadata, tag, tagged_object, columns)
add_owners_to_dashboards(metadata, tag, tagged_object, columns)
add_owners_to_saved_queries(metadata, tag, tagged_object, columns)
@@ -482,7 +482,7 @@ def add_favorites(metadata: MetaData) -> None:
insert = tag.insert()
for (id_,) in db.session.execute(ids):
with contextlib.suppress(IntegrityError): # already exists
- db.session.execute(insert, name=f"favorited_by:{id_}", type=TagTypes.type)
+ db.session.execute(insert, name=f"favorited_by:{id_}", type=TagType.type)
favstars = (
select(
[
diff --git a/superset/daos/tag.py b/superset/daos/tag.py
index 2acd221a35..ba5311ed44 100644
--- a/superset/daos/tag.py
+++ b/superset/daos/tag.py
@@ -32,10 +32,10 @@ from superset.tags.commands.exceptions import TagNotFoundError
from superset.tags.commands.utils import to_object_type
from superset.tags.models import (
get_tag,
- ObjectTypes,
+ ObjectType,
Tag,
TaggedObject,
- TagTypes,
+ TagType,
user_favorite_tag_table,
)
from superset.utils.core import get_user_id
@@ -56,7 +56,7 @@ class TagDAO(BaseDAO[Tag]):
@staticmethod
def create_custom_tagged_objects(
- object_type: ObjectTypes, object_id: int, tag_names: list[str]
+ object_type: ObjectType, object_id: int, tag_names: list[str]
) -> None:
tagged_objects = []
for name in tag_names:
@@ -64,7 +64,7 @@ class TagDAO(BaseDAO[Tag]):
raise DAOCreateFailedError(
message="Invalid Tag Name (cannot contain ':' or ',')"
)
- type_ = TagTypes.custom
+ type_ = TagType.custom
tag_name = name.strip()
tag = TagDAO.get_by_name(tag_name, type_)
tagged_objects.append(
@@ -76,7 +76,7 @@ class TagDAO(BaseDAO[Tag]):
@staticmethod
def delete_tagged_object(
- object_type: ObjectTypes, object_id: int, tag_name: str
+ object_type: ObjectType, object_id: int, tag_name: str
) -> None:
"""
deletes a tagged object by the object_id, object_type, and tag_name
@@ -128,7 +128,7 @@ class TagDAO(BaseDAO[Tag]):
raise DAODeleteFailedError(exception=ex) from ex
@staticmethod
- def get_by_name(name: str, type_: TagTypes = TagTypes.custom) -> Tag:
+ def get_by_name(name: str, type_: TagType = TagType.custom) -> Tag:
"""
returns a tag if one exists by that name, none otherwise.
important!: Creates a tag by that name if the tag is not found.
@@ -152,7 +152,7 @@ class TagDAO(BaseDAO[Tag]):
@staticmethod
def find_tagged_object(
- object_type: ObjectTypes, object_id: int, tag_id: int
+ object_type: ObjectType, object_id: int, tag_id: int
) -> TaggedObject:
"""
returns a tagged object if one exists by that name, none otherwise.
@@ -185,7 +185,7 @@ class TagDAO(BaseDAO[Tag]):
TaggedObject,
and_(
TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
+ TaggedObject.object_type == ObjectType.dashboard,
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
@@ -195,7 +195,7 @@ class TagDAO(BaseDAO[Tag]):
results.extend(
{
"id": obj.id,
- "type": ObjectTypes.dashboard.name,
+ "type": ObjectType.dashboard.name,
"name": obj.dashboard_title,
"url": obj.url,
"changed_on": obj.changed_on,
@@ -215,7 +215,7 @@ class TagDAO(BaseDAO[Tag]):
TaggedObject,
and_(
TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
+ TaggedObject.object_type == ObjectType.chart,
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
@@ -224,7 +224,7 @@ class TagDAO(BaseDAO[Tag]):
results.extend(
{
"id": obj.id,
- "type": ObjectTypes.chart.name,
+ "type": ObjectType.chart.name,
"name": obj.slice_name,
"url": obj.url,
"changed_on": obj.changed_on,
@@ -244,7 +244,7 @@ class TagDAO(BaseDAO[Tag]):
TaggedObject,
and_(
TaggedObject.object_id == SavedQuery.id,
- TaggedObject.object_type == ObjectTypes.query,
+ TaggedObject.object_type == ObjectType.query,
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
@@ -253,7 +253,7 @@ class TagDAO(BaseDAO[Tag]):
results.extend(
{
"id": obj.id,
- "type": ObjectTypes.query.name,
+ "type": ObjectType.query.name,
"name": obj.label,
"url": obj.url(),
"changed_on": obj.changed_on,
@@ -363,7 +363,7 @@ class TagDAO(BaseDAO[Tag]):
@staticmethod
def create_tag_relationship(
- objects_to_tag: list[tuple[ObjectTypes, int]],
+ objects_to_tag: list[tuple[ObjectType, int]],
tag: Tag,
bulk_create: bool = False,
) -> None:
@@ -373,7 +373,7 @@ class TagDAO(BaseDAO[Tag]):
and an id, and creates a TaggedObject for each one, associating it with
the provided tag. All created TaggedObjects are collected in a list.
Args:
- objects_to_tag (List[Tuple[ObjectTypes, int]]): A list of tuples, each
+ objects_to_tag (List[Tuple[ObjectType, int]]): A list of tuples, each
containing an ObjectType and an id, representing the objects to be tagged.
tag (Tag): The tag to be associated with the specified objects.
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index e467167297..8cbe482edd 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -22,7 +22,7 @@ from marshmallow import fields, post_load, pre_load, Schema
from marshmallow.validate import Length, ValidationError
from superset.exceptions import SupersetException
-from superset.tags.models import TagTypes
+from superset.tags.models import TagType
from superset.utils import core as utils
get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
@@ -169,7 +169,7 @@ class RolesSchema(Schema):
class TagSchema(Schema):
id = fields.Int()
name = fields.String()
- type = fields.Enum(TagTypes, by_value=True)
+ type = fields.Enum(TagType, by_value=True)
class DashboardGetResponseSchema(Schema):
diff --git a/superset/migrations/versions/2018-07-26_11-10_c82ee8a39623_add_implicit_tags.py b/superset/migrations/versions/2018-07-26_11-10_c82ee8a39623_add_implicit_tags.py
index 0179ba7d03..c6a66d6b53 100644
--- a/superset/migrations/versions/2018-07-26_11-10_c82ee8a39623_add_implicit_tags.py
+++ b/superset/migrations/versions/2018-07-26_11-10_c82ee8a39623_add_implicit_tags.py
@@ -33,7 +33,7 @@ from flask_appbuilder.models.mixins import AuditMixin
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base, declared_attr
-from superset.tags.models import ObjectTypes, TagTypes
+from superset.tags.models import ObjectType, TagType
from superset.utils.core import get_user_id
Base = declarative_base()
@@ -77,7 +77,7 @@ class Tag(Base, AuditMixinNullable):
id = Column(Integer, primary_key=True)
name = Column(String(250), unique=True)
- type = Column(Enum(TagTypes))
+ type = Column(Enum(TagType))
class TaggedObject(Base, AuditMixinNullable):
@@ -86,7 +86,7 @@ class TaggedObject(Base, AuditMixinNullable):
id = Column(Integer, primary_key=True)
tag_id = Column(Integer, ForeignKey("tag.id"))
object_id = Column(Integer)
- object_type = Column(Enum(ObjectTypes))
+ object_type = Column(Enum(ObjectType))
class User(Base):
diff --git a/superset/tags/api.py b/superset/tags/api.py
index e9842f5a6a..8e01fd240f 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -40,7 +40,7 @@ from superset.tags.commands.exceptions import (
TagUpdateFailedError,
)
from superset.tags.commands.update import UpdateTagCommand
-from superset.tags.models import ObjectTypes, Tag
+from superset.tags.models import ObjectType, Tag
from superset.tags.schemas import (
delete_tags_schema,
openapi_spec_methods_override,
@@ -364,7 +364,7 @@ class TagRestApi(BaseSupersetModelRestApi):
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_objects",
log_to_statsd=False,
)
- def add_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
+ def add_objects(self, object_type: ObjectType, object_id: int) -> Response:
"""Add tags to an object. Create new tags if they do not already exist.
---
post:
@@ -429,7 +429,7 @@ class TagRestApi(BaseSupersetModelRestApi):
log_to_statsd=True,
)
def delete_object(
- self, object_type: ObjectTypes, object_id: int, tag: str
+ self, object_type: ObjectType, object_id: int, tag: str
) -> Response:
"""Delete a tagged object.
---
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index cd3bcc176b..eb3d4458a2 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -24,13 +24,13 @@ from superset.daos.tag import TagDAO
from superset.exceptions import SupersetSecurityException
from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
from superset.tags.commands.utils import to_object_model, to_object_type
-from superset.tags.models import ObjectTypes, TagTypes
+from superset.tags.models import ObjectType, TagType
logger = logging.getLogger(__name__)
class CreateCustomTagCommand(CreateMixin, BaseCommand):
- def __init__(self, object_type: ObjectTypes, object_id: int, tags: list[str]):
+ def __init__(self, object_type: ObjectType, object_id: int, tags: list[str]):
self._object_type = object_type
self._object_id = object_id
self._tags = tags
@@ -76,7 +76,7 @@ class CreateCustomTagWithRelationshipsCommand(CreateMixin, BaseCommand):
try:
tag_name = self._properties["name"]
- tag = TagDAO.get_by_name(tag_name.strip(), TagTypes.custom)
+ tag = TagDAO.get_by_name(tag_name.strip(), TagType.custom)
TagDAO.create_tag_relationship(
objects_to_tag=self._properties.get("objects_to_tag", []),
tag=tag,
diff --git a/superset/tags/commands/delete.py b/superset/tags/commands/delete.py
index 4b92e40ff5..5c10a934e3 100644
--- a/superset/tags/commands/delete.py
+++ b/superset/tags/commands/delete.py
@@ -27,14 +27,14 @@ from superset.tags.commands.exceptions import (
TagNotFoundError,
)
from superset.tags.commands.utils import to_object_type
-from superset.tags.models import ObjectTypes
+from superset.tags.models import ObjectType
from superset.views.base import DeleteMixin
logger = logging.getLogger(__name__)
class DeleteTaggedObjectCommand(DeleteMixin, BaseCommand):
- def __init__(self, object_type: ObjectTypes, object_id: int, tag: str):
+ def __init__(self, object_type: ObjectType, object_id: int, tag: str):
self._object_type = object_type
self._object_id = object_id
self._tag = tag
diff --git a/superset/tags/commands/utils.py b/superset/tags/commands/utils.py
index 028465d83a..c3929cc41b 100644
--- a/superset/tags/commands/utils.py
+++ b/superset/tags/commands/utils.py
@@ -23,25 +23,25 @@ from superset.daos.query import SavedQueryDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
-from superset.tags.models import ObjectTypes
+from superset.tags.models import ObjectType
-def to_object_type(object_type: Union[ObjectTypes, int, str]) -> Optional[ObjectTypes]:
- if isinstance(object_type, ObjectTypes):
+def to_object_type(object_type: Union[ObjectType, int, str]) -> Optional[ObjectType]:
+ if isinstance(object_type, ObjectType):
return object_type
- for type_ in ObjectTypes:
+ for type_ in ObjectType:
if object_type in [type_.value, type_.name]:
return type_
return None
def to_object_model(
- object_type: ObjectTypes, object_id: int
+ object_type: ObjectType, object_id: int
) -> Optional[Union[Dashboard, SavedQuery, Slice]]:
- if ObjectTypes.dashboard == object_type:
+ if ObjectType.dashboard == object_type:
return DashboardDAO.find_by_id(object_id)
- if ObjectTypes.query == object_type:
+ if ObjectType.query == object_type:
return SavedQueryDAO.find_by_id(object_id)
- if ObjectTypes.chart == object_type:
+ if ObjectType.chart == object_type:
return ChartDAO.find_by_id(object_id)
return None
diff --git a/superset/tags/models.py b/superset/tags/models.py
index 7825f283bf..a469c7a33d 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -45,8 +45,7 @@ user_favorite_tag_table = Table(
)
-class TagTypes(enum.Enum):
-
+class TagType(enum.Enum):
"""
Types for tags.
@@ -65,8 +64,7 @@ class TagTypes(enum.Enum):
favorited_by = 4
-class ObjectTypes(enum.Enum):
-
+class ObjectType(enum.Enum):
"""Object types."""
# pylint: disable=invalid-name
@@ -83,7 +81,7 @@ class Tag(Model, AuditMixinNullable):
__tablename__ = "tag"
id = Column(Integer, primary_key=True)
name = Column(String(250), unique=True)
- type = Column(Enum(TagTypes))
+ type = Column(Enum(TagType))
description = Column(Text)
objects = relationship(
@@ -108,12 +106,12 @@ class TaggedObject(Model, AuditMixinNullable):
ForeignKey("slices.id"),
ForeignKey("saved_query.id"),
)
- object_type = Column(Enum(ObjectTypes))
+ object_type = Column(Enum(ObjectType))
tag = relationship("Tag", back_populates="objects", overlaps="tags")
-def get_tag(name: str, session: Session, type_: TagTypes) -> Tag:
+def get_tag(name: str, session: Session, type_: TagType) -> Tag:
tag_name = name.strip()
tag = session.query(Tag).filter_by(name=tag_name, type=type_).one_or_none()
if tag is None:
@@ -123,12 +121,12 @@ def get_tag(name: str, session: Session, type_: TagTypes) -> Tag:
return tag
-def get_object_type(class_name: str) -> ObjectTypes:
+def get_object_type(class_name: str) -> ObjectType:
mapping = {
- "slice": ObjectTypes.chart,
- "dashboard": ObjectTypes.dashboard,
- "query": ObjectTypes.query,
- "dataset": ObjectTypes.dataset,
+ "slice": ObjectType.chart,
+ "dashboard": ObjectType.dashboard,
+ "query": ObjectType.query,
+ "dataset": ObjectType.dataset,
}
try:
return mapping[class_name.lower()]
@@ -155,7 +153,7 @@ class ObjectUpdater:
) -> None:
for owner_id in cls.get_owners_ids(target):
name = f"owner:{owner_id}"
- tag = get_tag(name, session, TagTypes.owner)
+ tag = get_tag(name, session, TagType.owner)
tagged_object = TaggedObject(
tag_id=tag.id, object_id=target.id, object_type=cls.object_type
)
@@ -175,7 +173,7 @@ class ObjectUpdater:
cls._add_owners(session, target)
# add `type:` tags
- tag = get_tag(f"type:{cls.object_type}", session, TagTypes.type)
+ tag = get_tag(f"type:{cls.object_type}", session, TagType.type)
tagged_object = TaggedObject(
tag_id=tag.id, object_id=target.id, object_type=cls.object_type
)
@@ -201,7 +199,7 @@ class ObjectUpdater:
.filter(
TaggedObject.object_type == cls.object_type,
TaggedObject.object_id == target.id,
- Tag.type == TagTypes.owner,
+ Tag.type == TagType.owner,
)
)
ids = [row[0] for row in query]
@@ -276,7 +274,7 @@ class FavStarUpdater:
session = Session(bind=connection)
try:
name = f"favorited_by:{target.user_id}"
- tag = get_tag(name, session, TagTypes.favorited_by)
+ tag = get_tag(name, session, TagType.favorited_by)
tagged_object = TaggedObject(
tag_id=tag.id,
object_id=target.obj_id,
@@ -299,7 +297,7 @@ class FavStarUpdater:
.join(Tag)
.filter(
TaggedObject.object_id == target.obj_id,
- Tag.type == TagTypes.favorited_by,
+ Tag.type == TagType.favorited_by,
Tag.name == name,
)
)
diff --git a/superset/utils/url_map_converters.py b/superset/utils/url_map_converters.py
index 11e40267b3..ed10040227 100644
--- a/superset/utils/url_map_converters.py
+++ b/superset/utils/url_map_converters.py
@@ -18,7 +18,7 @@ from typing import Any
from werkzeug.routing import BaseConverter, Map
-from superset.tags.models import ObjectTypes
+from superset.tags.models import ObjectType
class RegexConverter(BaseConverter):
@@ -31,7 +31,7 @@ class ObjectTypeConverter(BaseConverter):
"""Validate that object_type is indeed an object type."""
def to_python(self, value: str) -> Any:
- return ObjectTypes[value]
+ return ObjectType[value]
def to_url(self, value: Any) -> str:
return value.name
diff --git a/tests/integration_tests/strategy_tests.py b/tests/integration_tests/strategy_tests.py
index 6fec16ca74..8a7477a8fc 100644
--- a/tests/integration_tests/strategy_tests.py
+++ b/tests/integration_tests/strategy_tests.py
@@ -33,7 +33,7 @@ from superset.utils.database import get_example_database
from superset import db
from superset.models.core import Log
-from superset.tags.models import get_tag, ObjectTypes, TaggedObject, TagTypes
+from superset.tags.models import get_tag, ObjectType, TaggedObject, TagType
from superset.tasks.cache import (
DashboardTagsStrategy,
TopNDashboardsStrategy,
@@ -93,7 +93,7 @@ class TestCacheWarmUp(SupersetTestCase):
"load_unicode_dashboard_with_slice", "load_birth_names_dashboard_with_slices"
)
def test_dashboard_tags_strategy(self):
- tag1 = get_tag("tag1", db.session, TagTypes.custom)
+ tag1 = get_tag("tag1", db.session, TagType.custom)
# delete first to make test idempotent
self.reset_tag(tag1)
@@ -103,11 +103,11 @@ class TestCacheWarmUp(SupersetTestCase):
self.assertEqual(result, expected)
# tag dashboard 'births' with `tag1`
- tag1 = get_tag("tag1", db.session, TagTypes.custom)
+ tag1 = get_tag("tag1", db.session, TagType.custom)
dash = self.get_dash_by_slug("births")
tag1_urls = [{"chart_id": chart.id} for chart in dash.slices]
tagged_object = TaggedObject(
- tag_id=tag1.id, object_id=dash.id, object_type=ObjectTypes.dashboard
+ tag_id=tag1.id, object_id=dash.id, object_type=ObjectType.dashboard
)
db.session.add(tagged_object)
db.session.commit()
@@ -115,7 +115,7 @@ class TestCacheWarmUp(SupersetTestCase):
self.assertCountEqual(strategy.get_payloads(), tag1_urls)
strategy = DashboardTagsStrategy(["tag2"])
- tag2 = get_tag("tag2", db.session, TagTypes.custom)
+ tag2 = get_tag("tag2", db.session, TagType.custom)
self.reset_tag(tag2)
result = strategy.get_payloads()
@@ -128,7 +128,7 @@ class TestCacheWarmUp(SupersetTestCase):
tag2_urls = [{"chart_id": chart.id}]
object_id = chart.id
tagged_object = TaggedObject(
- tag_id=tag2.id, object_id=object_id, object_type=ObjectTypes.chart
+ tag_id=tag2.id, object_id=object_id, object_type=ObjectType.chart
)
db.session.add(tagged_object)
db.session.commit()
diff --git a/tests/integration_tests/tagging_tests.py b/tests/integration_tests/tagging_tests.py
index 4ecfd1049f..36fb8df3ff 100644
--- a/tests/integration_tests/tagging_tests.py
+++ b/tests/integration_tests/tagging_tests.py
@@ -70,7 +70,7 @@ class TestTagging(SupersetTestCase):
# Test to make sure that a dataset tag was added to the tagged_object table
tags = self.query_tagged_object_table()
self.assertEqual(1, len(tags))
- self.assertEqual("ObjectTypes.dataset", str(tags[0].object_type))
+ self.assertEqual("ObjectType.dataset", str(tags[0].object_type))
self.assertEqual(test_dataset.id, tags[0].object_id)
# Cleanup the db
@@ -108,7 +108,7 @@ class TestTagging(SupersetTestCase):
# Test to make sure that a chart tag was added to the tagged_object table
tags = self.query_tagged_object_table()
self.assertEqual(1, len(tags))
- self.assertEqual("ObjectTypes.chart", str(tags[0].object_type))
+ self.assertEqual("ObjectType.chart", str(tags[0].object_type))
self.assertEqual(test_chart.id, tags[0].object_id)
# Cleanup the db
@@ -144,7 +144,7 @@ class TestTagging(SupersetTestCase):
# Test to make sure that a dashboard tag was added to the tagged_object table
tags = self.query_tagged_object_table()
self.assertEqual(1, len(tags))
- self.assertEqual("ObjectTypes.dashboard", str(tags[0].object_type))
+ self.assertEqual("ObjectType.dashboard", str(tags[0].object_type))
self.assertEqual(test_dashboard.id, tags[0].object_id)
# Cleanup the db
@@ -178,14 +178,14 @@ class TestTagging(SupersetTestCase):
self.assertEqual(2, len(tags))
- self.assertEqual("ObjectTypes.query", str(tags[0].object_type))
+ self.assertEqual("ObjectType.query", str(tags[0].object_type))
self.assertEqual("owner:None", str(tags[0].tag.name))
- self.assertEqual("TagTypes.owner", str(tags[0].tag.type))
+ self.assertEqual("TagType.owner", str(tags[0].tag.type))
self.assertEqual(test_saved_query.id, tags[0].object_id)
- self.assertEqual("ObjectTypes.query", str(tags[1].object_type))
+ self.assertEqual("ObjectType.query", str(tags[1].object_type))
self.assertEqual("type:query", str(tags[1].tag.name))
- self.assertEqual("TagTypes.type", str(tags[1].tag.type))
+ self.assertEqual("TagType.type", str(tags[1].tag.type))
self.assertEqual(test_saved_query.id, tags[1].object_id)
# Cleanup the db
@@ -217,7 +217,7 @@ class TestTagging(SupersetTestCase):
# Test to make sure that a favorited object tag was added to the tagged_object table
tags = self.query_tagged_object_table()
self.assertEqual(1, len(tags))
- self.assertEqual("ObjectTypes.chart", str(tags[0].object_type))
+ self.assertEqual("ObjectType.chart", str(tags[0].object_type))
self.assertEqual(test_saved_query.obj_id, tags[0].object_id)
# Cleanup the db
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 33fa4902b2..b678569933 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -35,7 +35,7 @@ from superset import db, 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
-from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
+from superset.tags.models import ObjectType, Tag, TagType, TaggedObject
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices,
load_birth_names_data,
@@ -47,7 +47,7 @@ from tests.integration_tests.fixtures.world_bank_dashboard import (
from tests.integration_tests.fixtures.tags import with_tagging_system_feature
from tests.integration_tests.base_tests import SupersetTestCase
from superset.daos.tag import TagDAO
-from superset.tags.models import ObjectTypes
+from superset.tags.models import ObjectType
TAGS_FIXTURE_COUNT = 10
@@ -84,7 +84,7 @@ class TestTagApi(SupersetTestCase):
self,
tag_id: int,
object_id: int,
- object_type: ObjectTypes,
+ object_type: ObjectType,
) -> TaggedObject:
tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
tagged_object = TaggedObject(
@@ -135,7 +135,7 @@ class TestTagApi(SupersetTestCase):
"created_by": None,
"id": tag.id,
"name": "test get tag",
- "type": TagTypes.custom.value,
+ "type": TagType.custom.value,
}
data = json.loads(rv.data.decode("utf-8"))
for key, value in expected_result.items():
@@ -192,7 +192,7 @@ class TestTagApi(SupersetTestCase):
.first()
)
dashboard_id = dashboard.id
- dashboard_type = ObjectTypes.dashboard.value
+ dashboard_type = ObjectType.dashboard.value
uri = f"api/v1/tag/{dashboard_type}/{dashboard_id}/"
example_tag_names = ["example_tag_1", "example_tag_2"]
data = {"properties": {"tags": example_tag_names}}
@@ -207,7 +207,7 @@ class TestTagApi(SupersetTestCase):
tagged_objects = db.session.query(TaggedObject).filter(
TaggedObject.tag_id.in_(tag_ids),
TaggedObject.object_id == dashboard_id,
- TaggedObject.object_type == ObjectTypes.dashboard,
+ TaggedObject.object_type == ObjectType.dashboard,
)
assert tagged_objects.count() == 2
# clean up tags and tagged objects
@@ -225,7 +225,7 @@ class TestTagApi(SupersetTestCase):
def test_delete_tagged_objects(self):
self.login(username="admin")
dashboard_id = 1
- dashboard_type = ObjectTypes.dashboard
+ dashboard_type = ObjectType.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
assert tags.count() == 2
@@ -295,7 +295,7 @@ class TestTagApi(SupersetTestCase):
.first()
)
dashboard_id = dashboard.id
- dashboard_type = ObjectTypes.dashboard
+ dashboard_type = ObjectType.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
for tag in tags:
@@ -331,7 +331,7 @@ class TestTagApi(SupersetTestCase):
.first()
)
dashboard_id = dashboard.id
- dashboard_type = ObjectTypes.dashboard
+ dashboard_type = ObjectType.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
for tag in tags:
@@ -480,7 +480,7 @@ class TestTagApi(SupersetTestCase):
user_id = self.get_user(username="admin").get_id()
tag = (
db.session.query(Tag)
- .filter(Tag.name == "my_tag", Tag.type == TagTypes.custom)
+ .filter(Tag.name == "my_tag", Tag.type == TagType.custom)
.one_or_none()
)
assert tag is not None
@@ -576,13 +576,13 @@ class TestTagApi(SupersetTestCase):
tagged_objects = db.session.query(TaggedObject).filter(
TaggedObject.object_id == dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
+ TaggedObject.object_type == ObjectType.dashboard,
)
assert tagged_objects.count() == 2
tagged_objects = db.session.query(TaggedObject).filter(
TaggedObject.object_id == chart.id,
- TaggedObject.object_type == ObjectTypes.chart,
+ TaggedObject.object_type == ObjectType.chart,
)
assert tagged_objects.count() == 2
diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py
index cd5a024840..057d28abe0 100644
--- a/tests/integration_tests/tags/commands_tests.py
+++ b/tests/integration_tests/tags/commands_tests.py
@@ -37,7 +37,7 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.tags.commands.create import CreateCustomTagCommand
from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
-from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.models import ObjectType, Tag, TaggedObject, TagType
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.importexport import (
chart_config,
@@ -65,7 +65,7 @@ class TestCreateCustomTagCommand(SupersetTestCase):
)
example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ ObjectType.dashboard.value, example_dashboard.id, example_tags
)
command.run()
@@ -74,7 +74,7 @@ class TestCreateCustomTagCommand(SupersetTestCase):
.join(TaggedObject)
.filter(
TaggedObject.object_id == example_dashboard.id,
- Tag.type == TagTypes.custom,
+ Tag.type == TagType.custom,
)
.all()
)
@@ -101,7 +101,7 @@ class TestDeleteTagsCommand(SupersetTestCase):
)
example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ ObjectType.dashboard.value, example_dashboard.id, example_tags
)
command.run()
@@ -110,7 +110,7 @@ class TestDeleteTagsCommand(SupersetTestCase):
.join(TaggedObject)
.filter(
TaggedObject.object_id == example_dashboard.id,
- Tag.type == TagTypes.custom,
+ Tag.type == TagType.custom,
)
.all()
)
@@ -133,7 +133,7 @@ class TestDeleteTaggedObjectCommand(SupersetTestCase):
)
example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ ObjectType.dashboard.value, example_dashboard.id, example_tags
)
command.run()
@@ -142,14 +142,14 @@ class TestDeleteTaggedObjectCommand(SupersetTestCase):
.join(Tag)
.filter(
TaggedObject.object_id == example_dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
+ TaggedObject.object_type == ObjectType.dashboard.name,
Tag.name.in_(example_tags),
)
)
assert tagged_objects.count() == 2
# delete one of the tagged objects
command = DeleteTaggedObjectCommand(
- object_type=ObjectTypes.dashboard.value,
+ object_type=ObjectType.dashboard.value,
object_id=example_dashboard.id,
tag=example_tags[0],
)
@@ -159,7 +159,7 @@ class TestDeleteTaggedObjectCommand(SupersetTestCase):
.join(Tag)
.filter(
TaggedObject.object_id == example_dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
+ TaggedObject.object_type == ObjectType.dashboard.name,
Tag.name.in_(example_tags),
)
)
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
index 8acaa353e9..ea4b3ba783 100644
--- a/tests/integration_tests/tags/dao_tests.py
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -23,7 +23,7 @@ from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.daos.tag import TagDAO
from superset.tags.exceptions import InvalidTagNameError
-from superset.tags.models import ObjectTypes, Tag, TaggedObject
+from superset.tags.models import ObjectType, Tag, TaggedObject
from tests.integration_tests.tags.api_tests import TAGS_FIXTURE_COUNT
import tests.integration_tests.test_app # pylint: disable=unused-import
@@ -57,7 +57,7 @@ class TestTagsDAO(SupersetTestCase):
self,
tag_id: int,
object_id: int,
- object_type: ObjectTypes,
+ object_type: ObjectType,
) -> TaggedObject:
tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
tagged_object = TaggedObject(
@@ -113,7 +113,7 @@ class TestTagsDAO(SupersetTestCase):
tagged_objects.append(
self.insert_tagged_object(
object_id=dashboard_id,
- object_type=ObjectTypes.dashboard,
+ object_type=ObjectType.dashboard,
tag_id=tag.id,
)
)
@@ -127,14 +127,14 @@ class TestTagsDAO(SupersetTestCase):
# test that a tag cannot be added if it has ':' in it
with pytest.raises(DAOCreateFailedError):
TagDAO.create_custom_tagged_objects(
- object_type=ObjectTypes.dashboard.name,
+ object_type=ObjectType.dashboard.name,
object_id=1,
tag_names=["invalid:example tag 1"],
)
# test that a tag can be added if it has a valid name
TagDAO.create_custom_tagged_objects(
- object_type=ObjectTypes.dashboard.name,
+ object_type=ObjectType.dashboard.name,
object_id=1,
tag_names=["example tag 1"],
)
@@ -155,7 +155,7 @@ class TestTagsDAO(SupersetTestCase):
dashboard_id = dashboard.id
tag = db.session.query(Tag).filter_by(name="example_tag_1").one()
self.insert_tagged_object(
- object_id=dashboard_id, object_type=ObjectTypes.dashboard, tag_id=tag.id
+ object_id=dashboard_id, object_type=ObjectType.dashboard, tag_id=tag.id
)
# get objects
tagged_objects = TagDAO.get_tagged_objects_for_tags(
@@ -179,7 +179,7 @@ class TestTagsDAO(SupersetTestCase):
TaggedObject,
and_(
TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
+ TaggedObject.object_type == ObjectType.chart,
),
)
.distinct(Slice.id)
@@ -191,7 +191,7 @@ class TestTagsDAO(SupersetTestCase):
TaggedObject,
and_(
TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
+ TaggedObject.object_type == ObjectType.dashboard,
),
)
.distinct(Dashboard.id)
@@ -213,7 +213,7 @@ class TestTagsDAO(SupersetTestCase):
def test_find_tagged_object(self):
tag = db.session.query(Tag).filter(Tag.name == "example_tag_1").first()
tagged_object = TagDAO.find_tagged_object(
- object_id=1, object_type=ObjectTypes.dashboard.name, tag_id=tag.id
+ object_id=1, object_type=ObjectType.dashboard.name, tag_id=tag.id
)
assert tagged_object is not None
@@ -269,20 +269,20 @@ class TestTagsDAO(SupersetTestCase):
.filter(
TaggedObject.tag_id == tag.id,
TaggedObject.object_id == 1,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
+ TaggedObject.object_type == ObjectType.dashboard.name,
)
.first()
)
assert tagged_object is not None
TagDAO.delete_tagged_object(
- object_type=ObjectTypes.dashboard.name, object_id=1, tag_name=tag.name
+ object_type=ObjectType.dashboard.name, object_id=1, tag_name=tag.name
)
tagged_object = (
db.session.query(TaggedObject)
.filter(
TaggedObject.tag_id == tag.id,
TaggedObject.object_id == 1,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
+ TaggedObject.object_type == ObjectType.dashboard.name,
)
.first()
)
diff --git a/tests/unit_tests/dao/tag_test.py b/tests/unit_tests/dao/tag_test.py
index 065ed75662..5f29d0f28c 100644
--- a/tests/unit_tests/dao/tag_test.py
+++ b/tests/unit_tests/dao/tag_test.py
@@ -149,7 +149,7 @@ def test_user_favorite_tag_exc_raise(mocker):
def test_create_tag_relationship(mocker):
from superset.daos.tag import TagDAO
from superset.tags.models import ( # Assuming these are defined in the same module
- ObjectTypes,
+ ObjectType,
TaggedObject,
)
@@ -157,9 +157,9 @@ def test_create_tag_relationship(mocker):
# Define a list of objects to tag
objects_to_tag = [
- (ObjectTypes.query, 1),
- (ObjectTypes.chart, 2),
- (ObjectTypes.dashboard, 3),
+ (ObjectType.query, 1),
+ (ObjectType.chart, 2),
+ (ObjectType.dashboard, 3),
]
# Call the function
diff --git a/tests/unit_tests/tags/commands/create_test.py b/tests/unit_tests/tags/commands/create_test.py
index d4143bd4ae..39f0e3c4eb 100644
--- a/tests/unit_tests/tags/commands/create_test.py
+++ b/tests/unit_tests/tags/commands/create_test.py
@@ -55,7 +55,7 @@ def test_create_command_success(session_with_data: Session, mocker: MockFixture)
from superset.models.slice import Slice
from superset.models.sql_lab import Query, SavedQuery
from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
- from superset.tags.models import ObjectTypes, TaggedObject
+ from superset.tags.models import ObjectType, TaggedObject
# Define a list of objects to tag
query = session_with_data.query(SavedQuery).first()
@@ -69,9 +69,9 @@ def test_create_command_success(session_with_data: Session, mocker: MockFixture)
mocker.patch("superset.daos.query.SavedQueryDAO.find_by_id", return_value=query)
objects_to_tag = [
- (ObjectTypes.query, query.id),
- (ObjectTypes.chart, chart.id),
- (ObjectTypes.dashboard, dashboard.id),
+ (ObjectType.query, query.id),
+ (ObjectType.chart, chart.id),
+ (ObjectType.dashboard, dashboard.id),
]
CreateCustomTagWithRelationshipsCommand(
@@ -98,7 +98,7 @@ def test_create_command_success_clear(session_with_data: Session, mocker: MockFi
from superset.models.slice import Slice
from superset.models.sql_lab import Query, SavedQuery
from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
- from superset.tags.models import ObjectTypes, TaggedObject
+ from superset.tags.models import ObjectType, TaggedObject
# Define a list of objects to tag
query = session_with_data.query(SavedQuery).first()
@@ -112,9 +112,9 @@ def test_create_command_success_clear(session_with_data: Session, mocker: MockFi
mocker.patch("superset.daos.query.SavedQueryDAO.find_by_id", return_value=query)
objects_to_tag = [
- (ObjectTypes.query, query.id),
- (ObjectTypes.chart, chart.id),
- (ObjectTypes.dashboard, dashboard.id),
+ (ObjectType.query, query.id),
+ (ObjectType.chart, chart.id),
+ (ObjectType.dashboard, dashboard.id),
]
CreateCustomTagWithRelationshipsCommand(
diff --git a/tests/unit_tests/tags/commands/update_test.py b/tests/unit_tests/tags/commands/update_test.py
index 84007fbb68..6d0a99b670 100644
--- a/tests/unit_tests/tags/commands/update_test.py
+++ b/tests/unit_tests/tags/commands/update_test.py
@@ -61,7 +61,7 @@ def test_update_command_success(session_with_data: Session, mocker: MockFixture)
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
from superset.tags.commands.update import UpdateTagCommand
- from superset.tags.models import ObjectTypes, TaggedObject
+ from superset.tags.models import ObjectType, TaggedObject
dashboard = session_with_data.query(Dashboard).first()
mocker.patch(
@@ -72,7 +72,7 @@ def test_update_command_success(session_with_data: Session, mocker: MockFixture)
)
objects_to_tag = [
- (ObjectTypes.dashboard, dashboard.id),
+ (ObjectType.dashboard, dashboard.id),
]
tag_to_update = TagDAO.find_by_name("test_name")
@@ -99,7 +99,7 @@ def test_update_command_success_duplicates(
from superset.models.slice import Slice
from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
from superset.tags.commands.update import UpdateTagCommand
- from superset.tags.models import ObjectTypes, TaggedObject
+ from superset.tags.models import ObjectType, TaggedObject
dashboard = session_with_data.query(Dashboard).first()
chart = session_with_data.query(Slice).first()
@@ -113,7 +113,7 @@ def test_update_command_success_duplicates(
)
objects_to_tag = [
- (ObjectTypes.dashboard, dashboard.id),
+ (ObjectType.dashboard, dashboard.id),
]
CreateCustomTagWithRelationshipsCommand(
@@ -123,7 +123,7 @@ def test_update_command_success_duplicates(
tag_to_update = TagDAO.find_by_name("test_tag")
objects_to_tag = [
- (ObjectTypes.chart, chart.id),
+ (ObjectType.chart, chart.id),
]
changed_model = UpdateTagCommand(
tag_to_update.id,
@@ -150,12 +150,12 @@ def test_update_command_failed_validation(
from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
from superset.tags.commands.exceptions import TagInvalidError
from superset.tags.commands.update import UpdateTagCommand
- from superset.tags.models import ObjectTypes
+ from superset.tags.models import ObjectType
dashboard = session_with_data.query(Dashboard).first()
chart = session_with_data.query(Slice).first()
objects_to_tag = [
- (ObjectTypes.chart, chart.id),
+ (ObjectType.chart, chart.id),
]
mocker.patch(
From 581d3c710867120f85ddfc097713e5f2880722c1 Mon Sep 17 00:00:00 2001
From: "JUST.in DO IT"
Date: Thu, 9 Nov 2023 09:26:21 -0800
Subject: [PATCH 003/119] fix(sqllab): invalid sanitization on comparison
symbol (#25903)
---
.../packages/superset-ui-core/src/utils/html.test.tsx | 3 +++
.../packages/superset-ui-core/src/utils/html.tsx | 4 +++-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx
index 8fd06cb6f8..9b950e4246 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx
@@ -44,6 +44,9 @@ describe('isProbablyHTML', () => {
const plainText = 'Just a plain text';
const isHTML = isProbablyHTML(plainText);
expect(isHTML).toBe(false);
+
+ const trickyText = 'a <= 10 and b > 10';
+ expect(isProbablyHTML(trickyText)).toBe(false);
});
});
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx
index 3215eb9b9d..fffd43bda8 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx
@@ -28,7 +28,9 @@ export function sanitizeHtml(htmlString: string) {
}
export function isProbablyHTML(text: string) {
- return /<[^>]+>/.test(text);
+ return Array.from(
+ new DOMParser().parseFromString(text, 'text/html').body.childNodes,
+ ).some(({ nodeType }) => nodeType === 1);
}
export function sanitizeHtmlIfNeeded(htmlString: string) {
From 33d8078a831a7784af5fb2615782a573fc1d4b60 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 9 Nov 2023 09:57:37 -0800
Subject: [PATCH 004/119] build(deps-dev): bump @types/node from 20.8.10 to
20.9.0 in /superset-websocket (#25928)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 88443ef398..8eb5349d94 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -24,7 +24,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.4",
- "@types/node": "^20.8.10",
+ "@types/node": "^20.9.0",
"@types/uuid": "^9.0.6",
"@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^5.61.0",
@@ -1429,9 +1429,9 @@
"integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
},
"node_modules/@types/node": {
- "version": "20.8.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
- "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -7283,9 +7283,9 @@
"integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
},
"@types/node": {
- "version": "20.8.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
- "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index 1de571d61a..fbe142d485 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -31,7 +31,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.4",
- "@types/node": "^20.8.10",
+ "@types/node": "^20.9.0",
"@types/uuid": "^9.0.6",
"@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^5.61.0",
From 83b7fa92af3994d2ac0cb296c04e5253f50a96d6 Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Thu, 9 Nov 2023 15:16:07 -0300
Subject: [PATCH 005/119] feat: Adds Line chart migration logic (#23973)
---
superset/cli/viz_migrations.py | 11 ++--
.../shared/migrate_viz/processors.py | 58 +++++++++++++++++++
2 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py
index 9e69135aea..4451580d0b 100644
--- a/superset/cli/viz_migrations.py
+++ b/superset/cli/viz_migrations.py
@@ -24,11 +24,12 @@ from superset import db
class VizType(str, Enum):
- TREEMAP = "treemap"
- DUAL_LINE = "dual_line"
AREA = "area"
+ DUAL_LINE = "dual_line"
+ LINE = "line"
PIVOT_TABLE = "pivot_table"
SUNBURST = "sunburst"
+ TREEMAP = "treemap"
@click.group()
@@ -76,17 +77,19 @@ def migrate(viz_type: VizType, is_downgrade: bool = False) -> None:
from superset.migrations.shared.migrate_viz.processors import (
MigrateAreaChart,
MigrateDualLine,
+ MigrateLineChart,
MigratePivotTable,
MigrateSunburst,
MigrateTreeMap,
)
migrations = {
- VizType.TREEMAP: MigrateTreeMap,
- VizType.DUAL_LINE: MigrateDualLine,
VizType.AREA: MigrateAreaChart,
+ VizType.DUAL_LINE: MigrateDualLine,
+ VizType.LINE: MigrateLineChart,
VizType.PIVOT_TABLE: MigratePivotTable,
VizType.SUNBURST: MigrateSunburst,
+ VizType.TREEMAP: MigrateTreeMap,
}
if is_downgrade:
migrations[viz_type].downgrade(db.session)
diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py
index 4ff6b2a934..8627e201f6 100644
--- a/superset/migrations/shared/migrate_viz/processors.py
+++ b/superset/migrations/shared/migrate_viz/processors.py
@@ -16,6 +16,8 @@
# under the License.
from typing import Any
+from superset.utils.core import as_list
+
from .base import MigrateViz
@@ -131,3 +133,59 @@ class MigrateSunburst(MigrateViz):
source_viz_type = "sunburst"
target_viz_type = "sunburst_v2"
rename_keys = {"groupby": "columns"}
+
+
+class TimeseriesChart(MigrateViz):
+ has_x_axis_control = True
+
+ def _pre_action(self) -> None:
+ self.data["contributionMode"] = "row" if self.data.get("contribution") else None
+ self.data["zoomable"] = self.data.get("show_brush") != "no"
+ self.data["markerEnabled"] = self.data.get("show_markers") or False
+ self.data["y_axis_showminmax"] = True
+
+ bottom_margin = self.data.get("bottom_margin")
+ if self.data.get("x_axis_label") and (
+ not bottom_margin or bottom_margin == "auto"
+ ):
+ self.data["bottom_margin"] = 30
+
+ if (rolling_type := self.data.get("rolling_type")) and rolling_type != "None":
+ self.data["rolling_type"] = rolling_type
+
+ if time_compare := self.data.get("time_compare"):
+ self.data["time_compare"] = [
+ value + " ago" for value in as_list(time_compare) if value
+ ]
+
+ comparison_type = self.data.get("comparison_type") or "values"
+ self.data["comparison_type"] = (
+ "difference" if comparison_type == "absolute" else comparison_type
+ )
+
+
+class MigrateLineChart(TimeseriesChart):
+ source_viz_type = "line"
+ target_viz_type = "echarts_timeseries_line"
+ rename_keys = {
+ "x_axis_label": "x_axis_title",
+ "bottom_margin": "x_axis_title_margin",
+ "x_axis_format": "x_axis_time_format",
+ "y_axis_label": "y_axis_title",
+ "left_margin": "y_axis_title_margin",
+ "y_axis_showminmax": "truncateYAxis",
+ "y_log_scale": "logAxis",
+ }
+
+ def _pre_action(self) -> None:
+ super()._pre_action()
+
+ line_interpolation = self.data.get("line_interpolation")
+ if line_interpolation == "cardinal":
+ self.target_viz_type = "echarts_timeseries_smooth"
+ elif line_interpolation == "step-before":
+ self.target_viz_type = "echarts_timeseries_step"
+ self.data["seriesType"] = "start"
+ elif line_interpolation == "step-after":
+ self.target_viz_type = "echarts_timeseries_step"
+ self.data["seriesType"] = "end"
From b6fb36f22b1279274e74e4c99566327507d69475 Mon Sep 17 00:00:00 2001
From: Evan Rusackas
Date: Thu, 9 Nov 2023 15:57:46 -0700
Subject: [PATCH 006/119] chore: removing unused chartMetadata field (#25926)
---
.../superset-ui-core/src/chart/models/ChartMetadata.ts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
index 34f373f0f4..dcb1de62a5 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
@@ -36,7 +36,6 @@ export interface ChartMetadataConfig {
description?: string;
datasourceCount?: number;
enableNoResults?: boolean;
- show?: boolean;
supportedAnnotationTypes?: string[];
thumbnail: string;
useLegacyApi?: boolean;
@@ -64,8 +63,6 @@ export default class ChartMetadata {
description: string;
- show: boolean;
-
supportedAnnotationTypes: string[];
thumbnail: string;
@@ -100,7 +97,6 @@ export default class ChartMetadata {
canBeAnnotationTypes = [],
credits = [],
description = '',
- show = true,
supportedAnnotationTypes = [],
thumbnail,
useLegacyApi = false,
@@ -120,7 +116,6 @@ export default class ChartMetadata {
this.name = name;
this.credits = credits;
this.description = description;
- this.show = show;
this.canBeAnnotationTypes = canBeAnnotationTypes;
this.canBeAnnotationTypesLookup = canBeAnnotationTypes.reduce(
(prev: LookupTable, type: string) => {
From 84a894c2c7d9d6119b4455ee67eeb37030af1b75 Mon Sep 17 00:00:00 2001
From: Evan Rusackas
Date: Thu, 9 Nov 2023 16:49:23 -0700
Subject: [PATCH 007/119] chore(issue template): attempting to fix two
entries/links (#25881)
Co-authored-by: Sam Firke
---
.github/ISSUE_TEMPLATE/config.yml | 4 ++--
.github/ISSUE_TEMPLATE/sip.md | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 25a814f751..5f465d2648 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -8,5 +8,5 @@ contact_links:
url: https://github.com/apache/superset/discussions/new?category=q-a-help
about: Open a community Q&A thread on GitHub Discussions
- name: Slack
- url: bit.ly/join-superset-slack
- about: Join the Superset Community on Slack for other discussions/assistance
+ url: https://bit.ly/join-superset-slack
+ about: Join the Superset Community on Slack for other discussions and assistance
diff --git a/.github/ISSUE_TEMPLATE/sip.md b/.github/ISSUE_TEMPLATE/sip.md
index 8261b0f881..d0ca3ef1d9 100644
--- a/.github/ISSUE_TEMPLATE/sip.md
+++ b/.github/ISSUE_TEMPLATE/sip.md
@@ -1,13 +1,13 @@
---
name: SIP
-about: "Superset Improvement Proposal. See https://github.com/apache/superset/issues/5602 for details. The purpose of a Superset Improvement Proposal (SIP) is to introduce any major change into Apache Superset, such as a major new feature, subsystem, or piece of functionality, or any change that impacts the public interfaces of the project"
+about: "Superset Improvement Proposal. See SIP-0 (https://github.com/apache/superset/issues/5602) for details. A SIP introduces any major change into Apache Superset's code or process."
labels: sip
title: "[SIP] Your Title Here (do not add SIP number)"
assignees: "apache/superset-committers"
---
*Please make sure you are familiar with the SIP process documented*
-(here)[https://github.com/apache/superset/issues/5602]. The SIP will be numbered by a committer upon acceptance.
+[here](https://github.com/apache/superset/issues/5602). The SIP will be numbered by a committer upon acceptance.
## [SIP] Proposal for ...
From 478ca904aedd3e27c7a93c98e522a2d4574f8965 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Nov 2023 09:59:09 -0800
Subject: [PATCH 008/119] build(deps): bump axios from 1.4.0 to 1.6.1 in
/superset-frontend (#25951)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-frontend/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 0a79bfe8de..b6d01c1418 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -47771,9 +47771,9 @@
"dev": true
},
"node_modules/nx/node_modules/axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
+ "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
@@ -101307,9 +101307,9 @@
"dev": true
},
"axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
+ "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.0",
From d95c200e6931e2bc14b0448e3da265d8ebedf249 Mon Sep 17 00:00:00 2001
From: Giacomo Barone <46573388+ggbaro@users.noreply.github.com>
Date: Sat, 11 Nov 2023 05:13:50 +0100
Subject: [PATCH 009/119] fix: update flask-caching to avoid breaking redis
cache, solves #25339 (#25947)
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
---
requirements/base.txt | 4 ++--
setup.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/base.txt b/requirements/base.txt
index d056b403c3..cc43587812 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -31,7 +31,7 @@ bottleneck==1.3.7
# via pandas
brotli==1.0.9
# via flask-compress
-cachelib==0.6.0
+cachelib==0.9.0
# via
# flask-caching
# flask-session
@@ -103,7 +103,7 @@ flask-appbuilder==4.3.9
# via apache-superset
flask-babel==1.0.0
# via flask-appbuilder
-flask-caching==1.11.1
+flask-caching==2.1.0
# via apache-superset
flask-compress==1.13
# via apache-superset
diff --git a/setup.py b/setup.py
index 5173ad6dea..88f2c0fba5 100644
--- a/setup.py
+++ b/setup.py
@@ -84,7 +84,7 @@ setup(
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
"flask-appbuilder>=4.3.9, <5.0.0",
- "flask-caching>=1.11.1, <2.0",
+ "flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",
From b430b5a1720225c7507682382ed69d969aea31a8 Mon Sep 17 00:00:00 2001
From: Sebastian Liebscher
<112352529+sebastianliebscher@users.noreply.github.com>
Date: Sun, 12 Nov 2023 21:11:19 +0100
Subject: [PATCH 010/119] chore: Simplify views/base (#25948)
---
superset/views/base.py | 56 ++++++++++++++++++------------------------
1 file changed, 24 insertions(+), 32 deletions(-)
diff --git a/superset/views/base.py b/superset/views/base.py
index 4015b7a028..62e4dd06cf 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -14,6 +14,8 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from __future__ import annotations
+
import dataclasses
import functools
import logging
@@ -21,7 +23,7 @@ import os
import traceback
from datetime import datetime
from importlib.resources import files
-from typing import Any, Callable, cast, Optional, Union
+from typing import Any, Callable, cast
import simplejson as json
import yaml
@@ -139,15 +141,11 @@ def get_error_msg() -> str:
def json_error_response(
- msg: Optional[str] = None,
+ msg: str | None = None,
status: int = 500,
- payload: Optional[dict[str, Any]] = None,
- link: Optional[str] = None,
+ payload: dict[str, Any] | None = None,
) -> FlaskResponse:
- if not payload:
- payload = {"error": f"{msg}"}
- if link:
- payload["link"] = link
+ payload = payload or {"error": f"{msg}"}
return Response(
json.dumps(payload, default=utils.json_iso_dttm_ser, ignore_nan=True),
@@ -159,10 +157,9 @@ def json_error_response(
def json_errors_response(
errors: list[SupersetError],
status: int = 500,
- payload: Optional[dict[str, Any]] = None,
+ payload: dict[str, Any] | None = None,
) -> FlaskResponse:
- if not payload:
- payload = {}
+ payload = payload or {}
payload["errors"] = [dataclasses.asdict(error) for error in errors]
return Response(
@@ -182,7 +179,7 @@ def data_payload_response(payload_json: str, has_error: bool = False) -> FlaskRe
def generate_download_headers(
- extension: str, filename: Optional[str] = None
+ extension: str, filename: str | None = None
) -> dict[str, Any]:
filename = filename if filename else datetime.now().strftime("%Y%m%d_%H%M%S")
content_disp = f"attachment; filename={filename}.{extension}"
@@ -192,7 +189,7 @@ def generate_download_headers(
def deprecated(
eol_version: str = "4.0.0",
- new_target: Optional[str] = None,
+ new_target: str | None = None,
) -> Callable[[Callable[..., FlaskResponse]], Callable[..., FlaskResponse]]:
"""
A decorator to set an API endpoint from SupersetView has deprecated.
@@ -200,7 +197,7 @@ def deprecated(
"""
def _deprecated(f: Callable[..., FlaskResponse]) -> Callable[..., FlaskResponse]:
- def wraps(self: "BaseSupersetView", *args: Any, **kwargs: Any) -> FlaskResponse:
+ def wraps(self: BaseSupersetView, *args: Any, **kwargs: Any) -> FlaskResponse:
message = (
"%s.%s "
"This API endpoint is deprecated and will be removed in version %s"
@@ -227,7 +224,7 @@ def api(f: Callable[..., FlaskResponse]) -> Callable[..., FlaskResponse]:
return the response in the JSON format
"""
- def wraps(self: "BaseSupersetView", *args: Any, **kwargs: Any) -> FlaskResponse:
+ def wraps(self: BaseSupersetView, *args: Any, **kwargs: Any) -> FlaskResponse:
try:
return f(self, *args, **kwargs)
except NoAuthorizationError:
@@ -249,7 +246,7 @@ def handle_api_exception(
exceptions.
"""
- def wraps(self: "BaseSupersetView", *args: Any, **kwargs: Any) -> FlaskResponse:
+ def wraps(self: BaseSupersetView, *args: Any, **kwargs: Any) -> FlaskResponse:
try:
return f(self, *args, **kwargs)
except SupersetSecurityException as ex:
@@ -294,7 +291,7 @@ class BaseSupersetView(BaseView):
)
def render_app_template(
- self, extra_bootstrap_data: Optional[dict[str, Any]] = None
+ self, extra_bootstrap_data: dict[str, Any] | None = None
) -> FlaskResponse:
payload = {
"user": bootstrap_user_data(g.user, include_perms=True),
@@ -335,21 +332,16 @@ def get_environment_tag() -> dict[str, Any]:
def menu_data(user: User) -> dict[str, Any]:
- menu = appbuilder.menu.get_data()
+ languages = {
+ lang: {**appbuilder.languages[lang], "url": appbuilder.get_url_for_locale(lang)}
+ for lang in appbuilder.languages
+ }
- languages = {}
- for lang in appbuilder.languages:
- languages[lang] = {
- **appbuilder.languages[lang],
- "url": appbuilder.get_url_for_locale(lang),
- }
- brand_text = appbuilder.app.config["LOGO_RIGHT_TEXT"]
- if callable(brand_text):
+ if callable(brand_text := appbuilder.app.config["LOGO_RIGHT_TEXT"]):
brand_text = brand_text()
- build_number = appbuilder.app.config["BUILD_NUMBER"]
return {
- "menu": menu,
+ "menu": appbuilder.menu.get_data(),
"brand": {
"path": appbuilder.app.config["LOGO_TARGET_PATH"] or "/superset/welcome/",
"icon": appbuilder.app_icon,
@@ -369,9 +361,9 @@ def menu_data(user: User) -> dict[str, Any]:
"documentation_text": appbuilder.app.config["DOCUMENTATION_TEXT"],
"version_string": appbuilder.app.config["VERSION_STRING"],
"version_sha": appbuilder.app.config["VERSION_SHA"],
- "build_number": build_number,
+ "build_number": appbuilder.app.config["BUILD_NUMBER"],
"languages": languages,
- "show_language_picker": len(languages.keys()) > 1,
+ "show_language_picker": len(languages) > 1,
"user_is_anonymous": user.is_anonymous,
"user_info_url": None
if is_feature_enabled("MENU_HIDE_USER_INFO")
@@ -595,11 +587,11 @@ class YamlExportMixin: # pylint: disable=too-few-public-methods
Used on DatabaseView for cli compatibility
"""
- yaml_dict_key: Optional[str] = None
+ yaml_dict_key: str | None = None
@action("yaml_export", __("Export to YAML"), __("Export to YAML?"), "fa-download")
def yaml_export(
- self, items: Union[ImportExportMixin, list[ImportExportMixin]]
+ self, items: ImportExportMixin | list[ImportExportMixin]
) -> FlaskResponse:
if not isinstance(items, list):
items = [items]
From c3249dd97d23a858a76dcac9d6ce9236df99d453 Mon Sep 17 00:00:00 2001
From: Sebastian Liebscher
<112352529+sebastianliebscher@users.noreply.github.com>
Date: Mon, 13 Nov 2023 17:40:52 +0100
Subject: [PATCH 011/119] test: Reduce flaky integration tests triggered by
`test_get_tag` (#25958)
---
tests/integration_tests/tags/api_tests.py | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index b678569933..863288a3e7 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -17,10 +17,12 @@
# isort:skip_file
"""Unit tests for Superset"""
import json
+from datetime import datetime
from flask import g
import pytest
import prison
+from freezegun import freeze_time
from sqlalchemy.sql import func
from sqlalchemy import and_
from superset.models.dashboard import Dashboard
@@ -121,13 +123,14 @@ class TestTagApi(SupersetTestCase):
"""
Query API: Test get query
"""
- tag = self.insert_tag(
- name="test get tag",
- tag_type="custom",
- )
- self.login(username="admin")
- uri = f"api/v1/tag/{tag.id}"
- rv = self.client.get(uri)
+ with freeze_time(datetime.now()):
+ tag = self.insert_tag(
+ name="test get tag",
+ tag_type="custom",
+ )
+ self.login(username="admin")
+ uri = f"api/v1/tag/{tag.id}"
+ rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200)
expected_result = {
"changed_by": None,
From 3bff1a00b6542e4f898149f927bfa0fec21a35a4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 10:42:32 -0700
Subject: [PATCH 012/119] build(deps-dev): bump @types/uuid from 9.0.6 to 9.0.7
in /superset-websocket (#25929)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 8eb5349d94..b048996e9c 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -25,7 +25,7 @@
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.4",
"@types/node": "^20.9.0",
- "@types/uuid": "^9.0.6",
+ "@types/uuid": "^9.0.7",
"@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.62.0",
@@ -1456,9 +1456,9 @@
"dev": true
},
"node_modules/@types/uuid": {
- "version": "9.0.6",
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz",
- "integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==",
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
+ "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==",
"dev": true
},
"node_modules/@types/ws": {
@@ -7310,9 +7310,9 @@
"dev": true
},
"@types/uuid": {
- "version": "9.0.6",
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz",
- "integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==",
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
+ "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==",
"dev": true
},
"@types/ws": {
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index fbe142d485..221dd9b677 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -32,7 +32,7 @@
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.4",
"@types/node": "^20.9.0",
- "@types/uuid": "^9.0.6",
+ "@types/uuid": "^9.0.7",
"@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.62.0",
From 943696a87f2bb22aac36c0b0626f024881ee1455 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 10:42:46 -0700
Subject: [PATCH 013/119] build(deps-dev): bump @types/jsonwebtoken from 9.0.4
to 9.0.5 in /superset-websocket (#25927)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index b048996e9c..671d3ecfa2 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -23,7 +23,7 @@
"@types/cookie": "^0.5.4",
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
- "@types/jsonwebtoken": "^9.0.4",
+ "@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.0",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.9",
@@ -1415,9 +1415,9 @@
"dev": true
},
"node_modules/@types/jsonwebtoken": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz",
- "integrity": "sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
+ "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -7269,9 +7269,9 @@
"dev": true
},
"@types/jsonwebtoken": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz",
- "integrity": "sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
+ "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
"dev": true,
"requires": {
"@types/node": "*"
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index 221dd9b677..894906d407 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -30,7 +30,7 @@
"@types/cookie": "^0.5.4",
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
- "@types/jsonwebtoken": "^9.0.4",
+ "@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.0",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.9",
From 8d8e1bb637be08b0345407ea13cfa81034eef1d5 Mon Sep 17 00:00:00 2001
From: "Hugh A. Miles II"
Date: Mon, 13 Nov 2023 13:18:28 -0500
Subject: [PATCH 014/119] fix: always denorm column value before querying
values (#25919)
---
superset/connectors/base/models.py | 7 ----
superset/connectors/sqla/models.py | 29 ----------------
superset/datasource/api.py | 4 +++
superset/models/helpers.py | 56 ++++++++++++++----------------
4 files changed, 31 insertions(+), 65 deletions(-)
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index d5386c7a66..1fc0fde575 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -496,13 +496,6 @@ class BaseDatasource(
"""
raise NotImplementedError()
- def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
- """Given a column, returns an iterable of distinct values
-
- This is used to populate the dropdown showing a list of
- values in filters in the explore view"""
- raise NotImplementedError()
-
@staticmethod
def default_query(qry: Query) -> Query:
return qry
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index e366940ff2..510ca54ae8 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -46,7 +46,6 @@ from sqlalchemy import (
inspect,
Integer,
or_,
- select,
String,
Table,
Text,
@@ -793,34 +792,6 @@ class SqlaTable(
)
) from ex
- def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
- """Runs query against sqla to retrieve some
- sample values for the given column.
- """
- cols = {col.column_name: col for col in self.columns}
- target_col = cols[column_name]
- tp = self.get_template_processor()
- tbl, cte = self.get_from_clause(tp)
-
- qry = (
- select([target_col.get_sqla_col(template_processor=tp)])
- .select_from(tbl)
- .distinct()
- )
- if limit:
- qry = qry.limit(limit)
-
- if self.fetch_values_predicate:
- qry = qry.where(self.get_fetch_values_predicate(template_processor=tp))
-
- with self.database.get_sqla_engine_with_context() as engine:
- sql = qry.compile(engine, compile_kwargs={"literal_binds": True})
- sql = self._apply_cte(sql, cte)
- sql = self.mutate_query_from_config(sql)
-
- df = pd.read_sql_query(sql=sql, con=engine)
- return df[column_name].to_list()
-
def mutate_query_from_config(self, sql: str) -> str:
"""Apply config's SQL_QUERY_MUTATOR
diff --git a/superset/datasource/api.py b/superset/datasource/api.py
index 0c4338e349..131d115755 100644
--- a/superset/datasource/api.py
+++ b/superset/datasource/api.py
@@ -120,6 +120,10 @@ class DatasourceRestApi(BaseSupersetApi):
column_name=column_name, limit=row_limit
)
return self.response(200, result=payload)
+ except KeyError:
+ return self.response(
+ 400, message=f"Column name {column_name} does not exist"
+ )
except NotImplementedError:
return self.response(
400,
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index aafd58f34d..316a46c10c 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -705,10 +705,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
"MIN": sa.func.MIN,
"MAX": sa.func.MAX,
}
-
- @property
- def fetch_value_predicate(self) -> str:
- return "fix this!"
+ fetch_values_predicate = None
@property
def type(self) -> str:
@@ -785,17 +782,20 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
def columns(self) -> list[Any]:
raise NotImplementedError()
- def get_fetch_values_predicate(
- self, template_processor: Optional[BaseTemplateProcessor] = None
- ) -> TextClause:
- raise NotImplementedError()
-
def get_extra_cache_keys(self, query_obj: dict[str, Any]) -> list[Hashable]:
raise NotImplementedError()
def get_template_processor(self, **kwargs: Any) -> BaseTemplateProcessor:
raise NotImplementedError()
+ def get_fetch_values_predicate(
+ self,
+ template_processor: Optional[ # pylint: disable=unused-argument
+ BaseTemplateProcessor
+ ] = None, # pylint: disable=unused-argument
+ ) -> TextClause:
+ return self.fetch_values_predicate
+
def get_sqla_row_level_filters(
self,
template_processor: BaseTemplateProcessor,
@@ -1341,36 +1341,34 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
return and_(*l)
def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
- """Runs query against sqla to retrieve some
- sample values for the given column.
- """
- cols = {}
- for col in self.columns:
- if isinstance(col, dict):
- cols[col.get("column_name")] = col
- else:
- cols[col.column_name] = col
-
- target_col = cols[column_name]
- tp = None # todo(hughhhh): add back self.get_template_processor()
+ # always denormalize column name before querying for values
+ db_dialect = self.database.get_dialect()
+ denomalized_col_name = self.database.db_engine_spec.denormalize_name(
+ db_dialect, column_name
+ )
+ cols = {col.column_name: col for col in self.columns}
+ target_col = cols[denomalized_col_name]
+ tp = self.get_template_processor()
tbl, cte = self.get_from_clause(tp)
- if isinstance(target_col, dict):
- sql_column = sa.column(target_col.get("name"))
- else:
- sql_column = target_col
-
- qry = sa.select([sql_column]).select_from(tbl).distinct()
+ qry = (
+ sa.select([target_col.get_sqla_col(template_processor=tp)])
+ .select_from(tbl)
+ .distinct()
+ )
if limit:
qry = qry.limit(limit)
+ if self.fetch_values_predicate:
+ qry = qry.where(self.get_fetch_values_predicate(template_processor=tp))
+
with self.database.get_sqla_engine_with_context() as engine:
sql = qry.compile(engine, compile_kwargs={"literal_binds": True})
sql = self._apply_cte(sql, cte)
sql = self.mutate_query_from_config(sql)
df = pd.read_sql_query(sql=sql, con=engine)
- return df[column_name].to_list()
+ return df[denomalized_col_name].to_list()
def get_timestamp_expression(
self,
@@ -1942,7 +1940,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
)
having_clause_and += [self.text(having)]
- if apply_fetch_values_predicate and self.fetch_values_predicate: # type: ignore
+ if apply_fetch_values_predicate and self.fetch_values_predicate:
qry = qry.where(
self.get_fetch_values_predicate(template_processor=template_processor)
)
From 6d8424c104f196bde54d1ff3d02269e4c71059b4 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Mon, 13 Nov 2023 11:25:14 -0800
Subject: [PATCH 015/119] chore(colors): Updating Airbnb brand colors (#23619)
---
.../cypress/e2e/dashboard/editmode.test.ts | 6 ++--
.../explore/visualizations/dist_bar.test.js | 2 +-
.../e2e/explore/visualizations/line.test.ts | 2 +-
.../color/colorSchemes/categorical/airbnb.ts | 34 +++++++------------
.../legacy-plugin-chart-map-box/Stories.tsx | 2 +-
5 files changed, 19 insertions(+), 27 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
index 812ad945da..62bab84d1b 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/editmode.test.ts
@@ -515,7 +515,7 @@ describe('Dashboard edit', () => {
// label Anthony
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.eq(2)
- .should('have.css', 'fill', 'rgb(0, 122, 135)');
+ .should('have.css', 'fill', 'rgb(244, 176, 42)');
// open main tab and nested tab
openTab(0, 0);
@@ -526,7 +526,7 @@ describe('Dashboard edit', () => {
'[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol',
)
.first()
- .should('have.css', 'fill', 'rgb(0, 122, 135)');
+ .should('have.css', 'fill', 'rgb(244, 176, 42)');
});
it('should apply the color scheme across main tabs', () => {
@@ -557,7 +557,7 @@ describe('Dashboard edit', () => {
cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol')
.first()
- .should('have.css', 'fill', 'rgb(204, 0, 134)');
+ .should('have.css', 'fill', 'rgb(156, 52, 152)');
// change scheme now that charts are rendered across the main tabs
editDashboard();
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
index 770e1e1c04..591ba31776 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/dist_bar.test.js
@@ -89,6 +89,6 @@ describe('Visualization > Distribution bar chart', () => {
).should('exist');
cy.get('.dist_bar .nv-legend .nv-legend-symbol')
.first()
- .should('have.css', 'fill', 'rgb(255, 90, 95)');
+ .should('have.css', 'fill', 'rgb(41, 105, 107)');
});
});
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/line.test.ts b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/line.test.ts
index 5cc398c7f3..8499db5946 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/line.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/line.test.ts
@@ -85,7 +85,7 @@ describe('Visualization > Line', () => {
).should('exist');
cy.get('.line .nv-legend .nv-legend-symbol')
.first()
- .should('have.css', 'fill', 'rgb(255, 90, 95)');
+ .should('have.css', 'fill', 'rgb(41, 105, 107)');
});
it('should work with adhoc metric', () => {
diff --git a/superset-frontend/packages/superset-ui-core/src/color/colorSchemes/categorical/airbnb.ts b/superset-frontend/packages/superset-ui-core/src/color/colorSchemes/categorical/airbnb.ts
index 462065b84f..a126f502a9 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/colorSchemes/categorical/airbnb.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/colorSchemes/categorical/airbnb.ts
@@ -24,27 +24,19 @@ const schemes = [
id: 'bnbColors',
label: 'Airbnb Colors',
colors: [
- '#ff5a5f', // rausch
- '#7b0051', // hackb
- '#007A87', // kazan
- '#00d1c1', // babu
- '#8ce071', // lima
- '#ffb400', // beach
- '#b4a76c', // barol
- '#ff8083',
- '#cc0086',
- '#00a1b3',
- '#00ffeb',
- '#bbedab',
- '#ffd266',
- '#cbc29a',
- '#ff3339',
- '#ff1ab1',
- '#005c66',
- '#00b3a5',
- '#55d12e',
- '#b37e00',
- '#988b4e',
+ '#29696B',
+ '#5BCACE',
+ '#F4B02A',
+ '#F1826A',
+ '#792EB2',
+ '#C96EC6',
+ '#921E50',
+ '#B27700',
+ '#9C3498',
+ '#9C3498',
+ '#E4679D',
+ '#C32F0E',
+ '#9D63CA',
],
},
].map(s => new CategoricalScheme(s));
diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-map-box/Stories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-map-box/Stories.tsx
index 6cdca623a1..dd95ffada5 100644
--- a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-map-box/Stories.tsx
+++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-map-box/Stories.tsx
@@ -42,7 +42,7 @@ export const Basic = () => {
allColumnsY: 'LAT',
clusteringRadius: '60',
globalOpacity: 1,
- mapboxColor: 'rgb(0, 122, 135)',
+ mapboxColor: 'rgb(244, 176, 42)',
mapboxLabel: [],
mapboxStyle: 'mapbox://styles/mapbox/light-v9',
pandasAggfunc: 'sum',
From 99d4f8931a34b680318a0bbef8181508e2b5f94f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 12:39:52 -0700
Subject: [PATCH 016/119] build(deps-dev): bump axios from 0.25.0 to 1.6.0 in
/superset-embedded-sdk (#25953)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-embedded-sdk/package-lock.json | 73 ++++++++++++++++++++-----
superset-embedded-sdk/package.json | 2 +-
2 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/superset-embedded-sdk/package-lock.json b/superset-embedded-sdk/package-lock.json
index febe3b9ea5..0112ed3a38 100644
--- a/superset-embedded-sdk/package-lock.json
+++ b/superset-embedded-sdk/package-lock.json
@@ -18,7 +18,7 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@types/jest": "^27.4.1",
- "axios": "^0.25.0",
+ "axios": "^1.6.0",
"babel-loader": "^8.2.3",
"jest": "^27.5.1",
"typescript": "^4.5.5",
@@ -2977,12 +2977,28 @@
"dev": true
},
"node_modules/axios": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
- "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
+ "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true,
"dependencies": {
- "follow-redirects": "^1.14.7"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
"node_modules/babel-jest": {
@@ -4087,9 +4103,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.14.8",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
- "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"dev": true,
"funding": [
{
@@ -7021,6 +7037,12 @@
"node": ">= 6"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -10431,12 +10453,27 @@
"dev": true
},
"axios": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
- "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
+ "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true,
"requires": {
- "follow-redirects": "^1.14.7"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ }
}
},
"babel-jest": {
@@ -11279,9 +11316,9 @@
}
},
"follow-redirects": {
- "version": "1.14.8",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
- "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"dev": true
},
"form-data": {
@@ -13464,6 +13501,12 @@
"sisteransi": "^1.0.5"
}
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
diff --git a/superset-embedded-sdk/package.json b/superset-embedded-sdk/package.json
index dfe1801ac9..55ed198598 100644
--- a/superset-embedded-sdk/package.json
+++ b/superset-embedded-sdk/package.json
@@ -42,7 +42,7 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@types/jest": "^27.4.1",
- "axios": "^0.25.0",
+ "axios": "^1.6.0",
"babel-loader": "^8.2.3",
"jest": "^27.5.1",
"typescript": "^4.5.5",
From 5def416f632ae7d7f90ae615a8600e8110797aec Mon Sep 17 00:00:00 2001
From: "Hugh A. Miles II"
Date: Mon, 13 Nov 2023 18:46:09 -0500
Subject: [PATCH 017/119] fix: naming denomalized to denormalized in helpers.py
(#25973)
---
superset/models/helpers.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 316a46c10c..fc947ff577 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -1343,11 +1343,11 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
# always denormalize column name before querying for values
db_dialect = self.database.get_dialect()
- denomalized_col_name = self.database.db_engine_spec.denormalize_name(
+ denormalized_col_name = self.database.db_engine_spec.denormalize_name(
db_dialect, column_name
)
cols = {col.column_name: col for col in self.columns}
- target_col = cols[denomalized_col_name]
+ target_col = cols[denormalized_col_name]
tp = self.get_template_processor()
tbl, cte = self.get_from_clause(tp)
@@ -1368,7 +1368,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
sql = self.mutate_query_from_config(sql)
df = pd.read_sql_query(sql=sql, con=engine)
- return df[denomalized_col_name].to_list()
+ return df[denormalized_col_name].to_list()
def get_timestamp_expression(
self,
From 007d22199d7a3a8aca163b7414d288434ab3680a Mon Sep 17 00:00:00 2001
From: Daniel Vaz Gaspar
Date: Tue, 14 Nov 2023 14:01:08 +0000
Subject: [PATCH 018/119] chore: support different JWT CSRF cookie names
(#25891)
---
superset-frontend/src/setup/setupClient.ts | 7 ++++++-
superset/views/base.py | 1 +
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/superset-frontend/src/setup/setupClient.ts b/superset-frontend/src/setup/setupClient.ts
index 80ce6b54bb..c6f2399436 100644
--- a/superset-frontend/src/setup/setupClient.ts
+++ b/superset-frontend/src/setup/setupClient.ts
@@ -18,13 +18,18 @@
*/
import { SupersetClient, logging, ClientConfig } from '@superset-ui/core';
import parseCookie from 'src/utils/parseCookie';
+import getBootstrapData from 'src/utils/getBootstrapData';
+
+const bootstrapData = getBootstrapData();
function getDefaultConfiguration(): ClientConfig {
const csrfNode = document.querySelector('#csrf_token');
const csrfToken = csrfNode?.value;
// when using flask-jwt-extended csrf is set in cookies
- const cookieCSRFToken = parseCookie().csrf_access_token || '';
+ const jwtAccessCsrfCookieName =
+ bootstrapData.common.conf.JWT_ACCESS_CSRF_COOKIE_NAME;
+ const cookieCSRFToken = parseCookie()[jwtAccessCsrfCookieName] || '';
return {
protocol: ['http:', 'https:'].includes(window?.location?.protocol)
diff --git a/superset/views/base.py b/superset/views/base.py
index 62e4dd06cf..8f8b4c1648 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -122,6 +122,7 @@ FRONTEND_CONF_KEYS = (
"ALERT_REPORTS_DEFAULT_WORKING_TIMEOUT",
"NATIVE_FILTER_DEFAULT_ROW_LIMIT",
"PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET",
+ "JWT_ACCESS_CSRF_COOKIE_NAME",
)
logger = logging.getLogger(__name__)
From 6b7761ecf2b1dbb341deca118c514404c658b70a Mon Sep 17 00:00:00 2001
From: Kamil Gabryjelski
Date: Tue, 14 Nov 2023 16:29:16 +0100
Subject: [PATCH 019/119] chore: Add entry point for SliceHeader frontend
extension (#25968)
---
.../superset-ui-core/src/ui-overrides/types.ts | 9 +++++++++
.../src/dashboard/components/FiltersBadge/index.tsx | 2 +-
.../components/SliceHeader/SliceHeader.test.tsx | 13 +++++++++++++
.../src/dashboard/components/SliceHeader/index.tsx | 11 ++++++++++-
4 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
index 0e7e0c9783..27646442de 100644
--- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
@@ -127,6 +127,14 @@ export interface SQLResultTableExtentionProps {
expandedColumns?: string[];
}
+/**
+ * Interface for extensions to Slice Header
+ */
+export interface SliceHeaderExtension {
+ sliceId: number;
+ dashboardId: number;
+}
+
export type Extensions = Partial<{
'alertsreports.header.icon': React.ComponentType;
'embedded.documentation.configuration_details': React.ComponentType;
@@ -147,4 +155,5 @@ export type Extensions = Partial<{
'dataset.delete.related': React.ComponentType;
'sqleditor.extension.form': React.ComponentType;
'sqleditor.extension.resultTable': React.ComponentType;
+ 'dashboard.slice.header': React.ComponentType;
}>;
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
index cb5d261a1b..6dba29c661 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx
@@ -59,7 +59,7 @@ const StyledFilterCount = styled.div`
vertical-align: middle;
color: ${theme.colors.grayscale.base};
&:hover {
- color: ${theme.colors.grayscale.light1}
+ color: ${theme.colors.grayscale.light1};
}
}
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
index e16cab8daa..f452e22ac8 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
@@ -19,6 +19,7 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
+import { getExtensionsRegistry } from '@superset-ui/core';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import SliceHeader from '.';
@@ -472,3 +473,15 @@ test('Correct actions to "SliceHeaderControls"', () => {
userEvent.click(screen.getByTestId('handleToggleFullSize'));
expect(props.handleToggleFullSize).toBeCalledTimes(1);
});
+
+test('Add extension to SliceHeader', () => {
+ const extensionsRegistry = getExtensionsRegistry();
+ extensionsRegistry.set('dashboard.slice.header', () => (
+
This is an extension
+ ));
+
+ const props = createProps();
+ render(, { useRedux: true, useRouter: true });
+
+ expect(screen.getByText('This is an extension')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index c9cb74a8af..ea4f3b63ba 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -24,7 +24,7 @@ import React, {
useRef,
useState,
} from 'react';
-import { css, styled, t } from '@superset-ui/core';
+import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
import { Tooltip } from 'src/components/Tooltip';
import { useSelector } from 'react-redux';
@@ -38,6 +38,8 @@ import { RootState } from 'src/dashboard/types';
import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip';
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
+const extensionsRegistry = getExtensionsRegistry();
+
type SliceHeaderProps = SliceHeaderControlsProps & {
innerRef?: string;
updateSliceName?: (arg0: string) => void;
@@ -161,6 +163,7 @@ const SliceHeader: FC = ({
width,
height,
}) => {
+ const SliceHeaderExtension = extensionsRegistry.get('dashboard.slice.header');
const uiConfig = useUiConfig();
const dashboardPageId = useContext(DashboardPageIdContext);
const [headerTooltip, setHeaderTooltip] = useState(null);
@@ -239,6 +242,12 @@ const SliceHeader: FC = ({
{!editMode && (
<>
+ {SliceHeaderExtension && (
+
+ )}
{crossFilterValue && (
Date: Tue, 14 Nov 2023 17:16:42 +0000
Subject: [PATCH 020/119] feat(explore): dataset macro: dttm filter context
(#25950)
---
superset/jinja_context.py | 42 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 41 insertions(+), 1 deletion(-)
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index c159a667ee..13a639df7b 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -17,9 +17,11 @@
"""Defines the templating context for SQL Lab"""
import json
import re
+from datetime import datetime
from functools import lru_cache, partial
from typing import Any, Callable, cast, Optional, TYPE_CHECKING, TypedDict, Union
+import dateutil
from flask import current_app, g, has_request_context, request
from flask_babel import gettext as _
from jinja2 import DebugUndefined
@@ -486,6 +488,19 @@ class BaseTemplateProcessor:
class JinjaTemplateProcessor(BaseTemplateProcessor):
+ def _parse_datetime(self, dttm: str) -> Optional[datetime]:
+ """
+ Try to parse a datetime and default to None in the worst case.
+
+ Since this may have been rendered by different engines, the datetime may
+ vary slightly in format. We try to make it consistent, and if all else
+ fails, just return None.
+ """
+ try:
+ return dateutil.parser.parse(dttm)
+ except dateutil.parser.ParserError:
+ return None
+
def set_context(self, **kwargs: Any) -> None:
super().set_context(**kwargs)
extra_cache = ExtraCache(
@@ -494,6 +509,23 @@ class JinjaTemplateProcessor(BaseTemplateProcessor):
removed_filters=self._removed_filters,
dialect=self._database.get_dialect(),
)
+
+ from_dttm = (
+ self._parse_datetime(dttm)
+ if (dttm := self._context.get("from_dttm"))
+ else None
+ )
+ to_dttm = (
+ self._parse_datetime(dttm)
+ if (dttm := self._context.get("to_dttm"))
+ else None
+ )
+
+ dataset_macro_with_context = partial(
+ dataset_macro,
+ from_dttm=from_dttm,
+ to_dttm=to_dttm,
+ )
self._context.update(
{
"url_param": partial(safe_proxy, extra_cache.url_param),
@@ -502,7 +534,7 @@ class JinjaTemplateProcessor(BaseTemplateProcessor):
"cache_key_wrapper": partial(safe_proxy, extra_cache.cache_key_wrapper),
"filter_values": partial(safe_proxy, extra_cache.filter_values),
"get_filters": partial(safe_proxy, extra_cache.get_filters),
- "dataset": partial(safe_proxy, dataset_macro),
+ "dataset": partial(safe_proxy, dataset_macro_with_context),
}
)
@@ -638,12 +670,18 @@ def dataset_macro(
dataset_id: int,
include_metrics: bool = False,
columns: Optional[list[str]] = None,
+ from_dttm: Optional[datetime] = None,
+ to_dttm: Optional[datetime] = None,
) -> str:
"""
Given a dataset ID, return the SQL that represents it.
The generated SQL includes all columns (including computed) by default. Optionally
the user can also request metrics to be included, and columns to group by.
+
+ The from_dttm and to_dttm parameters are filled in from filter values in explore
+ views, and we take them to make those properties available to jinja templates in
+ the underlying dataset.
"""
# pylint: disable=import-outside-toplevel
from superset.daos.dataset import DatasetDAO
@@ -659,6 +697,8 @@ def dataset_macro(
"filter": [],
"metrics": metrics if include_metrics else None,
"columns": columns,
+ "from_dttm": from_dttm,
+ "to_dttm": to_dttm,
}
sqla_query = dataset.get_query_str_extended(query_obj, mutate=False)
sql = sqla_query.sql
From f18fb24b3df0a618c57f9b0225494bf13f0ec1c8 Mon Sep 17 00:00:00 2001
From: "JUST.in DO IT"
Date: Tue, 14 Nov 2023 10:27:44 -0800
Subject: [PATCH 021/119] fix(sqllab): Allow router navigation to explore
(#25941)
---
.../src/SqlLab/components/ResultSet/index.tsx | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 35eac78044..58e55a1df7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -18,6 +18,7 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
+import { useHistory } from 'react-router-dom';
import ButtonGroup from 'src/components/ButtonGroup';
import Alert from 'src/components/Alert';
import Button from 'src/components/Button';
@@ -161,6 +162,7 @@ const ResultSet = ({
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const [alertIsOpen, setAlertIsOpen] = useState(false);
+ const history = useHistory();
const dispatch = useDispatch();
const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => {
@@ -215,9 +217,11 @@ const ResultSet = ({
setSearchText(event.target.value);
};
- const createExploreResultsOnClick = async () => {
+ const createExploreResultsOnClick = async (clickEvent: React.MouseEvent) => {
const { results } = query;
+ const openInNewWindow = clickEvent.metaKey;
+
if (results?.query_id) {
const key = await postFormData(results.query_id, 'query', {
...EXPLORE_CHART_DEFAULT,
@@ -229,7 +233,11 @@ const ResultSet = ({
const url = mountExploreUrl(null, {
[URL_PARAMS.formDataKey.name]: key,
});
- window.open(url, '_blank', 'noreferrer');
+ if (openInNewWindow) {
+ window.open(url, '_blank', 'noreferrer');
+ } else {
+ history.push(url);
+ }
} else {
addDangerToast(t('Unable to create chart without a query id.'));
}
From 5e1c4057a06e194462b53de04d621637860fc054 Mon Sep 17 00:00:00 2001
From: josedev-union <70741025+josedev-union@users.noreply.github.com>
Date: Wed, 15 Nov 2023 20:38:54 +0100
Subject: [PATCH 022/119] fix(helm): Restart all related deployments when
bootstrap script changed (#25703)
---
helm/superset/Chart.yaml | 2 +-
helm/superset/README.md | 2 +-
helm/superset/templates/deployment-beat.yaml | 1 +
helm/superset/templates/deployment-worker.yaml | 1 +
4 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml
index 60e2510eb9..36d40645df 100644
--- a/helm/superset/Chart.yaml
+++ b/helm/superset/Chart.yaml
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
-version: 0.10.14
+version: 0.10.15
dependencies:
- name: postgresql
version: 12.1.6
diff --git a/helm/superset/README.md b/helm/superset/README.md
index d32ee985fe..1c9bab285e 100644
--- a/helm/superset/README.md
+++ b/helm/superset/README.md
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
-![Version: 0.10.14](https://img.shields.io/badge/Version-0.10.14-informational?style=flat-square)
+![Version: 0.10.15](https://img.shields.io/badge/Version-0.10.15-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
diff --git a/helm/superset/templates/deployment-beat.yaml b/helm/superset/templates/deployment-beat.yaml
index 43754efb06..eab9a6f3eb 100644
--- a/helm/superset/templates/deployment-beat.yaml
+++ b/helm/superset/templates/deployment-beat.yaml
@@ -42,6 +42,7 @@ spec:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
+ checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }}
checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }}
checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }}
diff --git a/helm/superset/templates/deployment-worker.yaml b/helm/superset/templates/deployment-worker.yaml
index d84e7e9561..2710ff40fe 100644
--- a/helm/superset/templates/deployment-worker.yaml
+++ b/helm/superset/templates/deployment-worker.yaml
@@ -48,6 +48,7 @@ spec:
metadata:
annotations:
checksum/superset_config.py: {{ include "superset-config" . | sha256sum }}
+ checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }}
checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }}
checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }}
checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }}
From f22ae2dc5126f29843632c88278dacac932ea7b1 Mon Sep 17 00:00:00 2001
From: jdclarke5
Date: Thu, 16 Nov 2023 06:39:17 +1100
Subject: [PATCH 023/119] docs: add Tentacle to users list (#25059)
---
RESOURCES/INTHEWILD.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md
index 155cbe83b4..a049f2d7f4 100644
--- a/RESOURCES/INTHEWILD.md
+++ b/RESOURCES/INTHEWILD.md
@@ -111,6 +111,7 @@ Join our growing community!
- [Steamroot](https://streamroot.io/)
- [TechAudit](https://www.techaudit.info) [@ETselikov]
- [Tenable](https://www.tenable.com) [@dflionis]
+- [Tentacle](https://public.tentaclecmi.com) [@jdclarke5]
- [timbr.ai](https://timbr.ai/) [@semantiDan]
- [Tobii](http://www.tobii.com/) [@dwa]
- [Tooploox](https://www.tooploox.com/) [@jakubczaplicki]
From 63b81723218726f2fd2a1db7af4e56693261c257 Mon Sep 17 00:00:00 2001
From: Priyanshu Bartwal
<110045644+git-init-priyanshu@users.noreply.github.com>
Date: Thu, 16 Nov 2023 01:11:12 +0530
Subject: [PATCH 024/119] style: Transition of Navbar from dark to light and
vice-versa is now smooth (#24485)
---
docs/src/styles/main.less | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/src/styles/main.less b/docs/src/styles/main.less
index 80dee90eca..d10047fdea 100644
--- a/docs/src/styles/main.less
+++ b/docs/src/styles/main.less
@@ -117,6 +117,7 @@ a > span > svg {
font-size: 14px;
font-weight: 400;
background-color: #fff;
+ transition: all 0.5s;
.get-started-button {
border-radius: 10px;
From aee94b39baaceb51cb6042188fd7f4e753266396 Mon Sep 17 00:00:00 2001
From: "Hugh A. Miles II"
Date: Wed, 15 Nov 2023 18:05:18 -0500
Subject: [PATCH 025/119] fix(tag): update state to clear form on success
(#25934)
---
.../src/features/tags/TagModal.tsx | 21 ++++++++++++-------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/superset-frontend/src/features/tags/TagModal.tsx b/superset-frontend/src/features/tags/TagModal.tsx
index 4339d69130..a0ac8636a5 100644
--- a/superset-frontend/src/features/tags/TagModal.tsx
+++ b/superset-frontend/src/features/tags/TagModal.tsx
@@ -88,6 +88,14 @@ const TagModal: React.FC = ({
setSavedQueriesToTag([]);
};
+ const clearTagForm = () => {
+ setTagName('');
+ setDescription('');
+ setDashboardsToTag([]);
+ setChartsToTag([]);
+ setSavedQueriesToTag([]);
+ };
+
useEffect(() => {
const resourceMap: { [key: string]: TaggableResourceOption[] } = {
[TaggableResources.Dashboard]: [],
@@ -225,7 +233,9 @@ const TagModal: React.FC = ({
})
.then(({ json = {} }) => {
refreshData();
+ clearTagForm();
addSuccessToast(t('Tag updated'));
+ onHide();
})
.catch(err => {
addDangerToast(err.message || 'Error Updating Tag');
@@ -241,24 +251,19 @@ const TagModal: React.FC = ({
})
.then(({ json = {} }) => {
refreshData();
+ clearTagForm();
addSuccessToast(t('Tag created'));
+ onHide();
})
.catch(err => addDangerToast(err.message || 'Error Creating Tag'));
}
- onHide();
};
return (
{
- if (clearOnHide) {
- setTagName('');
- setDescription('');
- setDashboardsToTag([]);
- setChartsToTag([]);
- setSavedQueriesToTag([]);
- }
+ if (clearOnHide) clearTagForm();
onHide();
}}
show={show}
From d20b60edd4aaa604b520a7d09a9cf899cef6d61f Mon Sep 17 00:00:00 2001
From: Sebastian Liebscher
<112352529+sebastianliebscher@users.noreply.github.com>
Date: Thu, 16 Nov 2023 00:42:48 +0100
Subject: [PATCH 026/119] chore: Remove more redundant code in utils/core
(#25986)
---
superset/utils/core.py | 39 +-------------------------
tests/integration_tests/utils_tests.py | 10 -------
2 files changed, 1 insertion(+), 48 deletions(-)
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 1ef397053d..67edabe626 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -72,7 +72,7 @@ from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql.type_api import Variant
-from sqlalchemy.types import TEXT, TypeDecorator, TypeEngine
+from sqlalchemy.types import TypeEngine
from typing_extensions import TypeGuard
from superset.constants import (
@@ -122,18 +122,6 @@ InputType = TypeVar("InputType") # pylint: disable=invalid-name
ADHOC_FILTERS_REGEX = re.compile("^adhoc_filters")
-class LenientEnum(Enum):
- """Enums with a `get` method that convert a enum value to `Enum` if it is a
- valid value."""
-
- @classmethod
- def get(cls, value: Any) -> Any:
- try:
- return super().__new__(cls, value)
- except ValueError:
- return None
-
-
class AdhocMetricExpressionType(StrEnum):
SIMPLE = "SIMPLE"
SQL = "SQL"
@@ -280,15 +268,6 @@ class PostProcessingContributionOrientation(StrEnum):
COLUMN = "column"
-class QueryMode(str, LenientEnum):
- """
- Whether the query runs on aggregate or returns raw records
- """
-
- RAW = "raw"
- AGGREGATE = "aggregate"
-
-
class QuerySource(Enum):
"""
The source of a SQL query.
@@ -454,22 +433,6 @@ class DashboardEncoder(json.JSONEncoder):
return json.JSONEncoder(sort_keys=True).default(o)
-class JSONEncodedDict(TypeDecorator): # pylint: disable=abstract-method
- """Represents an immutable structure as a json-encoded string."""
-
- impl = TEXT
-
- def process_bind_param(
- self, value: dict[Any, Any] | None, dialect: str
- ) -> str | None:
- return json.dumps(value) if value is not None else None
-
- def process_result_value(
- self, value: str | None, dialect: str
- ) -> dict[Any, Any] | None:
- return json.loads(value) if value is not None else None
-
-
def format_timedelta(time_delta: timedelta) -> str:
"""
Ensures negative time deltas are easily interpreted by humans
diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py
index 6648d72c61..6f8a7ed457 100644
--- a/tests/integration_tests/utils_tests.py
+++ b/tests/integration_tests/utils_tests.py
@@ -59,7 +59,6 @@ from superset.utils.core import (
get_stacktrace,
json_int_dttm_ser,
json_iso_dttm_ser,
- JSONEncodedDict,
merge_extra_filters,
merge_extra_form_data,
merge_request_params,
@@ -583,15 +582,6 @@ class TestUtils(SupersetTestCase):
"-16 days, 4:03:00",
)
- def test_json_encoded_obj(self):
- obj = {"a": 5, "b": ["a", "g", 5]}
- val = '{"a": 5, "b": ["a", "g", 5]}'
- jsonObj = JSONEncodedDict()
- resp = jsonObj.process_bind_param(obj, "dialect")
- self.assertIn('"a": 5', resp)
- self.assertIn('"b": ["a", "g", 5]', resp)
- self.assertEqual(jsonObj.process_result_value(val, "dialect"), obj)
-
def test_validate_json(self):
valid = '{"a": 5, "b": [1, 5, ["g", "h"]]}'
self.assertIsNone(validate_json(valid))
From 7f0c3b20ad11a5e199d37c92aa3256279042d48c Mon Sep 17 00:00:00 2001
From: nitish-samsung-jha
<147135009+nitish-samsung-jha@users.noreply.github.com>
Date: Thu, 16 Nov 2023 21:30:42 +0530
Subject: [PATCH 027/119] docs: handling "System limit for number of file
watchers reached" error (#25551)
Co-authored-by: Sam Firke
---
CONTRIBUTING.md | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 343356b5a9..d427ba393d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -610,6 +610,31 @@ Then put this:
export NODE_OPTIONS=--no-experimental-fetch
```
+If while using the above commands you encounter an error related to the limit of file watchers:
+
+```bash
+Error: ENOSPC: System limit for number of file watchers reached
+```
+The error is thrown because the number of files monitored by the system has reached the limit.
+You can address this this error by increasing the number of inotify watchers.
+
+
+The current value of max watches can be checked with:
+```bash
+cat /proc/sys/fs/inotify/max_user_watches
+```
+Edit the file /etc/sysctl.conf to increase this value.
+The value needs to be decided based on the system memory [(see this StackOverflow answer for more context)](https://stackoverflow.com/questions/535768/what-is-a-reasonable-amount-of-inotify-watches-with-linux).
+
+Open the file in editor and add a line at the bottom specifying the max watches values.
+```bash
+fs.inotify.max_user_watches=524288
+```
+Save the file and exit editor.
+To confirm that the change succeeded, run the following command to load the updated value of max_user_watches from sysctl.conf:
+```bash
+sudo sysctl -p
+```
#### Webpack dev server
The dev server by default starts at `http://localhost:9000` and proxies the backend requests to `http://localhost:8088`.
From 97d89d734029ff4595f8c4975dfaf24114f649dd Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Thu, 16 Nov 2023 14:28:09 -0300
Subject: [PATCH 028/119] feat: Adds Area chart migration logic (#25952)
---
pytest.ini | 2 +-
.../charts/commands/importers/v1/utils.py | 1 -
.../migrations/shared/migrate_viz/base.py | 8 +-
.../shared/migrate_viz/processors.py | 83 ++++++++--------
...06e1e70058c7_migrate_legacy_area__tests.py | 99 -------------------
.../commands/importers/v1/utils_test.py | 14 ++-
.../viz/dual_line_to_mixed_chart_test.py | 39 +-------
.../viz/nvd3_area_chart_to_echarts_test.py | 42 ++++++++
.../viz/nvd3_line_chart_to_echarts_test.py | 39 ++++++++
.../migrations/viz/pivot_table_v1_v2_test.py | 94 ++----------------
.../viz/time_related_fields_test.py | 89 +++++++++++++++++
tests/unit_tests/migrations/viz/utils.py | 96 ++++++++++++++++++
12 files changed, 330 insertions(+), 276 deletions(-)
delete mode 100644 tests/integration_tests/migrations/06e1e70058c7_migrate_legacy_area__tests.py
create mode 100644 tests/unit_tests/migrations/viz/nvd3_area_chart_to_echarts_test.py
create mode 100644 tests/unit_tests/migrations/viz/nvd3_line_chart_to_echarts_test.py
create mode 100644 tests/unit_tests/migrations/viz/time_related_fields_test.py
create mode 100644 tests/unit_tests/migrations/viz/utils.py
diff --git a/pytest.ini b/pytest.ini
index fdb50114d8..3fec965e72 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -17,4 +17,4 @@
[pytest]
testpaths =
tests
-python_files = *_test.py test_*.py *_tests.py
+python_files = *_test.py test_*.py *_tests.py *viz/utils.py
diff --git a/superset/charts/commands/importers/v1/utils.py b/superset/charts/commands/importers/v1/utils.py
index 3ef0a2ed78..d27b631f97 100644
--- a/superset/charts/commands/importers/v1/utils.py
+++ b/superset/charts/commands/importers/v1/utils.py
@@ -75,7 +75,6 @@ def migrate_chart(config: dict[str, Any]) -> dict[str, Any]:
if isclass(class_)
and issubclass(class_, MigrateViz)
and hasattr(class_, "source_viz_type")
- and class_ != processors.MigrateAreaChart # incomplete
}
output = copy.deepcopy(config)
diff --git a/superset/migrations/shared/migrate_viz/base.py b/superset/migrations/shared/migrate_viz/base.py
index b9826fee34..a3360d365b 100644
--- a/superset/migrations/shared/migrate_viz/base.py
+++ b/superset/migrations/shared/migrate_viz/base.py
@@ -160,9 +160,7 @@ class MigrateViz:
slices = session.query(Slice).filter(Slice.viz_type == cls.source_viz_type)
for slc in paginated_update(
slices,
- lambda current, total: print(
- f" Updating {current}/{total} charts", end="\r"
- ),
+ lambda current, total: print(f"Upgraded {current}/{total} charts"),
):
new_viz = cls.upgrade_slice(slc)
session.merge(new_viz)
@@ -177,9 +175,7 @@ class MigrateViz:
)
for slc in paginated_update(
slices,
- lambda current, total: print(
- f" Downgrading {current}/{total} charts", end="\r"
- ),
+ lambda current, total: print(f"Downgraded {current}/{total} charts"),
):
new_viz = cls.downgrade_slice(slc)
session.merge(new_viz)
diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py
index 8627e201f6..2d2bebc618 100644
--- a/superset/migrations/shared/migrate_viz/processors.py
+++ b/superset/migrations/shared/migrate_viz/processors.py
@@ -36,40 +36,6 @@ class MigrateTreeMap(MigrateViz):
self.data["metric"] = self.data["metrics"][0]
-class MigrateAreaChart(MigrateViz):
- """
- Migrate area charts.
-
- This migration is incomplete, see https://github.com/apache/superset/pull/24703#discussion_r1265222611
- for more details. If you fix this migration, please update the ``migrate_chart``
- function in ``superset/charts/commands/importers/v1/utils.py`` so that it gets
- applied in chart imports.
- """
-
- source_viz_type = "area"
- target_viz_type = "echarts_area"
- remove_keys = {"contribution", "stacked_style", "x_axis_label"}
-
- def _pre_action(self) -> None:
- if self.data.get("contribution"):
- self.data["contributionMode"] = "row"
-
- if stacked := self.data.get("stacked_style"):
- stacked_map = {
- "expand": "Expand",
- "stack": "Stack",
- }
- self.data["show_extra_controls"] = True
- self.data["stack"] = stacked_map.get(stacked)
-
- if x_axis := self.data.get("granularity_sqla"):
- self.data["x_axis"] = x_axis
-
- if x_axis_label := self.data.get("x_axis_label"):
- self.data["x_axis_title"] = x_axis_label
- self.data["x_axis_title_margin"] = 30
-
-
class MigratePivotTable(MigrateViz):
source_viz_type = "pivot_table"
target_viz_type = "pivot_table_v2"
@@ -137,10 +103,22 @@ class MigrateSunburst(MigrateViz):
class TimeseriesChart(MigrateViz):
has_x_axis_control = True
+ rename_keys = {
+ "bottom_margin": "x_axis_title_margin",
+ "left_margin": "y_axis_title_margin",
+ "show_controls": "show_extra_controls",
+ "x_axis_label": "x_axis_title",
+ "x_axis_format": "x_axis_time_format",
+ "x_ticks_layout": "xAxisLabelRotation",
+ "y_axis_label": "y_axis_title",
+ "y_axis_showminmax": "truncateYAxis",
+ "y_log_scale": "logAxis",
+ }
+ remove_keys = {"contribution", "show_brush", "show_markers"}
def _pre_action(self) -> None:
self.data["contributionMode"] = "row" if self.data.get("contribution") else None
- self.data["zoomable"] = self.data.get("show_brush") != "no"
+ self.data["zoomable"] = self.data.get("show_brush") == "yes"
self.data["markerEnabled"] = self.data.get("show_markers") or False
self.data["y_axis_showminmax"] = True
@@ -163,23 +141,19 @@ class TimeseriesChart(MigrateViz):
"difference" if comparison_type == "absolute" else comparison_type
)
+ if x_ticks_layout := self.data.get("x_ticks_layout"):
+ self.data["x_ticks_layout"] = 45 if x_ticks_layout == "45°" else 0
+
class MigrateLineChart(TimeseriesChart):
source_viz_type = "line"
target_viz_type = "echarts_timeseries_line"
- rename_keys = {
- "x_axis_label": "x_axis_title",
- "bottom_margin": "x_axis_title_margin",
- "x_axis_format": "x_axis_time_format",
- "y_axis_label": "y_axis_title",
- "left_margin": "y_axis_title_margin",
- "y_axis_showminmax": "truncateYAxis",
- "y_log_scale": "logAxis",
- }
def _pre_action(self) -> None:
super()._pre_action()
+ self.remove_keys.add("line_interpolation")
+
line_interpolation = self.data.get("line_interpolation")
if line_interpolation == "cardinal":
self.target_viz_type = "echarts_timeseries_smooth"
@@ -189,3 +163,24 @@ class MigrateLineChart(TimeseriesChart):
elif line_interpolation == "step-after":
self.target_viz_type = "echarts_timeseries_step"
self.data["seriesType"] = "end"
+
+
+class MigrateAreaChart(TimeseriesChart):
+ source_viz_type = "area"
+ target_viz_type = "echarts_area"
+ stacked_map = {
+ "expand": "Expand",
+ "stack": "Stack",
+ "stream": "Stream",
+ }
+
+ def _pre_action(self) -> None:
+ super()._pre_action()
+
+ self.remove_keys.add("stacked_style")
+
+ self.data["stack"] = self.stacked_map.get(
+ self.data.get("stacked_style") or "stack"
+ )
+
+ self.data["opacity"] = 0.7
diff --git a/tests/integration_tests/migrations/06e1e70058c7_migrate_legacy_area__tests.py b/tests/integration_tests/migrations/06e1e70058c7_migrate_legacy_area__tests.py
deleted file mode 100644
index f02d069b2b..0000000000
--- a/tests/integration_tests/migrations/06e1e70058c7_migrate_legacy_area__tests.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# 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 json
-
-from superset.app import SupersetApp
-from superset.migrations.shared.migrate_viz import MigrateAreaChart
-
-area_form_data = """{
- "adhoc_filters": [],
- "annotation_layers": [],
- "bottom_margin": "auto",
- "color_scheme": "lyftColors",
- "comparison_type": "values",
- "contribution": true,
- "datasource": "2__table",
- "extra_form_data": {},
- "granularity_sqla": "ds",
- "groupby": [
- "gender"
- ],
- "line_interpolation": "linear",
- "metrics": [
- "sum__num"
- ],
- "order_desc": true,
- "rich_tooltip": true,
- "rolling_type": "None",
- "row_limit": 10000,
- "show_brush": "auto",
- "show_controls": true,
- "show_legend": true,
- "slice_id": 165,
- "stacked_style": "stack",
- "time_grain_sqla": "P1D",
- "time_range": "No filter",
- "viz_type": "area",
- "x_axis_format": "smart_date",
- "x_axis_label": "x asix label",
- "x_axis_showminmax": false,
- "x_ticks_layout": "auto",
- "y_axis_bounds": [
- null,
- null
- ],
- "y_axis_format": "SMART_NUMBER"
-}
-"""
-
-
-def test_area_migrate(app_context: SupersetApp) -> None:
- from superset.models.slice import Slice
-
- slc = Slice(
- viz_type=MigrateAreaChart.source_viz_type,
- datasource_type="table",
- params=area_form_data,
- query_context=f'{{"form_data": {area_form_data}}}',
- )
-
- slc = MigrateAreaChart.upgrade_slice(slc)
- assert slc.viz_type == MigrateAreaChart.target_viz_type
- # verify form_data
- new_form_data = json.loads(slc.params)
- assert new_form_data["contributionMode"] == "row"
- assert "contribution" not in new_form_data
- assert new_form_data["show_extra_controls"] is True
- assert new_form_data["stack"] == "Stack"
- assert new_form_data["x_axis_title"] == "x asix label"
- assert new_form_data["x_axis_title_margin"] == 30
- assert json.dumps(new_form_data["form_data_bak"], sort_keys=True) == json.dumps(
- json.loads(area_form_data), sort_keys=True
- )
-
- # verify query_context
- new_query_context = json.loads(slc.query_context)
- assert (
- new_query_context["form_data"]["viz_type"] == MigrateAreaChart.target_viz_type
- )
-
- # downgrade
- slc = MigrateAreaChart.downgrade_slice(slc)
- assert slc.viz_type == MigrateAreaChart.source_viz_type
- assert json.dumps(json.loads(slc.params), sort_keys=True) == json.dumps(
- json.loads(area_form_data), sort_keys=True
- )
diff --git a/tests/unit_tests/charts/commands/importers/v1/utils_test.py b/tests/unit_tests/charts/commands/importers/v1/utils_test.py
index 77d31e7d77..2addfa3016 100644
--- a/tests/unit_tests/charts/commands/importers/v1/utils_test.py
+++ b/tests/unit_tests/charts/commands/importers/v1/utils_test.py
@@ -31,13 +31,21 @@ def test_migrate_chart_area() -> None:
"description": None,
"certified_by": None,
"certification_details": None,
- "viz_type": "area",
+ "viz_type": "echarts_area",
"query_context": None,
"params": json.dumps(
{
- "adhoc_filters": [],
+ "adhoc_filters": [
+ {
+ "clause": "WHERE",
+ "subject": "ds",
+ "operator": "TEMPORAL_RANGE",
+ "comparator": "No filter",
+ "expressionType": "SIMPLE",
+ }
+ ],
"annotation_layers": [],
- "bottom_margin": "auto",
+ "x_axis_title_margin": "auto",
"color_scheme": "supersetColors",
"comparison_type": "values",
"dashboards": [],
diff --git a/tests/unit_tests/migrations/viz/dual_line_to_mixed_chart_test.py b/tests/unit_tests/migrations/viz/dual_line_to_mixed_chart_test.py
index 76addd8009..3d9dc53122 100644
--- a/tests/unit_tests/migrations/viz/dual_line_to_mixed_chart_test.py
+++ b/tests/unit_tests/migrations/viz/dual_line_to_mixed_chart_test.py
@@ -14,9 +14,10 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-import json
+from typing import Any
from superset.migrations.shared.migrate_viz import MigrateDualLine
+from tests.unit_tests.migrations.viz.utils import migrate_and_assert
ADHOC_FILTERS = [
{
@@ -28,7 +29,7 @@ ADHOC_FILTERS = [
}
]
-SOURCE_FORM_DATA = {
+SOURCE_FORM_DATA: dict[str, Any] = {
"metric": "num_boys",
"y_axis_format": ",d",
"y_axis_bounds": [50, 100],
@@ -42,7 +43,7 @@ SOURCE_FORM_DATA = {
"yAxisIndex": 0,
}
-TARGET_FORM_DATA = {
+TARGET_FORM_DATA: dict[str, Any] = {
"metrics": ["num_boys"],
"y_axis_format": ",d",
"y_axis_bounds": [50, 100],
@@ -64,34 +65,4 @@ TARGET_FORM_DATA = {
def test_migration() -> None:
source = SOURCE_FORM_DATA.copy()
target = TARGET_FORM_DATA.copy()
- upgrade_downgrade(source, target)
-
-
-def upgrade_downgrade(source, target) -> None:
- from superset.models.slice import Slice
-
- dumped_form_data = json.dumps(source)
-
- slc = Slice(
- viz_type=MigrateDualLine.source_viz_type,
- datasource_type="table",
- params=dumped_form_data,
- query_context=f'{{"form_data": {dumped_form_data}}}',
- )
-
- # upgrade
- slc = MigrateDualLine.upgrade_slice(slc)
-
- # verify form_data
- new_form_data = json.loads(slc.params)
- assert new_form_data == target
- assert new_form_data["form_data_bak"] == source
-
- # verify query_context
- new_query_context = json.loads(slc.query_context)
- assert new_query_context["form_data"]["viz_type"] == "mixed_timeseries"
-
- # downgrade
- slc = MigrateDualLine.downgrade_slice(slc)
- assert slc.viz_type == MigrateDualLine.source_viz_type
- assert json.loads(slc.params) == source
+ migrate_and_assert(MigrateDualLine, source, target)
diff --git a/tests/unit_tests/migrations/viz/nvd3_area_chart_to_echarts_test.py b/tests/unit_tests/migrations/viz/nvd3_area_chart_to_echarts_test.py
new file mode 100644
index 0000000000..a6b87c6d7a
--- /dev/null
+++ b/tests/unit_tests/migrations/viz/nvd3_area_chart_to_echarts_test.py
@@ -0,0 +1,42 @@
+# 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 typing import Any
+
+from superset.migrations.shared.migrate_viz import MigrateAreaChart
+from tests.unit_tests.migrations.viz.utils import (
+ migrate_and_assert,
+ TIMESERIES_SOURCE_FORM_DATA,
+ TIMESERIES_TARGET_FORM_DATA,
+)
+
+SOURCE_FORM_DATA: dict[str, Any] = {
+ "viz_type": "area",
+ "stacked_style": "stream",
+}
+
+TARGET_FORM_DATA: dict[str, Any] = {
+ "form_data_bak": SOURCE_FORM_DATA,
+ "viz_type": "echarts_area",
+ "opacity": 0.7,
+ "stack": "Stream",
+}
+
+
+def test_migration() -> None:
+ SOURCE_FORM_DATA.update(TIMESERIES_SOURCE_FORM_DATA)
+ TARGET_FORM_DATA.update(TIMESERIES_TARGET_FORM_DATA)
+ migrate_and_assert(MigrateAreaChart, SOURCE_FORM_DATA, TARGET_FORM_DATA)
diff --git a/tests/unit_tests/migrations/viz/nvd3_line_chart_to_echarts_test.py b/tests/unit_tests/migrations/viz/nvd3_line_chart_to_echarts_test.py
new file mode 100644
index 0000000000..5999a90702
--- /dev/null
+++ b/tests/unit_tests/migrations/viz/nvd3_line_chart_to_echarts_test.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 typing import Any
+
+from superset.migrations.shared.migrate_viz import MigrateLineChart
+from tests.unit_tests.migrations.viz.utils import (
+ migrate_and_assert,
+ TIMESERIES_SOURCE_FORM_DATA,
+ TIMESERIES_TARGET_FORM_DATA,
+)
+
+SOURCE_FORM_DATA: dict[str, Any] = {
+ "viz_type": "line",
+}
+
+TARGET_FORM_DATA: dict[str, Any] = {
+ "form_data_bak": SOURCE_FORM_DATA,
+ "viz_type": "echarts_timeseries_line",
+}
+
+
+def test_migration() -> None:
+ SOURCE_FORM_DATA.update(TIMESERIES_SOURCE_FORM_DATA)
+ TARGET_FORM_DATA.update(TIMESERIES_TARGET_FORM_DATA)
+ migrate_and_assert(MigrateLineChart, SOURCE_FORM_DATA, TARGET_FORM_DATA)
diff --git a/tests/unit_tests/migrations/viz/pivot_table_v1_v2_test.py b/tests/unit_tests/migrations/viz/pivot_table_v1_v2_test.py
index 1e2229ca83..788fd14770 100644
--- a/tests/unit_tests/migrations/viz/pivot_table_v1_v2_test.py
+++ b/tests/unit_tests/migrations/viz/pivot_table_v1_v2_test.py
@@ -14,122 +14,40 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-import json
+from typing import Any
from superset.migrations.shared.migrate_viz import MigratePivotTable
-from tests.unit_tests.conftest import with_feature_flags
+from tests.unit_tests.migrations.viz.utils import migrate_and_assert
-SOURCE_FORM_DATA = {
- "adhoc_filters": [],
+SOURCE_FORM_DATA: dict[str, Any] = {
"any_other_key": "untouched",
"columns": ["state"],
"combine_metric": True,
- "granularity_sqla": "ds",
"groupby": ["name"],
"number_format": "SMART_NUMBER",
"pandas_aggfunc": "sum",
"pivot_margins": True,
- "time_range": "100 years ago : now",
"timeseries_limit_metric": "count",
"transpose_pivot": True,
"viz_type": "pivot_table",
}
-TARGET_FORM_DATA = {
- "adhoc_filters": [],
+TARGET_FORM_DATA: dict[str, Any] = {
"any_other_key": "untouched",
"aggregateFunction": "Sum",
"colTotals": True,
"colSubTotals": True,
"combineMetric": True,
"form_data_bak": SOURCE_FORM_DATA,
- "granularity_sqla": "ds",
"groupbyColumns": ["state"],
"groupbyRows": ["name"],
"rowOrder": "value_z_to_a",
"series_limit_metric": "count",
- "time_range": "100 years ago : now",
"transposePivot": True,
"valueFormat": "SMART_NUMBER",
"viz_type": "pivot_table_v2",
}
-@with_feature_flags(GENERIC_CHART_AXES=False)
-def test_migration_without_generic_chart_axes() -> None:
- source = SOURCE_FORM_DATA.copy()
- target = TARGET_FORM_DATA.copy()
- upgrade_downgrade(source, target)
-
-
-@with_feature_flags(GENERIC_CHART_AXES=True)
-def test_migration_with_generic_chart_axes() -> None:
- source = SOURCE_FORM_DATA.copy()
- target = TARGET_FORM_DATA.copy()
- target["adhoc_filters"] = [
- {
- "clause": "WHERE",
- "comparator": "100 years ago : now",
- "expressionType": "SIMPLE",
- "operator": "TEMPORAL_RANGE",
- "subject": "ds",
- }
- ]
- target.pop("granularity_sqla")
- target.pop("time_range")
- upgrade_downgrade(source, target)
-
-
-@with_feature_flags(GENERIC_CHART_AXES=True)
-def test_custom_sql_time_column() -> None:
- source = SOURCE_FORM_DATA.copy()
- source["granularity_sqla"] = {
- "expressionType": "SQL",
- "label": "ds",
- "sqlExpression": "sum(ds)",
- }
- target = TARGET_FORM_DATA.copy()
- target["adhoc_filters"] = [
- {
- "clause": "WHERE",
- "comparator": None,
- "expressionType": "SQL",
- "operator": "TEMPORAL_RANGE",
- "sqlExpression": "sum(ds)",
- "subject": "ds",
- }
- ]
- target["form_data_bak"] = source
- target.pop("granularity_sqla")
- target.pop("time_range")
- upgrade_downgrade(source, target)
-
-
-def upgrade_downgrade(source, target) -> None:
- from superset.models.slice import Slice
-
- dumped_form_data = json.dumps(source)
-
- slc = Slice(
- viz_type=MigratePivotTable.source_viz_type,
- datasource_type="table",
- params=dumped_form_data,
- query_context=f'{{"form_data": {dumped_form_data}}}',
- )
-
- # upgrade
- slc = MigratePivotTable.upgrade_slice(slc)
-
- # verify form_data
- new_form_data = json.loads(slc.params)
- assert new_form_data == target
- assert new_form_data["form_data_bak"] == source
-
- # verify query_context
- new_query_context = json.loads(slc.query_context)
- assert new_query_context["form_data"]["viz_type"] == "pivot_table_v2"
-
- # downgrade
- slc = MigratePivotTable.downgrade_slice(slc)
- assert slc.viz_type == MigratePivotTable.source_viz_type
- assert json.loads(slc.params) == source
+def test_migration() -> None:
+ migrate_and_assert(MigratePivotTable, SOURCE_FORM_DATA, TARGET_FORM_DATA)
diff --git a/tests/unit_tests/migrations/viz/time_related_fields_test.py b/tests/unit_tests/migrations/viz/time_related_fields_test.py
new file mode 100644
index 0000000000..06fdf611ce
--- /dev/null
+++ b/tests/unit_tests/migrations/viz/time_related_fields_test.py
@@ -0,0 +1,89 @@
+# 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 typing import Any
+
+from superset.migrations.shared.migrate_viz import MigratePivotTable
+from tests.unit_tests.conftest import with_feature_flags
+from tests.unit_tests.migrations.viz.utils import migrate_and_assert
+
+SOURCE_FORM_DATA: dict[str, Any] = {
+ "granularity_sqla": "ds",
+ "time_range": "100 years ago : now",
+ "viz_type": "pivot_table",
+}
+
+TARGET_FORM_DATA: dict[str, Any] = {
+ "form_data_bak": SOURCE_FORM_DATA,
+ "granularity_sqla": "ds",
+ "rowOrder": "value_z_to_a",
+ "time_range": "100 years ago : now",
+ "viz_type": "pivot_table_v2",
+}
+
+
+@with_feature_flags(GENERIC_CHART_AXES=False)
+def test_migration_without_generic_chart_axes() -> None:
+ source = SOURCE_FORM_DATA.copy()
+ target = TARGET_FORM_DATA.copy()
+ upgrade_downgrade(source, target)
+
+
+@with_feature_flags(GENERIC_CHART_AXES=True)
+def test_migration_with_generic_chart_axes() -> None:
+ source = SOURCE_FORM_DATA.copy()
+ target = TARGET_FORM_DATA.copy()
+ target["adhoc_filters"] = [
+ {
+ "clause": "WHERE",
+ "comparator": "100 years ago : now",
+ "expressionType": "SIMPLE",
+ "operator": "TEMPORAL_RANGE",
+ "subject": "ds",
+ }
+ ]
+ target.pop("granularity_sqla")
+ target.pop("time_range")
+ upgrade_downgrade(source, target)
+
+
+@with_feature_flags(GENERIC_CHART_AXES=True)
+def test_custom_sql_time_column() -> None:
+ source = SOURCE_FORM_DATA.copy()
+ source["granularity_sqla"] = {
+ "expressionType": "SQL",
+ "label": "ds",
+ "sqlExpression": "sum(ds)",
+ }
+ target = TARGET_FORM_DATA.copy()
+ target["adhoc_filters"] = [
+ {
+ "clause": "WHERE",
+ "comparator": None,
+ "expressionType": "SQL",
+ "operator": "TEMPORAL_RANGE",
+ "sqlExpression": "sum(ds)",
+ "subject": "ds",
+ }
+ ]
+ target["form_data_bak"] = source
+ target.pop("granularity_sqla")
+ target.pop("time_range")
+ upgrade_downgrade(source, target)
+
+
+def upgrade_downgrade(source, target) -> None:
+ migrate_and_assert(MigratePivotTable, source, target)
diff --git a/tests/unit_tests/migrations/viz/utils.py b/tests/unit_tests/migrations/viz/utils.py
new file mode 100644
index 0000000000..92d2eccd70
--- /dev/null
+++ b/tests/unit_tests/migrations/viz/utils.py
@@ -0,0 +1,96 @@
+# 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 json
+from typing import Any
+
+from superset.migrations.shared.migrate_viz import MigrateViz
+
+TIMESERIES_SOURCE_FORM_DATA: dict[str, Any] = {
+ "bottom_margin": 20,
+ "comparison_type": "absolute",
+ "contribution": True,
+ "left_margin": 20,
+ "rich_tooltip": True,
+ "rolling_type": "sum",
+ "show_brush": "yes",
+ "show_controls": True,
+ "show_legend": True,
+ "show_markers": True,
+ "time_compare": "1 year",
+ "x_axis_label": "x",
+ "x_axis_format": "SMART_DATE",
+ "x_ticks_layout": "45°",
+ "y_axis_bounds": [0, 100],
+ "y_axis_format": "SMART_NUMBER",
+ "y_axis_label": "y",
+ "y_axis_showminmax": True,
+ "y_log_scale": True,
+}
+
+TIMESERIES_TARGET_FORM_DATA: dict[str, Any] = {
+ "comparison_type": "difference",
+ "contributionMode": "row",
+ "logAxis": True,
+ "markerEnabled": True,
+ "rich_tooltip": True,
+ "rolling_type": "sum",
+ "show_extra_controls": True,
+ "show_legend": True,
+ "time_compare": ["1 year ago"],
+ "truncateYAxis": True,
+ "x_axis_title_margin": 20,
+ "y_axis_title_margin": 20,
+ "x_axis_title": "x",
+ "x_axis_time_format": "SMART_DATE",
+ "xAxisLabelRotation": 45,
+ "y_axis_bounds": [0, 100],
+ "y_axis_format": "SMART_NUMBER",
+ "y_axis_title": "y",
+ "zoomable": True,
+}
+
+
+def migrate_and_assert(
+ cls: type[MigrateViz], source: dict[str, Any], target: dict[str, Any]
+) -> None:
+ from superset.models.slice import Slice
+
+ dumped_form_data = json.dumps(source)
+
+ slc = Slice(
+ viz_type=cls.source_viz_type,
+ datasource_type="table",
+ params=dumped_form_data,
+ query_context=f'{{"form_data": {dumped_form_data}}}',
+ )
+
+ # upgrade
+ slc = cls.upgrade_slice(slc)
+
+ # verify form_data
+ new_form_data = json.loads(slc.params)
+ assert new_form_data == target
+ assert new_form_data["form_data_bak"] == source
+
+ # verify query_context
+ new_query_context = json.loads(slc.query_context)
+ assert new_query_context["form_data"]["viz_type"] == cls.target_viz_type
+
+ # downgrade
+ slc = cls.downgrade_slice(slc)
+ assert slc.viz_type == cls.source_viz_type
+ assert json.loads(slc.params) == source
From 210f1f8f95531365da2c5a5897e801c4cb7edacd Mon Sep 17 00:00:00 2001
From: yousoph
Date: Thu, 16 Nov 2023 09:48:54 -0800
Subject: [PATCH 029/119] fix(rls): Update text from tables to datasets in RLS
modal (#25997)
---
superset-frontend/src/features/rls/RowLevelSecurityModal.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx b/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
index d7e7af7126..d14d48d0e5 100644
--- a/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
+++ b/superset-frontend/src/features/rls/RowLevelSecurityModal.tsx
@@ -385,10 +385,10 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
- {t('Tables')} *
+ {t('Datasets')} *
From 5fccf67cdc4a84edb067a3cde48efacc76dbe33a Mon Sep 17 00:00:00 2001
From: Jack Fragassi
Date: Thu, 16 Nov 2023 12:06:05 -0800
Subject: [PATCH 030/119] fix: Make Select component fire onChange listener
when a selection is pasted in (#25993)
---
.../src/components/Select/AsyncSelect.test.tsx | 14 ++++++++++++++
.../src/components/Select/AsyncSelect.tsx | 1 +
.../src/components/Select/Select.test.tsx | 14 ++++++++++++++
superset-frontend/src/components/Select/Select.tsx | 1 +
4 files changed, 30 insertions(+)
diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
index c1442a6b70..0bb24b474a 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -868,6 +868,20 @@ test('fires onChange when clearing the selection in multiple mode', async () =>
expect(onChange).toHaveBeenCalledTimes(1);
});
+test('fires onChange when pasting a selection', async () => {
+ const onChange = jest.fn();
+ render();
+ await open();
+ const input = getElementByClassName('.ant-select-selection-search-input');
+ const paste = createEvent.paste(input, {
+ clipboardData: {
+ getData: () => OPTIONS[0].label,
+ },
+ });
+ fireEvent(input, paste);
+ expect(onChange).toHaveBeenCalledTimes(1);
+});
+
test('does not duplicate options when using numeric values', async () => {
render(
expect(onChange).toHaveBeenCalledTimes(1);
});
+test('fires onChange when pasting a selection', async () => {
+ const onChange = jest.fn();
+ render();
+ await open();
+ const input = getElementByClassName('.ant-select-selection-search-input');
+ const paste = createEvent.paste(input, {
+ clipboardData: {
+ getData: () => OPTIONS[0].label,
+ },
+ });
+ fireEvent(input, paste);
+ expect(onChange).toHaveBeenCalledTimes(1);
+});
+
test('does not duplicate options when using numeric values', async () => {
render(
-
+
+
+
+
+
+
+
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
index bcd9fbe694..ba443e0099 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx
@@ -674,7 +674,7 @@ describe('DatabaseModal', () => {
const exposeInSQLLabCheckbox = screen.getByRole('checkbox', {
name: /expose database in sql lab/i,
});
- // This is both the checkbox and it's respective SVG
+ // This is both the checkbox and its respective SVG
// const exposeInSQLLabCheckboxSVG = checkboxOffSVGs[0].parentElement;
const exposeInSQLLabText = screen.getByText(
/expose database in sql lab/i,
@@ -721,6 +721,13 @@ describe('DatabaseModal', () => {
/Disable SQL Lab data preview queries/i,
);
+ const enableRowExpansionCheckbox = screen.getByRole('checkbox', {
+ name: /enable row expansion in schemas/i,
+ });
+ const enableRowExpansionText = screen.getByText(
+ /enable row expansion in schemas/i,
+ );
+
// ---------- Assertions ----------
const visibleComponents = [
closeButton,
@@ -737,6 +744,7 @@ describe('DatabaseModal', () => {
checkboxOffSVGs[2],
checkboxOffSVGs[3],
checkboxOffSVGs[4],
+ checkboxOffSVGs[5],
tooltipIcons[0],
tooltipIcons[1],
tooltipIcons[2],
@@ -744,6 +752,7 @@ describe('DatabaseModal', () => {
tooltipIcons[4],
tooltipIcons[5],
tooltipIcons[6],
+ tooltipIcons[7],
exposeInSQLLabText,
allowCTASText,
allowCVASText,
@@ -754,6 +763,7 @@ describe('DatabaseModal', () => {
enableQueryCostEstimationText,
allowDbExplorationText,
disableSQLLabDataPreviewQueriesText,
+ enableRowExpansionText,
];
// These components exist in the DOM but are not visible
const invisibleComponents = [
@@ -764,6 +774,7 @@ describe('DatabaseModal', () => {
enableQueryCostEstimationCheckbox,
allowDbExplorationCheckbox,
disableSQLLabDataPreviewQueriesCheckbox,
+ enableRowExpansionCheckbox,
];
visibleComponents.forEach(component => {
expect(component).toBeVisible();
@@ -771,8 +782,8 @@ describe('DatabaseModal', () => {
invisibleComponents.forEach(component => {
expect(component).not.toBeVisible();
});
- expect(checkboxOffSVGs).toHaveLength(5);
- expect(tooltipIcons).toHaveLength(7);
+ expect(checkboxOffSVGs).toHaveLength(6);
+ expect(tooltipIcons).toHaveLength(8);
});
test('renders the "Advanced" - PERFORMANCE tab correctly', async () => {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index 0c1ac56369..18c93f2bf4 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -307,6 +307,18 @@ export function dbReducer(
}),
};
}
+ if (action.payload.name === 'expand_rows') {
+ return {
+ ...trimmedState,
+ extra: JSON.stringify({
+ ...extraJson,
+ schema_options: {
+ ...extraJson?.schema_options,
+ [action.payload.name]: !!action.payload.value,
+ },
+ }),
+ };
+ }
return {
...trimmedState,
extra: JSON.stringify({
diff --git a/superset-frontend/src/features/databases/types.ts b/superset-frontend/src/features/databases/types.ts
index e138a91436..1d616fa13c 100644
--- a/superset-frontend/src/features/databases/types.ts
+++ b/superset-frontend/src/features/databases/types.ts
@@ -226,5 +226,8 @@ export interface ExtraJson {
table_cache_timeout?: number; // in Performance
}; // No field, holds schema and table timeout
schemas_allowed_for_file_upload?: string[]; // in Security
+ schema_options?: {
+ expand_rows?: boolean;
+ };
version?: string;
}
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 6bce03d931..9894232ab1 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -51,7 +51,7 @@ from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session
-from sqlalchemy.sql import quoted_name, text
+from sqlalchemy.sql import literal_column, quoted_name, text
from sqlalchemy.sql.expression import ColumnClause, Select, TextAsFrom, TextClause
from sqlalchemy.types import TypeEngine
from sqlparse.tokens import CTE
@@ -1322,8 +1322,12 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
return comment
@classmethod
- def get_columns(
- cls, inspector: Inspector, table_name: str, schema: str | None
+ def get_columns( # pylint: disable=unused-argument
+ cls,
+ inspector: Inspector,
+ table_name: str,
+ schema: str | None,
+ options: dict[str, Any] | None = None,
) -> list[ResultSetColumnType]:
"""
Get all columns from a given schema and table
@@ -1331,6 +1335,8 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
:param inspector: SqlAlchemy Inspector instance
:param table_name: Table name
:param schema: Schema name. If omitted, uses default schema for database
+ :param options: Extra options to customise the display of columns in
+ some databases
:return: All columns in table
"""
return convert_inspector_columns(
@@ -1382,7 +1388,12 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
@classmethod
def _get_fields(cls, cols: list[ResultSetColumnType]) -> list[Any]:
- return [column(c["column_name"]) for c in cols]
+ return [
+ literal_column(query_as)
+ if (query_as := c.get("query_as"))
+ else column(c["column_name"])
+ for c in cols
+ ]
@classmethod
def select_star( # pylint: disable=too-many-arguments,too-many-locals
diff --git a/superset/db_engine_specs/druid.py b/superset/db_engine_specs/druid.py
index 9bba3a7274..7cd85ec924 100644
--- a/superset/db_engine_specs/druid.py
+++ b/superset/db_engine_specs/druid.py
@@ -23,14 +23,12 @@ from datetime import datetime
from typing import Any, TYPE_CHECKING
from sqlalchemy import types
-from sqlalchemy.engine.reflection import Inspector
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.superset_typing import ResultSetColumnType
from superset.utils import core as utils
if TYPE_CHECKING:
@@ -130,15 +128,6 @@ class DruidEngineSpec(BaseEngineSpec):
"""
return "MILLIS_TO_TIMESTAMP({col})"
- @classmethod
- def get_columns(
- cls, inspector: Inspector, table_name: str, schema: str | None
- ) -> list[ResultSetColumnType]:
- """
- Update the Druid type map.
- """
- return super().get_columns(inspector, table_name, schema)
-
@classmethod
def get_dbapi_exception_mapping(cls) -> dict[type[Exception], type[Exception]]:
# pylint: disable=import-outside-toplevel
diff --git a/superset/db_engine_specs/hive.py b/superset/db_engine_specs/hive.py
index 4a881e15b2..bd303f928d 100644
--- a/superset/db_engine_specs/hive.py
+++ b/superset/db_engine_specs/hive.py
@@ -410,9 +410,13 @@ class HiveEngineSpec(PrestoEngineSpec):
@classmethod
def get_columns(
- cls, inspector: Inspector, table_name: str, schema: str | None
+ cls,
+ inspector: Inspector,
+ table_name: str,
+ schema: str | None,
+ options: dict[str, Any] | None = None,
) -> list[ResultSetColumnType]:
- return BaseEngineSpec.get_columns(inspector, table_name, schema)
+ return BaseEngineSpec.get_columns(inspector, table_name, schema, options)
@classmethod
def where_latest_partition( # pylint: disable=too-many-arguments
diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py
index 8afa82d9b5..27e86a7980 100644
--- a/superset/db_engine_specs/presto.py
+++ b/superset/db_engine_specs/presto.py
@@ -981,7 +981,11 @@ class PrestoEngineSpec(PrestoBaseEngineSpec):
@classmethod
def get_columns(
- cls, inspector: Inspector, table_name: str, schema: str | None
+ cls,
+ inspector: Inspector,
+ table_name: str,
+ schema: str | None,
+ options: dict[str, Any] | None = None,
) -> list[ResultSetColumnType]:
"""
Get columns from a Presto data source. This includes handling row and
@@ -989,6 +993,7 @@ class PrestoEngineSpec(PrestoBaseEngineSpec):
:param inspector: object that performs database schema inspection
:param table_name: table name
:param schema: schema name
+ :param options: Extra configuration options, not used by this backend
:return: a list of results that contain column info
(i.e. column name and data type)
"""
diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py
index 125a96ab82..d1c8e20bea 100644
--- a/superset/db_engine_specs/trino.py
+++ b/superset/db_engine_specs/trino.py
@@ -24,8 +24,10 @@ from typing import Any, TYPE_CHECKING
import simplejson as json
from flask import current_app
+from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
from sqlalchemy.orm import Session
+from trino.sqlalchemy import datatype
from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY, USER_AGENT
from superset.databases.utils import make_url_safe
@@ -33,6 +35,7 @@ from superset.db_engine_specs.base import BaseEngineSpec
from superset.db_engine_specs.exceptions import SupersetDBAPIConnectionError
from superset.db_engine_specs.presto import PrestoBaseEngineSpec
from superset.models.sql_lab import Query
+from superset.superset_typing import ResultSetColumnType
from superset.utils import core as utils
if TYPE_CHECKING:
@@ -331,3 +334,62 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
return {
requests_exceptions.ConnectionError: SupersetDBAPIConnectionError,
}
+
+ @classmethod
+ def _expand_columns(cls, col: ResultSetColumnType) -> list[ResultSetColumnType]:
+ """
+ Expand the given column out to one or more columns by analysing their types,
+ descending into ROWS and expanding out their inner fields recursively.
+
+ We can only navigate named fields in ROWs in this way, so we can't expand out
+ MAP or ARRAY types, nor fields in ROWs which have no name (in fact the trino
+ library doesn't correctly parse unnamed fields in ROWs). We won't be able to
+ expand ROWs which are nested underneath any of those types, either.
+
+ Expanded columns are named foo.bar.baz and we provide a query_as property to
+ instruct the base engine spec how to correctly query them: instead of quoting
+ the whole string they have to be quoted like "foo"."bar"."baz" and we then
+ alias them to the full dotted string for ease of reference.
+ """
+ cols = [col]
+ col_type = col.get("type")
+
+ if not isinstance(col_type, datatype.ROW):
+ return cols
+
+ for inner_name, inner_type in col_type.attr_types:
+ outer_name = col["name"]
+ name = ".".join([outer_name, inner_name])
+ query_name = ".".join([f'"{piece}"' for piece in name.split(".")])
+ column_spec = cls.get_column_spec(str(inner_type))
+ is_dttm = column_spec.is_dttm if column_spec else False
+
+ inner_col = ResultSetColumnType(
+ name=name,
+ column_name=name,
+ type=inner_type,
+ is_dttm=is_dttm,
+ query_as=f'{query_name} AS "{name}"',
+ )
+ cols.extend(cls._expand_columns(inner_col))
+
+ return cols
+
+ @classmethod
+ def get_columns(
+ cls,
+ inspector: Inspector,
+ table_name: str,
+ schema: str | None,
+ options: dict[str, Any] | None = None,
+ ) -> list[ResultSetColumnType]:
+ """
+ If the "expand_rows" feature is enabled on the database via
+ "schema_options", expand the schema definition out to show all
+ subfields of nested ROWs as their appropriate dotted paths.
+ """
+ base_cols = super().get_columns(inspector, table_name, schema, options)
+ if not (options or {}).get("expand_rows"):
+ return base_cols
+
+ return [col for base_col in base_cols for col in cls._expand_columns(base_col)]
diff --git a/superset/models/core.py b/superset/models/core.py
index 6fa394de06..d2b38ea806 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -237,6 +237,11 @@ class Database(
# this will prevent any 'trash value' strings from going through
return self.get_extra().get("disable_data_preview", False) is True
+ @property
+ def schema_options(self) -> dict[str, Any]:
+ """Additional schema display config for engines with complex schemas"""
+ return self.get_extra().get("schema_options", {})
+
@property
def data(self) -> dict[str, Any]:
return {
@@ -248,6 +253,7 @@ class Database(
"allows_cost_estimate": self.allows_cost_estimate,
"allows_virtual_table_explore": self.allows_virtual_table_explore,
"explore_database_id": self.explore_database_id,
+ "schema_options": self.schema_options,
"parameters": self.parameters,
"disable_data_preview": self.disable_data_preview,
"parameters_schema": self.parameters_schema,
@@ -838,7 +844,9 @@ class Database(
self, table_name: str, schema: str | None = None
) -> list[ResultSetColumnType]:
with self.get_inspector_with_context() as inspector:
- return self.db_engine_spec.get_columns(inspector, table_name, schema)
+ return self.db_engine_spec.get_columns(
+ inspector, table_name, schema, self.schema_options
+ )
def get_metrics(
self,
diff --git a/superset/superset_typing.py b/superset/superset_typing.py
index 953683b5dc..c71dcea3f1 100644
--- a/superset/superset_typing.py
+++ b/superset/superset_typing.py
@@ -84,6 +84,8 @@ class ResultSetColumnType(TypedDict):
scale: NotRequired[Any]
max_length: NotRequired[Any]
+ query_as: NotRequired[Any]
+
CacheConfig = dict[str, Any]
DbapiDescriptionRow = tuple[
diff --git a/tests/unit_tests/db_engine_specs/test_trino.py b/tests/unit_tests/db_engine_specs/test_trino.py
index 1b50a683a0..15e55fc5af 100644
--- a/tests/unit_tests/db_engine_specs/test_trino.py
+++ b/tests/unit_tests/db_engine_specs/test_trino.py
@@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=unused-argument, import-outside-toplevel, protected-access
+import copy
import json
from datetime import datetime
from typing import Any, Optional
@@ -24,9 +25,11 @@ import pandas as pd
import pytest
from pytest_mock import MockerFixture
from sqlalchemy import types
+from trino.sqlalchemy import datatype
import superset.config
from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY, USER_AGENT
+from superset.superset_typing import ResultSetColumnType, SQLAColumnType
from superset.utils.core import GenericDataType
from tests.unit_tests.db_engine_specs.utils import (
assert_column_spec,
@@ -35,6 +38,24 @@ from tests.unit_tests.db_engine_specs.utils import (
from tests.unit_tests.fixtures.common import dttm
+def _assert_columns_equal(actual_cols, expected_cols) -> None:
+ """
+ Assert equality of the given cols, bearing in mind sqlalchemy type
+ instances can't be compared for equality, so will have to be converted to
+ strings first.
+ """
+ actual = copy.deepcopy(actual_cols)
+ expected = copy.deepcopy(expected_cols)
+
+ for col in actual:
+ col["type"] = str(col["type"])
+
+ for col in expected:
+ col["type"] = str(col["type"])
+
+ assert actual == expected
+
+
@pytest.mark.parametrize(
"extra,expected",
[
@@ -395,3 +416,104 @@ def test_execute_with_cursor_in_parallel(mocker: MockerFixture):
mock_query.set_extra_json_key.assert_called_once_with(
key=QUERY_CANCEL_KEY, value=query_id
)
+
+
+def test_get_columns(mocker: MockerFixture):
+ """Test that ROW columns are not expanded without expand_rows"""
+ from superset.db_engine_specs.trino import TrinoEngineSpec
+
+ field1_type = datatype.parse_sqltype("row(a varchar, b date)")
+ field2_type = datatype.parse_sqltype("row(r1 row(a varchar, b varchar))")
+ field3_type = datatype.parse_sqltype("int")
+
+ sqla_columns = [
+ SQLAColumnType(name="field1", type=field1_type, is_dttm=False),
+ SQLAColumnType(name="field2", type=field2_type, is_dttm=False),
+ SQLAColumnType(name="field3", type=field3_type, is_dttm=False),
+ ]
+ mock_inspector = mocker.MagicMock()
+ mock_inspector.get_columns.return_value = sqla_columns
+
+ actual = TrinoEngineSpec.get_columns(mock_inspector, "table", "schema")
+ expected = [
+ ResultSetColumnType(
+ name="field1", column_name="field1", type=field1_type, is_dttm=False
+ ),
+ ResultSetColumnType(
+ name="field2", column_name="field2", type=field2_type, is_dttm=False
+ ),
+ ResultSetColumnType(
+ name="field3", column_name="field3", type=field3_type, is_dttm=False
+ ),
+ ]
+
+ _assert_columns_equal(actual, expected)
+
+
+def test_get_columns_expand_rows(mocker: MockerFixture):
+ """Test that ROW columns are correctly expanded with expand_rows"""
+ from superset.db_engine_specs.trino import TrinoEngineSpec
+
+ field1_type = datatype.parse_sqltype("row(a varchar, b date)")
+ field2_type = datatype.parse_sqltype("row(r1 row(a varchar, b varchar))")
+ field3_type = datatype.parse_sqltype("int")
+
+ sqla_columns = [
+ SQLAColumnType(name="field1", type=field1_type, is_dttm=False),
+ SQLAColumnType(name="field2", type=field2_type, is_dttm=False),
+ SQLAColumnType(name="field3", type=field3_type, is_dttm=False),
+ ]
+ mock_inspector = mocker.MagicMock()
+ mock_inspector.get_columns.return_value = sqla_columns
+
+ actual = TrinoEngineSpec.get_columns(
+ mock_inspector, "table", "schema", {"expand_rows": True}
+ )
+ expected = [
+ ResultSetColumnType(
+ name="field1", column_name="field1", type=field1_type, is_dttm=False
+ ),
+ ResultSetColumnType(
+ name="field1.a",
+ column_name="field1.a",
+ type=types.VARCHAR(),
+ is_dttm=False,
+ query_as='"field1"."a" AS "field1.a"',
+ ),
+ ResultSetColumnType(
+ name="field1.b",
+ column_name="field1.b",
+ type=types.DATE(),
+ is_dttm=True,
+ query_as='"field1"."b" AS "field1.b"',
+ ),
+ ResultSetColumnType(
+ name="field2", column_name="field2", type=field2_type, is_dttm=False
+ ),
+ ResultSetColumnType(
+ name="field2.r1",
+ column_name="field2.r1",
+ type=datatype.parse_sqltype("row(a varchar, b varchar)"),
+ is_dttm=False,
+ query_as='"field2"."r1" AS "field2.r1"',
+ ),
+ ResultSetColumnType(
+ name="field2.r1.a",
+ column_name="field2.r1.a",
+ type=types.VARCHAR(),
+ is_dttm=False,
+ query_as='"field2"."r1"."a" AS "field2.r1.a"',
+ ),
+ ResultSetColumnType(
+ name="field2.r1.b",
+ column_name="field2.r1.b",
+ type=types.VARCHAR(),
+ is_dttm=False,
+ query_as='"field2"."r1"."b" AS "field2.r1.b"',
+ ),
+ ResultSetColumnType(
+ name="field3", column_name="field3", type=field3_type, is_dttm=False
+ ),
+ ]
+
+ _assert_columns_equal(actual, expected)
From 92ac6b2c158d6c44988ddf9ba80dcd19087b9c80 Mon Sep 17 00:00:00 2001
From: Sebastian Liebscher
<112352529+sebastianliebscher@users.noreply.github.com>
Date: Mon, 20 Nov 2023 18:59:43 +0100
Subject: [PATCH 038/119] feat(sqllab): Show duration as separate column in
Query History view (#25861)
---
.../src/pages/QueryHistoryList/index.tsx | 40 ++++++++++++-------
1 file changed, 26 insertions(+), 14 deletions(-)
diff --git a/superset-frontend/src/pages/QueryHistoryList/index.tsx b/superset-frontend/src/pages/QueryHistoryList/index.tsx
index 63e916e399..77177188e0 100644
--- a/superset-frontend/src/pages/QueryHistoryList/index.tsx
+++ b/superset-frontend/src/pages/QueryHistoryList/index.tsx
@@ -34,6 +34,7 @@ import {
} from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import { useListViewResource } from 'src/views/CRUD/hooks';
+import Label from 'src/components/Label';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import Popover from 'src/components/Popover';
import { commonMenuData } from 'src/features/home/commonMenuData';
@@ -88,6 +89,11 @@ const StyledPopoverItem = styled.div`
color: ${({ theme }) => theme.colors.grayscale.dark2};
`;
+const TimerLabel = styled(Label)`
+ text-align: left;
+ font-family: ${({ theme }) => theme.typography.families.monospace};
+`;
+
function QueryList({ addDangerToast }: QueryListProps) {
const {
state: { loading, resourceCount: queryCount, resourceCollection: queries },
@@ -204,7 +210,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
size: 'xl',
Cell: ({
row: {
- original: { start_time, end_time },
+ original: { start_time },
},
}: any) => {
const startMoment = moment.utc(start_time).local();
@@ -218,19 +224,25 @@ function QueryList({ addDangerToast }: QueryListProps) {
{formattedStartTimeData[1]}
>
);
-
- return end_time ? (
-
- {formattedStartTime}
-
- ) : (
- formattedStartTime
+ return formattedStartTime;
+ },
+ },
+ {
+ Header: t('Duration'),
+ size: 'xl',
+ Cell: ({
+ row: {
+ original: { status, start_time, end_time },
+ },
+ }: any) => {
+ const timerType = status === QueryState.FAILED ? 'danger' : status;
+ const timerTime = end_time
+ ? moment(moment.utc(end_time - start_time)).format(TIME_WITH_MS)
+ : '00:00:00.000';
+ return (
+
+ {timerTime}
+
);
},
},
From e1d73d5420867b0310d4c2608686d5ccca94920f Mon Sep 17 00:00:00 2001
From: "JUST.in DO IT"
Date: Mon, 20 Nov 2023 10:01:56 -0800
Subject: [PATCH 039/119] fix(native filters): rendering performance
improvement by reduce overrendering (#25901)
---
.../superset-ui-core/src/chart/types/Base.ts | 1 -
.../src/dashboard/components/Dashboard.jsx | 15 +--
.../dashboard/components/Dashboard.test.jsx | 13 ++-
.../SyncDashboardState.test.tsx | 34 ++++++
.../components/SyncDashboardState/index.tsx | 103 ++++++++++++++++++
.../FilterBar/FilterControls/FilterValue.tsx | 3 +-
.../src/dashboard/containers/Dashboard.ts | 2 -
.../dashboard/containers/DashboardPage.tsx | 94 +++-------------
superset-frontend/src/dataMask/reducer.ts | 1 -
.../Select/SelectFilterPlugin.test.tsx | 24 ----
.../components/Select/SelectFilterPlugin.tsx | 41 ++++---
.../src/filters/components/common.ts | 4 +-
12 files changed, 191 insertions(+), 144 deletions(-)
create mode 100644 superset-frontend/src/dashboard/components/SyncDashboardState/SyncDashboardState.test.tsx
create mode 100644 superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
index 1c4d278f6c..b3884a8488 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
@@ -58,7 +58,6 @@ export enum AppSection {
export type FilterState = { value?: any; [key: string]: any };
export type DataMask = {
- __cache?: FilterState;
extraFormData?: ExtraFormData;
filterState?: FilterState;
ownState?: JsonObject;
diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx
index 827f0f455d..6e909f3b15 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.jsx
@@ -25,9 +25,8 @@ import Loading from 'src/components/Loading';
import getBootstrapData from 'src/utils/getBootstrapData';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
-import DashboardBuilder from './DashboardBuilder/DashboardBuilder';
+
import {
- chartPropShape,
slicePropShape,
dashboardInfoPropShape,
dashboardStatePropShape,
@@ -53,7 +52,6 @@ const propTypes = {
}).isRequired,
dashboardInfo: dashboardInfoPropShape.isRequired,
dashboardState: dashboardStatePropShape.isRequired,
- charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired,
activeFilters: PropTypes.object.isRequired,
chartConfiguration: PropTypes.object,
@@ -213,11 +211,6 @@ class Dashboard extends React.PureComponent {
}
}
- // return charts in array
- getAllCharts() {
- return Object.values(this.props.charts);
- }
-
applyFilters() {
const { appliedFilters } = this;
const { activeFilters, ownDataCharts } = this.props;
@@ -288,11 +281,7 @@ class Dashboard extends React.PureComponent {
if (this.context.loading) {
return ;
}
- return (
- <>
-
- >
- );
+ return this.props.children;
}
}
diff --git a/superset-frontend/src/dashboard/components/Dashboard.test.jsx b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
index 56a696f913..a66eab37e3 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.test.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
@@ -21,7 +21,6 @@ import { shallow } from 'enzyme';
import sinon from 'sinon';
import Dashboard from 'src/dashboard/components/Dashboard';
-import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
@@ -63,8 +62,14 @@ describe('Dashboard', () => {
loadStats: {},
};
+ const ChildrenComponent = () =>
Test
;
+
function setup(overrideProps) {
- const wrapper = shallow();
+ const wrapper = shallow(
+
+
+ ,
+ );
return wrapper;
}
@@ -76,9 +81,9 @@ describe('Dashboard', () => {
'3_country_name': { values: ['USA'], scope: [] },
};
- it('should render a DashboardBuilder', () => {
+ it('should render the children component', () => {
const wrapper = setup();
- expect(wrapper.find(DashboardBuilder)).toExist();
+ expect(wrapper.find(ChildrenComponent)).toExist();
});
describe('UNSAFE_componentWillReceiveProps', () => {
diff --git a/superset-frontend/src/dashboard/components/SyncDashboardState/SyncDashboardState.test.tsx b/superset-frontend/src/dashboard/components/SyncDashboardState/SyncDashboardState.test.tsx
new file mode 100644
index 0000000000..1565a43e19
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/SyncDashboardState/SyncDashboardState.test.tsx
@@ -0,0 +1,34 @@
+/**
+ * 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 React from 'react';
+import { render } from 'spec/helpers/testing-library';
+import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
+import SyncDashboardState from '.';
+
+test('stores the dashboard info with local storages', () => {
+ const testDashboardPageId = 'dashboardPageId';
+ render(, {
+ useRedux: true,
+ });
+ expect(getItem(LocalStorageKeys.dashboard__explore_context, {})).toEqual({
+ [testDashboardPageId]: expect.objectContaining({
+ dashboardPageId: testDashboardPageId,
+ }),
+ });
+});
diff --git a/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx b/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
new file mode 100644
index 0000000000..b25d243292
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
@@ -0,0 +1,103 @@
+/**
+ * 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 React, { useEffect } from 'react';
+import pick from 'lodash/pick';
+import { shallowEqual, useSelector } from 'react-redux';
+import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
+import {
+ getItem,
+ LocalStorageKeys,
+ setItem,
+} from 'src/utils/localStorageHelpers';
+import { RootState } from 'src/dashboard/types';
+import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
+
+type Props = { dashboardPageId: string };
+
+const EMPTY_OBJECT = {};
+
+export const getDashboardContextLocalStorage = () => {
+ const dashboardsContexts = getItem(
+ LocalStorageKeys.dashboard__explore_context,
+ {},
+ );
+ // A new dashboard tab id is generated on each dashboard page opening.
+ // We mark ids as redundant when user leaves the dashboard, because they won't be reused.
+ // Then we remove redundant dashboard contexts from local storage in order not to clutter it
+ return Object.fromEntries(
+ Object.entries(dashboardsContexts).filter(
+ ([, value]) => !value.isRedundant,
+ ),
+ );
+};
+
+const updateDashboardTabLocalStorage = (
+ dashboardPageId: string,
+ dashboardContext: DashboardContextForExplore,
+) => {
+ const dashboardsContexts = getDashboardContextLocalStorage();
+ setItem(LocalStorageKeys.dashboard__explore_context, {
+ ...dashboardsContexts,
+ [dashboardPageId]: dashboardContext,
+ });
+};
+
+const SyncDashboardState: React.FC = ({ dashboardPageId }) => {
+ const dashboardContextForExplore = useSelector<
+ RootState,
+ DashboardContextForExplore
+ >(
+ ({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
+ labelColors: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
+ sharedLabelColors:
+ dashboardInfo.metadata?.shared_label_colors || EMPTY_OBJECT,
+ colorScheme: dashboardState?.colorScheme,
+ chartConfiguration:
+ dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
+ nativeFilters: Object.entries(nativeFilters.filters).reduce(
+ (acc, [key, filterValue]) => ({
+ ...acc,
+ [key]: pick(filterValue, ['chartsInScope']),
+ }),
+ {},
+ ),
+ dataMask,
+ dashboardId: dashboardInfo.id,
+ filterBoxFilters: getActiveFilters(),
+ dashboardPageId,
+ }),
+ shallowEqual,
+ );
+
+ useEffect(() => {
+ updateDashboardTabLocalStorage(dashboardPageId, dashboardContextForExplore);
+ return () => {
+ // mark tab id as redundant when dashboard unmounts - case when user opens
+ // Explore in the same tab
+ updateDashboardTabLocalStorage(dashboardPageId, {
+ ...dashboardContextForExplore,
+ isRedundant: true,
+ });
+ };
+ }, [dashboardContextForExplore, dashboardPageId]);
+
+ return null;
+};
+
+export default SyncDashboardState;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index 5235edcdc3..f44a1a1df6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -52,6 +52,7 @@ import {
onFiltersRefreshSuccess,
setDirectPathToChild,
} from 'src/dashboard/actions/dashboardState';
+import { RESPONSIVE_WIDTH } from 'src/filters/components/common';
import { FAST_DEBOUNCE } from 'src/constants';
import { dispatchHoverAction, dispatchFocusAction } from './utils';
import { FilterControlProps } from './types';
@@ -322,7 +323,7 @@ const FilterValue: React.FC = ({
) : (
import(
/* webpackChunkName: "DashboardContainer" */
/* webpackPreload: true */
- 'src/dashboard/containers/Dashboard'
+ 'src/dashboard/components/DashboardBuilder/DashboardBuilder'
),
);
@@ -83,74 +81,15 @@ type PageProps = {
idOrSlug: string;
};
-const getDashboardContextLocalStorage = () => {
- const dashboardsContexts = getItem(
- LocalStorageKeys.dashboard__explore_context,
- {},
- );
- // A new dashboard tab id is generated on each dashboard page opening.
- // We mark ids as redundant when user leaves the dashboard, because they won't be reused.
- // Then we remove redundant dashboard contexts from local storage in order not to clutter it
- return Object.fromEntries(
- Object.entries(dashboardsContexts).filter(
- ([, value]) => !value.isRedundant,
- ),
- );
-};
-
-const updateDashboardTabLocalStorage = (
- dashboardPageId: string,
- dashboardContext: DashboardContextForExplore,
-) => {
- const dashboardsContexts = getDashboardContextLocalStorage();
- setItem(LocalStorageKeys.dashboard__explore_context, {
- ...dashboardsContexts,
- [dashboardPageId]: dashboardContext,
- });
-};
-
-const useSyncDashboardStateWithLocalStorage = () => {
- const dashboardPageId = useMemo(() => shortid.generate(), []);
- const dashboardContextForExplore = useSelector<
- RootState,
- DashboardContextForExplore
- >(({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
- labelColors: dashboardInfo.metadata?.label_colors || {},
- sharedLabelColors: dashboardInfo.metadata?.shared_label_colors || {},
- colorScheme: dashboardState?.colorScheme,
- chartConfiguration: dashboardInfo.metadata?.chart_configuration || {},
- nativeFilters: Object.entries(nativeFilters.filters).reduce(
- (acc, [key, filterValue]) => ({
- ...acc,
- [key]: pick(filterValue, ['chartsInScope']),
- }),
- {},
- ),
- dataMask,
- dashboardId: dashboardInfo.id,
- filterBoxFilters: getActiveFilters(),
- dashboardPageId,
- }));
-
- useEffect(() => {
- updateDashboardTabLocalStorage(dashboardPageId, dashboardContextForExplore);
- return () => {
- // mark tab id as redundant when dashboard unmounts - case when user opens
- // Explore in the same tab
- updateDashboardTabLocalStorage(dashboardPageId, {
- ...dashboardContextForExplore,
- isRedundant: true,
- });
- };
- }, [dashboardContextForExplore, dashboardPageId]);
- return dashboardPageId;
-};
-
export const DashboardPage: FC = ({ idOrSlug }: PageProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const history = useHistory();
- const dashboardPageId = useSyncDashboardStateWithLocalStorage();
+ const dashboardPageId = useMemo(() => shortid.generate(), []);
+ const hasDashboardInfoInitiated = useSelector(
+ ({ dashboardInfo }) =>
+ dashboardInfo && Object.keys(dashboardInfo).length > 0,
+ );
const { addDangerToast } = useToasts();
const { result: dashboard, error: dashboardApiError } =
useDashboard(idOrSlug);
@@ -284,7 +223,7 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => {
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
if (error) throw error; // caught in error boundary
- if (!readyToRender || !isDashboardHydrated.current) return ;
+ if (!readyToRender || !hasDashboardInfoInitiated) return ;
return (
<>
@@ -295,8 +234,11 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => {
chartContextMenuStyles(theme),
]}
/>
+
-
+
+
+
>
);
diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts
index 6e9a5fae54..f2163a54a4 100644
--- a/superset-frontend/src/dataMask/reducer.ts
+++ b/superset-frontend/src/dataMask/reducer.ts
@@ -56,7 +56,6 @@ export function getInitialDataMask(
}
return {
...otherProps,
- __cache: {},
extraFormData: {},
filterState: {},
ownState: {},
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
index c035f81c01..99e6259871 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx
@@ -91,15 +91,6 @@ describe('SelectFilterPlugin', () => {
test('Add multiple values with first render', async () => {
getWrapper();
expect(setDataMask).toHaveBeenCalledWith({
- extraFormData: {},
- filterState: {
- value: ['boy'],
- },
- });
- expect(setDataMask).toHaveBeenCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {
filters: [
{
@@ -118,9 +109,6 @@ describe('SelectFilterPlugin', () => {
userEvent.click(screen.getByTitle('girl'));
expect(await screen.findByTitle(/girl/i)).toBeInTheDocument();
expect(setDataMask).toHaveBeenCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {
filters: [
{
@@ -146,9 +134,6 @@ describe('SelectFilterPlugin', () => {
}),
);
expect(setDataMask).toHaveBeenCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {
adhoc_filters: [
{
@@ -174,9 +159,6 @@ describe('SelectFilterPlugin', () => {
}),
);
expect(setDataMask).toHaveBeenCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {},
filterState: {
label: undefined,
@@ -191,9 +173,6 @@ describe('SelectFilterPlugin', () => {
expect(await screen.findByTitle('girl')).toBeInTheDocument();
userEvent.click(screen.getByTitle('girl'));
expect(setDataMask).toHaveBeenCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {
filters: [
{
@@ -216,9 +195,6 @@ describe('SelectFilterPlugin', () => {
expect(await screen.findByRole('combobox')).toBeInTheDocument();
userEvent.click(screen.getByTitle(NULL_STRING));
expect(setDataMask).toHaveBeenLastCalledWith({
- __cache: {
- value: ['boy'],
- },
extraFormData: {
filters: [
{
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 7d8ab55fb5..a4b9f5b05e 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -37,7 +37,6 @@ import { Select } from 'src/components';
import { SLOW_DEBOUNCE } from 'src/constants';
import { hasOption, propertyComparator } from 'src/components/Select/utils';
import { FilterBarOrientation } from 'src/dashboard/types';
-import { uniqWith, isEqual } from 'lodash';
import { PluginFilterSelectProps, SelectValue } from './types';
import { FilterPluginStyle, StatusMessage, StyledFormItem } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
@@ -46,15 +45,11 @@ type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject }
| {
type: 'filterState';
- __cache: JsonObject;
extraFormData: ExtraFormData;
filterState: { value: SelectValue; label?: string };
};
-function reducer(
- draft: DataMask & { __cache?: JsonObject },
- action: DataMaskAction,
-) {
+function reducer(draft: DataMask, action: DataMaskAction) {
switch (action.type) {
case 'ownState':
draft.ownState = {
@@ -63,10 +58,18 @@ function reducer(
};
return draft;
case 'filterState':
- draft.extraFormData = action.extraFormData;
- // eslint-disable-next-line no-underscore-dangle
- draft.__cache = action.__cache;
- draft.filterState = { ...draft.filterState, ...action.filterState };
+ if (
+ JSON.stringify(draft.extraFormData) !==
+ JSON.stringify(action.extraFormData)
+ ) {
+ draft.extraFormData = action.extraFormData;
+ }
+ if (
+ JSON.stringify(draft.filterState) !== JSON.stringify(action.filterState)
+ ) {
+ draft.filterState = { ...draft.filterState, ...action.filterState };
+ }
+
return draft;
default:
return draft;
@@ -130,7 +133,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
const suffix = inverseSelection && values?.length ? t(' (excluded)') : '';
dispatchDataMask({
type: 'filterState',
- __cache: filterState,
extraFormData: getSelectExtraFormData(
col,
values,
@@ -219,16 +221,13 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
}, [filterState.validateMessage, filterState.validateStatus]);
const uniqueOptions = useMemo(() => {
- const allOptions = [...data];
- return uniqWith(allOptions, isEqual).map(row => {
- const [value] = groupby.map(col => row[col]);
- return {
- label: labelFormatter(value, datatype),
- value,
- isNewOption: false,
- };
- });
- }, [data, datatype, groupby, labelFormatter]);
+ const allOptions = new Set([...data.map(el => el[col])]);
+ return [...allOptions].map((value: string) => ({
+ label: labelFormatter(value, datatype),
+ value,
+ isNewOption: false,
+ }));
+ }, [data, datatype, col, labelFormatter]);
const options = useMemo(() => {
if (search && !multiSelect && !hasOption(search, uniqueOptions, true)) {
diff --git a/superset-frontend/src/filters/components/common.ts b/superset-frontend/src/filters/components/common.ts
index af1fe9c791..cb6d7f22f1 100644
--- a/superset-frontend/src/filters/components/common.ts
+++ b/superset-frontend/src/filters/components/common.ts
@@ -20,9 +20,11 @@ import { styled } from '@superset-ui/core';
import { PluginFilterStylesProps } from './types';
import FormItem from '../../components/Form/FormItem';
+export const RESPONSIVE_WIDTH = 0;
+
export const FilterPluginStyle = styled.div`
min-height: ${({ height }) => height}px;
- width: ${({ width }) => width}px;
+ width: ${({ width }) => (width === RESPONSIVE_WIDTH ? '100%' : `${width}px`)};
`;
export const StyledFormItem = styled(FormItem)`
From 628cd345f2b5a9128fcbfaaefa02b24c77d06155 Mon Sep 17 00:00:00 2001
From: Daniel Vaz Gaspar
Date: Mon, 20 Nov 2023 19:02:30 +0000
Subject: [PATCH 040/119] fix: update FAB to 4.3.10, Azure user info fix
(#26037)
---
requirements/base.txt | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/base.txt b/requirements/base.txt
index cc43587812..1d27016e63 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -99,7 +99,7 @@ flask==2.2.5
# flask-session
# flask-sqlalchemy
# flask-wtf
-flask-appbuilder==4.3.9
+flask-appbuilder==4.3.10
# via apache-superset
flask-babel==1.0.0
# via flask-appbuilder
diff --git a/setup.py b/setup.py
index 88f2c0fba5..eb442bcf72 100644
--- a/setup.py
+++ b/setup.py
@@ -83,7 +83,7 @@ setup(
"cryptography>=41.0.2, <41.1.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
- "flask-appbuilder>=4.3.9, <5.0.0",
+ "flask-appbuilder>=4.3.10, <5.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
From e2bfb1216b48ca64ead771c4327a4827e2084f6a Mon Sep 17 00:00:00 2001
From: "JUST.in DO IT"
Date: Mon, 20 Nov 2023 11:13:54 -0800
Subject: [PATCH 041/119] feat(sqllab): non-blocking persistence mode (#24539)
Co-authored-by: Justin Park
---
.../src/SqlLab/actions/sqlLab.js | 297 ++++--------------
.../src/SqlLab/actions/sqlLab.test.js | 156 ++-------
.../EditorAutoSync/EditorAutoSync.test.tsx | 137 ++++++++
.../components/EditorAutoSync/index.tsx | 106 +++++++
.../SqlLab/components/QueryTable/index.tsx | 6 +-
.../src/SqlLab/components/SqlEditor/index.tsx | 7 +-
superset-frontend/src/SqlLab/fixtures.ts | 2 +
.../middlewares/persistSqlLabStateEnhancer.js | 36 +++
.../SqlLab/reducers/getInitialState.test.ts | 113 ++++++-
.../src/SqlLab/reducers/getInitialState.ts | 76 +++--
.../src/SqlLab/reducers/sqlLab.js | 33 +-
superset-frontend/src/SqlLab/types.ts | 10 +-
.../SqlLab/utils/emptyQueryResults.test.js | 7 +-
.../utils/reduxStateToLocalStorageHelper.js | 1 +
.../hooks/apiResources/sqlEditorTabs.test.ts | 99 ++++++
.../src/hooks/apiResources/sqlEditorTabs.ts | 70 +++++
.../src/hooks/apiResources/sqlLab.ts | 2 +-
.../src/hooks/useDebounceValue.ts | 4 +-
superset-frontend/src/pages/SqlLab/index.tsx | 12 +-
superset-frontend/src/views/store.ts | 5 +-
20 files changed, 746 insertions(+), 433 deletions(-)
create mode 100644 superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
create mode 100644 superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
create mode 100644 superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
create mode 100644 superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 44b4307a19..567d3383d7 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -99,6 +99,8 @@ export const CREATE_DATASOURCE_STARTED = 'CREATE_DATASOURCE_STARTED';
export const CREATE_DATASOURCE_SUCCESS = 'CREATE_DATASOURCE_SUCCESS';
export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
+export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
+
export const addInfoToast = addInfoToastAction;
export const addSuccessToast = addSuccessToastAction;
export const addDangerToast = addDangerToastAction;
@@ -160,6 +162,10 @@ export function updateQueryEditor(alterations) {
return { type: UPDATE_QUERY_EDITOR, alterations };
}
+export function setEditorTabLastUpdate(timestamp) {
+ return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp };
+}
+
export function scheduleQuery(query) {
return dispatch =>
SupersetClient.post({
@@ -237,44 +243,11 @@ export function startQuery(query) {
}
export function querySuccess(query, results) {
- return function (dispatch) {
- const sqlEditorId = results?.query?.sqlEditorId;
- const sync =
- sqlEditorId &&
- !query.isDataPreview &&
- isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${sqlEditorId}`),
- postPayload: { latest_query_id: query.id },
- })
- : Promise.resolve();
-
- return sync
- .then(() => dispatch({ type: QUERY_SUCCESS, query, results }))
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while storing the latest query id in the backend. ' +
- 'Please contact your administrator if this problem persists.',
- ),
- ),
- ),
- );
- };
+ return { type: QUERY_SUCCESS, query, results };
}
export function queryFailed(query, msg, link, errors) {
return function (dispatch) {
- const sync =
- !query.isDataPreview &&
- isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${query.sqlEditorId}`),
- postPayload: { latest_query_id: query.id },
- })
- : Promise.resolve();
-
const eventData = {
has_err: true,
start_offset: query.startDttm,
@@ -295,22 +268,7 @@ export function queryFailed(query, msg, link, errors) {
});
});
- return (
- sync
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while storing the latest query id in the backend. ' +
- 'Please contact your administrator if this problem persists.',
- ),
- ),
- ),
- )
- // We should always show the error message, even if we couldn't sync the
- // state to the backend
- .then(() => dispatch({ type: QUERY_FAILED, query, msg, link, errors }))
- );
+ dispatch({ type: QUERY_FAILED, query, msg, link, errors });
};
}
@@ -557,14 +515,15 @@ export function addQueryEditor(queryEditor) {
? SupersetClient.post({
endpoint: '/tabstateview/',
postPayload: { queryEditor },
- })
- : Promise.resolve({ json: { id: shortid.generate() } });
+ }).then(({ json }) => ({ ...json, loaded: true }))
+ : Promise.resolve({ id: shortid.generate() });
return sync
- .then(({ json }) => {
+ .then(({ id, loaded }) => {
const newQueryEditor = {
...queryEditor,
- id: json.id.toString(),
+ id: id.toString(),
+ loaded,
};
return dispatch({
type: ADD_QUERY_EDITOR,
@@ -736,11 +695,6 @@ export function switchQueryEditor(queryEditor, displayLimit) {
schema: json.schema,
queryLimit: json.query_limit,
remoteId: json.saved_query?.id,
- validationResult: {
- id: null,
- errors: [],
- completed: false,
- },
hideLeftBar: json.hide_left_bar,
};
dispatch(loadQueryEditor(loadedQueryEditor));
@@ -770,31 +724,10 @@ export function setActiveSouthPaneTab(tabId) {
export function toggleLeftBar(queryEditor) {
const hideLeftBar = !queryEditor.hideLeftBar;
- return function (dispatch) {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { hide_left_bar: hideLeftBar },
- })
- : Promise.resolve();
-
- return sync
- .then(() =>
- dispatch({
- type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
- queryEditor,
- hideLeftBar,
- }),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while hiding the left bar. Please contact your administrator.',
- ),
- ),
- ),
- );
+ return {
+ type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditor,
+ hideLeftBar,
};
}
@@ -856,110 +789,26 @@ export function removeQuery(query) {
}
export function queryEditorSetDb(queryEditor, dbId) {
- return function (dispatch) {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { database_id: dbId },
- })
- : Promise.resolve();
-
- return sync
- .then(() => dispatch({ type: QUERY_EDITOR_SETDB, queryEditor, dbId }))
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab database ID. Please contact your administrator.',
- ),
- ),
- ),
- );
- };
+ return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
}
export function queryEditorSetSchema(queryEditor, schema) {
- return function (dispatch) {
- const sync =
- isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
- typeof queryEditor === 'object'
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { schema },
- })
- : Promise.resolve();
-
- return sync
- .then(() =>
- dispatch({
- type: QUERY_EDITOR_SET_SCHEMA,
- queryEditor: queryEditor || {},
- schema,
- }),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab schema. Please contact your administrator.',
- ),
- ),
- ),
- );
+ return {
+ type: QUERY_EDITOR_SET_SCHEMA,
+ queryEditor: queryEditor || {},
+ schema,
};
}
export function queryEditorSetAutorun(queryEditor, autorun) {
- return function (dispatch) {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { autorun },
- })
- : Promise.resolve();
-
- return sync
- .then(() =>
- dispatch({ type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun }),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab autorun. Please contact your administrator.',
- ),
- ),
- ),
- );
- };
+ return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
}
export function queryEditorSetTitle(queryEditor, name, id) {
- return function (dispatch) {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${id}`),
- postPayload: { label: name },
- })
- : Promise.resolve();
-
- return sync
- .then(() =>
- dispatch({
- type: QUERY_EDITOR_SET_TITLE,
- queryEditor: { ...queryEditor, id },
- name,
- }),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab name. Please contact your administrator.',
- ),
- ),
- ),
- );
+ return {
+ type: QUERY_EDITOR_SET_TITLE,
+ queryEditor: { ...queryEditor, id },
+ name,
};
}
@@ -1029,32 +878,19 @@ export function updateSavedQuery(query, clientId) {
.then(() => dispatch(updateQueryEditor(query)));
}
-export function queryEditorSetSql(queryEditor, sql) {
- return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
+export function queryEditorSetSql(queryEditor, sql, queryId) {
+ return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql, queryId };
}
-export function formatQuery(queryEditor) {
- return function (dispatch, getState) {
- const { sql } = getUpToDateQuery(getState(), queryEditor);
- return SupersetClient.post({
- endpoint: `/api/v1/sqllab/format_sql/`,
- body: JSON.stringify({ sql }),
- headers: { 'Content-Type': 'application/json' },
- }).then(({ json }) => {
- dispatch(queryEditorSetSql(queryEditor, json.result));
- });
- };
-}
-
-export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
+export function queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
return function (dispatch, getState) {
const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
// saved query and set tab state use this action
- dispatch(queryEditorSetSql(queryEditor, sql));
+ dispatch(queryEditorSetSql(queryEditor, sql, queryId));
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
+ postPayload: { sql, latest_query_id: queryId },
}).catch(() =>
dispatch(
addDangerToast(
@@ -1071,59 +907,32 @@ export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
};
}
-export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
- return function (dispatch) {
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { query_limit: queryLimit },
- })
- : Promise.resolve();
+export function formatQuery(queryEditor) {
+ return function (dispatch, getState) {
+ const { sql } = getUpToDateQuery(getState(), queryEditor);
+ return SupersetClient.post({
+ endpoint: `/api/v1/sqllab/format_sql/`,
+ body: JSON.stringify({ sql }),
+ headers: { 'Content-Type': 'application/json' },
+ }).then(({ json }) => {
+ dispatch(queryEditorSetSql(queryEditor, json.result));
+ });
+ };
+}
- return sync
- .then(() =>
- dispatch({
- type: QUERY_EDITOR_SET_QUERY_LIMIT,
- queryEditor,
- queryLimit,
- }),
- )
- .catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab name. Please contact your administrator.',
- ),
- ),
- ),
- );
+export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
+ return {
+ type: QUERY_EDITOR_SET_QUERY_LIMIT,
+ queryEditor,
+ queryLimit,
};
}
export function queryEditorSetTemplateParams(queryEditor, templateParams) {
- return function (dispatch) {
- dispatch({
- type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
- queryEditor,
- templateParams,
- });
- const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
- ? SupersetClient.put({
- endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
- postPayload: { template_params: templateParams },
- })
- : Promise.resolve();
-
- return sync.catch(() =>
- dispatch(
- addDangerToast(
- t(
- 'An error occurred while setting the tab template parameters. ' +
- 'Please contact your administrator.',
- ),
- ),
- ),
- );
+ return {
+ type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
+ queryEditor,
+ templateParams,
};
}
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index dbf4e8a5c5..175ea06ec3 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -32,7 +32,6 @@ import {
initialState,
queryId,
} from 'src/SqlLab/fixtures';
-import { QueryState } from '@superset-ui/core';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
@@ -531,88 +530,6 @@ describe('async actions', () => {
afterEach(fetchMock.resetHistory);
- describe('querySuccess', () => {
- it('updates the tab state in the backend', () => {
- expect.assertions(2);
-
- const store = mockStore({});
- const results = { query: { sqlEditorId: 'abcd' } };
- const expectedActions = [
- {
- type: actions.QUERY_SUCCESS,
- query,
- results,
- },
- ];
- return store.dispatch(actions.querySuccess(query, results)).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
- });
- });
-
- describe('fetchQueryResults', () => {
- it('updates the tab state in the backend', () => {
- expect.assertions(2);
-
- const results = {
- data: mockBigNumber,
- query: { sqlEditorId: 'abcd' },
- status: QueryState.SUCCESS,
- query_id: 'efgh',
- };
- fetchMock.get(fetchQueryEndpoint, JSON.stringify(results), {
- overwriteRoutes: true,
- });
- const store = mockStore({});
- const expectedActions = [
- {
- type: actions.REQUEST_QUERY_RESULTS,
- query,
- },
- // missing below
- {
- type: actions.QUERY_SUCCESS,
- query,
- results,
- },
- ];
- return store.dispatch(actions.fetchQueryResults(query)).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
- });
-
- it("doesn't update the tab state in the backend on stoppped query", () => {
- expect.assertions(2);
-
- const results = {
- status: QueryState.STOPPED,
- query_id: 'efgh',
- };
- fetchMock.get(fetchQueryEndpoint, JSON.stringify(results), {
- overwriteRoutes: true,
- });
- const store = mockStore({});
- const expectedActions = [
- {
- type: actions.REQUEST_QUERY_RESULTS,
- query,
- },
- // missing below
- {
- type: actions.QUERY_SUCCESS,
- query,
- results,
- },
- ];
- return store.dispatch(actions.fetchQueryResults(query)).then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
- });
- });
- });
-
describe('addQueryEditor', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
@@ -621,7 +538,7 @@ describe('async actions', () => {
const expectedActions = [
{
type: actions.ADD_QUERY_EDITOR,
- queryEditor: { ...queryEditor, id: '1' },
+ queryEditor: { ...queryEditor, id: '1', loaded: true },
},
];
return store.dispatch(actions.addQueryEditor(queryEditor)).then(() => {
@@ -673,7 +590,7 @@ describe('async actions', () => {
describe('queryEditorSetDb', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const dbId = 42;
const store = mockStore({});
@@ -684,18 +601,14 @@ describe('async actions', () => {
dbId,
},
];
- return store
- .dispatch(actions.queryEditorSetDb(queryEditor, dbId))
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(actions.queryEditorSetDb(queryEditor, dbId));
+ expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetSchema', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const schema = 'schema';
const store = mockStore({});
@@ -706,18 +619,14 @@ describe('async actions', () => {
schema,
},
];
- return store
- .dispatch(actions.queryEditorSetSchema(queryEditor, schema))
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(actions.queryEditorSetSchema(queryEditor, schema));
+ expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetAutorun', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const autorun = true;
const store = mockStore({});
@@ -728,18 +637,14 @@ describe('async actions', () => {
autorun,
},
];
- return store
- .dispatch(actions.queryEditorSetAutorun(queryEditor, autorun))
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun));
+ expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetTitle', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const name = 'name';
const store = mockStore({});
@@ -750,14 +655,10 @@ describe('async actions', () => {
name,
},
];
- return store
- .dispatch(
- actions.queryEditorSetTitle(queryEditor, name, queryEditor.id),
- )
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(
+ actions.queryEditorSetTitle(queryEditor, name, queryEditor.id),
+ );
+ expect(store.getActions()).toEqual(expectedActions);
});
});
@@ -803,7 +704,7 @@ describe('async actions', () => {
describe('queryEditorSetQueryLimit', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const queryLimit = 10;
const store = mockStore({});
@@ -814,18 +715,16 @@ describe('async actions', () => {
queryLimit,
},
];
- return store
- .dispatch(actions.queryEditorSetQueryLimit(queryEditor, queryLimit))
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(
+ actions.queryEditorSetQueryLimit(queryEditor, queryLimit),
+ );
+ expect(store.getActions()).toEqual(expectedActions);
});
});
describe('queryEditorSetTemplateParams', () => {
it('updates the tab state in the backend', () => {
- expect.assertions(2);
+ expect.assertions(1);
const templateParams = '{"foo": "bar"}';
const store = mockStore({});
@@ -836,14 +735,11 @@ describe('async actions', () => {
templateParams,
},
];
- return store
- .dispatch(
- actions.queryEditorSetTemplateParams(queryEditor, templateParams),
- )
- .then(() => {
- expect(store.getActions()).toEqual(expectedActions);
- expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
- });
+ store.dispatch(
+ actions.queryEditorSetTemplateParams(queryEditor, templateParams),
+ );
+
+ expect(store.getActions()).toEqual(expectedActions);
});
});
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
new file mode 100644
index 0000000000..52e1d44b24
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/EditorAutoSync.test.tsx
@@ -0,0 +1,137 @@
+/**
+ * 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.
+ */
+/**
+ * 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 React from 'react';
+import fetchMock from 'fetch-mock';
+import { render, waitFor } from 'spec/helpers/testing-library';
+import ToastContainer from 'src/components/MessageToasts/ToastContainer';
+import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import { logging } from '@superset-ui/core';
+import EditorAutoSync from '.';
+
+jest.mock('@superset-ui/core', () => ({
+ ...jest.requireActual('@superset-ui/core'),
+ logging: {
+ warn: jest.fn(),
+ },
+}));
+
+const editorTabLastUpdatedAt = Date.now();
+const unsavedSqlLabState = {
+ ...initialState.sqlLab,
+ unsavedQueryEditor: {
+ id: defaultQueryEditor.id,
+ name: 'updated tab name',
+ updatedAt: editorTabLastUpdatedAt + 100,
+ },
+ editorTabLastUpdatedAt,
+};
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
+test('sync the unsaved editor tab state when there are new changes since the last update', async () => {
+ const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+ fetchMock.put(updateEditorTabState, 200);
+ expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ render(, {
+ useRedux: true,
+ initialState: {
+ ...initialState,
+ sqlLab: unsavedSqlLabState,
+ },
+ });
+ await waitFor(() => jest.runAllTimers());
+ expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
+ fetchMock.restore();
+});
+
+test('skip syncing the unsaved editor tab state when the updates are already synced', async () => {
+ const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+ fetchMock.put(updateEditorTabState, 200);
+ expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ render(, {
+ useRedux: true,
+ initialState: {
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ unsavedQueryEditor: {
+ id: defaultQueryEditor.id,
+ name: 'updated tab name',
+ updatedAt: editorTabLastUpdatedAt - 100,
+ },
+ editorTabLastUpdatedAt,
+ },
+ },
+ });
+ await waitFor(() => jest.runAllTimers());
+ expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ fetchMock.restore();
+});
+
+test('renders an error toast when the sync failed', async () => {
+ const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
+ fetchMock.put(updateEditorTabState, {
+ throws: new Error('errorMessage'),
+ });
+ expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
+ render(
+ <>
+
+
+ >,
+ {
+ useRedux: true,
+ initialState: {
+ ...initialState,
+ sqlLab: unsavedSqlLabState,
+ },
+ },
+ );
+ await waitFor(() => jest.runAllTimers());
+
+ expect(logging.warn).toHaveBeenCalledTimes(1);
+ expect(logging.warn).toHaveBeenCalledWith(
+ 'An error occurred while saving your editor state.',
+ expect.anything(),
+ );
+ fetchMock.restore();
+});
diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
new file mode 100644
index 0000000000..51399753e9
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx
@@ -0,0 +1,106 @@
+/**
+ * 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 React, { useRef, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { logging } from '@superset-ui/core';
+import {
+ SqlLabRootState,
+ QueryEditor,
+ UnsavedQueryEditor,
+} from 'src/SqlLab/types';
+import { useUpdateSqlEditorTabMutation } from 'src/hooks/apiResources/sqlEditorTabs';
+import { useDebounceValue } from 'src/hooks/useDebounceValue';
+import { setEditorTabLastUpdate } from 'src/SqlLab/actions/sqlLab';
+
+const INTERVAL = 5000;
+
+function hasUnsavedChanges(
+ queryEditor: QueryEditor,
+ lastSavedTimestamp: number,
+) {
+ return (
+ queryEditor.inLocalStorage ||
+ (queryEditor.updatedAt && queryEditor.updatedAt > lastSavedTimestamp)
+ );
+}
+
+export function filterUnsavedQueryEditorList(
+ queryEditors: QueryEditor[],
+ unsavedQueryEditor: UnsavedQueryEditor,
+ lastSavedTimestamp: number,
+) {
+ return queryEditors
+ .map(queryEditor => ({
+ ...queryEditor,
+ ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
+ }))
+ .filter(queryEditor => hasUnsavedChanges(queryEditor, lastSavedTimestamp));
+}
+
+const EditorAutoSync: React.FC = () => {
+ const queryEditors = useSelector(
+ state => state.sqlLab.queryEditors,
+ );
+ const unsavedQueryEditor = useSelector(
+ state => state.sqlLab.unsavedQueryEditor,
+ );
+ const editorTabLastUpdatedAt = useSelector(
+ state => state.sqlLab.editorTabLastUpdatedAt,
+ );
+ const dispatch = useDispatch();
+ const lastSavedTimestampRef = useRef(editorTabLastUpdatedAt);
+ const [updateSqlEditor, { error }] = useUpdateSqlEditorTabMutation();
+
+ const debouncedUnsavedQueryEditor = useDebounceValue(
+ unsavedQueryEditor,
+ INTERVAL,
+ );
+
+ useEffect(() => {
+ const unsaved = filterUnsavedQueryEditorList(
+ queryEditors,
+ debouncedUnsavedQueryEditor,
+ lastSavedTimestampRef.current,
+ );
+
+ Promise.all(
+ unsaved
+ // TODO: Migrate migrateQueryEditorFromLocalStorage
+ // in TabbedSqlEditors logic by addSqlEditor mutation later
+ .filter(({ inLocalStorage }) => !inLocalStorage)
+ .map(queryEditor => updateSqlEditor({ queryEditor })),
+ ).then(resolvers => {
+ if (!resolvers.some(result => 'error' in result)) {
+ lastSavedTimestampRef.current = Date.now();
+ dispatch(setEditorTabLastUpdate(lastSavedTimestampRef.current));
+ }
+ });
+ }, [debouncedUnsavedQueryEditor, dispatch, queryEditors, updateSqlEditor]);
+
+ useEffect(() => {
+ if (error) {
+ logging.warn('An error occurred while saving your editor state.', error);
+ }
+ }, [dispatch, error]);
+
+ return null;
+};
+
+export default EditorAutoSync;
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index 6ddae08e68..5dc8a43c19 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -25,7 +25,7 @@ import { t, useTheme, QueryResponse } from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import {
- queryEditorSetAndSaveSql,
+ queryEditorSetSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
@@ -109,7 +109,9 @@ const QueryTable = ({
const data = useMemo(() => {
const restoreSql = (query: QueryResponse) => {
- dispatch(queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql));
+ dispatch(
+ queryEditorSetSql({ id: query.sqlEditorId }, query.sql, query.id),
+ );
};
const openQueryInNewTab = (query: QueryResponse) => {
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 609cb917b6..73941fbc79 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -557,10 +557,9 @@ const SqlEditor: React.FC = ({
[setQueryEditorAndSaveSql],
);
- const onSqlChanged = (sql: string) => {
+ const onSqlChanged = useEffectEvent((sql: string) => {
dispatch(queryEditorSetSql(queryEditor, sql));
- setQueryEditorAndSaveSqlWithDebounce(sql);
- };
+ });
// Return the heights for the ace editor and the south pane as an object
// given the height of the sql editor, north pane percent and south pane percent.
@@ -785,7 +784,7 @@ const SqlEditor: React.FC = ({
)}
1.0 for typescript support
import persistState from 'redux-localstorage';
+import { pickBy } from 'lodash';
+import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
+import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync';
import {
emptyTablePersistData,
emptyQueryResults,
@@ -38,6 +41,39 @@ const sqlLabPersistStateConfig = {
slicer: paths => state => {
const subset = {};
paths.forEach(path => {
+ if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
+ const {
+ queryEditors,
+ editorTabLastUpdatedAt,
+ unsavedQueryEditor,
+ tables,
+ queries,
+ tabHistory,
+ } = state.sqlLab;
+ const unsavedQueryEditors = filterUnsavedQueryEditorList(
+ queryEditors,
+ unsavedQueryEditor,
+ editorTabLastUpdatedAt,
+ );
+ if (unsavedQueryEditors.length > 0) {
+ const hasFinishedMigrationFromLocalStorage =
+ unsavedQueryEditors.every(
+ ({ inLocalStorage }) => !inLocalStorage,
+ );
+ subset.sqlLab = {
+ queryEditors: unsavedQueryEditors,
+ ...(!hasFinishedMigrationFromLocalStorage && {
+ tabHistory,
+ tables: tables.filter(table => table.inLocalStorage),
+ queries: pickBy(
+ queries,
+ query => query.inLocalStorage && !query.isDataPreview,
+ ),
+ }),
+ };
+ }
+ return;
+ }
// this line is used to remove old data from browser localStorage.
// we used to persist all redux state into localStorage, but
// it caused configurations passed from server-side got override.
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
index aca11e2cca..1dd3220fcc 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts
@@ -54,6 +54,10 @@ const apiDataWithTabState = {
},
};
describe('getInitialState', () => {
+ afterEach(() => {
+ localStorage.clear();
+ });
+
it('should output the user that is passed in', () => {
expect(getInitialState(apiData).user?.userId).toEqual(1);
});
@@ -134,10 +138,6 @@ describe('getInitialState', () => {
});
describe('dedupe tables schema', () => {
- afterEach(() => {
- localStorage.clear();
- });
-
it('should dedupe the table schema', () => {
localStorage.setItem(
'redux',
@@ -245,4 +245,109 @@ describe('getInitialState', () => {
);
});
});
+
+ describe('restore unsaved changes for PERSISTENCE mode', () => {
+ const lastUpdatedTime = Date.now();
+ const expectedValue = 'updated editor value';
+ beforeEach(() => {
+ localStorage.setItem(
+ 'redux',
+ JSON.stringify({
+ sqlLab: {
+ queryEditors: [
+ {
+ // restore cached value since updates are after server update time
+ id: '1',
+ name: expectedValue,
+ updatedAt: lastUpdatedTime + 100,
+ },
+ {
+ // no update required given that last updated time comes before server update time
+ id: '2',
+ name: expectedValue,
+ updatedAt: lastUpdatedTime - 100,
+ },
+ {
+ // no update required given that there's no updatedAt
+ id: '3',
+ name: expectedValue,
+ },
+ ],
+ },
+ }),
+ );
+ });
+
+ it('restore unsaved changes for PERSISTENCE mode', () => {
+ const apiDataWithLocalStorage = {
+ ...apiData,
+ active_tab: {
+ ...apiDataWithTabState.active_tab,
+ id: 1,
+ label: 'persisted tab',
+ table_schemas: [],
+ extra_json: {
+ updatedAt: lastUpdatedTime,
+ },
+ },
+ tab_state_ids: [{ id: 1, label: '' }],
+ };
+ expect(
+ getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[0],
+ ).toEqual(
+ expect.objectContaining({
+ id: '1',
+ name: expectedValue,
+ }),
+ );
+ });
+
+ it('skip unsaved changes for expired data', () => {
+ const apiDataWithLocalStorage = {
+ ...apiData,
+ active_tab: {
+ ...apiDataWithTabState.active_tab,
+ id: 2,
+ label: 'persisted tab',
+ table_schemas: [],
+ extra_json: {
+ updatedAt: lastUpdatedTime,
+ },
+ },
+ tab_state_ids: [{ id: 2, label: '' }],
+ };
+ expect(
+ getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[1],
+ ).toEqual(
+ expect.objectContaining({
+ id: '2',
+ name: apiDataWithLocalStorage.active_tab.label,
+ }),
+ );
+ });
+
+ it('skip unsaved changes for legacy cache data', () => {
+ const apiDataWithLocalStorage = {
+ ...apiData,
+ active_tab: {
+ ...apiDataWithTabState.active_tab,
+ id: 3,
+ label: 'persisted tab',
+ table_schemas: [],
+ extra_json: {
+ updatedAt: lastUpdatedTime,
+ },
+ },
+ tab_state_ids: [{ id: 3, label: '' }],
+ };
+ expect(
+ getInitialState(apiDataWithLocalStorage).sqlLab.queryEditors[2],
+ ).toEqual(
+ expect.objectContaining({
+ id: '3',
+ name: apiDataWithLocalStorage.active_tab.label,
+ }),
+ );
+ });
+ });
});
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
index e2aa1d4688..8d72a313b2 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
@@ -20,11 +20,13 @@ import { t } from '@superset-ui/core';
import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages';
import type { BootstrapData } from 'src/types/bootstrapTypes';
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
-import type {
+import {
QueryEditor,
UnsavedQueryEditor,
SqlLabRootState,
Table,
+ LatestQueryEditorVersion,
+ QueryEditorVersion,
} from 'src/SqlLab/types';
export function dedupeTabHistory(tabHistory: string[]) {
@@ -53,6 +55,7 @@ export default function getInitialState({
*/
let queryEditors: Record = {};
const defaultQueryEditor = {
+ version: LatestQueryEditorVersion,
loaded: true,
name: t('Untitled query'),
sql: 'SELECT *\nFROM\nWHERE',
@@ -73,6 +76,7 @@ export default function getInitialState({
let queryEditor: QueryEditor;
if (activeTab && activeTab.id === id) {
queryEditor = {
+ version: activeTab.extra_json?.version ?? QueryEditorVersion.v1,
id: id.toString(),
loaded: true,
name: activeTab.label,
@@ -88,6 +92,7 @@ export default function getInitialState({
schema: activeTab.schema,
queryLimit: activeTab.query_limit,
hideLeftBar: activeTab.hide_left_bar,
+ updatedAt: activeTab.extra_json?.updatedAt,
};
} else {
// dummy state, actual state will be loaded on tab switch
@@ -103,11 +108,12 @@ export default function getInitialState({
[queryEditor.id]: queryEditor,
};
});
-
const tabHistory = activeTab ? [activeTab.id.toString()] : [];
let tables = {} as Record;
- const editorTabLastUpdatedAt = Date.now();
+ let editorTabLastUpdatedAt = Date.now();
if (activeTab) {
+ editorTabLastUpdatedAt =
+ activeTab.extra_json?.updatedAt || editorTabLastUpdatedAt;
activeTab.table_schemas
.filter(tableSchema => tableSchema.description !== null)
.forEach(tableSchema => {
@@ -153,37 +159,57 @@ export default function getInitialState({
// add query editors and tables to state with a special flag so they can
// be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
sqlLab.queryEditors.forEach(qe => {
+ const hasConflictFromBackend = Boolean(queryEditors[qe.id]);
+ const unsavedUpdatedAt = queryEditors[qe.id]?.updatedAt;
+ const hasUnsavedUpdateSinceLastSave =
+ qe.updatedAt &&
+ (!unsavedUpdatedAt || qe.updatedAt > unsavedUpdatedAt);
+ const cachedQueryEditor: UnsavedQueryEditor =
+ !hasConflictFromBackend || hasUnsavedUpdateSinceLastSave ? qe : {};
queryEditors = {
...queryEditors,
[qe.id]: {
...queryEditors[qe.id],
- ...qe,
- name: qe.title || qe.name,
- ...(unsavedQueryEditor.id === qe.id && unsavedQueryEditor),
- inLocalStorage: true,
+ ...cachedQueryEditor,
+ name:
+ cachedQueryEditor.title ||
+ cachedQueryEditor.name ||
+ queryEditors[qe.id]?.name,
+ ...(cachedQueryEditor.id &&
+ unsavedQueryEditor.id === qe.id &&
+ unsavedQueryEditor),
+ inLocalStorage: !hasConflictFromBackend,
loaded: true,
},
};
});
const expandedTables = new Set();
- tables = sqlLab.tables.reduce((merged, table) => {
- const expanded = !expandedTables.has(table.queryEditorId);
- if (expanded) {
- expandedTables.add(table.queryEditorId);
- }
- return {
- ...merged,
- [table.id]: {
- ...tables[table.id],
- ...table,
- expanded,
- },
- };
- }, tables);
- Object.values(sqlLab.queries).forEach(query => {
- queries[query.id] = { ...query, inLocalStorage: true };
- });
- tabHistory.push(...sqlLab.tabHistory);
+
+ if (sqlLab.tables) {
+ tables = sqlLab.tables.reduce((merged, table) => {
+ const expanded = !expandedTables.has(table.queryEditorId);
+ if (expanded) {
+ expandedTables.add(table.queryEditorId);
+ }
+ return {
+ ...merged,
+ [table.id]: {
+ ...tables[table.id],
+ ...table,
+ expanded,
+ inLocalStorage: true,
+ },
+ };
+ }, tables);
+ }
+ if (sqlLab.queries) {
+ Object.values(sqlLab.queries).forEach(query => {
+ queries[query.id] = { ...query, inLocalStorage: true };
+ });
+ }
+ if (sqlLab.tabHistory) {
+ tabHistory.push(...sqlLab.tabHistory);
+ }
}
}
} catch (error) {
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index 278109564f..59bd0558a1 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -29,7 +29,7 @@ import {
extendArr,
} from '../../reduxUtils';
-function alterUnsavedQueryEditorState(state, updatedState, id) {
+function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) {
if (state.tabHistory[state.tabHistory.length - 1] !== id) {
const { queryEditors } = alterInArr(
state,
@@ -45,6 +45,7 @@ function alterUnsavedQueryEditorState(state, updatedState, id) {
unsavedQueryEditor: {
...(state.unsavedQueryEditor.id === id && state.unsavedQueryEditor),
...(id ? { id, ...updatedState } : state.unsavedQueryEditor),
+ ...(!silent && { updatedAt: new Date().getTime() }),
},
};
}
@@ -64,7 +65,10 @@ export default function sqlLabReducer(state = {}, action) {
...mergeUnsavedState,
tabHistory: [...state.tabHistory, action.queryEditor.id],
};
- return addToArr(newState, 'queryEditors', action.queryEditor);
+ return addToArr(newState, 'queryEditors', {
+ ...action.queryEditor,
+ updatedAt: new Date().getTime(),
+ });
},
[actions.QUERY_EDITOR_SAVED]() {
const { query, result, clientId } = action;
@@ -308,6 +312,7 @@ export default function sqlLabReducer(state = {}, action) {
latestQueryId: action.query.id,
},
action.query.sqlEditorId,
+ action.query.isDataPreview,
),
};
},
@@ -378,14 +383,12 @@ export default function sqlLabReducer(state = {}, action) {
qeIds.indexOf(action.queryEditor?.id) > -1 &&
state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id
) {
- const mergeUnsavedState = alterInArr(
- state,
- 'queryEditors',
- state.unsavedQueryEditor,
- {
+ const mergeUnsavedState = {
+ ...alterInArr(state, 'queryEditors', state.unsavedQueryEditor, {
...state.unsavedQueryEditor,
- },
- );
+ }),
+ unsavedQueryEditor: {},
+ };
return {
...(action.queryEditor.id === state.unsavedQueryEditor.id
? alterInArr(
@@ -522,12 +525,20 @@ export default function sqlLabReducer(state = {}, action) {
};
},
[actions.QUERY_EDITOR_SET_SQL]() {
+ const { unsavedQueryEditor } = state;
+ if (
+ unsavedQueryEditor?.id === action.queryEditor.id &&
+ unsavedQueryEditor.sql === action.sql
+ ) {
+ return state;
+ }
return {
...state,
...alterUnsavedQueryEditorState(
state,
{
sql: action.sql,
+ ...(action.queryId && { latestQueryId: action.queryId }),
},
action.queryEditor.id,
),
@@ -566,6 +577,7 @@ export default function sqlLabReducer(state = {}, action) {
selectedText: action.sql,
},
action.queryEditor.id,
+ true,
),
};
},
@@ -708,6 +720,9 @@ export default function sqlLabReducer(state = {}, action) {
[actions.CREATE_DATASOURCE_FAILED]() {
return { ...state, isDatasourceLoading: false, errorMessage: action.err };
},
+ [actions.SET_EDITOR_TAB_LAST_UPDATE]() {
+ return { ...state, editorTabLastUpdatedAt: action.timestamp };
+ },
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index 5ecd69293c..6eb42718f0 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -29,7 +29,14 @@ export type QueryDictionary = {
[id: string]: QueryResponse;
};
+export enum QueryEditorVersion {
+ v1 = 1,
+}
+
+export const LatestQueryEditorVersion = QueryEditorVersion.v1;
+
export interface QueryEditor {
+ version: QueryEditorVersion;
id: string;
dbId?: number;
name: string;
@@ -48,6 +55,7 @@ export interface QueryEditor {
inLocalStorage?: boolean;
northPercent?: number;
southPercent?: number;
+ updatedAt?: number;
}
export type toastState = {
@@ -86,7 +94,7 @@ export type SqlLabRootState = {
errorMessage: string | null;
unsavedQueryEditor: UnsavedQueryEditor;
queryCostEstimates?: Record;
- editorTabLastUpdatedAt?: number;
+ editorTabLastUpdatedAt: number;
};
localStorageUsageInKilobytes: number;
messageToasts: toastState[];
diff --git a/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js b/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js
index 9984e1efca..f08fccbef7 100644
--- a/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js
+++ b/superset-frontend/src/SqlLab/utils/emptyQueryResults.test.js
@@ -83,10 +83,11 @@ describe('reduxStateToLocalStorageHelper', () => {
});
it('should only return selected keys for query editor', () => {
- const queryEditors = [defaultQueryEditor];
- expect(Object.keys(queryEditors[0])).toContain('schema');
+ const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }];
+ expect(Object.keys(queryEditors[0])).toContain('dummy');
const clearedQueryEditors = clearQueryEditors(queryEditors);
- expect(Object.keys(clearedQueryEditors)[0]).not.toContain('schema');
+ expect(Object.keys(clearedQueryEditors[0])).toContain('version');
+ expect(Object.keys(clearedQueryEditors[0])).not.toContain('dummy');
});
});
diff --git a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js
index 281f08bcb3..f82711362d 100644
--- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js
+++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.js
@@ -26,6 +26,7 @@ import {
} from '../constants';
const PERSISTENT_QUERY_EDITOR_KEYS = new Set([
+ 'version',
'remoteId',
'autorun',
'dbId',
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
new file mode 100644
index 0000000000..d0f2230f13
--- /dev/null
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts
@@ -0,0 +1,99 @@
+/**
+ * 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 fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+ createWrapper,
+ defaultStore as store,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { LatestQueryEditorVersion } from 'src/SqlLab/types';
+import { useUpdateSqlEditorTabMutation } from './sqlEditorTabs';
+
+const expectedQueryEditor = {
+ version: LatestQueryEditorVersion,
+ id: '123',
+ dbId: 456,
+ name: 'tab 1',
+ sql: 'SELECT * from example_table',
+ schema: 'my_schema',
+ templateParams: '{"a": 1, "v": "str"}',
+ queryLimit: 1000,
+ remoteId: null,
+ autorun: false,
+ hideLeftBar: false,
+ updatedAt: Date.now(),
+};
+
+afterEach(() => {
+ fetchMock.reset();
+ act(() => {
+ store.dispatch(api.util.resetApiState());
+ });
+});
+
+test('puts api request with formData', async () => {
+ const tabStateMutationApiRoute = `glob:*/tabstateview/${expectedQueryEditor.id}`;
+ fetchMock.put(tabStateMutationApiRoute, 200);
+ const { result, waitFor } = renderHook(
+ () => useUpdateSqlEditorTabMutation(),
+ {
+ wrapper: createWrapper({
+ useRedux: true,
+ store,
+ }),
+ },
+ );
+ act(() => {
+ result.current[0]({
+ queryEditor: expectedQueryEditor,
+ });
+ });
+ await waitFor(() =>
+ expect(fetchMock.calls(tabStateMutationApiRoute).length).toBe(1),
+ );
+ const formData = fetchMock.calls(tabStateMutationApiRoute)[0][1]
+ ?.body as FormData;
+ expect(formData.get('database_id')).toBe(`${expectedQueryEditor.dbId}`);
+ expect(formData.get('schema')).toBe(
+ JSON.stringify(`${expectedQueryEditor.schema}`),
+ );
+ expect(formData.get('sql')).toBe(
+ JSON.stringify(`${expectedQueryEditor.sql}`),
+ );
+ expect(formData.get('label')).toBe(
+ JSON.stringify(`${expectedQueryEditor.name}`),
+ );
+ expect(formData.get('query_limit')).toBe(`${expectedQueryEditor.queryLimit}`);
+ expect(formData.has('latest_query_id')).toBe(false);
+ expect(formData.get('template_params')).toBe(
+ JSON.stringify(`${expectedQueryEditor.templateParams}`),
+ );
+ expect(formData.get('hide_left_bar')).toBe(
+ `${expectedQueryEditor.hideLeftBar}`,
+ );
+ expect(formData.get('extra_json')).toBe(
+ JSON.stringify(
+ JSON.stringify({
+ updatedAt: expectedQueryEditor.updatedAt,
+ version: LatestQueryEditorVersion,
+ }),
+ ),
+ );
+});
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
new file mode 100644
index 0000000000..71e0cf2936
--- /dev/null
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
@@ -0,0 +1,70 @@
+/**
+ * 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 { pickBy } from 'lodash';
+import { QueryEditor, LatestQueryEditorVersion } from 'src/SqlLab/types';
+import { api, JsonResponse } from './queryApi';
+
+export type EditorMutationParams = {
+ queryEditor: QueryEditor;
+ extra?: Record;
+};
+
+const sqlEditorApi = api.injectEndpoints({
+ endpoints: builder => ({
+ updateSqlEditorTab: builder.mutation({
+ query: ({
+ queryEditor: {
+ version = LatestQueryEditorVersion,
+ id,
+ dbId,
+ schema,
+ queryLimit,
+ sql,
+ name,
+ latestQueryId,
+ hideLeftBar,
+ templateParams,
+ autorun,
+ updatedAt,
+ },
+ extra,
+ }) => ({
+ method: 'PUT',
+ endpoint: encodeURI(`/tabstateview/${id}`),
+ postPayload: pickBy(
+ {
+ database_id: dbId,
+ schema,
+ sql,
+ label: name,
+ query_limit: queryLimit,
+ latest_query_id: latestQueryId,
+ template_params: templateParams,
+ hide_left_bar: hideLeftBar,
+ autorun,
+ extra_json: JSON.stringify({ updatedAt, version, ...extra }),
+ },
+ value => value !== undefined,
+ ),
+ }),
+ }),
+ }),
+});
+
+export const { useUpdateSqlEditorTabMutation } = sqlEditorApi;
diff --git a/superset-frontend/src/hooks/apiResources/sqlLab.ts b/superset-frontend/src/hooks/apiResources/sqlLab.ts
index 123db414e2..16e8ffde6c 100644
--- a/superset-frontend/src/hooks/apiResources/sqlLab.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlLab.ts
@@ -50,7 +50,7 @@ export type InitialState = {
template_params: string | null;
hide_left_bar?: boolean;
saved_query: { id: number } | null;
- extra_json?: object;
+ extra_json?: Record;
};
databases: object[];
queries: Record<
diff --git a/superset-frontend/src/hooks/useDebounceValue.ts b/superset-frontend/src/hooks/useDebounceValue.ts
index 711b2dbd5a..862c837707 100644
--- a/superset-frontend/src/hooks/useDebounceValue.ts
+++ b/superset-frontend/src/hooks/useDebounceValue.ts
@@ -19,8 +19,8 @@
import { useState, useEffect } from 'react';
import { FAST_DEBOUNCE } from 'src/constants';
-export function useDebounceValue(value: string, delay = FAST_DEBOUNCE) {
- const [debouncedValue, setDebouncedValue] = useState(value);
+export function useDebounceValue(value: T, delay = FAST_DEBOUNCE) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {
diff --git a/superset-frontend/src/pages/SqlLab/index.tsx b/superset-frontend/src/pages/SqlLab/index.tsx
index e9f84f1b1d..3f19b54c29 100644
--- a/superset-frontend/src/pages/SqlLab/index.tsx
+++ b/superset-frontend/src/pages/SqlLab/index.tsx
@@ -18,7 +18,7 @@
*/
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { css } from '@superset-ui/core';
+import { css, isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import { useSqlLabInitialState } from 'src/hooks/apiResources/sqlLab';
import type { InitialState } from 'src/hooks/apiResources/sqlLab';
import { resetState } from 'src/SqlLab/actions/sqlLab';
@@ -27,16 +27,17 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
import { SqlLabGlobalStyles } from 'src/SqlLab//SqlLabGlobalStyles';
import App from 'src/SqlLab/components/App';
import Loading from 'src/components/Loading';
+import EditorAutoSync from 'src/SqlLab/components/EditorAutoSync';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { LocationProvider } from './LocationContext';
export default function SqlLab() {
- const editorTabLastUpdatedAt = useSelector(
- state => state.sqlLab.editorTabLastUpdatedAt || 0,
+ const lastInitializedAt = useSelector(
+ state => state.sqlLab.queriesLastUpdate || 0,
);
const { data, isLoading, isError, error, fulfilledTimeStamp } =
useSqlLabInitialState();
- const shouldInitialize = editorTabLastUpdatedAt <= (fulfilledTimeStamp || 0);
+ const shouldInitialize = lastInitializedAt <= (fulfilledTimeStamp || 0);
const dispatch = useDispatch();
const initBootstrapData = useEffectEvent(
@@ -72,6 +73,9 @@ export default function SqlLab() {
>
+ {isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && (
+
+ )}
);
diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts
index 55df81c588..a9c3a9eb13 100644
--- a/superset-frontend/src/views/store.ts
+++ b/superset-frontend/src/views/store.ts
@@ -38,7 +38,6 @@ import logger from 'src/middleware/loggerMiddleware';
import saveModal from 'src/explore/reducers/saveModalReducer';
import explore from 'src/explore/reducers/exploreReducer';
import exploreDatasources from 'src/explore/reducers/datasourcesReducer';
-import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
@@ -167,9 +166,7 @@ export function setupStore({
},
middleware: getMiddleware,
devTools: process.env.WEBPACK_MODE === 'development' && !disableDebugger,
- ...(!isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && {
- enhancers: [persistSqlLabStateEnhancer as StoreEnhancer],
- }),
+ enhancers: [persistSqlLabStateEnhancer as StoreEnhancer],
...overrides,
});
}
From e7797b65d1dadc1c466d1852747657b0aade9690 Mon Sep 17 00:00:00 2001
From: Ross Mabbett <92495987+rtexelm@users.noreply.github.com>
Date: Mon, 20 Nov 2023 14:24:02 -0500
Subject: [PATCH 042/119] fix(horizontal filter bar filter labels): Increase
max-width to 96px (#25883)
Co-authored-by: Elizabeth Thompson
---
.../nativeFilters/FilterBar/FilterControls/FilterControl.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
index 37739e5370..96f51f5359 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
@@ -49,7 +49,6 @@ const VerticalFilterControlTitle = styled.h4`
const HorizontalFilterControlTitle = styled(VerticalFilterControlTitle)`
font-weight: ${({ theme }) => theme.typography.weights.normal};
color: ${({ theme }) => theme.colors.grayscale.base};
- max-width: ${({ theme }) => theme.gridUnit * 15}px;
${truncationCSS};
`;
From dd58b31cc45452e79ce5daca4b3e3b3880cd51e0 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Mon, 20 Nov 2023 17:25:41 -0800
Subject: [PATCH 043/119] chore(sqlalchemy): Remove erroneous SQLAlchemy ORM
session.merge operations (#24776)
---
superset/examples/bart_lines.py | 2 +-
superset/examples/country_map.py | 2 +-
superset/examples/css_templates.py | 4 ++--
superset/examples/deck.py | 2 +-
superset/examples/energy.py | 2 +-
superset/examples/flights.py | 2 +-
superset/examples/long_lat.py | 2 +-
superset/examples/misc_dashboard.py | 2 +-
superset/examples/multiformat_time_series.py | 2 +-
superset/examples/paris.py | 2 +-
superset/examples/random_time_series.py | 2 +-
superset/examples/sf_population_polygons.py | 2 +-
superset/examples/tabbed_dashboard.py | 3 +--
superset/examples/world_bank.py | 4 ++--
superset/key_value/commands/update.py | 1 -
superset/key_value/commands/upsert.py | 1 -
superset/migrations/shared/migrate_viz/base.py | 12 ++++--------
superset/migrations/shared/security_converge.py | 1 -
...25_08-54_c3a8f8611885_materializing_permission.py | 1 -
...16-09-07_23-50_33d996bcc382_update_slice_model.py | 2 --
...-24_12-31_db0c65b146bd_update_slice_model_json.py | 1 -
...2f7c195a_rewriting_url_from_shortner_with_new_.py | 1 -
...12-08_08-19_67a6ac9b727b_update_spatial_params.py | 1 -
...-12-17_11-06_21e88bc06c02_annotation_migration.py | 2 --
.../2018-02-13_08-07_e866bd2d4976_smaller_grid.py | 2 --
...-19_bf706ae5eb46_cal_heatmap_metric_to_metrics.py | 1 -
...59_bebcf3fed1fe_convert_dashboard_v1_positions.py | 1 -
.../versions/2018-08-01_11-47_7fcdcde0761c_.py | 1 -
...0aa3f04bc82_add_parent_ids_in_dashboard_layout.py | 2 --
...07_14-13_3325d4caccc8_dashboard_scoped_filters.py | 2 --
...4_978245563a02_migrate_iframe_to_dash_markdown.py | 1 -
...7_b56500de1855_add_uuid_column_to_import_mixin.py | 1 -
...6_1412ec1e5a7b_legacy_force_directed_to_echart.py | 2 --
...9d665d_fix_table_chart_conditional_formatting_.py | 1 -
superset/reports/commands/execute.py | 2 --
superset/security/manager.py | 2 --
tests/integration_tests/charts/data/api_tests.py | 3 ---
tests/integration_tests/conftest.py | 8 +++++---
tests/integration_tests/dashboard_tests.py | 7 ++-----
tests/integration_tests/dashboard_utils.py | 2 +-
tests/integration_tests/dashboards/dao_tests.py | 2 --
.../dashboards/dashboard_test_utils.py | 2 --
.../dashboards/security/security_dataset_tests.py | 4 ++--
tests/integration_tests/datasource_tests.py | 1 -
tests/integration_tests/fixtures/energy_dashboard.py | 2 --
.../c747c78868b6_migrating_legacy_treemap__tests.py | 4 ++--
tests/integration_tests/reports/commands_tests.py | 2 --
.../security/migrate_roles_tests.py | 1 -
tests/unit_tests/migrations/viz/utils.py | 4 ++--
49 files changed, 34 insertions(+), 82 deletions(-)
diff --git a/superset/examples/bart_lines.py b/superset/examples/bart_lines.py
index e18f6e4632..ad96aecac4 100644
--- a/superset/examples/bart_lines.py
+++ b/superset/examples/bart_lines.py
@@ -60,9 +60,9 @@ def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = "BART lines"
tbl.database = database
tbl.filter_select_enabled = True
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
diff --git a/superset/examples/country_map.py b/superset/examples/country_map.py
index 4331033ca8..3caf637584 100644
--- a/superset/examples/country_map.py
+++ b/superset/examples/country_map.py
@@ -80,13 +80,13 @@ def load_country_map_data(only_metadata: bool = False, force: bool = False) -> N
obj = db.session.query(table).filter_by(table_name=tbl_name).first()
if not obj:
obj = table(table_name=tbl_name, schema=schema)
+ db.session.add(obj)
obj.main_dttm_col = "dttm"
obj.database = database
obj.filter_select_enabled = True
if not any(col.metric_name == "avg__2004" for col in obj.metrics):
col = str(column("2004").compile(db.engine))
obj.metrics.append(SqlMetric(metric_name="avg__2004", expression=f"AVG({col})"))
- db.session.merge(obj)
db.session.commit()
obj.fetch_metadata()
tbl = obj
diff --git a/superset/examples/css_templates.py b/superset/examples/css_templates.py
index 4f3f355895..2f67d2e1fa 100644
--- a/superset/examples/css_templates.py
+++ b/superset/examples/css_templates.py
@@ -27,6 +27,7 @@ def load_css_templates() -> None:
obj = db.session.query(CssTemplate).filter_by(template_name="Flat").first()
if not obj:
obj = CssTemplate(template_name="Flat")
+ db.session.add(obj)
css = textwrap.dedent(
"""\
.navbar {
@@ -51,12 +52,12 @@ def load_css_templates() -> None:
"""
)
obj.css = css
- db.session.merge(obj)
db.session.commit()
obj = db.session.query(CssTemplate).filter_by(template_name="Courier Black").first()
if not obj:
obj = CssTemplate(template_name="Courier Black")
+ db.session.add(obj)
css = textwrap.dedent(
"""\
h2 {
@@ -96,5 +97,4 @@ def load_css_templates() -> None:
"""
)
obj.css = css
- db.session.merge(obj)
db.session.commit()
diff --git a/superset/examples/deck.py b/superset/examples/deck.py
index fc1e8ba00c..326977054e 100644
--- a/superset/examples/deck.py
+++ b/superset/examples/deck.py
@@ -532,6 +532,7 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
if not dash:
dash = Dashboard()
+ db.session.add(dash)
dash.published = True
js = POSITION_JSON
pos = json.loads(js)
@@ -540,5 +541,4 @@ def load_deck_dash() -> None: # pylint: disable=too-many-statements
dash.dashboard_title = title
dash.slug = slug
dash.slices = slices
- db.session.merge(dash)
db.session.commit()
diff --git a/superset/examples/energy.py b/superset/examples/energy.py
index 6688e5d088..998ee97a30 100644
--- a/superset/examples/energy.py
+++ b/superset/examples/energy.py
@@ -66,6 +66,7 @@ def load_energy(
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = "Energy consumption"
tbl.database = database
tbl.filter_select_enabled = True
@@ -76,7 +77,6 @@ def load_energy(
SqlMetric(metric_name="sum__value", expression=f"SUM({col})")
)
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
diff --git a/superset/examples/flights.py b/superset/examples/flights.py
index 7c8f980298..c7890cfa18 100644
--- a/superset/examples/flights.py
+++ b/superset/examples/flights.py
@@ -63,10 +63,10 @@ def load_flights(only_metadata: bool = False, force: bool = False) -> None:
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = "Random set of flights in the US"
tbl.database = database
tbl.filter_select_enabled = True
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
print("Done loading table!")
diff --git a/superset/examples/long_lat.py b/superset/examples/long_lat.py
index 88b45548f4..6f7cc64020 100644
--- a/superset/examples/long_lat.py
+++ b/superset/examples/long_lat.py
@@ -92,10 +92,10 @@ def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None
obj = db.session.query(table).filter_by(table_name=tbl_name).first()
if not obj:
obj = table(table_name=tbl_name, schema=schema)
+ db.session.add(obj)
obj.main_dttm_col = "datetime"
obj.database = database
obj.filter_select_enabled = True
- db.session.merge(obj)
db.session.commit()
obj.fetch_metadata()
tbl = obj
diff --git a/superset/examples/misc_dashboard.py b/superset/examples/misc_dashboard.py
index 4146ea1bd3..6336a78820 100644
--- a/superset/examples/misc_dashboard.py
+++ b/superset/examples/misc_dashboard.py
@@ -34,6 +34,7 @@ def load_misc_dashboard() -> None:
if not dash:
dash = Dashboard()
+ db.session.add(dash)
js = textwrap.dedent(
"""\
{
@@ -215,5 +216,4 @@ def load_misc_dashboard() -> None:
dash.position_json = json.dumps(pos, indent=4)
dash.slug = DASH_SLUG
dash.slices = slices
- db.session.merge(dash)
db.session.commit()
diff --git a/superset/examples/multiformat_time_series.py b/superset/examples/multiformat_time_series.py
index 6bad2a7ac2..4c1e796316 100644
--- a/superset/examples/multiformat_time_series.py
+++ b/superset/examples/multiformat_time_series.py
@@ -82,6 +82,7 @@ def load_multiformat_time_series( # pylint: disable=too-many-locals
obj = db.session.query(table).filter_by(table_name=tbl_name).first()
if not obj:
obj = table(table_name=tbl_name, schema=schema)
+ db.session.add(obj)
obj.main_dttm_col = "ds"
obj.database = database
obj.filter_select_enabled = True
@@ -100,7 +101,6 @@ def load_multiformat_time_series( # pylint: disable=too-many-locals
col.python_date_format = dttm_and_expr[0]
col.database_expression = dttm_and_expr[1]
col.is_dttm = True
- db.session.merge(obj)
db.session.commit()
obj.fetch_metadata()
tbl = obj
diff --git a/superset/examples/paris.py b/superset/examples/paris.py
index 1180c428fe..fa5c77b84d 100644
--- a/superset/examples/paris.py
+++ b/superset/examples/paris.py
@@ -57,9 +57,9 @@ def load_paris_iris_geojson(only_metadata: bool = False, force: bool = False) ->
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = "Map of Paris"
tbl.database = database
tbl.filter_select_enabled = True
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
diff --git a/superset/examples/random_time_series.py b/superset/examples/random_time_series.py
index 9a296ec2c4..4a2d10aee9 100644
--- a/superset/examples/random_time_series.py
+++ b/superset/examples/random_time_series.py
@@ -67,10 +67,10 @@ def load_random_time_series_data(
obj = db.session.query(table).filter_by(table_name=tbl_name).first()
if not obj:
obj = table(table_name=tbl_name, schema=schema)
+ db.session.add(obj)
obj.main_dttm_col = "ds"
obj.database = database
obj.filter_select_enabled = True
- db.session.merge(obj)
db.session.commit()
obj.fetch_metadata()
tbl = obj
diff --git a/superset/examples/sf_population_polygons.py b/superset/examples/sf_population_polygons.py
index 76c039afb8..ba5905f58a 100644
--- a/superset/examples/sf_population_polygons.py
+++ b/superset/examples/sf_population_polygons.py
@@ -59,9 +59,9 @@ def load_sf_population_polygons(
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = "Population density of San Francisco"
tbl.database = database
tbl.filter_select_enabled = True
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
diff --git a/superset/examples/tabbed_dashboard.py b/superset/examples/tabbed_dashboard.py
index 58c0ba3e4c..b057263345 100644
--- a/superset/examples/tabbed_dashboard.py
+++ b/superset/examples/tabbed_dashboard.py
@@ -33,6 +33,7 @@ def load_tabbed_dashboard(_: bool = False) -> None:
if not dash:
dash = Dashboard()
+ db.session.add(dash)
js = textwrap.dedent(
"""
@@ -556,6 +557,4 @@ def load_tabbed_dashboard(_: bool = False) -> None:
dash.slices = slices
dash.dashboard_title = "Tabbed Dashboard"
dash.slug = slug
-
- db.session.merge(dash)
db.session.commit()
diff --git a/superset/examples/world_bank.py b/superset/examples/world_bank.py
index 31d956f5fd..5e86cff1e4 100644
--- a/superset/examples/world_bank.py
+++ b/superset/examples/world_bank.py
@@ -87,6 +87,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:
tbl = table(table_name=tbl_name, schema=schema)
+ db.session.add(tbl)
tbl.description = utils.readfile(
os.path.join(get_examples_folder(), "countries.md")
)
@@ -110,7 +111,6 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
SqlMetric(metric_name=metric, expression=f"{aggr_func}({col})")
)
- db.session.merge(tbl)
db.session.commit()
tbl.fetch_metadata()
@@ -126,6 +126,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
if not dash:
dash = Dashboard()
+ db.session.add(dash)
dash.published = True
pos = dashboard_positions
slices = update_slice_ids(pos)
@@ -134,7 +135,6 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
dash.position_json = json.dumps(pos, indent=4)
dash.slug = slug
dash.slices = slices
- db.session.merge(dash)
db.session.commit()
diff --git a/superset/key_value/commands/update.py b/superset/key_value/commands/update.py
index 4bcd496243..ca940adf60 100644
--- a/superset/key_value/commands/update.py
+++ b/superset/key_value/commands/update.py
@@ -84,7 +84,6 @@ class UpdateKeyValueCommand(BaseCommand):
entry.expires_on = self.expires_on
entry.changed_on = datetime.now()
entry.changed_by_fk = get_user_id()
- db.session.merge(entry)
db.session.commit()
return Key(id=entry.id, uuid=entry.uuid)
diff --git a/superset/key_value/commands/upsert.py b/superset/key_value/commands/upsert.py
index 9a4092c002..66d6785f2e 100644
--- a/superset/key_value/commands/upsert.py
+++ b/superset/key_value/commands/upsert.py
@@ -88,7 +88,6 @@ class UpsertKeyValueCommand(BaseCommand):
entry.expires_on = self.expires_on
entry.changed_on = datetime.now()
entry.changed_by_fk = get_user_id()
- db.session.merge(entry)
db.session.commit()
return Key(entry.id, entry.uuid)
diff --git a/superset/migrations/shared/migrate_viz/base.py b/superset/migrations/shared/migrate_viz/base.py
index a3360d365b..f9e1b9d3c9 100644
--- a/superset/migrations/shared/migrate_viz/base.py
+++ b/superset/migrations/shared/migrate_viz/base.py
@@ -123,7 +123,7 @@ class MigrateViz:
]
@classmethod
- def upgrade_slice(cls, slc: Slice) -> Slice:
+ def upgrade_slice(cls, slc: Slice) -> None:
clz = cls(slc.params)
form_data_bak = copy.deepcopy(clz.data)
@@ -141,10 +141,9 @@ class MigrateViz:
if "form_data" in (query_context := try_load_json(slc.query_context)):
query_context["form_data"] = clz.data
slc.query_context = json.dumps(query_context)
- return slc
@classmethod
- def downgrade_slice(cls, slc: Slice) -> Slice:
+ def downgrade_slice(cls, slc: Slice) -> None:
form_data = try_load_json(slc.params)
if "viz_type" in (form_data_bak := form_data.get(FORM_DATA_BAK_FIELD_NAME, {})):
slc.params = json.dumps(form_data_bak)
@@ -153,7 +152,6 @@ class MigrateViz:
if "form_data" in query_context:
query_context["form_data"] = form_data_bak
slc.query_context = json.dumps(query_context)
- return slc
@classmethod
def upgrade(cls, session: Session) -> None:
@@ -162,8 +160,7 @@ class MigrateViz:
slices,
lambda current, total: print(f"Upgraded {current}/{total} charts"),
):
- new_viz = cls.upgrade_slice(slc)
- session.merge(new_viz)
+ cls.upgrade_slice(slc)
@classmethod
def downgrade(cls, session: Session) -> None:
@@ -177,5 +174,4 @@ class MigrateViz:
slices,
lambda current, total: print(f"Downgraded {current}/{total} charts"),
):
- new_viz = cls.downgrade_slice(slc)
- session.merge(new_viz)
+ cls.downgrade_slice(slc)
diff --git a/superset/migrations/shared/security_converge.py b/superset/migrations/shared/security_converge.py
index 9b1730a2a1..42a68acb24 100644
--- a/superset/migrations/shared/security_converge.py
+++ b/superset/migrations/shared/security_converge.py
@@ -243,7 +243,6 @@ def migrate_roles(
if new_pvm not in role.permissions:
logger.info(f"Add {new_pvm} to {role}")
role.permissions.append(new_pvm)
- session.merge(role)
# Delete old permissions
_delete_old_permissions(session, pvm_map)
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 b92378f092..c3d04e875a 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
@@ -56,7 +56,6 @@ def upgrade():
for slc in session.query(Slice).all():
if slc.datasource:
slc.perm = slc.datasource.perm
- session.merge(slc)
session.commit()
db.session.close()
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 f4373a3f38..8f4542cb3c 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
@@ -56,7 +56,6 @@ def upgrade():
slc.datasource_id = slc.druid_datasource_id
if slc.table_id:
slc.datasource_id = slc.table_id
- session.merge(slc)
session.commit()
session.close()
@@ -69,7 +68,6 @@ def downgrade():
slc.druid_datasource_id = slc.datasource_id
if slc.datasource_type == "table":
slc.table_id = slc.datasource_id
- session.merge(slc)
session.commit()
session.close()
op.drop_column("slices", "datasource_id")
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 1f3dbab636..0bae8cd9a3 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
@@ -57,7 +57,6 @@ def upgrade():
try:
d = json.loads(slc.params or "{}")
slc.params = json.dumps(d, indent=2, sort_keys=True)
- session.merge(slc)
session.commit()
print(f"Upgraded ({i}/{slice_len}): {slc.slice_name}")
except Exception as ex:
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 8e97ada3cd..8dafb77bee 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
@@ -80,7 +80,6 @@ def upgrade():
"/".join(split[:-1]) + "/?form_data=" + parse.quote_plus(json.dumps(d))
)
url.url = newurl
- session.merge(url)
session.commit()
print(f"Updating url ({i}/{urls_len})")
session.close()
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 6073e8b84c..81bbb47914 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
@@ -58,7 +58,6 @@ def upgrade():
del params["latitude"]
del params["longitude"]
slc.params = json.dumps(params)
- session.merge(slc)
session.commit()
session.close()
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 4b1b807a6f..785e282397 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
@@ -69,7 +69,6 @@ def upgrade():
)
params["annotation_layers"] = new_layers
slc.params = json.dumps(params)
- session.merge(slc)
session.commit()
session.close()
@@ -86,6 +85,5 @@ def downgrade():
if layers:
params["annotation_layers"] = [layer["value"] for layer in layers]
slc.params = json.dumps(params)
- session.merge(slc)
session.commit()
session.close()
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 bf6276d702..6241ab2a39 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
@@ -62,7 +62,6 @@ def upgrade():
pos["v"] = 1
dashboard.position_json = json.dumps(positions, indent=2)
- session.merge(dashboard)
session.commit()
session.close()
@@ -85,6 +84,5 @@ def downgrade():
pos["v"] = 0
dashboard.position_json = json.dumps(positions, indent=2)
- session.merge(dashboard)
session.commit()
pass
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 49b19b9c69..2aa703cfec 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
@@ -59,7 +59,6 @@ def upgrade():
params["metrics"] = [params.get("metric")]
del params["metric"]
slc.params = json.dumps(params, indent=2, sort_keys=True)
- session.merge(slc)
session.commit()
print(f"Upgraded ({i}/{slice_len}): {slc.slice_name}")
except Exception as ex:
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 620e2c5008..3dc0bcc455 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
@@ -647,7 +647,6 @@ def upgrade():
sorted_by_key = collections.OrderedDict(sorted(v2_layout.items()))
dashboard.position_json = json.dumps(sorted_by_key, indent=2)
- session.merge(dashboard)
session.commit()
else:
print(f"Skip converted dash_id: {dashboard.id}")
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 02021799e9..111cea4506 100644
--- a/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py
+++ b/superset/migrations/versions/2018-08-01_11-47_7fcdcde0761c_.py
@@ -76,7 +76,6 @@ def upgrade():
dashboard.id, len(original_text), len(text)
)
)
- session.merge(dashboard)
session.commit()
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 c6361009ee..47c8a6cbcc 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
@@ -80,7 +80,6 @@ def upgrade():
dashboard.position_json = json.dumps(
layout, indent=None, separators=(",", ":"), sort_keys=True
)
- session.merge(dashboard)
except Exception as ex:
logging.exception(ex)
@@ -110,7 +109,6 @@ def downgrade():
dashboard.position_json = json.dumps(
layout, indent=None, separators=(",", ":"), sort_keys=True
)
- session.merge(dashboard)
except Exception as ex:
logging.exception(ex)
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 5aa38fd13a..ec02a8ca84 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
@@ -99,8 +99,6 @@ def upgrade():
)
else:
dashboard.json_metadata = None
-
- session.merge(dashboard)
except Exception as ex:
logging.exception(f"dashboard {dashboard.id} has error: {ex}")
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 4202de4560..70f1fcc07c 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
@@ -163,7 +163,6 @@ def upgrade():
separators=(",", ":"),
sort_keys=True,
)
- session.merge(dashboard)
# remove iframe, separator and markup charts
slices_to_remove = (
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 9ff117b1e2..574ca1536a 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
@@ -96,7 +96,6 @@ def update_position_json(dashboard, session, uuid_map):
del object_["meta"]["uuid"]
dashboard.position_json = json.dumps(layout, indent=4)
- session.merge(dashboard)
def update_dashboards(session, uuid_map):
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 4407c1f8b7..24a81270d1 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
@@ -70,7 +70,6 @@ def upgrade():
slc.params = json.dumps(params)
slc.viz_type = "graph_chart"
- session.merge(slc)
session.commit()
session.close()
@@ -100,6 +99,5 @@ def downgrade():
slc.params = json.dumps(params)
slc.viz_type = "directed_force"
- session.merge(slc)
session.commit()
session.close()
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 30caf7efa1..8d9f070935 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
@@ -72,7 +72,6 @@ def upgrade():
new_conditional_formatting.append(formatter)
params["conditional_formatting"] = new_conditional_formatting
slc.params = json.dumps(params)
- session.merge(slc)
session.commit()
session.close()
diff --git a/superset/reports/commands/execute.py b/superset/reports/commands/execute.py
index 301bac4531..7cd8203a51 100644
--- a/superset/reports/commands/execute.py
+++ b/superset/reports/commands/execute.py
@@ -123,8 +123,6 @@ class BaseReportState:
self._report_schedule.last_state = state
self._report_schedule.last_eval_dttm = datetime.utcnow()
-
- self._session.merge(self._report_schedule)
self._session.commit()
def create_log(self, error_message: Optional[str] = None) -> None:
diff --git a/superset/security/manager.py b/superset/security/manager.py
index c8d2c236ab..5cfa6a15c5 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -876,7 +876,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
):
role_from_permissions.append(permission_view)
role_to.permissions = role_from_permissions
- self.get_session.merge(role_to)
self.get_session.commit()
def set_role(
@@ -898,7 +897,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
permission_view for permission_view in pvms if pvm_check(permission_view)
]
role.permissions = role_pvms
- self.get_session.merge(role)
self.get_session.commit()
def _is_admin_only(self, pvm: PermissionView) -> bool:
diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py
index 32a4be160c..5a62ce0a82 100644
--- a/tests/integration_tests/charts/data/api_tests.py
+++ b/tests/integration_tests/charts/data/api_tests.py
@@ -1293,7 +1293,6 @@ def test_chart_cache_timeout(
slice_with_cache_timeout = load_energy_table_with_slice[0]
slice_with_cache_timeout.cache_timeout = 20
- db.session.merge(slice_with_cache_timeout)
datasource: SqlaTable = (
db.session.query(SqlaTable)
@@ -1301,7 +1300,6 @@ def test_chart_cache_timeout(
.first()
)
datasource.cache_timeout = 1254
- db.session.merge(datasource)
db.session.commit()
@@ -1331,7 +1329,6 @@ def test_chart_cache_timeout_not_present(
.first()
)
datasource.cache_timeout = 1980
- db.session.merge(datasource)
db.session.commit()
rv = test_client.post(CHART_DATA_URI, json=physical_query_context)
diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py
index 28da7b7913..3e6aa96307 100644
--- a/tests/integration_tests/conftest.py
+++ b/tests/integration_tests/conftest.py
@@ -326,7 +326,8 @@ def virtual_dataset():
TableColumn(column_name="col5", type="VARCHAR(255)", table=dataset)
SqlMetric(metric_name="count", expression="count(*)", table=dataset)
- db.session.merge(dataset)
+ db.session.add(dataset)
+ db.session.commit()
yield dataset
@@ -390,7 +391,7 @@ def physical_dataset():
table=dataset,
)
SqlMetric(metric_name="count", expression="count(*)", table=dataset)
- db.session.merge(dataset)
+ db.session.add(dataset)
db.session.commit()
yield dataset
@@ -425,7 +426,8 @@ def virtual_dataset_comma_in_column_value():
TableColumn(column_name="col2", type="VARCHAR(255)", table=dataset)
SqlMetric(metric_name="count", expression="count(*)", table=dataset)
- db.session.merge(dataset)
+ db.session.add(dataset)
+ db.session.commit()
yield dataset
diff --git a/tests/integration_tests/dashboard_tests.py b/tests/integration_tests/dashboard_tests.py
index 0df9b22267..0275152231 100644
--- a/tests/integration_tests/dashboard_tests.py
+++ b/tests/integration_tests/dashboard_tests.py
@@ -78,8 +78,8 @@ class TestDashboard(SupersetTestCase):
hidden_dash.slices = [slice]
hidden_dash.published = False
- db.session.merge(published_dash)
- db.session.merge(hidden_dash)
+ db.session.add(published_dash)
+ db.session.add(hidden_dash)
yield db.session.commit()
self.revoke_public_access_to_table(table)
@@ -137,8 +137,6 @@ class TestDashboard(SupersetTestCase):
# Make the births dash published so it can be seen
births_dash = db.session.query(Dashboard).filter_by(slug="births").one()
births_dash.published = True
-
- db.session.merge(births_dash)
db.session.commit()
# Try access before adding appropriate permissions.
@@ -180,7 +178,6 @@ class TestDashboard(SupersetTestCase):
dash = db.session.query(Dashboard).filter_by(slug="births").first()
dash.owners = [security_manager.find_user("admin")]
dash.created_by = security_manager.find_user("admin")
- db.session.merge(dash)
db.session.commit()
res: Response = self.client.get("/superset/dashboard/births/")
diff --git a/tests/integration_tests/dashboard_utils.py b/tests/integration_tests/dashboard_utils.py
index c08a3ec292..41dd8dc978 100644
--- a/tests/integration_tests/dashboard_utils.py
+++ b/tests/integration_tests/dashboard_utils.py
@@ -59,11 +59,11 @@ def create_table_metadata(
normalize_columns=False,
always_filter_main_dttm=False,
)
+ db.session.add(table)
if fetch_values_predicate:
table.fetch_values_predicate = fetch_values_predicate
table.database = database
table.description = table_description
- db.session.merge(table)
db.session.commit()
return table
diff --git a/tests/integration_tests/dashboards/dao_tests.py b/tests/integration_tests/dashboards/dao_tests.py
index 91e27af3b6..8f73e5c2f8 100644
--- a/tests/integration_tests/dashboards/dao_tests.py
+++ b/tests/integration_tests/dashboards/dao_tests.py
@@ -113,7 +113,6 @@ class TestDashboardDAO(SupersetTestCase):
data.update({"foo": "bar"})
DashboardDAO.set_dash_metadata(dashboard, data)
- db.session.merge(dashboard)
db.session.commit()
new_changed_on = DashboardDAO.get_dashboard_changed_on(dashboard)
assert old_changed_on.replace(microsecond=0) < new_changed_on
@@ -125,7 +124,6 @@ class TestDashboardDAO(SupersetTestCase):
)
DashboardDAO.set_dash_metadata(dashboard, original_data)
- db.session.merge(dashboard)
db.session.commit()
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
diff --git a/tests/integration_tests/dashboards/dashboard_test_utils.py b/tests/integration_tests/dashboards/dashboard_test_utils.py
index ee8001cdba..39bce02caa 100644
--- a/tests/integration_tests/dashboards/dashboard_test_utils.py
+++ b/tests/integration_tests/dashboards/dashboard_test_utils.py
@@ -110,12 +110,10 @@ def random_str():
def grant_access_to_dashboard(dashboard, role_name):
role = security_manager.find_role(role_name)
dashboard.roles.append(role)
- db.session.merge(dashboard)
db.session.commit()
def revoke_access_to_dashboard(dashboard, role_name):
role = security_manager.find_role(role_name)
dashboard.roles.remove(role)
- db.session.merge(dashboard)
db.session.commit()
diff --git a/tests/integration_tests/dashboards/security/security_dataset_tests.py b/tests/integration_tests/dashboards/security/security_dataset_tests.py
index 54e8b81442..550d25ab59 100644
--- a/tests/integration_tests/dashboards/security/security_dataset_tests.py
+++ b/tests/integration_tests/dashboards/security/security_dataset_tests.py
@@ -61,8 +61,8 @@ class TestDashboardDatasetSecurity(DashboardTestCase):
hidden_dash.slices = [slice]
hidden_dash.published = False
- db.session.merge(published_dash)
- db.session.merge(hidden_dash)
+ db.session.add(published_dash)
+ db.session.add(hidden_dash)
yield db.session.commit()
self.revoke_public_access_to_table(table)
diff --git a/tests/integration_tests/datasource_tests.py b/tests/integration_tests/datasource_tests.py
index c2865f7b63..802c67e852 100644
--- a/tests/integration_tests/datasource_tests.py
+++ b/tests/integration_tests/datasource_tests.py
@@ -550,7 +550,6 @@ def test_get_samples_with_incorrect_cc(test_client, login_as_admin, virtual_data
table=virtual_dataset,
expression="INCORRECT SQL",
)
- db.session.merge(virtual_dataset)
uri = (
f"/datasource/samples?datasource_id={virtual_dataset.id}&datasource_type=table"
diff --git a/tests/integration_tests/fixtures/energy_dashboard.py b/tests/integration_tests/fixtures/energy_dashboard.py
index 5b4690f572..9687fb4aff 100644
--- a/tests/integration_tests/fixtures/energy_dashboard.py
+++ b/tests/integration_tests/fixtures/energy_dashboard.py
@@ -82,8 +82,6 @@ def _create_energy_table() -> list[Slice]:
table.metrics.append(
SqlMetric(metric_name="sum__value", expression=f"SUM({col})")
)
- db.session.merge(table)
- db.session.commit()
table.fetch_metadata()
slices = []
diff --git a/tests/integration_tests/migrations/c747c78868b6_migrating_legacy_treemap__tests.py b/tests/integration_tests/migrations/c747c78868b6_migrating_legacy_treemap__tests.py
index 3e9ef33092..e67d87fa13 100644
--- a/tests/integration_tests/migrations/c747c78868b6_migrating_legacy_treemap__tests.py
+++ b/tests/integration_tests/migrations/c747c78868b6_migrating_legacy_treemap__tests.py
@@ -68,7 +68,7 @@ def test_treemap_migrate(app_context: SupersetApp) -> None:
query_context=f'{{"form_data": {treemap_form_data}}}',
)
- slc = MigrateTreeMap.upgrade_slice(slc)
+ MigrateTreeMap.upgrade_slice(slc)
assert slc.viz_type == MigrateTreeMap.target_viz_type
# verify form_data
new_form_data = json.loads(slc.params)
@@ -84,7 +84,7 @@ def test_treemap_migrate(app_context: SupersetApp) -> None:
assert new_query_context["form_data"]["viz_type"] == "treemap_v2"
# downgrade
- slc = MigrateTreeMap.downgrade_slice(slc)
+ MigrateTreeMap.downgrade_slice(slc)
assert slc.viz_type == MigrateTreeMap.source_viz_type
assert json.dumps(json.loads(slc.params), sort_keys=True) == json.dumps(
json.loads(treemap_form_data), sort_keys=True
diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py
index 120559f8fd..11ec170121 100644
--- a/tests/integration_tests/reports/commands_tests.py
+++ b/tests/integration_tests/reports/commands_tests.py
@@ -1919,7 +1919,6 @@ def test_grace_period_error_flap(
# Change report_schedule to valid
create_invalid_sql_alert_email_chart.sql = "SELECT 1 AS metric"
create_invalid_sql_alert_email_chart.grace_period = 0
- db.session.merge(create_invalid_sql_alert_email_chart)
db.session.commit()
with freeze_time("2020-01-01T00:31:00Z"):
@@ -1936,7 +1935,6 @@ def test_grace_period_error_flap(
create_invalid_sql_alert_email_chart.sql = "SELECT 'first'"
create_invalid_sql_alert_email_chart.grace_period = 10
- db.session.merge(create_invalid_sql_alert_email_chart)
db.session.commit()
# assert that after a success, when back to error we send the error notification
diff --git a/tests/integration_tests/security/migrate_roles_tests.py b/tests/integration_tests/security/migrate_roles_tests.py
index ae89fea068..39d66a82aa 100644
--- a/tests/integration_tests/security/migrate_roles_tests.py
+++ b/tests/integration_tests/security/migrate_roles_tests.py
@@ -62,7 +62,6 @@ def create_old_role(pvm_map: PvmMigrationMapType, external_pvms):
db.session.query(Role).filter(Role.name == "Dummy Role").one_or_none()
)
new_role.permissions = []
- db.session.merge(new_role)
for old_pvm, new_pvms in pvm_map.items():
security_manager.del_permission_view_menu(old_pvm.permission, old_pvm.view)
for new_pvm in new_pvms:
diff --git a/tests/unit_tests/migrations/viz/utils.py b/tests/unit_tests/migrations/viz/utils.py
index 92d2eccd70..9da90c853f 100644
--- a/tests/unit_tests/migrations/viz/utils.py
+++ b/tests/unit_tests/migrations/viz/utils.py
@@ -79,7 +79,7 @@ def migrate_and_assert(
)
# upgrade
- slc = cls.upgrade_slice(slc)
+ cls.upgrade_slice(slc)
# verify form_data
new_form_data = json.loads(slc.params)
@@ -91,6 +91,6 @@ def migrate_and_assert(
assert new_query_context["form_data"]["viz_type"] == cls.target_viz_type
# downgrade
- slc = cls.downgrade_slice(slc)
+ cls.downgrade_slice(slc)
assert slc.viz_type == cls.source_viz_type
assert json.loads(slc.params) == source
From bba7763825013689a4f4b4985b54d8802e61eef6 Mon Sep 17 00:00:00 2001
From: Sam Firke
Date: Tue, 21 Nov 2023 09:10:56 -0500
Subject: [PATCH 044/119] fix(security): restore default value of
SESSION_COOKIE_SECURE to False (#26005)
---
superset/config.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/superset/config.py b/superset/config.py
index 401dfd2f3d..98f87e6f02 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1442,6 +1442,7 @@ TALISMAN_CONFIG = {
},
"content_security_policy_nonce_in": ["script-src"],
"force_https": False,
+ "session_cookie_secure": False,
}
# React requires `eval` to work correctly in dev mode
TALISMAN_DEV_CONFIG = {
@@ -1463,6 +1464,7 @@ TALISMAN_DEV_CONFIG = {
},
"content_security_policy_nonce_in": ["script-src"],
"force_https": False,
+ "session_cookie_secure": False,
}
#
From 1af5fbbd6a58bcce4962cf25f3ea4785d14f32f3 Mon Sep 17 00:00:00 2001
From: Evan Rusackas
Date: Tue, 21 Nov 2023 07:12:50 -0700
Subject: [PATCH 045/119] docs(BH#109): Athena URI spec fix (#26044)
---
.../databases/installing-database-drivers.mdx | 82 +++++++++----------
1 file changed, 41 insertions(+), 41 deletions(-)
diff --git a/docs/docs/databases/installing-database-drivers.mdx b/docs/docs/databases/installing-database-drivers.mdx
index b4be939c3b..f698b7ab8e 100644
--- a/docs/docs/databases/installing-database-drivers.mdx
+++ b/docs/docs/databases/installing-database-drivers.mdx
@@ -22,47 +22,47 @@ as well as the packages needed to connect to the databases you want to access th
Some of the recommended packages are shown below. Please refer to [setup.py](https://github.com/apache/superset/blob/master/setup.py) for the versions that are compatible with Superset.
-| Database | PyPI package | Connection String |
-| --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
-| [Amazon Athena](/docs/databases/athena) | `pip install pyathena[pandas]` , `pip install PyAthenaJDBC` | `awsathena+rest://{aws_access_key_id}:{aws_secret_access_key}@athena.{region_name}.amazonaws.com/{ ` |
-| [Amazon DynamoDB](/docs/databases/dynamodb) | `pip install pydynamodb` | `dynamodb://{access_key_id}:{secret_access_key}@dynamodb.{region_name}.amazonaws.com?connector=superset` |
-| [Amazon Redshift](/docs/databases/redshift) | `pip install sqlalchemy-redshift` | ` redshift+psycopg2://:@:5439/` |
-| [Apache Drill](/docs/databases/drill) | `pip install sqlalchemy-drill` | `drill+sadrill:// For JDBC drill+jdbc://` |
-| [Apache Druid](/docs/databases/druid) | `pip install pydruid` | `druid://:@:/druid/v2/sql` |
-| [Apache Hive](/docs/databases/hive) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
-| [Apache Impala](/docs/databases/impala) | `pip install impyla` | `impala://{hostname}:{port}/{database}` |
-| [Apache Kylin](/docs/databases/kylin) | `pip install kylinpy` | `kylin://:@:/?=&=` |
-| [Apache Pinot](/docs/databases/pinot) | `pip install pinotdb` | `pinot://BROKER:5436/query?server=http://CONTROLLER:5983/` |
-| [Apache Solr](/docs/databases/solr) | `pip install sqlalchemy-solr` | `solr://{username}:{password}@{hostname}:{port}/{server_path}/{collection}` |
-| [Apache Spark SQL](/docs/databases/spark-sql) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
-| [Ascend.io](/docs/databases/ascend) | `pip install impyla` | `ascend://{username}:{password}@{hostname}:{port}/{database}?auth_mechanism=PLAIN;use_ssl=true` |
-| [Azure MS SQL](/docs/databases/sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
-| [Big Query](/docs/databases/bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
-| [ClickHouse](/docs/databases/clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
-| [CockroachDB](/docs/databases/cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
-| [Dremio](/docs/databases/dremio) | `pip install sqlalchemy_dremio` | `dremio://user:pwd@host:31010/` |
-| [Elasticsearch](/docs/databases/elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
-| [Exasol](/docs/databases/exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
-| [Google Sheets](/docs/databases/google-sheets) | `pip install shillelagh[gsheetsapi]` | `gsheets://` |
-| [Firebolt](/docs/databases/firebolt) | `pip install firebolt-sqlalchemy` | `firebolt://{username}:{password}@{database} or firebolt://{username}:{password}@{database}/{engine_name}` |
-| [Hologres](/docs/databases/hologres) | `pip install psycopg2` | `postgresql+psycopg2://:@/` |
-| [IBM Db2](/docs/databases/ibm-db2) | `pip install ibm_db_sa` | `db2+ibm_db://` |
-| [IBM Netezza Performance Server](/docs/databases/netezza) | `pip install nzalchemy` | `netezza+nzpy://:@/` |
-| [MySQL](/docs/databases/mysql) | `pip install mysqlclient` | `mysql://:@/` |
-| [Oracle](/docs/databases/oracle) | `pip install cx_Oracle` | `oracle://` |
-| [PostgreSQL](/docs/databases/postgres) | `pip install psycopg2` | `postgresql://:@/` |
-| [Presto](/docs/databases/presto) | `pip install pyhive` | `presto://` |
-| [Rockset](/docs/databases/rockset) | `pip install rockset-sqlalchemy` | `rockset://:@` |
-| [SAP Hana](/docs/databases/hana) | `pip install hdbcli sqlalchemy-hana or pip install apache-superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
-| [StarRocks](/docs/databases/starrocks) | `pip install starrocks` | `starrocks://:@:/.` |
-| [Snowflake](/docs/databases/snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
-| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
-| [SQL Server](/docs/databases/sql-server) | `pip install pymssql` | `mssql+pymssql://` |
-| [Teradata](/docs/databases/teradata) | `pip install teradatasqlalchemy` | `teradatasql://{user}:{password}@{host}` |
-| [TimescaleDB](/docs/databases/timescaledb) | `pip install psycopg2` | `postgresql://:@:/` |
-| [Trino](/docs/databases/trino) | `pip install trino` | `trino://{username}:{password}@{hostname}:{port}/{catalog}` |
-| [Vertica](/docs/databases/vertica) | `pip install sqlalchemy-vertica-python` | `vertica+vertica_python://:@/` |
-| [YugabyteDB](/docs/databases/yugabytedb) | `pip install psycopg2` | `postgresql://:@/` |
+| Database | PyPI package | Connection String |
+| --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| [Amazon Athena](/docs/databases/athena) | `pip install pyathena[pandas]` , `pip install PyAthenaJDBC` | `awsathena+rest://{aws_access_key_id}:{aws_secret_access_key}@athena.{region_name}.amazonaws.com/{schema_name}?s3_staging_dir={s3_staging_dir}&... ` |
+| [Amazon DynamoDB](/docs/databases/dynamodb) | `pip install pydynamodb` | `dynamodb://{access_key_id}:{secret_access_key}@dynamodb.{region_name}.amazonaws.com?connector=superset` |
+| [Amazon Redshift](/docs/databases/redshift) | `pip install sqlalchemy-redshift` | ` redshift+psycopg2://:@:5439/` |
+| [Apache Drill](/docs/databases/drill) | `pip install sqlalchemy-drill` | `drill+sadrill:// For JDBC drill+jdbc://` |
+| [Apache Druid](/docs/databases/druid) | `pip install pydruid` | `druid://:@:/druid/v2/sql` |
+| [Apache Hive](/docs/databases/hive) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
+| [Apache Impala](/docs/databases/impala) | `pip install impyla` | `impala://{hostname}:{port}/{database}` |
+| [Apache Kylin](/docs/databases/kylin) | `pip install kylinpy` | `kylin://:@:/?=&=` |
+| [Apache Pinot](/docs/databases/pinot) | `pip install pinotdb` | `pinot://BROKER:5436/query?server=http://CONTROLLER:5983/` |
+| [Apache Solr](/docs/databases/solr) | `pip install sqlalchemy-solr` | `solr://{username}:{password}@{hostname}:{port}/{server_path}/{collection}` |
+| [Apache Spark SQL](/docs/databases/spark-sql) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
+| [Ascend.io](/docs/databases/ascend) | `pip install impyla` | `ascend://{username}:{password}@{hostname}:{port}/{database}?auth_mechanism=PLAIN;use_ssl=true` |
+| [Azure MS SQL](/docs/databases/sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
+| [Big Query](/docs/databases/bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
+| [ClickHouse](/docs/databases/clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
+| [CockroachDB](/docs/databases/cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
+| [Dremio](/docs/databases/dremio) | `pip install sqlalchemy_dremio` | `dremio://user:pwd@host:31010/` |
+| [Elasticsearch](/docs/databases/elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
+| [Exasol](/docs/databases/exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
+| [Google Sheets](/docs/databases/google-sheets) | `pip install shillelagh[gsheetsapi]` | `gsheets://` |
+| [Firebolt](/docs/databases/firebolt) | `pip install firebolt-sqlalchemy` | `firebolt://{username}:{password}@{database} or firebolt://{username}:{password}@{database}/{engine_name}` |
+| [Hologres](/docs/databases/hologres) | `pip install psycopg2` | `postgresql+psycopg2://:@/` |
+| [IBM Db2](/docs/databases/ibm-db2) | `pip install ibm_db_sa` | `db2+ibm_db://` |
+| [IBM Netezza Performance Server](/docs/databases/netezza) | `pip install nzalchemy` | `netezza+nzpy://:@/` |
+| [MySQL](/docs/databases/mysql) | `pip install mysqlclient` | `mysql://:@/` |
+| [Oracle](/docs/databases/oracle) | `pip install cx_Oracle` | `oracle://` |
+| [PostgreSQL](/docs/databases/postgres) | `pip install psycopg2` | `postgresql://:@/` |
+| [Presto](/docs/databases/presto) | `pip install pyhive` | `presto://` |
+| [Rockset](/docs/databases/rockset) | `pip install rockset-sqlalchemy` | `rockset://:@` |
+| [SAP Hana](/docs/databases/hana) | `pip install hdbcli sqlalchemy-hana or pip install apache-superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
+| [StarRocks](/docs/databases/starrocks) | `pip install starrocks` | `starrocks://:@:/.` |
+| [Snowflake](/docs/databases/snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
+| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
+| [SQL Server](/docs/databases/sql-server) | `pip install pymssql` | `mssql+pymssql://` |
+| [Teradata](/docs/databases/teradata) | `pip install teradatasqlalchemy` | `teradatasql://{user}:{password}@{host}` |
+| [TimescaleDB](/docs/databases/timescaledb) | `pip install psycopg2` | `postgresql://:@:/` |
+| [Trino](/docs/databases/trino) | `pip install trino` | `trino://{username}:{password}@{hostname}:{port}/{catalog}` |
+| [Vertica](/docs/databases/vertica) | `pip install sqlalchemy-vertica-python` | `vertica+vertica_python://:@/` |
+| [YugabyteDB](/docs/databases/yugabytedb) | `pip install psycopg2` | `postgresql://:@/` |
---
Note that many other databases are supported, the main criteria being the existence of a functional
From b5e7e707b3486e842102972d01f2fb077f232e98 Mon Sep 17 00:00:00 2001
From: Kamil Gabryjelski
Date: Tue, 21 Nov 2023 15:43:50 +0100
Subject: [PATCH 046/119] chore: Allow external extensions to include their own
package.json files (#26004)
---
superset-frontend/lerna.json | 2 +-
superset-frontend/package-lock.json | 3 ++-
superset-frontend/package.json | 3 ++-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/superset-frontend/lerna.json b/superset-frontend/lerna.json
index 3a16712db2..07bef7fcfd 100644
--- a/superset-frontend/lerna.json
+++ b/superset-frontend/lerna.json
@@ -1,7 +1,7 @@
{
"lerna": "3.2.1",
"npmClient": "npm",
- "packages": ["packages/*", "plugins/*"],
+ "packages": ["packages/*", "plugins/*", "src/setup/*"],
"useWorkspaces": true,
"version": "0.18.25",
"ignoreChanges": [
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index b6d01c1418..6affdb893a 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -10,7 +10,8 @@
"license": "Apache-2.0",
"workspaces": [
"packages/*",
- "plugins/*"
+ "plugins/*",
+ "src/setup/*"
],
"dependencies": {
"@ant-design/icons": "^5.0.1",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index a0f5bdd8c0..5f6a2e7e2d 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -33,7 +33,8 @@
},
"workspaces": [
"packages/*",
- "plugins/*"
+ "plugins/*",
+ "src/setup/*"
],
"scripts": {
"_lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx .",
From fbc66a817780ef7381e5097ad42de92ee9dc6541 Mon Sep 17 00:00:00 2001
From: Beto Dealmeida
Date: Tue, 21 Nov 2023 09:48:39 -0500
Subject: [PATCH 047/119] chore: bump shillelagh (#26043)
---
requirements/base.txt | 35 ++++++++++++++++++++++++++++++-----
requirements/development.txt | 2 --
requirements/testing.txt | 6 ------
setup.py | 6 +++---
4 files changed, 33 insertions(+), 16 deletions(-)
diff --git a/requirements/base.txt b/requirements/base.txt
index 1d27016e63..e8b1b43f91 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -18,7 +18,10 @@ apsw==3.42.0.1
async-timeout==4.0.2
# via redis
attrs==23.1.0
- # via jsonschema
+ # via
+ # cattrs
+ # jsonschema
+ # requests-cache
babel==2.9.1
# via flask-babel
backoff==1.11.1
@@ -35,6 +38,8 @@ cachelib==0.9.0
# via
# flask-caching
# flask-session
+cattrs==23.2.1
+ # via requests-cache
celery==5.2.2
# via apache-superset
certifi==2023.7.22
@@ -85,6 +90,8 @@ dnspython==2.1.0
# via email-validator
email-validator==1.1.3
# via flask-appbuilder
+exceptiongroup==1.1.1
+ # via cattrs
flask==2.2.5
# via
# apache-superset
@@ -136,7 +143,9 @@ geographiclib==1.52
geopy==2.2.0
# via apache-superset
greenlet==2.0.2
- # via shillelagh
+ # via
+ # shillelagh
+ # sqlalchemy
gunicorn==21.2.0
# via apache-superset
hashids==1.3.1
@@ -152,7 +161,10 @@ idna==3.2
# email-validator
# requests
importlib-metadata==6.6.0
- # via apache-superset
+ # via
+ # apache-superset
+ # flask
+ # shillelagh
importlib-resources==5.12.0
# via limits
isodate==0.6.0
@@ -232,6 +244,8 @@ parsedatetime==2.6
# via apache-superset
pgsanity==0.2.9
# via apache-superset
+platformdirs==3.8.1
+ # via requests-cache
polyline==2.0.0
# via apache-superset
prison==0.2.1
@@ -285,12 +299,16 @@ pyyaml==6.0.1
redis==4.5.4
# via apache-superset
requests==2.31.0
+ # via
+ # requests-cache
+ # shillelagh
+requests-cache==1.1.1
# via shillelagh
rich==13.3.4
# via flask-limiter
selenium==3.141.0
# via apache-superset
-shillelagh==1.2.6
+shillelagh==1.2.10
# via apache-superset
shortid==0.1.2
# via apache-superset
@@ -303,6 +321,7 @@ six==1.16.0
# paramiko
# prison
# python-dateutil
+ # url-normalize
# wtforms-json
slack-sdk==3.21.3
# via apache-superset
@@ -328,14 +347,18 @@ tabulate==0.8.9
typing-extensions==4.4.0
# via
# apache-superset
+ # cattrs
# flask-limiter
# limits
# shillelagh
tzdata==2023.3
# via pandas
+url-normalize==1.4.3
+ # via requests-cache
urllib3==1.26.6
# via
# requests
+ # requests-cache
# selenium
vine==5.0.0
# via
@@ -363,7 +386,9 @@ wtforms-json==0.3.5
xlsxwriter==3.0.7
# via apache-superset
zipp==3.15.0
- # via importlib-metadata
+ # via
+ # importlib-metadata
+ # importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# setuptools
diff --git a/requirements/development.txt b/requirements/development.txt
index 04962ae537..a73e3a70c5 100644
--- a/requirements/development.txt
+++ b/requirements/development.txt
@@ -74,8 +74,6 @@ pickleshare==0.7.5
# via ipython
pillow==9.5.0
# via apache-superset
-platformdirs==3.8.1
- # via pylint
progress==1.6
# via -r requirements/development.in
psycopg2-binary==2.9.6
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 00fe734540..c1f6e55d12 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -26,8 +26,6 @@ docker==6.1.1
# via -r requirements/testing.in
ephem==4.1.4
# via lunarcalendar
-exceptiongroup==1.1.1
- # via pytest
flask-testing==0.8.1
# via -r requirements/testing.in
fonttools==4.39.4
@@ -123,8 +121,6 @@ pyee==9.0.4
# via playwright
pyfakefs==5.2.2
# via -r requirements/testing.in
-pyhive[presto]==0.7.0
- # via apache-superset
pytest==7.3.1
# via
# -r requirements/testing.in
@@ -144,8 +140,6 @@ rsa==4.9
# via google-auth
setuptools-git==1.2
# via prophet
-shillelagh[gsheetsapi]==1.2.6
- # via apache-superset
sqlalchemy-bigquery==1.6.1
# via apache-superset
statsd==4.0.1
diff --git a/setup.py b/setup.py
index eb442bcf72..e4d437b4d1 100644
--- a/setup.py
+++ b/setup.py
@@ -118,7 +118,7 @@ setup(
"PyJWT>=2.4.0, <3.0",
"redis>=4.5.4, <5.0",
"selenium>=3.141.0, <4.10.0",
- "shillelagh>=1.2.6,<2.0",
+ "shillelagh>=1.2.10, <2.0",
"shortid",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
@@ -162,7 +162,7 @@ setup(
"excel": ["xlrd>=1.2.0, <1.3"],
"firebird": ["sqlalchemy-firebird>=0.7.0, <0.8"],
"firebolt": ["firebolt-sqlalchemy>=0.0.1"],
- "gsheets": ["shillelagh[gsheetsapi]>=1.2.6, <2"],
+ "gsheets": ["shillelagh[gsheetsapi]>=1.2.10, <2"],
"hana": ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"],
"hive": [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
@@ -191,7 +191,7 @@ setup(
"redshift": ["sqlalchemy-redshift>=0.8.1, < 0.9"],
"rockset": ["rockset-sqlalchemy>=0.0.1, <1.0.0"],
"shillelagh": [
- "shillelagh[datasetteapi,gsheetsapi,socrata,weatherapi]>=1.2.6,<2"
+ "shillelagh[datasetteapi,gsheetsapi,socrata,weatherapi]>=1.2.10, <2"
],
"snowflake": ["snowflake-sqlalchemy>=1.2.4, <2"],
"spark": [
From f99c874962eed6e0b52c4721b13238a63130430a Mon Sep 17 00:00:00 2001
From: Beto Dealmeida
Date: Tue, 21 Nov 2023 11:36:31 -0500
Subject: [PATCH 048/119] feat(metadb): handle decimals (#25921)
---
superset/extensions/metadb.py | 30 ++++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/superset/extensions/metadb.py b/superset/extensions/metadb.py
index 5b014b7af6..bdfe1ae1e7 100644
--- a/superset/extensions/metadb.py
+++ b/superset/extensions/metadb.py
@@ -38,6 +38,7 @@ joins and unions are done in memory, using the SQLite engine.
from __future__ import annotations
import datetime
+import decimal
import operator
import urllib.parse
from collections.abc import Iterator
@@ -49,7 +50,6 @@ from shillelagh.adapters.base import Adapter
from shillelagh.backends.apsw.dialects.base import APSWDialect
from shillelagh.exceptions import ProgrammingError
from shillelagh.fields import (
- Blob,
Boolean,
Date,
DateTime,
@@ -86,7 +86,7 @@ class SupersetAPSWDialect(APSWDialect):
Queries can also join data across different Superset databases.
- The dialect is built in top of the shillelagh library, leveraging SQLite to
+ The dialect is built in top of the Shillelagh library, leveraging SQLite to
create virtual tables on-the-fly proxying Superset tables. The
`SupersetShillelaghAdapter` adapter is responsible for returning data when a
Superset table is accessed.
@@ -164,11 +164,32 @@ class Duration(Field[datetime.timedelta, datetime.timedelta]):
db_api_type = "DATETIME"
+class Decimal(Field[decimal.Decimal, decimal.Decimal]):
+ """
+ Shillelagh field used for representing decimals.
+ """
+
+ type = "DECIMAL"
+ db_api_type = "NUMBER"
+
+
+class FallbackField(Field[Any, str]):
+ """
+ Fallback field for unknown types; converts to string.
+ """
+
+ type = "TEXT"
+ db_api_type = "STRING"
+
+ def parse(self, value: Any) -> str | None:
+ return value if value is None else str(value)
+
+
# pylint: disable=too-many-instance-attributes
class SupersetShillelaghAdapter(Adapter):
"""
- A shillelagh adapter for Superset tables.
+ A Shillelagh adapter for Superset tables.
Shillelagh adapters are responsible for fetching data from a given resource,
allowing it to be represented as a virtual table in SQLite. This one works
@@ -190,6 +211,7 @@ class SupersetShillelaghAdapter(Adapter):
datetime.datetime: DateTime,
datetime.time: Time,
datetime.timedelta: Duration,
+ decimal.Decimal: Decimal,
}
@staticmethod
@@ -268,7 +290,7 @@ class SupersetShillelaghAdapter(Adapter):
"""
Convert a Python type into a Shillelagh field.
"""
- class_ = cls.type_map.get(python_type, Blob)
+ class_ = cls.type_map.get(python_type, FallbackField)
return class_(filters=[Equal, Range], order=Order.ANY, exact=True)
def _set_columns(self) -> None:
From adb86d35ecdd09a0360d31ca94e7731afeb9ced3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 21 Nov 2023 09:51:42 -0700
Subject: [PATCH 049/119] build(deps-dev): bump @types/ws from 8.5.9 to 8.5.10
in /superset-websocket (#26048)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 6efe9b8971..5ed9613fcc 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -26,7 +26,7 @@
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.1",
"@types/uuid": "^9.0.7",
- "@types/ws": "^8.5.9",
+ "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.54.0",
@@ -1462,9 +1462,9 @@
"dev": true
},
"node_modules/@types/ws": {
- "version": "8.5.9",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz",
- "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==",
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
+ "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -7316,9 +7316,9 @@
"dev": true
},
"@types/ws": {
- "version": "8.5.9",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz",
- "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==",
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
+ "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"requires": {
"@types/node": "*"
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index f60170ad4e..361b065111 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -33,7 +33,7 @@
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.9.1",
"@types/uuid": "^9.0.7",
- "@types/ws": "^8.5.9",
+ "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.54.0",
From 945266407963c74827942179f10f833e9e65773d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 21 Nov 2023 09:52:00 -0700
Subject: [PATCH 050/119] build(deps-dev): bump @types/node from 20.9.1 to
20.9.3 in /superset-websocket (#26049)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 5ed9613fcc..2ced4f3b2f 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -24,7 +24,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.5",
- "@types/node": "^20.9.1",
+ "@types/node": "^20.9.3",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
@@ -1429,9 +1429,9 @@
"integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
},
"node_modules/@types/node": {
- "version": "20.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
- "integrity": "sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==",
+ "version": "20.9.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz",
+ "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -7283,9 +7283,9 @@
"integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
},
"@types/node": {
- "version": "20.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
- "integrity": "sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==",
+ "version": "20.9.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz",
+ "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index 361b065111..7ebac134f1 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -31,7 +31,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.5",
- "@types/node": "^20.9.1",
+ "@types/node": "^20.9.3",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
From 25a737e83cdabb3158cd3dacda7c5d5332e2c3d2 Mon Sep 17 00:00:00 2001
From: aehanno <104373599+aehanno@users.noreply.github.com>
Date: Tue, 21 Nov 2023 11:53:26 -0500
Subject: [PATCH 051/119] fix: Remove annotation Fuzzy to get french
translation (#26010)
---
.../translations/fr/LC_MESSAGES/messages.json | 75 +++++++++++----
.../translations/fr/LC_MESSAGES/messages.po | 95 ++++++++-----------
2 files changed, 92 insertions(+), 78 deletions(-)
diff --git a/superset/translations/fr/LC_MESSAGES/messages.json b/superset/translations/fr/LC_MESSAGES/messages.json
index 7f03dc9f92..2391b33db8 100644
--- a/superset/translations/fr/LC_MESSAGES/messages.json
+++ b/superset/translations/fr/LC_MESSAGES/messages.json
@@ -100,7 +100,7 @@
"1H": [""],
"1M": [""],
"1T": [""],
- "2 years ago": ["il y a 2 ans"],
+ "2 years ago": ["Il y a 2 ans"],
"2/98 percentiles": [""],
"28 days ago": [""],
"2D": [""],
@@ -226,6 +226,7 @@
"Add calculated temporal columns to dataset in \"Edit datasource\" modal": [
""
],
+ "Add cross-filter": ["Ajouter un filtre"],
"Add custom scoping": [""],
"Add delivery method": ["Ajouter méthode de livraison"],
"Add filter": ["Ajouter un filtre"],
@@ -556,6 +557,8 @@
""
],
"Append": ["Ajouter"],
+ "Applied filters (%d)": ["Filtres appliqués (%d)"],
+ "Applied filters: %s": ["Filtres appliqué: %s"],
"Applied rolling window did not return any data. Please make sure the source query satisfies the minimum periods defined in the rolling window.": [
"La fenêtre glissante appliquée n'a pas retourné de données. Assurez-vous que la requête source satisfasse les périodes minimum définies dans la fenêtre glissante."
],
@@ -630,6 +633,7 @@
"Batch editing %d filters:": ["Edition Batch %d filtres:"],
"Battery level over time": [""],
"Be careful.": ["Faites attention."],
+ "Before": ["Avant"],
"Big Number": ["Gros nombre"],
"Big Number Font Size": [""],
"Big Number with Trendline": ["Gros nombre avec tendance"],
@@ -1088,9 +1092,10 @@
],
"Creator": ["Créateur"],
"Cross-filter will be applied to all of the charts that use this dataset.": [
- ""
+ "Le filtre va être appliqué à tous les graphiques qui utilise cet ensemble de données"
],
"Currently rendered: %s": [""],
+ "Custom": ["Personnalisée"],
"Custom Plugin": ["Plugin custom"],
"Custom Plugins": ["Plugins custom"],
"Custom SQL": ["SQL personnalisé"],
@@ -1259,6 +1264,7 @@
"Datetime format": ["Format Datetime"],
"Day": ["Jour"],
"Day (freq=D)": [""],
+ "Days %s": ["Jours %s"],
"Db engine did not return all queried columns": [
"La base de données n'a pas retourné toutes les colonnes demandées"
],
@@ -1415,6 +1421,7 @@
"Divider": ["Diviseur"],
"Do you want a donut or a pie?": [""],
"Documentation": ["Documentation"],
+ "Download": ["Télécharger"],
"Download as image": ["Télécharger comme image"],
"Download to CSV": ["Télécharger en CSV"],
"Draft": ["Brouillon"],
@@ -1429,6 +1436,7 @@
"Drill by": [""],
"Drill by is not available for this data point": [""],
"Drill by is not yet supported for this chart type": [""],
+ "Drill by: %s": ["Trier par %s"],
"Drill to detail": [""],
"Drill to detail by": [""],
"Drill to detail by value is not yet supported for this chart type.": [
@@ -1635,7 +1643,10 @@
"Export": ["Exporter"],
"Export dashboards?": ["Exporter les tableaux de bords ?"],
"Export query": ["Exporter la requête"],
- "Export to YAML": ["Exporter en YAML"],
+ "Export to .CSV": ["Exporter au format CSV"],
+ "Export to .JSON": ["Exporter au format JSON"],
+ "Export to Excel": ["Exporter vers Excel"],
+ "Export to YAML": ["Exporter au format YAML"],
"Export to YAML?": ["Exporter en YAML?"],
"Export to original .CSV": [""],
"Export to pivoted .CSV": [""],
@@ -1818,6 +1829,7 @@
"Host": [""],
"Hostname or IP address": ["Nom d'hôte ou adresse IP"],
"Hour": ["Heure"],
+ "Hours %s": ["Heures %s"],
"Hours offset": ["Offset des heures"],
"How do you want to enter service account credentials?": [
"Comment voulez-vous entrer les informations de connexion du compte de service ?"
@@ -1981,9 +1993,11 @@
"Labels for the marker lines": [""],
"Labels for the markers": [""],
"Labels for the ranges": [""],
+ "Last": ["Dernier"],
"Last Changed": ["Dernière modification"],
"Last Modified": ["Dernière modification"],
"Last Updated %s": ["Dernière mise à jour %s"],
+ "Last Updated %s by %s": ["Dernière mise à jour %s"],
"Last modified": ["Dernière modification"],
"Last modified by %s": ["Dernière modification par %s"],
"Last run": ["Dernière exécution"],
@@ -2141,6 +2155,7 @@
"Minimum value on the gauge axis": [""],
"Minor Split Line": [""],
"Minute": ["Minute"],
+ "Minutes %s": ["Minutes %s"],
"Missing dataset": ["Jeu de données manquant"],
"Mixed Time-Series": [""],
"Modified": ["Modifié"],
@@ -2149,6 +2164,7 @@
"Modified columns: %s": ["Colonnes modifiées : %s"],
"Monday": ["Lundi"],
"Month": ["Mois"],
+ "Months %s": ["Mois %s"],
"Move only": [""],
"Moves the given set of dates by a specified interval.": [
"Décale l'ensemble de dates d'un intervalle spécifié."
@@ -2234,6 +2250,7 @@
"No filter": ["Pas de filtre"],
"No filter is selected.": ["Pas de filtre sélectionné."],
"No form settings were maintained": [""],
+ "No matching records found": ["Aucun enregistrement trouvé"],
"No records found": ["Aucun enregistrement trouvé"],
"No results found": ["Aucun résultat trouvé"],
"No results match your filter criteria": [""],
@@ -2264,6 +2281,7 @@
"Nothing triggered": ["Rien déclenché"],
"Notification method": ["Méthode de notification"],
"November": ["Novembre"],
+ "Now": ["Maintenant"],
"Null or Empty": ["Null ou Vide"],
"Null values": ["Valeurs NULL"],
"Number bounds used for color encoding from red to blue.\n Reverse the numbers for blue to red. To get pure red or blue,\n you can enter either only min or max.": [
@@ -2589,6 +2607,7 @@
"Python datetime string pattern": ["Python datetime string pattern"],
"QUERY DATA IN SQL LAB": [""],
"Quarter": ["Trimestre"],
+ "Quarters %s": ["Trimestres %s"],
"Query": ["Requête"],
"Query %s: %s": [""],
"Query History": ["Historiques des requêtes"],
@@ -2651,8 +2670,10 @@
"Refresh frequency": ["Fréquence de rafraichissement"],
"Refresh interval": ["Intervalle d'actualisation"],
"Refresh the default values": ["Rafraichir les valeurs par défaut"],
+ "Refreshing charts": ["Rafraîchissement en cours"],
"Regular": [""],
"Relationships between community channels": [""],
+ "Relative Date/Time": ["Date/Heure Relative"],
"Relative period": ["Période relative"],
"Relative quantity": ["Quantité relative"],
"Remind me in 24 hours": ["Me le rappeler dans 24 heures"],
@@ -2729,6 +2750,7 @@
"Resource already has an attached report.": [""],
"Restore Filter": ["Restaurer le Filtre"],
"Results": ["Résultats"],
+ "Results %s": ["Résultats"],
"Results backend is not configured.": [
"Le backend des résultats n'est pas configuré."
],
@@ -2883,6 +2905,7 @@
"Secondary y-axis Bounds": [""],
"Secondary y-axis format": [""],
"Secondary y-axis title": [""],
+ "Seconds %s": ["Secondes %s"],
"Secure Extra": ["Sécurité"],
"Secure extra": ["Sécurité"],
"Security": ["Sécurité"],
@@ -2893,7 +2916,7 @@
"Select": ["Sélectionner"],
"Select ...": ["Sélectionner..."],
"Select Delivery Method": ["Choisir la méthode de livraison"],
- "Select Viz Type": ["Selectionner un type de visualisation"],
+ "Select Viz Type": ["Sélectionner un type de visualisation"],
"Select a Columnar file to be uploaded to a database.": [
"Sélectionner un fichier en colonne à téléverser dans une base de données."
],
@@ -2903,7 +2926,7 @@
"Select a column": ["Sélectionner une colonne"],
"Select a dashboard": ["Sélectionner un tableau de bord"],
"Select a database to upload the file to": [""],
- "Select a visualization type": ["Selectionner un type de visualisation"],
+ "Select a visualization type": ["Sélectionner un type de visualisation"],
"Select aggregate options": ["Sélectionner les options d’agrégat"],
"Select any columns for metadata inspection": [""],
"Select color scheme": ["Sélectionner un schéma de couleurs"],
@@ -2911,17 +2934,17 @@
"Select databases require additional fields to be completed in the Advanced tab to successfully connect the database. Learn what requirements your databases has ": [
""
],
- "Select filter": ["Selectionner un filtre"],
+ "Select filter": ["Sélectionner un filtre"],
"Select filter plugin using AntD": [""],
"Select first filter value by default": [
- "Selectionne la première valeur du filtre par défaut"
+ "Sélectionne la première valeur du filtre par défaut"
],
"Select operator": ["Sélectionner l'opérateur"],
"Select or type a value": ["Sélectionner ou renseigner une valeur"],
"Select owners": ["Sélectionner les propriétaires"],
"Select saved metrics": ["Sélectionner les métriques sauvegardées"],
"Select start and end date": [
- "Selectionner la date de début et la date de fin"
+ "Sélectionner la date de début et la date de fin"
],
"Select subject": ["Sélectionner un objet"],
"Select the charts to which you want to apply cross-filters in this dashboard. Deselecting a chart will exclude it from being filtered when applying cross-filters from any chart on the dashboard. You can select \"All charts\" to apply cross-filters to all charts that use the same dataset or contain the same column name in the dashboard.": [
@@ -2935,7 +2958,7 @@
"Select values in highlighted field(s) in the control panel. Then run the query by clicking on the %s button.": [
""
],
- "Send as CSV": ["Envoyer comme CSV"],
+ "Send as CSV": ["Envoyer au format CSV"],
"Send as PNG": ["Envoyer comme PNG"],
"Send as text": ["Envoyer comme texte"],
"Send range filter events to other charts": [""],
@@ -3081,6 +3104,7 @@
"Sort ascending": ["Tri croissant"],
"Sort bars by x labels.": [""],
"Sort by": ["Trier par"],
+ "Sort by %s": ["Trier par %s"],
"Sort columns alphabetically": ["Trier les colonnes alphabétiquement"],
"Sort descending": ["Tri décroissant"],
"Sort filter values": ["Trier les valeurs de filtre"],
@@ -3091,6 +3115,7 @@
"Source SQL": ["SQL source"],
"Sparkline": [""],
"Spatial": ["Spatial"],
+ "Specific Date/Time": ["Date/Heure Spécifique"],
"Specify a schema (if database flavor supports this).": [
"Spécifier un schéma (si la base de données soutient cette fonctionnalités)."
],
@@ -3633,7 +3658,7 @@
"Cela peut être soit une adresse IP (ex 127.0.0.1) ou un nom de domaine (ex mydatabase.com)."
],
"This chart applies cross-filters to charts whose datasets contain columns with the same name.": [
- ""
+ "Ce graphique filtre automatiquement les graphiques ayant des colonnes de même nom dans leurs ensembles de données."
],
"This chart has been moved to a different filter scope.": [
"Ce graphique a été déplacé vers un autre champ d'application du filtre."
@@ -3739,6 +3764,9 @@
"This value should be smaller than the right target value": [
"Cette valeur devrait être plus petite que la valeur cible de droite"
],
+ "This visualization type does not support cross-filtering.": [
+ "Ce type de visualisation ne supporte pas le cross-filtering."
+ ],
"This visualization type is not supported.": [
"Ce type de visualisation n'est pas supporté."
],
@@ -3915,6 +3943,7 @@
"Unexpected error occurred, please check your logs for details": [
"Erreur inattendue, consultez les logs pour plus de détails"
],
+ "Unexpected time range: %s": ["Intervalle de temps inattendu: %s"],
"Unknown": ["Erreur inconnue"],
"Unknown MySQL server host \"%(hostname)s\".": [
"Hôte MySQL \"%(hostname)s\" inconnu."
@@ -4133,6 +4162,7 @@
"Week_ending Sunday": ["Semaine terminant le dimanche"],
"Weekly Report for %s": [""],
"Weekly seasonality": [""],
+ "Weeks %s": ["Semaines %s"],
"What should be shown on the label?": [""],
"When `Calculation type` is set to \"Percentage change\", the Y Axis Format is forced to `.1%`": [
"Lorsque `Type de calcul` vaut \"Pourcentage de changement\", le format de l'axe Y est à forcé à `.1%`"
@@ -4282,6 +4312,7 @@
"Year": ["Année"],
"Year (freq=AS)": [""],
"Yearly seasonality": [""],
+ "Years %s": ["Année %s"],
"Yes": ["Oui"],
"Yes, cancel": ["Oui, annuler"],
"Yes, overwrite changes": [""],
@@ -4303,7 +4334,9 @@
"You can add the components in the": [
"Vous pouvez ajouter les composants via le"
],
- "You can also just click on the chart to apply cross-filter.": [""],
+ "You can also just click on the chart to apply cross-filter.": [
+ "Vous pouvez juste cliquer sur le graphique pour appliquer le filtre"
+ ],
"You can choose to display all charts that you have access to or only the ones you own.\n Your filter selection will be saved and remain active until you choose to change it.": [
""
],
@@ -4313,7 +4346,9 @@
"You can preview the list of dashboards in the chart settings dropdown.": [
""
],
- "You can't apply cross-filter on this data point.": [""],
+ "You can't apply cross-filter on this data point.": [
+ "Vous ne pouvez pas ajouter de filtre sur ce point de donnée"
+ ],
"You cannot delete the last temporal filter as it's used for time range filters in dashboards.": [
""
],
@@ -4439,6 +4474,7 @@
"aggregate": ["agrégat"],
"alert": ["alerte"],
"alerts": ["alertes"],
+ "all": ["Tous"],
"also copy (duplicate) charts": [
"copier également les graphiques (dupliquer)"
],
@@ -4524,6 +4560,11 @@
"json isn't valid": ["le json n'est pas valide"],
"key a-z": [""],
"key z-a": [""],
+ "last day": ["hier"],
+ "last month": ["le mois dernier"],
+ "last quarter": ["le trimestre dernier"],
+ "last week": ["la semaine dernière"],
+ "last year": ["l'année dernière"],
"latest partition:": ["dernière partition :"],
"less than {min} {name}": [""],
"log": ["log"],
@@ -4590,18 +4631,10 @@
"y: values are normalized within each row": [""],
"year": ["année"],
"zoom area": [""],
- "No matching records found": ["Aucun résultat trouvé"],
- "Seconds %s": ["%s secondes"],
- "Minutes %s": ["%s minutes "],
"10 seconds": ["10 secondes"],
"6 hours": ["6 heures"],
"12 hours": ["12 heures"],
- "24 hours": ["24 heures"],
- "Last day": ["Hier"],
- "Last week": ["La semaine derniere"],
- "Last month": ["Le mois dernier"],
- "Last quarter": ["Le trimestre dernier"],
- "Last year": ["L'année dernière"]
+ "24 hours": ["24 heures"]
}
}
}
diff --git a/superset/translations/fr/LC_MESSAGES/messages.po b/superset/translations/fr/LC_MESSAGES/messages.po
index 35382732ec..ab2b065ce2 100644
--- a/superset/translations/fr/LC_MESSAGES/messages.po
+++ b/superset/translations/fr/LC_MESSAGES/messages.po
@@ -1204,7 +1204,6 @@ msgid "Add calculated temporal columns to dataset in \"Edit datasource\" modal"
msgstr ""
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:197
-#, fuzzy
msgid "Add cross-filter"
msgstr "Ajouter un filtre"
@@ -2400,14 +2399,14 @@ msgid "Applied cross-filters (%d)"
msgstr "Filtres croisés appliqués (%d)"
#: superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel/index.tsx:149
-#, fuzzy, python-format
+#, python-format
msgid "Applied filters (%d)"
msgstr "Filtres appliqués (%d)"
#: superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx:260
-#, fuzzy, python-format
+#, python-format
msgid "Applied filters: %s"
-msgstr "Filtres appliqués (%d)"
+msgstr "Filtres appliqué: %s"
#: superset/viz.py:250
msgid ""
@@ -2797,7 +2796,6 @@ msgstr "Faites attention."
#: superset-frontend/src/components/AlteredSliceTag/index.jsx:178
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:75
-#, fuzzy
msgid "Before"
msgstr "Avant"
@@ -4921,7 +4919,7 @@ msgstr "Action"
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:152
msgid "Cross-filter will be applied to all of the charts that use this dataset."
-msgstr ""
+msgstr "Le filtre va être appliqué à tous les graphiques qui utilise cet ensemble de données"
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:164
#, fuzzy
@@ -4956,7 +4954,6 @@ msgid "Currently rendered: %s"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:33
-#, fuzzy
msgid "Custom"
msgstr "Personnalisée"
@@ -5598,7 +5595,7 @@ msgid "Day (freq=D)"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:65
-#, fuzzy, python-format
+#, python-format
msgid "Days %s"
msgstr "Jours %s"
@@ -6258,9 +6255,8 @@ msgstr "Édité"
#: superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:482
#: superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx:292
-#, fuzzy
msgid "Download"
-msgstr "télécharger en CSV"
+msgstr "Télécharger"
#: superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx:317
#: superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:512
@@ -6335,7 +6331,7 @@ msgid "Drill by is not yet supported for this chart type"
msgstr ""
#: superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx:420
-#, fuzzy, python-format
+#, python-format
msgid "Drill by: %s"
msgstr "Trier par %s"
@@ -7312,24 +7308,21 @@ msgstr "Exporter la requête"
#: superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:487
#: superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx:316
-#, fuzzy
msgid "Export to .CSV"
-msgstr "Exporter en YAML"
+msgstr "Exporter au format CSV"
#: superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx:323
-#, fuzzy
msgid "Export to .JSON"
-msgstr "Exporter en YAML"
+msgstr "Exporter au format JSON"
#: superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx:506
#: superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx:335
-#, fuzzy
msgid "Export to Excel"
-msgstr "Exporter en YAML"
+msgstr "Exporter vers Excel"
#: superset/views/base.py:607
msgid "Export to YAML"
-msgstr "Exporter en YAML"
+msgstr "Exporter au format YAML"
#: superset/views/base.py:607
msgid "Export to YAML?"
@@ -8251,7 +8244,7 @@ msgid "Hour"
msgstr "Heure"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:64
-#, fuzzy, python-format
+#, python-format
msgid "Hours %s"
msgstr "Heures %s"
@@ -9038,9 +9031,8 @@ msgstr "Partage de requête"
#: superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx:190
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:31
-#, fuzzy
msgid "Last"
-msgstr "à"
+msgstr "Dernier"
#: superset/connectors/sqla/views.py:388 superset/views/database/mixins.py:190
msgid "Last Changed"
@@ -9056,7 +9048,7 @@ msgid "Last Updated %s"
msgstr "Dernière mise à jour %s"
#: superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.tsx:182
-#, fuzzy, python-format
+#, python-format
msgid "Last Updated %s by %s"
msgstr "Dernière mise à jour %s"
@@ -10036,7 +10028,7 @@ msgid "Minute"
msgstr "Minute"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:63
-#, fuzzy, python-format
+#, python-format
msgid "Minutes %s"
msgstr "Minutes %s"
@@ -10102,7 +10094,7 @@ msgid "Month"
msgstr "Mois"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:67
-#, fuzzy, python-format
+#, python-format
msgid "Months %s"
msgstr "Mois %s"
@@ -10567,7 +10559,6 @@ msgid "No global filters are currently added"
msgstr "Aucun filtre ajouté"
#: superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx:204
-#, fuzzy
msgid "No matching records found"
msgstr "Aucun enregistrement trouvé"
@@ -10788,7 +10779,6 @@ msgid "November"
msgstr "Novembre"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:89
-#, fuzzy
msgid "Now"
msgstr "Maintenant"
@@ -12204,7 +12194,7 @@ msgid "Quarter"
msgstr "Trimestre"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:68
-#, fuzzy, python-format
+#, python-format
msgid "Quarters %s"
msgstr "Trimestres %s"
@@ -12582,9 +12572,8 @@ msgid "Refresh the default values"
msgstr "Rafraichir les valeurs par défaut"
#: superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx:163
-#, fuzzy
msgid "Refreshing charts"
-msgstr "Une erreur s'est produite durant la récupération des tableaux de bord : %s"
+msgstr "Rafraîchissement en cours"
#: superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.tsx:175
#, fuzzy
@@ -12632,7 +12621,6 @@ msgid "Relationships between community channels"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:88
-#, fuzzy
msgid "Relative Date/Time"
msgstr "Date/Heure Relative"
@@ -12927,7 +12915,7 @@ msgstr "Résultats"
#: superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx:58
#: superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx:212
#: superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx:84
-#, fuzzy, python-format
+#, python-format
msgid "Results %s"
msgstr "Résultats"
@@ -13798,9 +13786,9 @@ msgid "Secondary y-axis title"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:62
-#, fuzzy, python-format
+#, python-format
msgid "Seconds %s"
-msgstr "%s secondes"
+msgstr "Secondes %s"
#: superset/views/database/mixins.py:194
msgid "Secure Extra"
@@ -13865,7 +13853,7 @@ msgstr "Choisir la méthode de livraison"
#: superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx:94
msgid "Select Viz Type"
-msgstr "Selectionner un type de visualisation"
+msgstr "Sélectionner un type de visualisation"
#: superset/views/database/forms.py:425
msgid "Select a Columnar file to be uploaded to a database."
@@ -13926,7 +13914,7 @@ msgstr ""
#: superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx:130
msgid "Select a visualization type"
-msgstr "Selectionner un type de visualisation"
+msgstr "Sélectionner un type de visualisation"
#: superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx:331
msgid "Select aggregate options"
@@ -14007,7 +13995,7 @@ msgstr "Selectionner un filtre"
#: superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx:318
#: superset-frontend/src/filters/components/Select/index.ts:28
msgid "Select filter"
-msgstr "Selectionner un filtre"
+msgstr "Sélectionner un filtre"
#: superset-frontend/src/filters/components/Select/index.ts:29
msgid "Select filter plugin using AntD"
@@ -14015,7 +14003,7 @@ msgstr ""
#: superset-frontend/src/filters/components/Select/controlPanel.ts:104
msgid "Select first filter value by default"
-msgstr "Selectionne la première valeur du filtre par défaut"
+msgstr "Sélectionne la première valeur du filtre par défaut"
#: superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx:362
msgid "Select operator"
@@ -14052,7 +14040,7 @@ msgstr "Sélectionner un schéma de couleurs"
#: superset-frontend/src/visualizations/FilterBox/FilterBox.jsx:307
msgid "Select start and end date"
-msgstr "Selectionner la date de début et la date de fin"
+msgstr "Sélectionner la date de début et la date de fin"
#: superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx:334
msgid "Select subject"
@@ -14109,7 +14097,7 @@ msgstr ""
#: superset-frontend/src/features/alerts/AlertReportModal.tsx:408
msgid "Send as CSV"
-msgstr "Envoyer comme CSV"
+msgstr "Envoyer au format CSV"
#: superset-frontend/src/features/alerts/AlertReportModal.tsx:407
msgid "Send as PNG"
@@ -14827,7 +14815,7 @@ msgid "Sort by"
msgstr "Trier par"
#: superset-frontend/src/dashboard/components/SliceAdder.jsx:362
-#, fuzzy, python-format
+#, python-format
msgid "Sort by %s"
msgstr "Trier par %s"
@@ -14915,7 +14903,6 @@ msgid "Spatial"
msgstr "Spatial"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:87
-#, fuzzy
msgid "Specific Date/Time"
msgstr "Date/Heure Spécifique"
@@ -16814,7 +16801,8 @@ msgstr ""
msgid ""
"This chart applies cross-filters to charts whose datasets contain columns"
" with the same name."
-msgstr ""
+msgstr "Ce graphique filtre automatiquement les graphiques ayant des colonnes de même nom dans leurs"
+" ensembles de données."
#: superset-frontend/src/dashboard/actions/dashboardLayout.js:260
msgid "This chart has been moved to a different filter scope."
@@ -17073,9 +17061,8 @@ msgid "This value should be smaller than the right target value"
msgstr "Cette valeur devrait être plus petite que la valeur cible de droite"
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:171
-#, fuzzy
msgid "This visualization type does not support cross-filtering."
-msgstr "Ce type de visualisation n'est pas supporté."
+msgstr "Ce type de visualisation ne supporte pas le cross-filtering."
#: superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx:64
msgid "This visualization type is not supported."
@@ -17927,9 +17914,9 @@ msgid "Unexpected error: "
msgstr "Erreur inattendue"
#: superset/views/api.py:108
-#, fuzzy, python-format
+#, python-format
msgid "Unexpected time range: %s"
-msgstr "Erreur inattendue"
+msgstr "Intervalle de temps inattendu: %s"
#: superset-frontend/src/features/home/ActivityTable.tsx:86
msgid "Unknown"
@@ -18697,7 +18684,7 @@ msgid "Weekly seasonality"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:66
-#, fuzzy, python-format
+#, python-format
msgid "Weeks %s"
msgstr "Semaines %s"
@@ -19346,7 +19333,7 @@ msgid "Yearly seasonality"
msgstr ""
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:69
-#, fuzzy, python-format
+#, python-format
msgid "Years %s"
msgstr "Année %s"
@@ -19448,7 +19435,7 @@ msgstr "Vous pouvez ajouter les composants via mode edition"
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:157
msgid "You can also just click on the chart to apply cross-filter."
-msgstr ""
+msgstr "Vous pouvez juste cliquer sur le graphique pour appliquer le filtre"
#: superset-frontend/src/dashboard/components/SliceAdder.jsx:386
msgid ""
@@ -19473,7 +19460,7 @@ msgstr ""
#: superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx:178
msgid "You can't apply cross-filter on this data point."
-msgstr ""
+msgstr "Vous ne pouvez pas ajouter de filtre sur ce point de donnée"
#: superset-frontend/src/explore/components/ControlPanelsContainer.tsx:501
msgid ""
@@ -19823,7 +19810,6 @@ msgstr "alertes"
#: superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx:160
#: superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx:205
#: superset-frontend/src/explore/controls.jsx:254
-#, fuzzy
msgid "all"
msgstr "Tous"
@@ -20417,27 +20403,22 @@ msgid "label"
msgstr "Label"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:39
-#, fuzzy
msgid "last day"
msgstr "hier"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:41
-#, fuzzy
msgid "last month"
msgstr "le mois dernier"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:42
-#, fuzzy
msgid "last quarter"
msgstr "le trimestre dernier"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:40
-#, fuzzy
msgid "last week"
-msgstr "la semaine derniere"
+msgstr "la semaine dernière"
#: superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts:43
-#, fuzzy
msgid "last year"
msgstr "l'année dernière"
From f934278fefd615b3d91cfa48f5a1221fae35b8f8 Mon Sep 17 00:00:00 2001
From: Sam Firke
Date: Tue, 21 Nov 2023 11:54:16 -0500
Subject: [PATCH 052/119] docs(intro): fix a single broken link (BugHerd #97)
(#26039)
---
docs/docs/intro.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/intro.mdx b/docs/docs/intro.mdx
index 0f0315fc05..cd93aa6be6 100644
--- a/docs/docs/intro.mdx
+++ b/docs/docs/intro.mdx
@@ -15,7 +15,7 @@ Here are a **few different ways you can get started with Superset**:
- Install Superset [from scratch](https://superset.apache.org/docs/installation/installing-superset-from-scratch/)
- Deploy Superset locally with one command
- [using Docker Compose](installation/installing-superset-using-docker-compose)
+ [using Docker Compose](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose)
- Deploy Superset [with Kubernetes](https://superset.apache.org/docs/installation/running-on-kubernetes)
- Run a [Docker image](https://hub.docker.com/r/apache/superset) from Dockerhub
- Download Superset [from Pypi here](https://pypi.org/project/apache-superset/)
From 68e5e1afea0f2c898a641988f509427cce5484df Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Tue, 21 Nov 2023 15:05:41 -0300
Subject: [PATCH 053/119] feat: Add Bubble chart migration logic (#26033)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
---
.../src/Bubble/transformProps.ts | 11 ++-
superset/cli/viz_migrations.py | 3 +
.../shared/migrate_viz/processors.py | 29 +++++++
.../viz/nvd3_bubble_chart_to_echarts_test.py | 76 +++++++++++++++++++
4 files changed, 113 insertions(+), 6 deletions(-)
create mode 100644 tests/unit_tests/migrations/viz/nvd3_bubble_chart_to_echarts_test.py
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
index 7962bc2c36..ce53fdf266 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
@@ -104,7 +104,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
- const legends: string[] = [];
+ const legends = new Set();
const series: ScatterSeriesOption[] = [];
const xAxisLabel: string = getMetricLabel(x);
@@ -114,9 +114,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
const refs: Refs = {};
data.forEach(datum => {
- const name =
- ((bubbleSeries ? datum[bubbleSeries] : datum[entity]) as string) ||
- NULL_STRING;
+ const dataName = bubbleSeries ? datum[bubbleSeries] : datum[entity];
+ const name = dataName ? String(dataName) : NULL_STRING;
const bubbleSeriesValue = bubbleSeries ? datum[bubbleSeries] : null;
series.push({
@@ -133,7 +132,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
type: 'scatter',
itemStyle: { color: colorFn(name), opacity },
});
- legends.push(name);
+ legends.add(name);
});
normalizeSymbolSize(series, maxBubbleSize);
@@ -196,7 +195,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
},
legend: {
...getLegendProps(legendType, legendOrientation, showLegend, theme),
- data: legends,
+ data: Array.from(legends),
},
tooltip: {
show: !inContextMenu,
diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py
index 4451580d0b..f24dd8f444 100644
--- a/superset/cli/viz_migrations.py
+++ b/superset/cli/viz_migrations.py
@@ -25,6 +25,7 @@ from superset import db
class VizType(str, Enum):
AREA = "area"
+ BUBBLE = "bubble"
DUAL_LINE = "dual_line"
LINE = "line"
PIVOT_TABLE = "pivot_table"
@@ -76,6 +77,7 @@ def migrate(viz_type: VizType, is_downgrade: bool = False) -> None:
# pylint: disable=import-outside-toplevel
from superset.migrations.shared.migrate_viz.processors import (
MigrateAreaChart,
+ MigrateBubbleChart,
MigrateDualLine,
MigrateLineChart,
MigratePivotTable,
@@ -85,6 +87,7 @@ def migrate(viz_type: VizType, is_downgrade: bool = False) -> None:
migrations = {
VizType.AREA: MigrateAreaChart,
+ VizType.BUBBLE: MigrateBubbleChart,
VizType.DUAL_LINE: MigrateDualLine,
VizType.LINE: MigrateLineChart,
VizType.PIVOT_TABLE: MigratePivotTable,
diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py
index 2d2bebc618..5fbd624aa8 100644
--- a/superset/migrations/shared/migrate_viz/processors.py
+++ b/superset/migrations/shared/migrate_viz/processors.py
@@ -184,3 +184,32 @@ class MigrateAreaChart(TimeseriesChart):
)
self.data["opacity"] = 0.7
+
+
+class MigrateBubbleChart(MigrateViz):
+ source_viz_type = "bubble"
+ target_viz_type = "bubble_v2"
+ rename_keys = {
+ "bottom_margin": "x_axis_title_margin",
+ "left_margin": "y_axis_title_margin",
+ "limit": "row_limit",
+ "x_axis_format": "xAxisFormat",
+ "x_log_scale": "logXAxis",
+ "x_ticks_layout": "xAxisLabelRotation",
+ "y_axis_showminmax": "truncateYAxis",
+ "y_log_scale": "logYAxis",
+ }
+ remove_keys = {"x_axis_showminmax"}
+
+ def _pre_action(self) -> None:
+ bottom_margin = self.data.get("bottom_margin")
+ if self.data.get("x_axis_label") and (
+ not bottom_margin or bottom_margin == "auto"
+ ):
+ self.data["bottom_margin"] = 30
+
+ if x_ticks_layout := self.data.get("x_ticks_layout"):
+ self.data["x_ticks_layout"] = 45 if x_ticks_layout == "45°" else 0
+
+ # Truncate y-axis by default to preserve layout
+ self.data["y_axis_showminmax"] = True
diff --git a/tests/unit_tests/migrations/viz/nvd3_bubble_chart_to_echarts_test.py b/tests/unit_tests/migrations/viz/nvd3_bubble_chart_to_echarts_test.py
new file mode 100644
index 0000000000..070083b7ae
--- /dev/null
+++ b/tests/unit_tests/migrations/viz/nvd3_bubble_chart_to_echarts_test.py
@@ -0,0 +1,76 @@
+# 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 typing import Any
+
+from superset.migrations.shared.migrate_viz import MigrateBubbleChart
+from tests.unit_tests.migrations.viz.utils import migrate_and_assert
+
+SOURCE_FORM_DATA: dict[str, Any] = {
+ "adhoc_filters": [],
+ "bottom_margin": 20,
+ "color_scheme": "default",
+ "entity": "count",
+ "left_margin": 20,
+ "limit": 100,
+ "max_bubble_size": 50,
+ "series": ["region"],
+ "show_legend": True,
+ "size": 75,
+ "viz_type": "bubble",
+ "x": "year",
+ "x_axis_format": "SMART_DATE",
+ "x_axis_label": "Year",
+ "x_axis_showminmax": True,
+ "x_log_scale": True,
+ "x_ticks_layout": "45°",
+ "y": "country",
+ "y_axis_bounds": [0, 100],
+ "y_axis_format": "SMART_DATE",
+ "y_axis_label": "Year",
+ "y_axis_showminmax": False,
+ "y_log_scale": True,
+}
+
+TARGET_FORM_DATA: dict[str, Any] = {
+ "adhoc_filters": [],
+ "color_scheme": "default",
+ "entity": "count",
+ "form_data_bak": SOURCE_FORM_DATA,
+ "logXAxis": True,
+ "logYAxis": True,
+ "max_bubble_size": 50,
+ "row_limit": 100,
+ "series": ["region"],
+ "show_legend": True,
+ "size": 75,
+ "truncateYAxis": True,
+ "viz_type": "bubble_v2",
+ "x": "year",
+ "xAxisFormat": "SMART_DATE",
+ "xAxisLabelRotation": 45,
+ "x_axis_label": "Year",
+ "x_axis_title_margin": 20,
+ "y": "country",
+ "y_axis_bounds": [0, 100],
+ "y_axis_format": "SMART_DATE",
+ "y_axis_label": "Year",
+ "y_axis_title_margin": 20,
+}
+
+
+def test_migration() -> None:
+ migrate_and_assert(MigrateBubbleChart, SOURCE_FORM_DATA, TARGET_FORM_DATA)
From 07551dc3d44f3a5666915e16a8ad97d831d972e8 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Tue, 21 Nov 2023 10:11:50 -0800
Subject: [PATCH 054/119] chore(connector): Cleanup base models and views
according to SIP-92 (#24773)
---
superset/charts/data/api.py | 2 +-
superset/charts/post_processing.py | 2 +-
superset/commands/utils.py | 2 +-
superset/common/query_actions.py | 2 +-
superset/common/query_context.py | 2 +-
superset/common/query_context_factory.py | 2 +-
superset/common/query_context_processor.py | 2 +-
superset/common/query_object.py | 2 +-
superset/common/query_object_factory.py | 2 +-
superset/connectors/base/__init__.py | 16 -
superset/connectors/base/models.py | 769 ------------------
superset/connectors/base/views.py | 48 --
superset/connectors/sqla/models.py | 662 ++++++++++++++-
superset/connectors/sqla/views.py | 3 +-
superset/daos/chart.py | 2 +-
superset/datasets/commands/importers/v0.py | 28 +-
superset/examples/world_bank.py | 14 +-
superset/explore/commands/get.py | 3 +-
superset/models/dashboard.py | 8 +-
superset/models/slice.py | 2 +-
superset/security/manager.py | 7 +-
superset/utils/core.py | 4 +-
superset/views/core.py | 3 +-
superset/viz.py | 2 +-
tests/integration_tests/base_tests.py | 3 +-
.../common/test_get_aggregated_join_column.py | 2 +-
26 files changed, 680 insertions(+), 914 deletions(-)
delete mode 100644 superset/connectors/base/__init__.py
delete mode 100644 superset/connectors/base/models.py
delete mode 100644 superset/connectors/base/views.py
diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py
index c8ed840c7c..885b6691b3 100644
--- a/superset/charts/data/api.py
+++ b/superset/charts/data/api.py
@@ -42,7 +42,7 @@ from superset.charts.data.query_context_cache_loader import QueryContextCacheLoa
from superset.charts.post_processing import apply_post_process
from superset.charts.schemas import ChartDataQueryContextSchema
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
-from superset.connectors.base.models import BaseDatasource
+from superset.connectors.sqla.models import BaseDatasource
from superset.daos.exceptions import DatasourceNotFound
from superset.exceptions import QueryObjectValidationError
from superset.extensions import event_logger
diff --git a/superset/charts/post_processing.py b/superset/charts/post_processing.py
index 939714642f..ebcae32f8f 100644
--- a/superset/charts/post_processing.py
+++ b/superset/charts/post_processing.py
@@ -40,7 +40,7 @@ from superset.utils.core import (
)
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
from superset.models.sql_lab import Query
diff --git a/superset/commands/utils.py b/superset/commands/utils.py
index 02b6b5f383..8cfeab3c11 100644
--- a/superset/commands/utils.py
+++ b/superset/commands/utils.py
@@ -33,7 +33,7 @@ from superset.extensions import db
from superset.utils.core import DatasourceType, get_user_id
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
def populate_owners(
diff --git a/superset/common/query_actions.py b/superset/common/query_actions.py
index 22c778b77b..d73a99d027 100644
--- a/superset/common/query_actions.py
+++ b/superset/common/query_actions.py
@@ -24,7 +24,7 @@ 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.base.models import BaseDatasource
+from superset.connectors.sqla.models import BaseDatasource
from superset.exceptions import QueryObjectValidationError
from superset.utils.core import (
extract_column_dtype,
diff --git a/superset/common/query_context.py b/superset/common/query_context.py
index 1a8d3c518b..4f517cd905 100644
--- a/superset/common/query_context.py
+++ b/superset/common/query_context.py
@@ -30,7 +30,7 @@ from superset.common.query_object import QueryObject
from superset.models.slice import Slice
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
from superset.models.helpers import QueryResult
diff --git a/superset/common/query_context_factory.py b/superset/common/query_context_factory.py
index d6510ccd9a..708907d4a9 100644
--- a/superset/common/query_context_factory.py
+++ b/superset/common/query_context_factory.py
@@ -29,7 +29,7 @@ from superset.models.slice import Slice
from superset.utils.core import DatasourceDict, DatasourceType, is_adhoc_column
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
config = app.config
diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index 5a0468b671..7967313cd7 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -36,7 +36,7 @@ from superset.common.utils.time_range_utils import (
get_since_until_from_query_object,
get_since_until_from_time_range,
)
-from superset.connectors.base.models import BaseDatasource
+from superset.connectors.sqla.models import BaseDatasource
from superset.constants import CacheRegion, TimeGrain
from superset.daos.annotation import AnnotationLayerDAO
from superset.daos.chart import ChartDAO
diff --git a/superset/common/query_object.py b/superset/common/query_object.py
index 1e826761ec..989df5775b 100644
--- a/superset/common/query_object.py
+++ b/superset/common/query_object.py
@@ -49,7 +49,7 @@ from superset.utils.core import (
from superset.utils.hashing import md5_sha_from_dict
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
logger = logging.getLogger(__name__)
diff --git a/superset/common/query_object_factory.py b/superset/common/query_object_factory.py
index d993eca279..d2aa140dfe 100644
--- a/superset/common/query_object_factory.py
+++ b/superset/common/query_object_factory.py
@@ -35,7 +35,7 @@ from superset.utils.core import (
if TYPE_CHECKING:
from sqlalchemy.orm import sessionmaker
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
from superset.daos.datasource import DatasourceDAO
diff --git a/superset/connectors/base/__init__.py b/superset/connectors/base/__init__.py
deleted file mode 100644
index 13a83393a9..0000000000
--- a/superset/connectors/base/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# 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/superset/connectors/base/models.py b/superset/connectors/base/models.py
deleted file mode 100644
index 1fc0fde575..0000000000
--- a/superset/connectors/base/models.py
+++ /dev/null
@@ -1,769 +0,0 @@
-# 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 __future__ import annotations
-
-import builtins
-import json
-import logging
-from collections.abc import Hashable
-from datetime import datetime
-from json.decoder import JSONDecodeError
-from typing import Any, TYPE_CHECKING
-
-from flask_appbuilder.security.sqla.models import User
-from flask_babel import gettext as __
-from sqlalchemy import and_, Boolean, Column, Integer, String, Text
-from sqlalchemy.ext.declarative import declared_attr
-from sqlalchemy.orm import foreign, Query, relationship, RelationshipProperty, Session
-from sqlalchemy.sql import literal_column
-
-from superset import security_manager
-from superset.constants import EMPTY_STRING, NULL_STRING
-from superset.datasets.commands.exceptions import DatasetNotFoundError
-from superset.models.helpers import AuditMixinNullable, ImportExportMixin, QueryResult
-from superset.models.slice import Slice
-from superset.superset_typing import (
- FilterValue,
- FilterValues,
- QueryObjectDict,
- ResultSetColumnType,
-)
-from superset.utils import core as utils
-from superset.utils.backports import StrEnum
-from superset.utils.core import GenericDataType, MediumText
-
-if TYPE_CHECKING:
- from superset.db_engine_specs.base import BaseEngineSpec
-
-logger = logging.getLogger(__name__)
-
-METRIC_FORM_DATA_PARAMS = [
- "metric",
- "metric_2",
- "metrics",
- "metrics_b",
- "percent_metrics",
- "secondary_metric",
- "size",
- "timeseries_limit_metric",
- "x",
- "y",
-]
-
-COLUMN_FORM_DATA_PARAMS = [
- "all_columns",
- "all_columns_x",
- "columns",
- "entity",
- "groupby",
- "order_by_cols",
- "series",
-]
-
-
-class DatasourceKind(StrEnum):
- VIRTUAL = "virtual"
- PHYSICAL = "physical"
-
-
-class BaseDatasource(
- AuditMixinNullable, ImportExportMixin
-): # pylint: disable=too-many-public-methods
- """A common interface to objects that are queryable
- (tables and datasources)"""
-
- # ---------------------------------------------------------------
- # class attributes to define when deriving BaseDatasource
- # ---------------------------------------------------------------
- __tablename__: str | None = None # {connector_name}_datasource
- baselink: str | None = None # url portion pointing to ModelView endpoint
-
- @property
- def column_class(self) -> type[BaseColumn]:
- # link to derivative of BaseColumn
- raise NotImplementedError()
-
- @property
- def metric_class(self) -> type[BaseMetric]:
- # link to derivative of BaseMetric
- raise NotImplementedError()
-
- owner_class: User | None = None
-
- # Used to do code highlighting when displaying the query in the UI
- query_language: str | None = None
-
- # Only some datasources support Row Level Security
- is_rls_supported: bool = False
-
- @property
- def name(self) -> str:
- # can be a Column or a property pointing to one
- raise NotImplementedError()
-
- # ---------------------------------------------------------------
-
- # Columns
- id = Column(Integer, primary_key=True)
- description = Column(Text)
- default_endpoint = Column(Text)
- is_featured = Column(Boolean, default=False) # TODO deprecating
- filter_select_enabled = Column(Boolean, default=True)
- offset = Column(Integer, default=0)
- cache_timeout = Column(Integer)
- params = Column(String(1000))
- perm = Column(String(1000))
- schema_perm = Column(String(1000))
- is_managed_externally = Column(Boolean, nullable=False, default=False)
- external_url = Column(Text, nullable=True)
-
- sql: str | None = None
- owners: list[User]
- update_from_object_fields: list[str]
-
- extra_import_fields = ["is_managed_externally", "external_url"]
-
- @property
- def kind(self) -> DatasourceKind:
- return DatasourceKind.VIRTUAL if self.sql else DatasourceKind.PHYSICAL
-
- @property
- def owners_data(self) -> list[dict[str, Any]]:
- return [
- {
- "first_name": o.first_name,
- "last_name": o.last_name,
- "username": o.username,
- "id": o.id,
- }
- for o in self.owners
- ]
-
- @property
- def is_virtual(self) -> bool:
- return self.kind == DatasourceKind.VIRTUAL
-
- @declared_attr
- def slices(self) -> RelationshipProperty:
- return relationship(
- "Slice",
- overlaps="table",
- primaryjoin=lambda: and_(
- foreign(Slice.datasource_id) == self.id,
- foreign(Slice.datasource_type) == self.type,
- ),
- )
-
- columns: list[BaseColumn] = []
- metrics: list[BaseMetric] = []
-
- @property
- def type(self) -> str:
- raise NotImplementedError()
-
- @property
- def uid(self) -> str:
- """Unique id across datasource types"""
- return f"{self.id}__{self.type}"
-
- @property
- def column_names(self) -> list[str]:
- return sorted([c.column_name for c in self.columns], key=lambda x: x or "")
-
- @property
- def columns_types(self) -> dict[str, str]:
- return {c.column_name: c.type for c in self.columns}
-
- @property
- def main_dttm_col(self) -> str:
- return "timestamp"
-
- @property
- def datasource_name(self) -> str:
- raise NotImplementedError()
-
- @property
- def connection(self) -> str | None:
- """String representing the context of the Datasource"""
- return None
-
- @property
- def schema(self) -> str | None:
- """String representing the schema of the Datasource (if it applies)"""
- return None
-
- @property
- def filterable_column_names(self) -> list[str]:
- return sorted([c.column_name for c in self.columns if c.filterable])
-
- @property
- def dttm_cols(self) -> list[str]:
- return []
-
- @property
- def url(self) -> str:
- return f"/{self.baselink}/edit/{self.id}"
-
- @property
- def explore_url(self) -> str:
- if self.default_endpoint:
- return self.default_endpoint
- return f"/explore/?datasource_type={self.type}&datasource_id={self.id}"
-
- @property
- def column_formats(self) -> dict[str, str | None]:
- return {m.metric_name: m.d3format for m in self.metrics if m.d3format}
-
- @property
- def currency_formats(self) -> dict[str, dict[str, str | None] | None]:
- return {m.metric_name: m.currency_json for m in self.metrics if m.currency_json}
-
- def add_missing_metrics(self, metrics: list[BaseMetric]) -> None:
- existing_metrics = {m.metric_name for m in self.metrics}
- for metric in metrics:
- if metric.metric_name not in existing_metrics:
- metric.table_id = self.id
- self.metrics.append(metric)
-
- @property
- def short_data(self) -> dict[str, Any]:
- """Data representation of the datasource sent to the frontend"""
- return {
- "edit_url": self.url,
- "id": self.id,
- "uid": self.uid,
- "schema": self.schema,
- "name": self.name,
- "type": self.type,
- "connection": self.connection,
- "creator": str(self.created_by),
- }
-
- @property
- def select_star(self) -> str | None:
- pass
-
- @property
- def order_by_choices(self) -> list[tuple[str, str]]:
- choices = []
- # self.column_names return sorted column_names
- for column_name in self.column_names:
- column_name = str(column_name or "")
- choices.append(
- (json.dumps([column_name, True]), f"{column_name} " + __("[asc]"))
- )
- choices.append(
- (json.dumps([column_name, False]), f"{column_name} " + __("[desc]"))
- )
- return choices
-
- @property
- def verbose_map(self) -> dict[str, str]:
- verb_map = {"__timestamp": "Time"}
- verb_map.update(
- {o.metric_name: o.verbose_name or o.metric_name for o in self.metrics}
- )
- verb_map.update(
- {o.column_name: o.verbose_name or o.column_name for o in self.columns}
- )
- return verb_map
-
- @property
- def data(self) -> dict[str, Any]:
- """Data representation of the datasource sent to the frontend"""
- return {
- # simple fields
- "id": self.id,
- "uid": self.uid,
- "column_formats": self.column_formats,
- "currency_formats": self.currency_formats,
- "description": self.description,
- "database": self.database.data, # pylint: disable=no-member
- "default_endpoint": self.default_endpoint,
- "filter_select": self.filter_select_enabled, # TODO deprecate
- "filter_select_enabled": self.filter_select_enabled,
- "name": self.name,
- "datasource_name": self.datasource_name,
- "table_name": self.datasource_name,
- "type": self.type,
- "schema": self.schema,
- "offset": self.offset,
- "cache_timeout": self.cache_timeout,
- "params": self.params,
- "perm": self.perm,
- "edit_url": self.url,
- # sqla-specific
- "sql": self.sql,
- # one to many
- "columns": [o.data for o in self.columns],
- "metrics": [o.data for o in self.metrics],
- # TODO deprecate, move logic to JS
- "order_by_choices": self.order_by_choices,
- "owners": [owner.id for owner in self.owners],
- "verbose_map": self.verbose_map,
- "select_star": self.select_star,
- }
-
- def data_for_slices( # pylint: disable=too-many-locals
- self, slices: list[Slice]
- ) -> dict[str, Any]:
- """
- The representation of the datasource containing only the required data
- to render the provided slices.
-
- Used to reduce the payload when loading a dashboard.
- """
- data = self.data
- metric_names = set()
- column_names = set()
- for slc in slices:
- form_data = slc.form_data
- # pull out all required metrics from the form_data
- for metric_param in METRIC_FORM_DATA_PARAMS:
- for metric in utils.as_list(form_data.get(metric_param) or []):
- metric_names.add(utils.get_metric_name(metric))
- if utils.is_adhoc_metric(metric):
- column = metric.get("column") or {}
- if column_name := column.get("column_name"):
- column_names.add(column_name)
-
- # Columns used in query filters
- column_names.update(
- filter_["subject"]
- for filter_ in form_data.get("adhoc_filters") or []
- if filter_.get("clause") == "WHERE" and filter_.get("subject")
- )
-
- # columns used by Filter Box
- column_names.update(
- filter_config["column"]
- for filter_config in form_data.get("filter_configs") or []
- if "column" in filter_config
- )
-
- # for legacy dashboard imports which have the wrong query_context in them
- try:
- query_context = slc.get_query_context()
- except DatasetNotFoundError:
- query_context = None
-
- # legacy charts don't have query_context charts
- if query_context:
- column_names.update(
- [
- utils.get_column_name(column)
- for query in query_context.queries
- for column in query.columns
- ]
- or []
- )
- else:
- _columns = [
- utils.get_column_name(column)
- if utils.is_adhoc_column(column)
- else column
- for column_param in COLUMN_FORM_DATA_PARAMS
- for column in utils.as_list(form_data.get(column_param) or [])
- ]
- column_names.update(_columns)
-
- filtered_metrics = [
- metric
- for metric in data["metrics"]
- if metric["metric_name"] in metric_names
- ]
-
- filtered_columns: list[Column] = []
- column_types: set[GenericDataType] = set()
- for column in data["columns"]:
- generic_type = column.get("type_generic")
- if generic_type is not None:
- column_types.add(generic_type)
- if column["column_name"] in column_names:
- filtered_columns.append(column)
-
- data["column_types"] = list(column_types)
- del data["description"]
- data.update({"metrics": filtered_metrics})
- data.update({"columns": filtered_columns})
- verbose_map = {"__timestamp": "Time"}
- verbose_map.update(
- {
- metric["metric_name"]: metric["verbose_name"] or metric["metric_name"]
- for metric in filtered_metrics
- }
- )
- verbose_map.update(
- {
- column["column_name"]: column["verbose_name"] or column["column_name"]
- for column in filtered_columns
- }
- )
- data["verbose_map"] = verbose_map
-
- return data
-
- @staticmethod
- def filter_values_handler( # pylint: disable=too-many-arguments
- values: FilterValues | None,
- operator: str,
- target_generic_type: GenericDataType,
- target_native_type: str | None = None,
- is_list_target: bool = False,
- db_engine_spec: builtins.type[BaseEngineSpec] | None = None,
- db_extra: dict[str, Any] | None = None,
- ) -> FilterValues | None:
- if values is None:
- return None
-
- def handle_single_value(value: FilterValue | None) -> FilterValue | None:
- if operator == utils.FilterOperator.TEMPORAL_RANGE:
- return value
- if (
- isinstance(value, (float, int))
- and target_generic_type == utils.GenericDataType.TEMPORAL
- and target_native_type is not None
- and db_engine_spec is not None
- ):
- value = db_engine_spec.convert_dttm(
- target_type=target_native_type,
- dttm=datetime.utcfromtimestamp(value / 1000),
- db_extra=db_extra,
- )
- value = literal_column(value)
- if isinstance(value, str):
- value = value.strip("\t\n")
-
- if (
- target_generic_type == utils.GenericDataType.NUMERIC
- and operator
- not in {
- utils.FilterOperator.ILIKE,
- utils.FilterOperator.LIKE,
- }
- ):
- # For backwards compatibility and edge cases
- # where a column data type might have changed
- return utils.cast_to_num(value)
- if value == NULL_STRING:
- return None
- if value == EMPTY_STRING:
- return ""
- if target_generic_type == utils.GenericDataType.BOOLEAN:
- return utils.cast_to_boolean(value)
- return value
-
- if isinstance(values, (list, tuple)):
- values = [handle_single_value(v) for v in values] # type: ignore
- else:
- values = handle_single_value(values)
- if is_list_target and not isinstance(values, (tuple, list)):
- values = [values] # type: ignore
- elif not is_list_target and isinstance(values, (tuple, list)):
- values = values[0] if values else None
- return values
-
- def external_metadata(self) -> list[ResultSetColumnType]:
- """Returns column information from the external system"""
- raise NotImplementedError()
-
- def get_query_str(self, query_obj: QueryObjectDict) -> str:
- """Returns a query as a string
-
- This is used to be displayed to the user so that they can
- understand what is taking place behind the scene"""
- raise NotImplementedError()
-
- def query(self, query_obj: QueryObjectDict) -> QueryResult:
- """Executes the query and returns a dataframe
-
- query_obj is a dictionary representing Superset's query interface.
- Should return a ``superset.models.helpers.QueryResult``
- """
- raise NotImplementedError()
-
- @staticmethod
- def default_query(qry: Query) -> Query:
- return qry
-
- def get_column(self, column_name: str | None) -> BaseColumn | None:
- if not column_name:
- return None
- for col in self.columns:
- if col.column_name == column_name:
- return col
- return None
-
- @staticmethod
- def get_fk_many_from_list(
- object_list: list[Any],
- fkmany: list[Column],
- fkmany_class: builtins.type[BaseColumn | BaseMetric],
- key_attr: str,
- ) -> list[Column]:
- """Update ORM one-to-many list from object list
-
- Used for syncing metrics and columns using the same code"""
-
- object_dict = {o.get(key_attr): o for o in object_list}
-
- # delete fks that have been removed
- fkmany = [o for o in fkmany if getattr(o, key_attr) in object_dict]
-
- # sync existing fks
- for fk in fkmany:
- obj = object_dict.get(getattr(fk, key_attr))
- if obj:
- for attr in fkmany_class.update_from_object_fields:
- setattr(fk, attr, obj.get(attr))
-
- # create new fks
- new_fks = []
- orm_keys = [getattr(o, key_attr) for o in fkmany]
- for obj in object_list:
- key = obj.get(key_attr)
- if key not in orm_keys:
- del obj["id"]
- orm_kwargs = {}
- for k in obj:
- if k in fkmany_class.update_from_object_fields and k in obj:
- orm_kwargs[k] = obj[k]
- new_obj = fkmany_class(**orm_kwargs)
- new_fks.append(new_obj)
- fkmany += new_fks
- return fkmany
-
- def update_from_object(self, obj: dict[str, Any]) -> None:
- """Update datasource from a data structure
-
- The UI's table editor crafts a complex data structure that
- contains most of the datasource's properties as well as
- an array of metrics and columns objects. This method
- receives the object from the UI and syncs the datasource to
- match it. Since the fields are different for the different
- connectors, the implementation uses ``update_from_object_fields``
- which can be defined for each connector and
- defines which fields should be synced"""
- for attr in self.update_from_object_fields:
- setattr(self, attr, obj.get(attr))
-
- self.owners = obj.get("owners", [])
-
- # Syncing metrics
- metrics = (
- self.get_fk_many_from_list(
- obj["metrics"], self.metrics, self.metric_class, "metric_name"
- )
- if self.metric_class and "metrics" in obj
- else []
- )
- self.metrics = metrics
-
- # Syncing columns
- self.columns = (
- self.get_fk_many_from_list(
- obj["columns"], self.columns, self.column_class, "column_name"
- )
- if self.column_class and "columns" in obj
- else []
- )
-
- def get_extra_cache_keys(
- self, query_obj: QueryObjectDict # pylint: disable=unused-argument
- ) -> list[Hashable]:
- """If a datasource needs to provide additional keys for calculation of
- cache keys, those can be provided via this method
-
- :param query_obj: The dict representation of a query object
- :return: list of keys
- """
- return []
-
- def __hash__(self) -> int:
- return hash(self.uid)
-
- def __eq__(self, other: object) -> bool:
- if not isinstance(other, BaseDatasource):
- return NotImplemented
- return self.uid == other.uid
-
- def raise_for_access(self) -> None:
- """
- Raise an exception if the user cannot access the resource.
-
- :raises SupersetSecurityException: If the user cannot access the resource
- """
-
- security_manager.raise_for_access(datasource=self)
-
- @classmethod
- def get_datasource_by_name(
- cls, session: Session, datasource_name: str, schema: str, database_name: str
- ) -> BaseDatasource | None:
- raise NotImplementedError()
-
-
-class BaseColumn(AuditMixinNullable, ImportExportMixin):
- """Interface for column"""
-
- __tablename__: str | None = None # {connector_name}_column
-
- id = Column(Integer, primary_key=True)
- column_name = Column(String(255), nullable=False)
- verbose_name = Column(String(1024))
- is_active = Column(Boolean, default=True)
- type = Column(Text)
- advanced_data_type = Column(String(255))
- groupby = Column(Boolean, default=True)
- filterable = Column(Boolean, default=True)
- description = Column(MediumText())
- is_dttm = None
-
- # [optional] Set this to support import/export functionality
- export_fields: list[Any] = []
-
- def __repr__(self) -> str:
- return str(self.column_name)
-
- bool_types = ("BOOL",)
- num_types = (
- "DOUBLE",
- "FLOAT",
- "INT",
- "BIGINT",
- "NUMBER",
- "LONG",
- "REAL",
- "NUMERIC",
- "DECIMAL",
- "MONEY",
- )
- date_types = ("DATE", "TIME")
- str_types = ("VARCHAR", "STRING", "CHAR")
-
- @property
- def is_numeric(self) -> bool:
- return self.type and any(map(lambda t: t in self.type.upper(), self.num_types))
-
- @property
- def is_temporal(self) -> bool:
- return self.type and any(map(lambda t: t in self.type.upper(), self.date_types))
-
- @property
- def is_string(self) -> bool:
- return self.type and any(map(lambda t: t in self.type.upper(), self.str_types))
-
- @property
- def is_boolean(self) -> bool:
- return self.type and any(map(lambda t: t in self.type.upper(), self.bool_types))
-
- @property
- def type_generic(self) -> utils.GenericDataType | None:
- if self.is_string:
- return utils.GenericDataType.STRING
- if self.is_boolean:
- return utils.GenericDataType.BOOLEAN
- if self.is_numeric:
- return utils.GenericDataType.NUMERIC
- if self.is_temporal:
- return utils.GenericDataType.TEMPORAL
- return None
-
- @property
- def expression(self) -> Column:
- raise NotImplementedError()
-
- @property
- def python_date_format(self) -> Column:
- raise NotImplementedError()
-
- @property
- def data(self) -> dict[str, Any]:
- attrs = (
- "id",
- "column_name",
- "verbose_name",
- "description",
- "expression",
- "filterable",
- "groupby",
- "is_dttm",
- "type",
- "advanced_data_type",
- )
- return {s: getattr(self, s) for s in attrs if hasattr(self, s)}
-
-
-class BaseMetric(AuditMixinNullable, ImportExportMixin):
- """Interface for Metrics"""
-
- __tablename__: str | None = None # {connector_name}_metric
-
- id = Column(Integer, primary_key=True)
- metric_name = Column(String(255), nullable=False)
- verbose_name = Column(String(1024))
- metric_type = Column(String(32))
- description = Column(MediumText())
- d3format = Column(String(128))
- currency = Column(String(128))
- warning_text = Column(Text)
-
- """
- The interface should also declare a datasource relationship pointing
- to a derivative of BaseDatasource, along with a FK
-
- datasource_name = Column(
- String(255),
- ForeignKey('datasources.datasource_name'))
- datasource = relationship(
- # needs to be altered to point to {Connector}Datasource
- 'BaseDatasource',
- backref=backref('metrics', cascade='all, delete-orphan'),
- enable_typechecks=False)
- """
-
- @property
- def currency_json(self) -> dict[str, str | None] | None:
- try:
- return json.loads(self.currency or "{}") or None
- except (TypeError, JSONDecodeError) as exc:
- logger.error(
- "Unable to load currency json: %r. Leaving empty.", exc, exc_info=True
- )
- return None
-
- @property
- def perm(self) -> str | None:
- raise NotImplementedError()
-
- @property
- def expression(self) -> Column:
- raise NotImplementedError()
-
- @property
- def data(self) -> dict[str, Any]:
- attrs = (
- "id",
- "metric_name",
- "verbose_name",
- "description",
- "expression",
- "warning_text",
- "d3format",
- "currency",
- )
- return {s: getattr(self, s) for s in attrs}
diff --git a/superset/connectors/base/views.py b/superset/connectors/base/views.py
deleted file mode 100644
index ae5013ebbf..0000000000
--- a/superset/connectors/base/views.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# 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 typing import Any
-
-from flask import Markup
-from flask_appbuilder.fieldwidgets import BS3TextFieldWidget
-
-from superset.connectors.base.models import BaseDatasource
-from superset.exceptions import SupersetException
-from superset.views.base import SupersetModelView
-
-
-class BS3TextFieldROWidget( # pylint: disable=too-few-public-methods
- BS3TextFieldWidget
-):
- """
- Custom read only text field widget.
- """
-
- def __call__(self, field: Any, **kwargs: Any) -> Markup:
- kwargs["readonly"] = "true"
- return super().__call__(field, **kwargs)
-
-
-class DatasourceModelView(SupersetModelView):
- def pre_delete(self, item: BaseDatasource) -> None:
- if item.slices:
- raise SupersetException(
- Markup(
- "Cannot delete a datasource that has slices attached to it."
- "Here's the list of associated charts: "
- + "".join([i.slice_name for i in item.slices])
- )
- )
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 510ca54ae8..3d1435dc7b 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -17,6 +17,7 @@
# pylint: disable=too-many-lines
from __future__ import annotations
+import builtins
import dataclasses
import json
import logging
@@ -25,6 +26,7 @@ from collections import defaultdict
from collections.abc import Hashable
from dataclasses import dataclass, field
from datetime import datetime, timedelta
+from json.decoder import JSONDecodeError
from typing import Any, Callable, cast
import dateutil.parser
@@ -34,7 +36,8 @@ import sqlalchemy as sa
import sqlparse
from flask import escape, Markup
from flask_appbuilder import Model
-from flask_babel import lazy_gettext as _
+from flask_appbuilder.security.sqla.models import User
+from flask_babel import gettext as __, lazy_gettext as _
from jinja2.exceptions import TemplateError
from sqlalchemy import (
and_,
@@ -52,9 +55,11 @@ from sqlalchemy import (
update,
)
from sqlalchemy.engine.base import Connection
+from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import (
backref,
+ foreign,
Mapped,
Query,
reconstructor,
@@ -71,12 +76,13 @@ from sqlalchemy.sql.selectable import Alias, TableClause
from superset import app, db, is_feature_enabled, security_manager
from superset.common.db_query_status import QueryStatus
-from superset.connectors.base.models import BaseColumn, BaseDatasource, BaseMetric
from superset.connectors.sqla.utils import (
get_columns_description,
get_physical_table_metadata,
get_virtual_table_metadata,
)
+from superset.constants import EMPTY_STRING, NULL_STRING
+from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.db_engine_specs.base import BaseEngineSpec, TimestampExpression
from superset.exceptions import (
ColumnNotFoundException,
@@ -97,19 +103,24 @@ from superset.models.helpers import (
AuditMixinNullable,
CertificationMixin,
ExploreMixin,
+ ImportExportMixin,
QueryResult,
QueryStringExtended,
validate_adhoc_subquery,
)
+from superset.models.slice import Slice
from superset.sql_parse import ParsedQuery, sanitize_clause
from superset.superset_typing import (
AdhocColumn,
AdhocMetric,
+ FilterValue,
+ FilterValues,
Metric,
QueryObjectDict,
ResultSetColumnType,
)
from superset.utils import core as utils
+from superset.utils.backports import StrEnum
from superset.utils.core import GenericDataType, MediumText
config = app.config
@@ -134,6 +145,565 @@ class MetadataResult:
modified: list[str] = field(default_factory=list)
+logger = logging.getLogger(__name__)
+
+METRIC_FORM_DATA_PARAMS = [
+ "metric",
+ "metric_2",
+ "metrics",
+ "metrics_b",
+ "percent_metrics",
+ "secondary_metric",
+ "size",
+ "timeseries_limit_metric",
+ "x",
+ "y",
+]
+
+COLUMN_FORM_DATA_PARAMS = [
+ "all_columns",
+ "all_columns_x",
+ "columns",
+ "entity",
+ "groupby",
+ "order_by_cols",
+ "series",
+]
+
+
+class DatasourceKind(StrEnum):
+ VIRTUAL = "virtual"
+ PHYSICAL = "physical"
+
+
+class BaseDatasource(
+ AuditMixinNullable, ImportExportMixin
+): # pylint: disable=too-many-public-methods
+ """A common interface to objects that are queryable
+ (tables and datasources)"""
+
+ # ---------------------------------------------------------------
+ # class attributes to define when deriving BaseDatasource
+ # ---------------------------------------------------------------
+ __tablename__: str | None = None # {connector_name}_datasource
+ baselink: str | None = None # url portion pointing to ModelView endpoint
+
+ owner_class: User | None = None
+
+ # Used to do code highlighting when displaying the query in the UI
+ query_language: str | None = None
+
+ # Only some datasources support Row Level Security
+ is_rls_supported: bool = False
+
+ @property
+ def name(self) -> str:
+ # can be a Column or a property pointing to one
+ raise NotImplementedError()
+
+ # ---------------------------------------------------------------
+
+ # Columns
+ id = Column(Integer, primary_key=True)
+ description = Column(Text)
+ default_endpoint = Column(Text)
+ is_featured = Column(Boolean, default=False) # TODO deprecating
+ filter_select_enabled = Column(Boolean, default=True)
+ offset = Column(Integer, default=0)
+ cache_timeout = Column(Integer)
+ params = Column(String(1000))
+ perm = Column(String(1000))
+ schema_perm = Column(String(1000))
+ is_managed_externally = Column(Boolean, nullable=False, default=False)
+ external_url = Column(Text, nullable=True)
+
+ sql: str | None = None
+ owners: list[User]
+ update_from_object_fields: list[str]
+
+ extra_import_fields = ["is_managed_externally", "external_url"]
+
+ @property
+ def kind(self) -> DatasourceKind:
+ return DatasourceKind.VIRTUAL if self.sql else DatasourceKind.PHYSICAL
+
+ @property
+ def owners_data(self) -> list[dict[str, Any]]:
+ return [
+ {
+ "first_name": o.first_name,
+ "last_name": o.last_name,
+ "username": o.username,
+ "id": o.id,
+ }
+ for o in self.owners
+ ]
+
+ @property
+ def is_virtual(self) -> bool:
+ return self.kind == DatasourceKind.VIRTUAL
+
+ @declared_attr
+ def slices(self) -> RelationshipProperty:
+ return relationship(
+ "Slice",
+ overlaps="table",
+ primaryjoin=lambda: and_(
+ foreign(Slice.datasource_id) == self.id,
+ foreign(Slice.datasource_type) == self.type,
+ ),
+ )
+
+ columns: list[TableColumn] = []
+ metrics: list[SqlMetric] = []
+
+ @property
+ def type(self) -> str:
+ raise NotImplementedError()
+
+ @property
+ def uid(self) -> str:
+ """Unique id across datasource types"""
+ return f"{self.id}__{self.type}"
+
+ @property
+ def column_names(self) -> list[str]:
+ return sorted([c.column_name for c in self.columns], key=lambda x: x or "")
+
+ @property
+ def columns_types(self) -> dict[str, str]:
+ return {c.column_name: c.type for c in self.columns}
+
+ @property
+ def main_dttm_col(self) -> str:
+ return "timestamp"
+
+ @property
+ def datasource_name(self) -> str:
+ raise NotImplementedError()
+
+ @property
+ def connection(self) -> str | None:
+ """String representing the context of the Datasource"""
+ return None
+
+ @property
+ def schema(self) -> str | None:
+ """String representing the schema of the Datasource (if it applies)"""
+ return None
+
+ @property
+ def filterable_column_names(self) -> list[str]:
+ return sorted([c.column_name for c in self.columns if c.filterable])
+
+ @property
+ def dttm_cols(self) -> list[str]:
+ return []
+
+ @property
+ def url(self) -> str:
+ return f"/{self.baselink}/edit/{self.id}"
+
+ @property
+ def explore_url(self) -> str:
+ if self.default_endpoint:
+ return self.default_endpoint
+ return f"/explore/?datasource_type={self.type}&datasource_id={self.id}"
+
+ @property
+ def column_formats(self) -> dict[str, str | None]:
+ return {m.metric_name: m.d3format for m in self.metrics if m.d3format}
+
+ @property
+ def currency_formats(self) -> dict[str, dict[str, str | None] | None]:
+ return {m.metric_name: m.currency_json for m in self.metrics if m.currency_json}
+
+ def add_missing_metrics(self, metrics: list[SqlMetric]) -> None:
+ existing_metrics = {m.metric_name for m in self.metrics}
+ for metric in metrics:
+ if metric.metric_name not in existing_metrics:
+ metric.table_id = self.id
+ self.metrics.append(metric)
+
+ @property
+ def short_data(self) -> dict[str, Any]:
+ """Data representation of the datasource sent to the frontend"""
+ return {
+ "edit_url": self.url,
+ "id": self.id,
+ "uid": self.uid,
+ "schema": self.schema,
+ "name": self.name,
+ "type": self.type,
+ "connection": self.connection,
+ "creator": str(self.created_by),
+ }
+
+ @property
+ def select_star(self) -> str | None:
+ pass
+
+ @property
+ def order_by_choices(self) -> list[tuple[str, str]]:
+ choices = []
+ # self.column_names return sorted column_names
+ for column_name in self.column_names:
+ column_name = str(column_name or "")
+ choices.append(
+ (json.dumps([column_name, True]), f"{column_name} " + __("[asc]"))
+ )
+ choices.append(
+ (json.dumps([column_name, False]), f"{column_name} " + __("[desc]"))
+ )
+ return choices
+
+ @property
+ def verbose_map(self) -> dict[str, str]:
+ verb_map = {"__timestamp": "Time"}
+ verb_map.update(
+ {o.metric_name: o.verbose_name or o.metric_name for o in self.metrics}
+ )
+ verb_map.update(
+ {o.column_name: o.verbose_name or o.column_name for o in self.columns}
+ )
+ return verb_map
+
+ @property
+ def data(self) -> dict[str, Any]:
+ """Data representation of the datasource sent to the frontend"""
+ return {
+ # simple fields
+ "id": self.id,
+ "uid": self.uid,
+ "column_formats": self.column_formats,
+ "currency_formats": self.currency_formats,
+ "description": self.description,
+ "database": self.database.data, # pylint: disable=no-member
+ "default_endpoint": self.default_endpoint,
+ "filter_select": self.filter_select_enabled, # TODO deprecate
+ "filter_select_enabled": self.filter_select_enabled,
+ "name": self.name,
+ "datasource_name": self.datasource_name,
+ "table_name": self.datasource_name,
+ "type": self.type,
+ "schema": self.schema,
+ "offset": self.offset,
+ "cache_timeout": self.cache_timeout,
+ "params": self.params,
+ "perm": self.perm,
+ "edit_url": self.url,
+ # sqla-specific
+ "sql": self.sql,
+ # one to many
+ "columns": [o.data for o in self.columns],
+ "metrics": [o.data for o in self.metrics],
+ # TODO deprecate, move logic to JS
+ "order_by_choices": self.order_by_choices,
+ "owners": [owner.id for owner in self.owners],
+ "verbose_map": self.verbose_map,
+ "select_star": self.select_star,
+ }
+
+ def data_for_slices( # pylint: disable=too-many-locals
+ self, slices: list[Slice]
+ ) -> dict[str, Any]:
+ """
+ The representation of the datasource containing only the required data
+ to render the provided slices.
+
+ Used to reduce the payload when loading a dashboard.
+ """
+ data = self.data
+ metric_names = set()
+ column_names = set()
+ for slc in slices:
+ form_data = slc.form_data
+ # pull out all required metrics from the form_data
+ for metric_param in METRIC_FORM_DATA_PARAMS:
+ for metric in utils.as_list(form_data.get(metric_param) or []):
+ metric_names.add(utils.get_metric_name(metric))
+ if utils.is_adhoc_metric(metric):
+ column_ = metric.get("column") or {}
+ if column_name := column_.get("column_name"):
+ column_names.add(column_name)
+
+ # Columns used in query filters
+ column_names.update(
+ filter_["subject"]
+ for filter_ in form_data.get("adhoc_filters") or []
+ if filter_.get("clause") == "WHERE" and filter_.get("subject")
+ )
+
+ # columns used by Filter Box
+ column_names.update(
+ filter_config["column"]
+ for filter_config in form_data.get("filter_configs") or []
+ if "column" in filter_config
+ )
+
+ # for legacy dashboard imports which have the wrong query_context in them
+ try:
+ query_context = slc.get_query_context()
+ except DatasetNotFoundError:
+ query_context = None
+
+ # legacy charts don't have query_context charts
+ if query_context:
+ column_names.update(
+ [
+ utils.get_column_name(column_)
+ for query in query_context.queries
+ for column_ in query.columns
+ ]
+ or []
+ )
+ else:
+ _columns = [
+ utils.get_column_name(column_)
+ if utils.is_adhoc_column(column_)
+ else column_
+ for column_param in COLUMN_FORM_DATA_PARAMS
+ for column_ in utils.as_list(form_data.get(column_param) or [])
+ ]
+ column_names.update(_columns)
+
+ filtered_metrics = [
+ metric
+ for metric in data["metrics"]
+ if metric["metric_name"] in metric_names
+ ]
+
+ filtered_columns: list[Column] = []
+ column_types: set[GenericDataType] = set()
+ for column_ in data["columns"]:
+ generic_type = column_.get("type_generic")
+ if generic_type is not None:
+ column_types.add(generic_type)
+ if column_["column_name"] in column_names:
+ filtered_columns.append(column_)
+
+ data["column_types"] = list(column_types)
+ del data["description"]
+ data.update({"metrics": filtered_metrics})
+ data.update({"columns": filtered_columns})
+ verbose_map = {"__timestamp": "Time"}
+ verbose_map.update(
+ {
+ metric["metric_name"]: metric["verbose_name"] or metric["metric_name"]
+ for metric in filtered_metrics
+ }
+ )
+ verbose_map.update(
+ {
+ column_["column_name"]: column_["verbose_name"]
+ or column_["column_name"]
+ for column_ in filtered_columns
+ }
+ )
+ data["verbose_map"] = verbose_map
+
+ return data
+
+ @staticmethod
+ def filter_values_handler( # pylint: disable=too-many-arguments
+ values: FilterValues | None,
+ operator: str,
+ target_generic_type: GenericDataType,
+ target_native_type: str | None = None,
+ is_list_target: bool = False,
+ db_engine_spec: builtins.type[BaseEngineSpec] | None = None,
+ db_extra: dict[str, Any] | None = None,
+ ) -> FilterValues | None:
+ if values is None:
+ return None
+
+ def handle_single_value(value: FilterValue | None) -> FilterValue | None:
+ if operator == utils.FilterOperator.TEMPORAL_RANGE:
+ return value
+ if (
+ isinstance(value, (float, int))
+ and target_generic_type == utils.GenericDataType.TEMPORAL
+ and target_native_type is not None
+ and db_engine_spec is not None
+ ):
+ value = db_engine_spec.convert_dttm(
+ target_type=target_native_type,
+ dttm=datetime.utcfromtimestamp(value / 1000),
+ db_extra=db_extra,
+ )
+ value = literal_column(value)
+ if isinstance(value, str):
+ value = value.strip("\t\n")
+
+ if (
+ target_generic_type == utils.GenericDataType.NUMERIC
+ and operator
+ not in {
+ utils.FilterOperator.ILIKE,
+ utils.FilterOperator.LIKE,
+ }
+ ):
+ # For backwards compatibility and edge cases
+ # where a column data type might have changed
+ return utils.cast_to_num(value)
+ if value == NULL_STRING:
+ return None
+ if value == EMPTY_STRING:
+ return ""
+ if target_generic_type == utils.GenericDataType.BOOLEAN:
+ return utils.cast_to_boolean(value)
+ return value
+
+ if isinstance(values, (list, tuple)):
+ values = [handle_single_value(v) for v in values] # type: ignore
+ else:
+ values = handle_single_value(values)
+ if is_list_target and not isinstance(values, (tuple, list)):
+ values = [values] # type: ignore
+ elif not is_list_target and isinstance(values, (tuple, list)):
+ values = values[0] if values else None
+ return values
+
+ def external_metadata(self) -> list[ResultSetColumnType]:
+ """Returns column information from the external system"""
+ raise NotImplementedError()
+
+ def get_query_str(self, query_obj: QueryObjectDict) -> str:
+ """Returns a query as a string
+
+ This is used to be displayed to the user so that they can
+ understand what is taking place behind the scene"""
+ raise NotImplementedError()
+
+ def query(self, query_obj: QueryObjectDict) -> QueryResult:
+ """Executes the query and returns a dataframe
+
+ query_obj is a dictionary representing Superset's query interface.
+ Should return a ``superset.models.helpers.QueryResult``
+ """
+ raise NotImplementedError()
+
+ @staticmethod
+ def default_query(qry: Query) -> Query:
+ return qry
+
+ def get_column(self, column_name: str | None) -> TableColumn | None:
+ if not column_name:
+ return None
+ for col in self.columns:
+ if col.column_name == column_name:
+ return col
+ return None
+
+ @staticmethod
+ def get_fk_many_from_list(
+ object_list: list[Any],
+ fkmany: list[Column],
+ fkmany_class: builtins.type[TableColumn | SqlMetric],
+ key_attr: str,
+ ) -> list[Column]:
+ """Update ORM one-to-many list from object list
+
+ Used for syncing metrics and columns using the same code"""
+
+ object_dict = {o.get(key_attr): o for o in object_list}
+
+ # delete fks that have been removed
+ fkmany = [o for o in fkmany if getattr(o, key_attr) in object_dict]
+
+ # sync existing fks
+ for fk in fkmany:
+ obj = object_dict.get(getattr(fk, key_attr))
+ if obj:
+ for attr in fkmany_class.update_from_object_fields:
+ setattr(fk, attr, obj.get(attr))
+
+ # create new fks
+ new_fks = []
+ orm_keys = [getattr(o, key_attr) for o in fkmany]
+ for obj in object_list:
+ key = obj.get(key_attr)
+ if key not in orm_keys:
+ del obj["id"]
+ orm_kwargs = {}
+ for k in obj:
+ if k in fkmany_class.update_from_object_fields and k in obj:
+ orm_kwargs[k] = obj[k]
+ new_obj = fkmany_class(**orm_kwargs)
+ new_fks.append(new_obj)
+ fkmany += new_fks
+ return fkmany
+
+ def update_from_object(self, obj: dict[str, Any]) -> None:
+ """Update datasource from a data structure
+
+ The UI's table editor crafts a complex data structure that
+ contains most of the datasource's properties as well as
+ an array of metrics and columns objects. This method
+ receives the object from the UI and syncs the datasource to
+ match it. Since the fields are different for the different
+ connectors, the implementation uses ``update_from_object_fields``
+ which can be defined for each connector and
+ defines which fields should be synced"""
+ for attr in self.update_from_object_fields:
+ setattr(self, attr, obj.get(attr))
+
+ self.owners = obj.get("owners", [])
+
+ # Syncing metrics
+ metrics = (
+ self.get_fk_many_from_list(
+ obj["metrics"], self.metrics, SqlMetric, "metric_name"
+ )
+ if "metrics" in obj
+ else []
+ )
+ self.metrics = metrics
+
+ # Syncing columns
+ self.columns = (
+ self.get_fk_many_from_list(
+ obj["columns"], self.columns, TableColumn, "column_name"
+ )
+ if "columns" in obj
+ else []
+ )
+
+ def get_extra_cache_keys(
+ self, query_obj: QueryObjectDict # pylint: disable=unused-argument
+ ) -> list[Hashable]:
+ """If a datasource needs to provide additional keys for calculation of
+ cache keys, those can be provided via this method
+
+ :param query_obj: The dict representation of a query object
+ :return: list of keys
+ """
+ return []
+
+ def __hash__(self) -> int:
+ return hash(self.uid)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, BaseDatasource):
+ return NotImplemented
+ return self.uid == other.uid
+
+ def raise_for_access(self) -> None:
+ """
+ Raise an exception if the user cannot access the resource.
+
+ :raises SupersetSecurityException: If the user cannot access the resource
+ """
+
+ security_manager.raise_for_access(datasource=self)
+
+ @classmethod
+ def get_datasource_by_name(
+ cls, session: Session, datasource_name: str, schema: str, database_name: str
+ ) -> BaseDatasource | None:
+ raise NotImplementedError()
+
+
class AnnotationDatasource(BaseDatasource):
"""Dummy object so we can query annotations using 'Viz' objects just like
regular datasources.
@@ -187,22 +757,33 @@ class AnnotationDatasource(BaseDatasource):
raise NotImplementedError()
-class TableColumn(Model, BaseColumn, CertificationMixin):
+class TableColumn(Model, AuditMixinNullable, ImportExportMixin, CertificationMixin):
"""ORM object for table columns, each table can have multiple columns"""
__tablename__ = "table_columns"
__table_args__ = (UniqueConstraint("table_id", "column_name"),)
+
+ id = Column(Integer, primary_key=True)
+ column_name = Column(String(255), nullable=False)
+ verbose_name = Column(String(1024))
+ is_active = Column(Boolean, default=True)
+ type = Column(Text)
+ advanced_data_type = Column(String(255))
+ groupby = Column(Boolean, default=True)
+ filterable = Column(Boolean, default=True)
+ description = Column(MediumText())
table_id = Column(Integer, ForeignKey("tables.id", ondelete="CASCADE"))
- table: Mapped[SqlaTable] = relationship(
- "SqlaTable",
- back_populates="columns",
- )
is_dttm = Column(Boolean, default=False)
expression = Column(MediumText())
python_date_format = Column(String(255))
extra = Column(Text)
+ table: Mapped[SqlaTable] = relationship(
+ "SqlaTable",
+ back_populates="columns",
+ )
+
export_fields = [
"table_id",
"column_name",
@@ -246,6 +827,9 @@ class TableColumn(Model, BaseColumn, CertificationMixin):
self._database = None
+ def __repr__(self) -> str:
+ return str(self.column_name)
+
@property
def is_boolean(self) -> bool:
"""
@@ -284,7 +868,7 @@ class TableColumn(Model, BaseColumn, CertificationMixin):
return self.table.database if self.table else self._database
@property
- def db_engine_spec(self) -> type[BaseEngineSpec]:
+ def db_engine_spec(self) -> builtins.type[BaseEngineSpec]:
return self.database.db_engine_spec
@property
@@ -366,44 +950,50 @@ class TableColumn(Model, BaseColumn, CertificationMixin):
@property
def data(self) -> dict[str, Any]:
attrs = (
- "id",
+ "advanced_data_type",
+ "certification_details",
+ "certified_by",
"column_name",
- "verbose_name",
"description",
"expression",
"filterable",
"groupby",
+ "id",
+ "is_certified",
"is_dttm",
+ "python_date_format",
"type",
"type_generic",
- "advanced_data_type",
- "python_date_format",
- "is_certified",
- "certified_by",
- "certification_details",
+ "verbose_name",
"warning_markdown",
)
- attr_dict = {s: getattr(self, s) for s in attrs if hasattr(self, s)}
-
- attr_dict.update(super().data)
-
- return attr_dict
+ return {s: getattr(self, s) for s in attrs if hasattr(self, s)}
-class SqlMetric(Model, BaseMetric, CertificationMixin):
+class SqlMetric(Model, AuditMixinNullable, ImportExportMixin, CertificationMixin):
"""ORM object for metrics, each table can have multiple metrics"""
__tablename__ = "sql_metrics"
__table_args__ = (UniqueConstraint("table_id", "metric_name"),)
+
+ id = Column(Integer, primary_key=True)
+ metric_name = Column(String(255), nullable=False)
+ verbose_name = Column(String(1024))
+ metric_type = Column(String(32))
+ description = Column(MediumText())
+ d3format = Column(String(128))
+ currency = Column(String(128))
+ warning_text = Column(Text)
table_id = Column(Integer, ForeignKey("tables.id", ondelete="CASCADE"))
+ expression = Column(MediumText(), nullable=False)
+ extra = Column(Text)
+
table: Mapped[SqlaTable] = relationship(
"SqlaTable",
back_populates="metrics",
)
- expression = Column(MediumText(), nullable=False)
- extra = Column(Text)
export_fields = [
"metric_name",
@@ -449,18 +1039,34 @@ class SqlMetric(Model, BaseMetric, CertificationMixin):
def get_perm(self) -> str | None:
return self.perm
+ @property
+ def currency_json(self) -> dict[str, str | None] | None:
+ try:
+ return json.loads(self.currency or "{}") or None
+ except (TypeError, JSONDecodeError) as exc:
+ logger.error(
+ "Unable to load currency json: %r. Leaving empty.", exc, exc_info=True
+ )
+ return None
+
@property
def data(self) -> dict[str, Any]:
attrs = (
- "is_certified",
- "certified_by",
"certification_details",
+ "certified_by",
+ "currency",
+ "d3format",
+ "description",
+ "expression",
+ "id",
+ "is_certified",
+ "metric_name",
"warning_markdown",
+ "warning_text",
+ "verbose_name",
)
- attr_dict = {s: getattr(self, s) for s in attrs}
- attr_dict.update(super().data)
- return attr_dict
+ return {s: getattr(self, s) for s in attrs}
sqlatable_user = Table(
diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py
index 1ba10f18b2..36eebcb3f7 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -28,7 +28,6 @@ from flask_babel import lazy_gettext as _
from wtforms.validators import DataRequired, Regexp
from superset import db
-from superset.connectors.base.views import DatasourceModelView
from superset.connectors.sqla import models
from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.superset_typing import FlaskResponse
@@ -282,7 +281,7 @@ class RowLevelSecurityView(BaseSupersetView):
class TableModelView( # pylint: disable=too-many-ancestors
- DatasourceModelView, DeleteMixin, YamlExportMixin
+ SupersetModelView, DeleteMixin, YamlExportMixin
):
datamodel = SQLAInterface(models.SqlaTable)
class_permission_name = "Dataset"
diff --git a/superset/daos/chart.py b/superset/daos/chart.py
index 7eae38cb0e..eb8b3e809e 100644
--- a/superset/daos/chart.py
+++ b/superset/daos/chart.py
@@ -28,7 +28,7 @@ from superset.models.slice import Slice
from superset.utils.core import get_user_id
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/commands/importers/v0.py b/superset/datasets/commands/importers/v0.py
index a34d9be1ac..0647263085 100644
--- a/superset/datasets/commands/importers/v0.py
+++ b/superset/datasets/commands/importers/v0.py
@@ -26,8 +26,12 @@ 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.sqla.models import SqlaTable, SqlMetric, TableColumn
+from superset.connectors.sqla.models import (
+ BaseDatasource,
+ SqlaTable,
+ SqlMetric,
+ TableColumn,
+)
from superset.databases.commands.exceptions import DatabaseNotFoundError
from superset.datasets.commands.exceptions import DatasetInvalidError
from superset.models.core import Database
@@ -102,14 +106,8 @@ def lookup_sqla_metric(session: Session, metric: SqlMetric) -> SqlMetric:
)
-def import_metric(session: Session, metric: BaseMetric) -> BaseMetric:
- if isinstance(metric, SqlMetric):
- lookup_metric = lookup_sqla_metric
- else:
- raise Exception( # pylint: disable=broad-exception-raised
- f"Invalid metric type: {metric}"
- )
- return import_simple_obj(session, metric, lookup_metric)
+def import_metric(session: Session, metric: SqlMetric) -> SqlMetric:
+ return import_simple_obj(session, metric, lookup_sqla_metric)
def lookup_sqla_column(session: Session, column: TableColumn) -> TableColumn:
@@ -123,14 +121,8 @@ def lookup_sqla_column(session: Session, column: TableColumn) -> TableColumn:
)
-def import_column(session: Session, column: BaseColumn) -> BaseColumn:
- if isinstance(column, TableColumn):
- lookup_column = lookup_sqla_column
- else:
- raise Exception( # pylint: disable=broad-exception-raised
- f"Invalid column type: {column}"
- )
- return import_simple_obj(session, column, lookup_column)
+def import_column(session: Session, column: TableColumn) -> TableColumn:
+ return import_simple_obj(session, column, lookup_sqla_column)
def import_datasource( # pylint: disable=too-many-arguments
diff --git a/superset/examples/world_bank.py b/superset/examples/world_bank.py
index 5e86cff1e4..5513a52d64 100644
--- a/superset/examples/world_bank.py
+++ b/superset/examples/world_bank.py
@@ -24,14 +24,8 @@ from sqlalchemy.sql import column
import superset.utils.database
from superset import app, db
-from superset.connectors.sqla.models import SqlMetric
-from superset.models.dashboard import Dashboard
-from superset.models.slice import Slice
-from superset.utils import core as utils
-from superset.utils.core import DatasourceType
-
-from ..connectors.base.models import BaseDatasource
-from .helpers import (
+from superset.connectors.sqla.models import BaseDatasource, SqlMetric
+from superset.examples.helpers import (
get_example_url,
get_examples_folder,
get_slice_json,
@@ -40,6 +34,10 @@ from .helpers import (
misc_dash_slices,
update_slice_ids,
)
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from superset.utils import core as utils
+from superset.utils.core import DatasourceType
def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-statements
diff --git a/superset/explore/commands/get.py b/superset/explore/commands/get.py
index d348b16251..1994e7ad43 100644
--- a/superset/explore/commands/get.py
+++ b/superset/explore/commands/get.py
@@ -26,8 +26,7 @@ from sqlalchemy.exc import SQLAlchemyError
from superset import db
from superset.commands.base import BaseCommand
-from superset.connectors.base.models import BaseDatasource
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.datasource import DatasourceDAO
from superset.daos.exceptions import DatasourceNotFound
from superset.exceptions import SupersetException
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 18c94aa179..919c832ab5 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -47,8 +47,12 @@ from sqlalchemy.sql import join, select
from sqlalchemy.sql.elements import BinaryExpression
from superset import app, db, is_feature_enabled, security_manager
-from superset.connectors.base.models import BaseDatasource
-from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
+from superset.connectors.sqla.models import (
+ BaseDatasource,
+ SqlaTable,
+ SqlMetric,
+ TableColumn,
+)
from superset.daos.datasource import DatasourceDAO
from superset.extensions import cache_manager
from superset.models.filter_set import FilterSet
diff --git a/superset/models/slice.py b/superset/models/slice.py
index 248f4ee947..b41bb72a85 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -51,7 +51,7 @@ from superset.viz import BaseViz, viz_types
if TYPE_CHECKING:
from superset.common.query_context import QueryContext
from superset.common.query_context_factory import QueryContextFactory
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
metadata = Model.metadata # pylint: disable=no-member
slice_user = Table(
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 5cfa6a15c5..03f0ee56cf 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -78,8 +78,11 @@ from superset.utils.urls import get_url_host
if TYPE_CHECKING:
from superset.common.query_context import QueryContext
- from superset.connectors.base.models import BaseDatasource
- from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
+ from superset.connectors.sqla.models import (
+ BaseDatasource,
+ RowLevelSecurityFilter,
+ SqlaTable,
+ )
from superset.models.core import Database
from superset.models.dashboard import Dashboard
from superset.models.sql_lab import Query
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 67edabe626..b9c24076a4 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -105,7 +105,7 @@ from superset.utils.dates import datetime_to_epoch, EPOCH
from superset.utils.hashing import md5_sha_from_dict, md5_sha_from_str
if TYPE_CHECKING:
- from superset.connectors.base.models import BaseColumn, BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource, TableColumn
from superset.models.sql_lab import Query
logging.getLogger("MARKDOWN").setLevel(logging.INFO)
@@ -1628,7 +1628,7 @@ def extract_dataframe_dtypes(
return generic_types
-def extract_column_dtype(col: BaseColumn) -> GenericDataType:
+def extract_column_dtype(col: TableColumn) -> GenericDataType:
if col.is_temporal:
return GenericDataType.TEMPORAL
if col.is_numeric:
diff --git a/superset/views/core.py b/superset/views/core.py
index 2f9b99eba0..bb273eb53c 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -47,8 +47,7 @@ from superset.async_events.async_query_manager import AsyncQueryTokenException
from superset.charts.commands.exceptions import ChartNotFoundError
from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
-from superset.connectors.base.models import BaseDatasource
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.chart import ChartDAO
from superset.daos.datasource import DatasourceDAO
from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
diff --git a/superset/viz.py b/superset/viz.py
index 2e697a77be..8ba785ddcf 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -84,7 +84,7 @@ from superset.utils.hashing import md5_sha_from_str
if TYPE_CHECKING:
from superset.common.query_context_factory import QueryContextFactory
- from superset.connectors.base.models import BaseDatasource
+ from superset.connectors.sqla.models import BaseDatasource
config = app.config
stats_logger = config["STATS_LOGGER"]
diff --git a/tests/integration_tests/base_tests.py b/tests/integration_tests/base_tests.py
index 7f7c543d8b..0040ec60f6 100644
--- a/tests/integration_tests/base_tests.py
+++ b/tests/integration_tests/base_tests.py
@@ -36,8 +36,7 @@ from sqlalchemy.dialects.mysql import dialect
from tests.integration_tests.test_app import app, login
from superset.sql_parse import CtasMethod
from superset import db, security_manager
-from superset.connectors.base.models import BaseDatasource
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.models import core as models
from superset.models.slice import Slice
from superset.models.core import Database
diff --git a/tests/unit_tests/common/test_get_aggregated_join_column.py b/tests/unit_tests/common/test_get_aggregated_join_column.py
index 8effacf249..de0b6b92b2 100644
--- a/tests/unit_tests/common/test_get_aggregated_join_column.py
+++ b/tests/unit_tests/common/test_get_aggregated_join_column.py
@@ -24,7 +24,7 @@ from superset.common.query_context_processor import (
AGGREGATED_JOIN_COLUMN,
QueryContextProcessor,
)
-from superset.connectors.base.models import BaseDatasource
+from superset.connectors.sqla.models import BaseDatasource
from superset.constants import TimeGrain
query_context_processor = QueryContextProcessor(
From 97121465ddf772013604ffdb5d7378885bc6ee26 Mon Sep 17 00:00:00 2001
From: Jiwen liu <61498169+liujiwen-up@users.noreply.github.com>
Date: Wed, 22 Nov 2023 03:42:10 +0800
Subject: [PATCH 055/119] feat: Add Apache Doris support (#24714)
Co-authored-by: Evan Rusackas
---
README.md | 1 +
docs/docs/databases/doris.mdx | 26 ++
.../databases/installing-database-drivers.mdx | 1 +
docs/src/resources/data.js | 5 +
docs/static/img/databases/doris.png | Bin 0 -> 11539 bytes
setup.py | 1 +
superset-frontend/src/assets/images/doris.png | Bin 0 -> 11539 bytes
superset/db_engine_specs/doris.py | 278 ++++++++++++++++++
.../unit_tests/db_engine_specs/test_doris.py | 147 +++++++++
9 files changed, 459 insertions(+)
create mode 100644 docs/docs/databases/doris.mdx
create mode 100644 docs/static/img/databases/doris.png
create mode 100644 superset-frontend/src/assets/images/doris.png
create mode 100644 superset/db_engine_specs/doris.py
create mode 100644 tests/unit_tests/db_engine_specs/test_doris.py
diff --git a/README.md b/README.md
index 757c0fb503..3588d99419 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,7 @@ Here are some of the major database solutions that are supported:
+
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/databases/installing-database-drivers).
diff --git a/docs/docs/databases/doris.mdx b/docs/docs/databases/doris.mdx
new file mode 100644
index 0000000000..62c16afeb3
--- /dev/null
+++ b/docs/docs/databases/doris.mdx
@@ -0,0 +1,26 @@
+---
+title: Apache Doris
+hide_title: true
+sidebar_position: 5
+version: 1
+---
+
+## Doris
+
+The [sqlalchemy-doris](https://pypi.org/project/pydoris/) library is the recommended way to connect to Apache Doris through SQLAlchemy.
+
+You'll need the following setting values to form the connection string:
+
+- **User**: User Name
+- **Password**: Password
+- **Host**: Doris FE Host
+- **Port**: Doris FE port
+- **Catalog**: Catalog Name
+- **Database**: Database Name
+
+
+Here's what the connection string looks like:
+
+```
+doris://:@:/.
+```
diff --git a/docs/docs/databases/installing-database-drivers.mdx b/docs/docs/databases/installing-database-drivers.mdx
index f698b7ab8e..f11b4ec5eb 100644
--- a/docs/docs/databases/installing-database-drivers.mdx
+++ b/docs/docs/databases/installing-database-drivers.mdx
@@ -25,6 +25,7 @@ Some of the recommended packages are shown below. Please refer to [setup.py](htt
| Database | PyPI package | Connection String |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Amazon Athena](/docs/databases/athena) | `pip install pyathena[pandas]` , `pip install PyAthenaJDBC` | `awsathena+rest://{aws_access_key_id}:{aws_secret_access_key}@athena.{region_name}.amazonaws.com/{schema_name}?s3_staging_dir={s3_staging_dir}&... ` |
+| [Apache Doris](/docs/databases/doris) | `pip install pydoris` | `doris://:@:/.` |
| [Amazon DynamoDB](/docs/databases/dynamodb) | `pip install pydynamodb` | `dynamodb://{access_key_id}:{secret_access_key}@dynamodb.{region_name}.amazonaws.com?connector=superset` |
| [Amazon Redshift](/docs/databases/redshift) | `pip install sqlalchemy-redshift` | ` redshift+psycopg2://:@:5439/` |
| [Apache Drill](/docs/databases/drill) | `pip install sqlalchemy-drill` | `drill+sadrill:// For JDBC drill+jdbc://` |
diff --git a/docs/src/resources/data.js b/docs/src/resources/data.js
index a07be55267..42cf835a49 100644
--- a/docs/src/resources/data.js
+++ b/docs/src/resources/data.js
@@ -117,4 +117,9 @@ export const Databases = [
href: 'https://www.microsoft.com/en-us/sql-server',
imgName: 'msql.png',
},
+ {
+ title: 'Apache Doris',
+ href: 'https://doris.apache.org/',
+ imgName: 'doris.png',
+ },
];
diff --git a/docs/static/img/databases/doris.png b/docs/static/img/databases/doris.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d88f2a36cf721a1a8817f2bfc8c94eedf4f61c1
GIT binary patch
literal 11539
zcma*N1yCG8*EYHY4FpR91h)`^2e;tv?(Tub-DMLzxCeKFEbgw024@$C1ZQyz{%_vz
z{&nkCyAGt;S-X3gxmp3lES=1)sN@{XY^>C+%q)FeMyx&q0L+bYl46?POYn3N<^E4^
zhe!vImO}6&Ly#L@A7teKkdWU<@JRC^ZKHkvo{;{otVK*fNA?mR`|A6vBkWN(E1gs;
z`pM^ksS|@Ak|+FUY;3u?%ZFlkD2VeVLDx%}>gX6xf!j`s$q@F}l}K8)O3vt_#25=m
z1kU6FvRul!J=MXSLNFG?jZ1)^&;3G#$=#fwLt#HFU_n*Rnb)voIjz=v(XR)!(A+ED
zdQwNqz>4t{6K?O{?~4y9wP`w|2
zk;PbxZI7FlUi%AVN0xk?eLk5nEmr~DAD8+RJc_a!B}%9h3@xiO8``fAY{tw}U;lmo
zA;SVWEbUqfhnG!?(m?J@m1^@}zpOYV1{
z`U{cdoz*FZn-FwsdVfLqQXF&iOveZ3MtJrEK_1tBwn(!$uL!3
z&Jfg<@1{sPXxEsA$RS!m0i@@diPRLLJfd0~wXG>SpU$)}4ll%7wFvrmv{~o+0)^ho
zI5RsyuZ7+2>7|>e-AchZz+|KO_JFntqwqXf$<-b$naImrp{T~I09>7N1+DGwjU^!N
z-L);x%@BRJk7O&lR>*E&?Xp`_TG=_K0V{m>La7wqA>2kQ94Bu{Ia_sBa|zftzK$aXVahMV5+jzafo-|mQ-8yMtsCEar2+4`Zr;D-$v_Vd~YAX
zCo{}}j@7ke7*|-jNM%5Bes|^IHdLNZ4rm+VHmJ-!p;=r9i)X_WqUdMdy>p`c7P_&d
zyG)x7rRg*g*t!}NU$FIo!etezdM^q{+=vUBXAX6_UaTa!-gg~U_!qrC0y9&Nt<*tI
zs^g;e+hZe_OS*OZW%g|=s(J^LYFb~`mNH#8e{Uw};ca?6NC_{Fx@^%v@|Q9bs-46D
zvt)M53CnR(FDkNL2OQm58r^DhZ1AaMzaYhM{AuU)Mb+TOjlZY3XGytE9=O(~9N|=r
zI}hAv83^oWXB(LPF}+4j@Vyg8S4NaLoezpqx6Z}cBO(lhGQ8Z2bdHvTgh?+ADzi(MPgnn(Lp9jF7
zR--436}9^V4l(R&g-r8rL|rAZwfR$(PDz8x8_6x(c>Abc`k9?MlSRk*Wj;2=(+!H3
zb}Y%bUj&+
zPoo$Is{IFV3a|0C!x?QW(&s-Dn+%E;9n`@&o-R_+A{Nb8UC>mMyfp(x@M}^ts8X1Y
zP<;$|9*vgr)b888Q2HW&-l+xHE4^8lm)pAch;Z)R+&8Lz7q*&EaMDwP#%)Wl4Hihk
z+@V`ET~FX+gLOGY{i8GXK;$y8Pz4fW@?<|r
zC#yxIhNd{km8}iHcMFR0iA5yTcNO(9f(
zC20yOg8gF3VI{kJbWg*g;`ZceEO-YMnuWQHP2K1*@b0W-O^
zvujxjPGk%;=6%_O{_VA>-!Bje?f*SQok1pXcXyl1Qg^$~SbuM;y_^W`J@>L}JZ`>j
zA1_UjnmPE9QL(tQRstn(icpCJocxOBT+r~E68xKGvDl?xq%K5MOzOwA?o8F591-j59pw?>^Ia%np9rph(fab7wW
zF><hV%t7{7~*X+)MBKED6ndbUU?o+HSjcP*?Lw>)J+rmJxH@v!?cZ(=r~=1n>4wcK|y
z*A-L$m`Pr(6%?Dn$lvkmFX(MaM-LvB@McQX=<#W$hrZ`g{E7Pv^{V)LrbBm*BM+0e
z4X|YCuvt9!^aGF7(&ykiuFvZgNk6%nf%r%YYP1s;X!Sp^obaVZv^sq1S}Gb$|67%e
zyC8y|4BUQCXofrEc;{^QOo?Y#u`7>$Xr;F;S>dlOO_YsCKfDy1$Nfft(l$5tsj2Zf
z`RP%$iK5ouMRnxQ>O6a0llI_n3$B*AJS00U<>3G)@8dV}s7iG!Q6K+YxvjPMgSM)#
z#YNhk-_>C~b+HDXo_g#rrz1E9kw_Ix|jCPAG*0
zX!i=82XH<
zm!pZcgn;3hs>iXg}KtgrmHuwwdqttM%0AL8rTb|
zbp6Aj|Iaby7KqieM!d!2D1oGjvjey;7<+*;g*+h2zFey^W!bh@{-xs(Jo3^!muep@
zSh`mpt?c^MrTOcNGbGFb(KMq0-eLa_?wcfpa&kqoawkY(*5(EUl4tve(M3rASH^$4
zlE_=^wmtm5QL)hTXFg52?OyN9R@(*yo8l^5NfP2xXg9uIj4cZUh}KlQ2H?~z#YCj$
zY6F5DF`FbA0n;x4pqO{9H2Q$OCQ~V}O5L6ejH>`OcmeC0-xVK^1wf@Mi3RxSlI(q}
zzArzoQa_RNEMyCdmN_$Mu9xIWRPR$4pJ!y?d4JY-fEWQ8>oJf&|rk
zgeOa)$ghOMP-gH|5IW^rVE5)*WYHQ1Odh%EA2Y}_Fp2v)jOoDJd8i%VIc)Y|u79LH
z=;;YaZfoMNh}n>QuJR&f((9%_CD}t$=GIcMGwM({WiUJ3d`(u572n>Y_XF+HYgdQw
zUxcu(@#I@>kjAP>j%<6*Rf-0GNy=j1bslx06y
zo`_M;%cLvU|Db9Fgw3FzsXO16ut&s3Yfnd5ViaQxY1#`!RltCXi(U(I~vNPDK
zyE$288*5A8cT)_}hivgL+)nx}DgoJn!%E~WzrGivW6v#kCtGUB5u=D-QL=K+>54Ej
zyy?9q9Ljf?}FkJJK3!o)p_3Y|d-_k~!Xbd?sIy7gCBa1;&0+GRG#X{n6C<
z#pP`~kF2@%t*>!|n_lTB#$=IX!+9THwM%gRkOj}|w(|(``u?!zjmPSi+mq5lps^&=
zbmf(0g`oMwrlIldD?`##B^oIoBGDZAtViT!VuP*&kByL0Uy>s@R6KW=)o|}m4IK=C
zhhmUpi#*U4y4g7Rp6i+$kVg}QrfW03cf^s86)V>?yUO$PzEjxJ5x09tkpJFx{4EA3
z1v7s5s)^csm1CqWQt4G@7FtFQ8R^(+^f@EDtQR+SkJpfzWP3}gQ
z+HMES)4-TbKh0N1uUZCOxrOZAS5G=V-UwXw$H%|PYmu&B++@`$WxyJ`c;bz1D#G}Y
zjtMJn)F3^0`0*3CFz2zr!tIyX&hM^m?oxZyj6h#zjaje{-xU_QD!-HZeO~R;%dk
zH_L14aSX!RCrDWp0}^D!0!<2%RDu4!&1t)kZl;p!K4r`hoxOlj2;P|XrqOt0y;?o;
z+Fv+$P6#(l35c#lz-(UCn!nY-Jy%VMne&j+hQA-tY!k^Ua_nI~8PEs5sg^}%{oExo
zz~Z*1sHNY<;J&n8jXc;H+n|H9yXs?|G5+D|Qf&5vA2k6U@9)ad_mXLhkMR|;IQz(y
zoP4)g@Ik9UG?Q(nwk{Ia{^qtZB7A!Y?E?78|2s*4khTO@Da|bDYMG5*U~>rv8RX6i
z4HB^aP94o)WH72_H|MbLFfycQga)ra)nH;u%J-#3hyhA=ea;F?J
zmfUs%z!Eb|Q{x4K*ig1JQ(N6tsuD2up_fJ*vF3*8+TcSdX|(}m;%_Ki=hyrI|HIj?
z(uG_Er3JqhhmQ`B$BADVYp(TUgMI1%p9$cuQzKZwtsMAW7YWci6!T@5W`S4>7l6@}
z?hWw^Qtx(aEcx-jq=c2f%m6=|%4O+TG*h31z+#&17aY#GR7mK}^de&Rr7*^bxnhI+
zm*yfN*{s=bi{$YIZ%`H*hmaPK$xO+?E8@OinV*eFruYd*h_P(~UAu7czI|MX;FJiw
zzzu(R`QOsd|Gzemy0kl-jt5sLw^QlP)o2IW^`By&iHXDhlWq!Tz6nIOijMTKTN?@q
zV(lX*Sp@U{OJL2>S_SiCi6$l_Bgbw_2wyyXgrZ+8RGVrD73cvXCRARIcxWXUhNdj8
z1<&FFs)#BRHmiINwL2F55J{=(nC8>#7*k3br(9V=z!E)-WH8(k
zDbE8R?hw-*in!AP9W6ZgA^P-bVIM^60P)SHq^V?ysKJ%AFvtGxI7_5p))xq-Bx*v_
zJ;O)si3RcP06>H(ZS8M703wAFDbeZyhUIHGrL6WzMq!7l(v0tw;dH5k%IAzxvu)@Pr61WL8SMSe-7iSjr#L^$%RzT*FD{@
zu#o}ya9mdcvsgILN|=(C_
z`jM{`d&{e_AeHUW`>Z3tI+q$e?^E~PWsULy8LEF@wpriDAL9T7J+)dWd
z3`4s%tfrCqbHlF`4GrxZ>LfpXs>Umt2KH&RnZjLAGc%V0-qAUCP>z-Jc-2Z)Uawp-
z!3?`W?EsVt#kw~#(BKL&T>TaL`(1Av(#8S%j#bRTyX0lqc6D7gY|kjuuvl@HhbK6B
zhJc4*v2k}Zwfg*(zHnO}B~xErfKSDb(RHN_-8cu4>L0`0GoiS>J2ZOrt2wm3*j3{k
zZFxuhRwoW$fgBt^oI0ZFb>_1I4wyV?Zxq_F)P09db7~|;W0GZh;lUDAq!nv
z8f)1bnsYSb^Qu)h%F+*?qGntocLrT}ASv3XP(d8kU58AV_s;T9z0;0IQ*9?L6Wo#U
zh<=^``yk3P0O*nuDcjz9H4?=<0bC#Ddmnx>hm+5^?2N9x$C}DRgXz1(Tu8xFKwoW1
zD!i7|gJm*H<7$NBD;0j7(VdoWE7X1x{dP%uRAFc!wNe8txOQxFzcDuI$+ki0pC)X(
zUYj)hrQ7m$x9OoP;zq2u_F7K+@N02}(Fs8PT|r8lsfGLkJ@g*aQ3K5rZl4NUUX7Gd
z52fctiKx;UEJBY={;Z~MGE(zV$7?oWrp;x+_9Z{FuWqBI#FoBRKS}0M3`Z(_oOV(C
z-qFeDo}v}Z#QZ#kh-Fkr`t}ySJx3z;K4F=F-H2~C8`EN_#1E+gZ=FklNNQ3hy|hbi
zL#k@^@;5)|C8=QEiP#Ciahfm80E9H0Ygz0qQ;
zYVfvTOUgw>uSJ=fQMzcu`5NY1A%Ymi5Ide1HLTj24-Rn4T1jGz2u(4=&cRSB`mk!+$_
zWw9%sz0Xs$AvOCbj>Um(&Z-r3ZmGV2g>&0b-I&$?jp!at8UCA2V`?1&wZ({Ybd5{G
zAcy*JI(83j$@T=|iao^!u|eQ4HH>_#E!=QkRa^C9V9Z2Xv~DuLxi6fDUnz0enU!hD
zI3(b>h-$GC6=BL*xg~OzAcXR`0tEI681P}G3U~E
zhI`d7%cnJ!oV5;;XEr%*h1zwCFL^c{Lsl+)*91w_5VUgr*zisn5Idb49CiawDj9Ya
zCck-iu-=d=#MJv?ZMQ8Ki2PDEnS5s0@O6$fYd0|Gp`)a~<%n8aXO?3X-zBD8HPeKL
zwD$umwBqn9FTYGp1xLa@=Q!Ex?{(6t2aj#9H5JsjBqrYpJ)}MMu}$&(?R}5M
zQ3Bi!a-Qq?prI8<`gvfPm18wa{ZXiw8|_vcn4d(z?5f4EkU&szA%nEelEM=7
zr*FgWOT!nA`qW(-H@%B3Ea;iPB0nF@jMQlTJByOuubf27I-)K8YA*6;mgk!|zWNAWlTCz)m9lep`j+HntNmPM(;}qX~w%sSUbx)a-73E=HRk
zPTAO6)oJx0rq(G#7BoC`u<;W{Nti{S=nvo!q~tLFlLA3aa)D
zzsIHn*_&gx?n2&-Q|8@JOeCslj3xkr4W9kCJ&$jO|InwHMIGiACScJWpGpUTUJ#vB
zNWF}QK~i9VC<8Dnn2>ip5wNRvj?%(TYyG8vkQ2Dm?mBl8{fo1w
zjVyOXMSE8z82ta;&Hb+`{4b5c#|COBV`nGN7iP)EJhRtsOAB`Xb5={rGuOu^K|byU
zmPi1k)DOsqV`TcfEaFEM4dg|tem^h&wcdvD7U$R9QK<0kPe`9s(
z8H;KH6H3vL0lET0HKFQx_gp>IRWgJKWduo#AzIJjxEOT+Q6^$>(U%g4@3=@!7OVsz
zR9FD!aB7rLb<=y@#-I_HR5UdzdxK~XI$%c{*S-}RwZSxvMcP?ANq`FY#O7-Xj4;@p
zzjAQz@;tBZ{=B6A3LrXr*|SiekKx#uk+z76khV;tLQ6NW=9nc8R~Q5UxJK|mXhrcn
z@@$kT16A4@u-uPmfV`K!rlfeA%noN38=AKMvz$^0!tL<0I32%mXGB7D{I%KY$
z=$w4-HYfBvI+qrQmSse;D`Ekn@0RdD81a8dD9yfzWn|b;%WisPjoBW2M%U|H1W@&0
z)_~xKa_&AH6{f^$Iwl8`X&nq{)yzCE$+5r
z<<#W*!F#}C(+Vq2j%)n>=R@4w{!S-`l8sHfG6IBK=4{Sm~ZP-=lpSq8UtJ
zYCvp?x=7;IwSZ9#svLSMNkT~WEL*vENB;nVV|lcx$^2CGLV+ox>mX?dA4vw2+r_Uc
zZC>S3wG4UP!%n?ADiyoA=MV$4dDeH7x5@jFb8Z?0vUi8>)UzGOwHYVhAYI=)k`$-a
z$3C9EF-3(bqrmb}7AwNaoril`?)=*-Z<9Nz1Sf|AgNB};L@Gplmt?dajcj8pJn=M7a`Qe$l0z%fM8r0Bd@Z^
z?%lMdk%>I+Ld*%l`fg2r9I)uD)54^X0?zF{VL0k=3KAf%H?|+QHKEy)tiHES8?AFd
z;(0!8`{;k)7o@tahnKVUr;82|h(@n!0$$d6d_8RW;Z%L-b+h_z(GwkJaD61@>$4!5
z9hWn5g_?FaUmmFSxVBRq!#1(q4egPsVsjM$^Ps|<9z@IB$PHmep@e9#nS9;~hupgF
z_xDLOx>5VD8;NY0+N_8a=1PvtxX}3V*ccy#6ob4vom+bJmsnHZ8A~MJNEv?&!HL{R
zZ3=HSTdCGSFA)3HX%_wEp%A)VAnn~LaPr%a?P9ac@Ja0&_d+;DU<}D}@9LH-ZC7kl
z=2hz}g}%eKQ^p>S4k`oQ7GYU~OW{V_nn&xUQWe`}ZEQhO>%y~YH_|JMW7JF=@0cDR
z!`+VPwck7wWq9|Ael>?Ei@U8hnnq8MbuX--c%gK!$D@Q!GP-m<
zipn76N_myou;Ph1=(cz-(umv1o}RQ>&Muusa^f!ey1p%h3YR?n%Aapv!n>2$T!a5X
zcS86ZO)T^m-l2UCzQuy^-bKhHJxMl)hk#N+pmeo;O8;rafp4G@^dWW%d3l1$V1;&B
z)A>j(?$GG7>C7gcmQi&XSM&th(`7yA27X!dpwKmYjdyqu!w<4)qqv
z*u|(l5$ct-gv#cL7!P4~nIP|(!;$@ERPHK9V=ejapD=*J@>e(j;XaWH>ueAG(;SzW
zZk_WruhD1DuX{&P;S1*n+H`-4p)68&S`nT6yl2x{&xQ}f0uMBEUA*)szlo8TtEt*A
zuB(F_;af+y@1RmBB&o|~**j+rRckWa&lwOQ>`Hv!-_%t{0wRb_&;bYNS9=ek_crvn
z6A@-e(dnBsOOai*l=Jl@Oc|A&F=VB+ZKVsU`aOTSEQ|&^S;)ox=*UWz2FVsQVl6YJxigY_quCH
z>jF2ofi5fUbNrP7I%vu+htt5Vs}ugBa)Ysj=ji;XFc;^o#Ld?)ep680y?nXxwDv
zt+mc$L`Jr4LmQEyj(q=NPia3RqZ7hjy4g3}XBNelWOpvPsSV@n+btVa)dD1r=GMV_
zX_Ko8+n3YoCE3^w><-ZAhN=PW`5YujhRKHJUdU)@s;dJ(VZ%~5N8(?y79X$aN5x*P%>RBS9sdI#jc
z6CgEKAOWqG4CQZIY1%wisu{PPi-nIpiRGBvX`WvHVvGLaw@owf>#mOX?G8ts}hcRR#>O&
z`&)^HQ@L%?y{~xua25v~DLVa3!qj_zfv0m1I#k=w6w1;X^T7lkM&&2Foc^V3ZuHh?G<|xnYl1wR%R{pcIkm{RB<+8YJh+4-q9NPjGa`-
zME=GOwF<;oM>HI*y7+N+)okN6()u@f7(zWO>E4f=xwf-RUpPLNfJ1IY~jncst!c|b2
zR_a_Xce0(@gQ+}UqwWg{>-HZ3Ilt43H)Mxz$LCjr{e_hV`SEra+oe`wCvlC)d7P`x
zAEZK3r~?)eKv&EZl|9!0m8px2`Xa_8Y8gt}k}$hYPWUuN{rJT5Gf
zadD5GErXM1K)peYmj{iDdr@APx2fVVTzI(!lyM;$Okjv_=y!ZM#Qxa*&^@_I^4M7$
z>*}4*%*eY^xbj8C#y%a&PYC(lm{S-^ewfb5vy`lthLp=H)jkhNnK=GfU3frxQM$u$
zSb}RCZcF--xAd~TJIw=MAZT06>5k5EzW&92Y$|P
zxsmGmzR{2gI<&2td^)vWXnnv#Yz!cIzRdsIpNTODkvF%NVQA1#`BzgCkr~srI5NKM
zMQkCMw&NLR{xHyXMg_E>*W=A$RYUqJHZTGIiiqmv6f@-iNcSAhq4^KLrZ+i(-fw0t
zW0Jr957C(@J|YJN0~C0ViZ*wij`o?Z5&3jPe^#|rt+0P;qW
z8pNCW0#Id&A%?9Y(&3X~oPz~M{6E+}O&qT0tM>+>HVTY26c(o?vJ={=xGaqc-i>8M
zxZql3pi70#0-?2N@R52%On6%?LI9%v1d8#9+h^K*{EvpI)#CQdyrg2D%m^U}E3oR9
z_vuPjd~1q~_>`8lF~MoEXb5H;!1UE|GtY+LskVyde}1qPm_neCZ%dH*E9b9zUyt59
t;Hy8UHYL5)rI#o8UyA+z+Pt*?jMPW=0.5.9, < 0.6"],
"netezza": ["nzalchemy>=11.0.2"],
"starrocks": ["starrocks>=1.0.0"],
+ "doris": ["pydoris>=1.0.0, <2.0.0"],
},
python_requires="~=3.9",
author="Apache Software Foundation",
diff --git a/superset-frontend/src/assets/images/doris.png b/superset-frontend/src/assets/images/doris.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d88f2a36cf721a1a8817f2bfc8c94eedf4f61c1
GIT binary patch
literal 11539
zcma*N1yCG8*EYHY4FpR91h)`^2e;tv?(Tub-DMLzxCeKFEbgw024@$C1ZQyz{%_vz
z{&nkCyAGt;S-X3gxmp3lES=1)sN@{XY^>C+%q)FeMyx&q0L+bYl46?POYn3N<^E4^
zhe!vImO}6&Ly#L@A7teKkdWU<@JRC^ZKHkvo{;{otVK*fNA?mR`|A6vBkWN(E1gs;
z`pM^ksS|@Ak|+FUY;3u?%ZFlkD2VeVLDx%}>gX6xf!j`s$q@F}l}K8)O3vt_#25=m
z1kU6FvRul!J=MXSLNFG?jZ1)^&;3G#$=#fwLt#HFU_n*Rnb)voIjz=v(XR)!(A+ED
zdQwNqz>4t{6K?O{?~4y9wP`w|2
zk;PbxZI7FlUi%AVN0xk?eLk5nEmr~DAD8+RJc_a!B}%9h3@xiO8``fAY{tw}U;lmo
zA;SVWEbUqfhnG!?(m?J@m1^@}zpOYV1{
z`U{cdoz*FZn-FwsdVfLqQXF&iOveZ3MtJrEK_1tBwn(!$uL!3
z&Jfg<@1{sPXxEsA$RS!m0i@@diPRLLJfd0~wXG>SpU$)}4ll%7wFvrmv{~o+0)^ho
zI5RsyuZ7+2>7|>e-AchZz+|KO_JFntqwqXf$<-b$naImrp{T~I09>7N1+DGwjU^!N
z-L);x%@BRJk7O&lR>*E&?Xp`_TG=_K0V{m>La7wqA>2kQ94Bu{Ia_sBa|zftzK$aXVahMV5+jzafo-|mQ-8yMtsCEar2+4`Zr;D-$v_Vd~YAX
zCo{}}j@7ke7*|-jNM%5Bes|^IHdLNZ4rm+VHmJ-!p;=r9i)X_WqUdMdy>p`c7P_&d
zyG)x7rRg*g*t!}NU$FIo!etezdM^q{+=vUBXAX6_UaTa!-gg~U_!qrC0y9&Nt<*tI
zs^g;e+hZe_OS*OZW%g|=s(J^LYFb~`mNH#8e{Uw};ca?6NC_{Fx@^%v@|Q9bs-46D
zvt)M53CnR(FDkNL2OQm58r^DhZ1AaMzaYhM{AuU)Mb+TOjlZY3XGytE9=O(~9N|=r
zI}hAv83^oWXB(LPF}+4j@Vyg8S4NaLoezpqx6Z}cBO(lhGQ8Z2bdHvTgh?+ADzi(MPgnn(Lp9jF7
zR--436}9^V4l(R&g-r8rL|rAZwfR$(PDz8x8_6x(c>Abc`k9?MlSRk*Wj;2=(+!H3
zb}Y%bUj&+
zPoo$Is{IFV3a|0C!x?QW(&s-Dn+%E;9n`@&o-R_+A{Nb8UC>mMyfp(x@M}^ts8X1Y
zP<;$|9*vgr)b888Q2HW&-l+xHE4^8lm)pAch;Z)R+&8Lz7q*&EaMDwP#%)Wl4Hihk
z+@V`ET~FX+gLOGY{i8GXK;$y8Pz4fW@?<|r
zC#yxIhNd{km8}iHcMFR0iA5yTcNO(9f(
zC20yOg8gF3VI{kJbWg*g;`ZceEO-YMnuWQHP2K1*@b0W-O^
zvujxjPGk%;=6%_O{_VA>-!Bje?f*SQok1pXcXyl1Qg^$~SbuM;y_^W`J@>L}JZ`>j
zA1_UjnmPE9QL(tQRstn(icpCJocxOBT+r~E68xKGvDl?xq%K5MOzOwA?o8F591-j59pw?>^Ia%np9rph(fab7wW
zF><hV%t7{7~*X+)MBKED6ndbUU?o+HSjcP*?Lw>)J+rmJxH@v!?cZ(=r~=1n>4wcK|y
z*A-L$m`Pr(6%?Dn$lvkmFX(MaM-LvB@McQX=<#W$hrZ`g{E7Pv^{V)LrbBm*BM+0e
z4X|YCuvt9!^aGF7(&ykiuFvZgNk6%nf%r%YYP1s;X!Sp^obaVZv^sq1S}Gb$|67%e
zyC8y|4BUQCXofrEc;{^QOo?Y#u`7>$Xr;F;S>dlOO_YsCKfDy1$Nfft(l$5tsj2Zf
z`RP%$iK5ouMRnxQ>O6a0llI_n3$B*AJS00U<>3G)@8dV}s7iG!Q6K+YxvjPMgSM)#
z#YNhk-_>C~b+HDXo_g#rrz1E9kw_Ix|jCPAG*0
zX!i=82XH<
zm!pZcgn;3hs>iXg}KtgrmHuwwdqttM%0AL8rTb|
zbp6Aj|Iaby7KqieM!d!2D1oGjvjey;7<+*;g*+h2zFey^W!bh@{-xs(Jo3^!muep@
zSh`mpt?c^MrTOcNGbGFb(KMq0-eLa_?wcfpa&kqoawkY(*5(EUl4tve(M3rASH^$4
zlE_=^wmtm5QL)hTXFg52?OyN9R@(*yo8l^5NfP2xXg9uIj4cZUh}KlQ2H?~z#YCj$
zY6F5DF`FbA0n;x4pqO{9H2Q$OCQ~V}O5L6ejH>`OcmeC0-xVK^1wf@Mi3RxSlI(q}
zzArzoQa_RNEMyCdmN_$Mu9xIWRPR$4pJ!y?d4JY-fEWQ8>oJf&|rk
zgeOa)$ghOMP-gH|5IW^rVE5)*WYHQ1Odh%EA2Y}_Fp2v)jOoDJd8i%VIc)Y|u79LH
z=;;YaZfoMNh}n>QuJR&f((9%_CD}t$=GIcMGwM({WiUJ3d`(u572n>Y_XF+HYgdQw
zUxcu(@#I@>kjAP>j%<6*Rf-0GNy=j1bslx06y
zo`_M;%cLvU|Db9Fgw3FzsXO16ut&s3Yfnd5ViaQxY1#`!RltCXi(U(I~vNPDK
zyE$288*5A8cT)_}hivgL+)nx}DgoJn!%E~WzrGivW6v#kCtGUB5u=D-QL=K+>54Ej
zyy?9q9Ljf?}FkJJK3!o)p_3Y|d-_k~!Xbd?sIy7gCBa1;&0+GRG#X{n6C<
z#pP`~kF2@%t*>!|n_lTB#$=IX!+9THwM%gRkOj}|w(|(``u?!zjmPSi+mq5lps^&=
zbmf(0g`oMwrlIldD?`##B^oIoBGDZAtViT!VuP*&kByL0Uy>s@R6KW=)o|}m4IK=C
zhhmUpi#*U4y4g7Rp6i+$kVg}QrfW03cf^s86)V>?yUO$PzEjxJ5x09tkpJFx{4EA3
z1v7s5s)^csm1CqWQt4G@7FtFQ8R^(+^f@EDtQR+SkJpfzWP3}gQ
z+HMES)4-TbKh0N1uUZCOxrOZAS5G=V-UwXw$H%|PYmu&B++@`$WxyJ`c;bz1D#G}Y
zjtMJn)F3^0`0*3CFz2zr!tIyX&hM^m?oxZyj6h#zjaje{-xU_QD!-HZeO~R;%dk
zH_L14aSX!RCrDWp0}^D!0!<2%RDu4!&1t)kZl;p!K4r`hoxOlj2;P|XrqOt0y;?o;
z+Fv+$P6#(l35c#lz-(UCn!nY-Jy%VMne&j+hQA-tY!k^Ua_nI~8PEs5sg^}%{oExo
zz~Z*1sHNY<;J&n8jXc;H+n|H9yXs?|G5+D|Qf&5vA2k6U@9)ad_mXLhkMR|;IQz(y
zoP4)g@Ik9UG?Q(nwk{Ia{^qtZB7A!Y?E?78|2s*4khTO@Da|bDYMG5*U~>rv8RX6i
z4HB^aP94o)WH72_H|MbLFfycQga)ra)nH;u%J-#3hyhA=ea;F?J
zmfUs%z!Eb|Q{x4K*ig1JQ(N6tsuD2up_fJ*vF3*8+TcSdX|(}m;%_Ki=hyrI|HIj?
z(uG_Er3JqhhmQ`B$BADVYp(TUgMI1%p9$cuQzKZwtsMAW7YWci6!T@5W`S4>7l6@}
z?hWw^Qtx(aEcx-jq=c2f%m6=|%4O+TG*h31z+#&17aY#GR7mK}^de&Rr7*^bxnhI+
zm*yfN*{s=bi{$YIZ%`H*hmaPK$xO+?E8@OinV*eFruYd*h_P(~UAu7czI|MX;FJiw
zzzu(R`QOsd|Gzemy0kl-jt5sLw^QlP)o2IW^`By&iHXDhlWq!Tz6nIOijMTKTN?@q
zV(lX*Sp@U{OJL2>S_SiCi6$l_Bgbw_2wyyXgrZ+8RGVrD73cvXCRARIcxWXUhNdj8
z1<&FFs)#BRHmiINwL2F55J{=(nC8>#7*k3br(9V=z!E)-WH8(k
zDbE8R?hw-*in!AP9W6ZgA^P-bVIM^60P)SHq^V?ysKJ%AFvtGxI7_5p))xq-Bx*v_
zJ;O)si3RcP06>H(ZS8M703wAFDbeZyhUIHGrL6WzMq!7l(v0tw;dH5k%IAzxvu)@Pr61WL8SMSe-7iSjr#L^$%RzT*FD{@
zu#o}ya9mdcvsgILN|=(C_
z`jM{`d&{e_AeHUW`>Z3tI+q$e?^E~PWsULy8LEF@wpriDAL9T7J+)dWd
z3`4s%tfrCqbHlF`4GrxZ>LfpXs>Umt2KH&RnZjLAGc%V0-qAUCP>z-Jc-2Z)Uawp-
z!3?`W?EsVt#kw~#(BKL&T>TaL`(1Av(#8S%j#bRTyX0lqc6D7gY|kjuuvl@HhbK6B
zhJc4*v2k}Zwfg*(zHnO}B~xErfKSDb(RHN_-8cu4>L0`0GoiS>J2ZOrt2wm3*j3{k
zZFxuhRwoW$fgBt^oI0ZFb>_1I4wyV?Zxq_F)P09db7~|;W0GZh;lUDAq!nv
z8f)1bnsYSb^Qu)h%F+*?qGntocLrT}ASv3XP(d8kU58AV_s;T9z0;0IQ*9?L6Wo#U
zh<=^``yk3P0O*nuDcjz9H4?=<0bC#Ddmnx>hm+5^?2N9x$C}DRgXz1(Tu8xFKwoW1
zD!i7|gJm*H<7$NBD;0j7(VdoWE7X1x{dP%uRAFc!wNe8txOQxFzcDuI$+ki0pC)X(
zUYj)hrQ7m$x9OoP;zq2u_F7K+@N02}(Fs8PT|r8lsfGLkJ@g*aQ3K5rZl4NUUX7Gd
z52fctiKx;UEJBY={;Z~MGE(zV$7?oWrp;x+_9Z{FuWqBI#FoBRKS}0M3`Z(_oOV(C
z-qFeDo}v}Z#QZ#kh-Fkr`t}ySJx3z;K4F=F-H2~C8`EN_#1E+gZ=FklNNQ3hy|hbi
zL#k@^@;5)|C8=QEiP#Ciahfm80E9H0Ygz0qQ;
zYVfvTOUgw>uSJ=fQMzcu`5NY1A%Ymi5Ide1HLTj24-Rn4T1jGz2u(4=&cRSB`mk!+$_
zWw9%sz0Xs$AvOCbj>Um(&Z-r3ZmGV2g>&0b-I&$?jp!at8UCA2V`?1&wZ({Ybd5{G
zAcy*JI(83j$@T=|iao^!u|eQ4HH>_#E!=QkRa^C9V9Z2Xv~DuLxi6fDUnz0enU!hD
zI3(b>h-$GC6=BL*xg~OzAcXR`0tEI681P}G3U~E
zhI`d7%cnJ!oV5;;XEr%*h1zwCFL^c{Lsl+)*91w_5VUgr*zisn5Idb49CiawDj9Ya
zCck-iu-=d=#MJv?ZMQ8Ki2PDEnS5s0@O6$fYd0|Gp`)a~<%n8aXO?3X-zBD8HPeKL
zwD$umwBqn9FTYGp1xLa@=Q!Ex?{(6t2aj#9H5JsjBqrYpJ)}MMu}$&(?R}5M
zQ3Bi!a-Qq?prI8<`gvfPm18wa{ZXiw8|_vcn4d(z?5f4EkU&szA%nEelEM=7
zr*FgWOT!nA`qW(-H@%B3Ea;iPB0nF@jMQlTJByOuubf27I-)K8YA*6;mgk!|zWNAWlTCz)m9lep`j+HntNmPM(;}qX~w%sSUbx)a-73E=HRk
zPTAO6)oJx0rq(G#7BoC`u<;W{Nti{S=nvo!q~tLFlLA3aa)D
zzsIHn*_&gx?n2&-Q|8@JOeCslj3xkr4W9kCJ&$jO|InwHMIGiACScJWpGpUTUJ#vB
zNWF}QK~i9VC<8Dnn2>ip5wNRvj?%(TYyG8vkQ2Dm?mBl8{fo1w
zjVyOXMSE8z82ta;&Hb+`{4b5c#|COBV`nGN7iP)EJhRtsOAB`Xb5={rGuOu^K|byU
zmPi1k)DOsqV`TcfEaFEM4dg|tem^h&wcdvD7U$R9QK<0kPe`9s(
z8H;KH6H3vL0lET0HKFQx_gp>IRWgJKWduo#AzIJjxEOT+Q6^$>(U%g4@3=@!7OVsz
zR9FD!aB7rLb<=y@#-I_HR5UdzdxK~XI$%c{*S-}RwZSxvMcP?ANq`FY#O7-Xj4;@p
zzjAQz@;tBZ{=B6A3LrXr*|SiekKx#uk+z76khV;tLQ6NW=9nc8R~Q5UxJK|mXhrcn
z@@$kT16A4@u-uPmfV`K!rlfeA%noN38=AKMvz$^0!tL<0I32%mXGB7D{I%KY$
z=$w4-HYfBvI+qrQmSse;D`Ekn@0RdD81a8dD9yfzWn|b;%WisPjoBW2M%U|H1W@&0
z)_~xKa_&AH6{f^$Iwl8`X&nq{)yzCE$+5r
z<<#W*!F#}C(+Vq2j%)n>=R@4w{!S-`l8sHfG6IBK=4{Sm~ZP-=lpSq8UtJ
zYCvp?x=7;IwSZ9#svLSMNkT~WEL*vENB;nVV|lcx$^2CGLV+ox>mX?dA4vw2+r_Uc
zZC>S3wG4UP!%n?ADiyoA=MV$4dDeH7x5@jFb8Z?0vUi8>)UzGOwHYVhAYI=)k`$-a
z$3C9EF-3(bqrmb}7AwNaoril`?)=*-Z<9Nz1Sf|AgNB};L@Gplmt?dajcj8pJn=M7a`Qe$l0z%fM8r0Bd@Z^
z?%lMdk%>I+Ld*%l`fg2r9I)uD)54^X0?zF{VL0k=3KAf%H?|+QHKEy)tiHES8?AFd
z;(0!8`{;k)7o@tahnKVUr;82|h(@n!0$$d6d_8RW;Z%L-b+h_z(GwkJaD61@>$4!5
z9hWn5g_?FaUmmFSxVBRq!#1(q4egPsVsjM$^Ps|<9z@IB$PHmep@e9#nS9;~hupgF
z_xDLOx>5VD8;NY0+N_8a=1PvtxX}3V*ccy#6ob4vom+bJmsnHZ8A~MJNEv?&!HL{R
zZ3=HSTdCGSFA)3HX%_wEp%A)VAnn~LaPr%a?P9ac@Ja0&_d+;DU<}D}@9LH-ZC7kl
z=2hz}g}%eKQ^p>S4k`oQ7GYU~OW{V_nn&xUQWe`}ZEQhO>%y~YH_|JMW7JF=@0cDR
z!`+VPwck7wWq9|Ael>?Ei@U8hnnq8MbuX--c%gK!$D@Q!GP-m<
zipn76N_myou;Ph1=(cz-(umv1o}RQ>&Muusa^f!ey1p%h3YR?n%Aapv!n>2$T!a5X
zcS86ZO)T^m-l2UCzQuy^-bKhHJxMl)hk#N+pmeo;O8;rafp4G@^dWW%d3l1$V1;&B
z)A>j(?$GG7>C7gcmQi&XSM&th(`7yA27X!dpwKmYjdyqu!w<4)qqv
z*u|(l5$ct-gv#cL7!P4~nIP|(!;$@ERPHK9V=ejapD=*J@>e(j;XaWH>ueAG(;SzW
zZk_WruhD1DuX{&P;S1*n+H`-4p)68&S`nT6yl2x{&xQ}f0uMBEUA*)szlo8TtEt*A
zuB(F_;af+y@1RmBB&o|~**j+rRckWa&lwOQ>`Hv!-_%t{0wRb_&;bYNS9=ek_crvn
z6A@-e(dnBsOOai*l=Jl@Oc|A&F=VB+ZKVsU`aOTSEQ|&^S;)ox=*UWz2FVsQVl6YJxigY_quCH
z>jF2ofi5fUbNrP7I%vu+htt5Vs}ugBa)Ysj=ji;XFc;^o#Ld?)ep680y?nXxwDv
zt+mc$L`Jr4LmQEyj(q=NPia3RqZ7hjy4g3}XBNelWOpvPsSV@n+btVa)dD1r=GMV_
zX_Ko8+n3YoCE3^w><-ZAhN=PW`5YujhRKHJUdU)@s;dJ(VZ%~5N8(?y79X$aN5x*P%>RBS9sdI#jc
z6CgEKAOWqG4CQZIY1%wisu{PPi-nIpiRGBvX`WvHVvGLaw@owf>#mOX?G8ts}hcRR#>O&
z`&)^HQ@L%?y{~xua25v~DLVa3!qj_zfv0m1I#k=w6w1;X^T7lkM&&2Foc^V3ZuHh?G<|xnYl1wR%R{pcIkm{RB<+8YJh+4-q9NPjGa`-
zME=GOwF<;oM>HI*y7+N+)okN6()u@f7(zWO>E4f=xwf-RUpPLNfJ1IY~jncst!c|b2
zR_a_Xce0(@gQ+}UqwWg{>-HZ3Ilt43H)Mxz$LCjr{e_hV`SEra+oe`wCvlC)d7P`x
zAEZK3r~?)eKv&EZl|9!0m8px2`Xa_8Y8gt}k}$hYPWUuN{rJT5Gf
zadD5GErXM1K)peYmj{iDdr@APx2fVVTzI(!lyM;$Okjv_=y!ZM#Qxa*&^@_I^4M7$
z>*}4*%*eY^xbj8C#y%a&PYC(lm{S-^ewfb5vy`lthLp=H)jkhNnK=GfU3frxQM$u$
zSb}RCZcF--xAd~TJIw=MAZT06>5k5EzW&92Y$|P
zxsmGmzR{2gI<&2td^)vWXnnv#Yz!cIzRdsIpNTODkvF%NVQA1#`BzgCkr~srI5NKM
zMQkCMw&NLR{xHyXMg_E>*W=A$RYUqJHZTGIiiqmv6f@-iNcSAhq4^KLrZ+i(-fw0t
zW0Jr957C(@J|YJN0~C0ViZ*wij`o?Z5&3jPe^#|rt+0P;qW
z8pNCW0#Id&A%?9Y(&3X~oPz~M{6E+}O&qT0tM>+>HVTY26c(o?vJ={=xGaqc-i>8M
zxZql3pi70#0-?2N@R52%On6%?LI9%v1d8#9+h^K*{EvpI)#CQdyrg2D%m^U}E3oR9
z_vuPjd~1q~_>`8lF~MoEXb5H;!1UE|GtY+LskVyde}1qPm_neCZ%dH*E9b9zUyt59
t;Hy8UHYL5)rI#o8UyA+z+Pt*?jMPW.*?)'"
+)
+CONNECTION_INVALID_HOSTNAME_REGEX = re.compile(
+ "Unknown Doris server host '(?P.*?)'"
+)
+CONNECTION_UNKNOWN_DATABASE_REGEX = re.compile("Unknown database '(?P.*?)'")
+CONNECTION_HOST_DOWN_REGEX = re.compile(
+ "Can't connect to Doris server on '(?P.*?)'"
+)
+SYNTAX_ERROR_REGEX = re.compile(
+ "check the manual that corresponds to your MySQL server "
+ "version for the right syntax to use near '(?P.*)"
+)
+
+logger = logging.getLogger(__name__)
+
+
+class TINYINT(Integer):
+ __visit_name__ = "TINYINT"
+
+
+class LARGEINT(Integer):
+ __visit_name__ = "LARGEINT"
+
+
+class DOUBLE(Float):
+ __visit_name__ = "DOUBLE"
+
+
+class HLL(Numeric):
+ __visit_name__ = "HLL"
+
+
+class BITMAP(Numeric):
+ __visit_name__ = "BITMAP"
+
+
+class QuantileState(Numeric):
+ __visit_name__ = "QUANTILE_STATE"
+
+
+class AggState(Numeric):
+ __visit_name__ = "AGG_STATE"
+
+
+class ARRAY(TypeEngine):
+ __visit_name__ = "ARRAY"
+
+ @property
+ def python_type(self) -> Optional[type[list[Any]]]:
+ return list
+
+
+class MAP(TypeEngine):
+ __visit_name__ = "MAP"
+
+ @property
+ def python_type(self) -> Optional[type[dict[Any, Any]]]:
+ return dict
+
+
+class STRUCT(TypeEngine):
+ __visit_name__ = "STRUCT"
+
+ @property
+ def python_type(self) -> Optional[type[Any]]:
+ return None
+
+
+class DorisEngineSpec(MySQLEngineSpec):
+ engine = "pydoris"
+ engine_aliases = {"doris"}
+ engine_name = "Apache Doris"
+ max_column_name_length = 64
+ default_driver = "pydoris"
+ sqlalchemy_uri_placeholder = (
+ "doris://user:password@host:port/catalog.db[?key=value&key=value...]"
+ )
+ encryption_parameters = {"ssl": "0"}
+ supports_dynamic_schema = True
+
+ column_type_mappings = ( # type: ignore
+ (
+ re.compile(r"^tinyint", re.IGNORECASE),
+ TINYINT(),
+ GenericDataType.NUMERIC,
+ ),
+ (
+ re.compile(r"^largeint", re.IGNORECASE),
+ LARGEINT(),
+ GenericDataType.NUMERIC,
+ ),
+ (
+ re.compile(r"^decimal.*", re.IGNORECASE),
+ types.DECIMAL(),
+ GenericDataType.NUMERIC,
+ ),
+ (
+ re.compile(r"^double", re.IGNORECASE),
+ DOUBLE(),
+ GenericDataType.NUMERIC,
+ ),
+ (
+ re.compile(r"^varchar(\((\d+)\))*$", re.IGNORECASE),
+ types.VARCHAR(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^char(\((\d+)\))*$", re.IGNORECASE),
+ types.CHAR(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^json.*", re.IGNORECASE),
+ types.JSON(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^binary.*", re.IGNORECASE),
+ types.BINARY(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^quantile_state", re.IGNORECASE),
+ QuantileState(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^agg_state.*", re.IGNORECASE),
+ AggState(),
+ GenericDataType.STRING,
+ ),
+ (re.compile(r"^hll", re.IGNORECASE), HLL(), GenericDataType.STRING),
+ (
+ re.compile(r"^bitmap", re.IGNORECASE),
+ BITMAP(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^array.*", re.IGNORECASE),
+ ARRAY(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^map.*", re.IGNORECASE),
+ MAP(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^struct.*", re.IGNORECASE),
+ STRUCT(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^datetime.*", re.IGNORECASE),
+ types.DATETIME(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^date.*", re.IGNORECASE),
+ types.DATE(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^text.*", re.IGNORECASE),
+ TEXT(),
+ GenericDataType.STRING,
+ ),
+ (
+ re.compile(r"^string.*", re.IGNORECASE),
+ String(),
+ GenericDataType.STRING,
+ ),
+ )
+
+ custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]] = {
+ CONNECTION_ACCESS_DENIED_REGEX: (
+ __('Either the username "%(username)s" or the password is incorrect.'),
+ SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
+ {"invalid": ["username", "password"]},
+ ),
+ CONNECTION_INVALID_HOSTNAME_REGEX: (
+ __('Unknown Doris server host "%(hostname)s".'),
+ SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
+ {"invalid": ["host"]},
+ ),
+ CONNECTION_HOST_DOWN_REGEX: (
+ __('The host "%(hostname)s" might be down and can\'t be reached.'),
+ SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
+ {"invalid": ["host", "port"]},
+ ),
+ CONNECTION_UNKNOWN_DATABASE_REGEX: (
+ __('Unable to connect to database "%(database)s".'),
+ SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
+ {"invalid": ["database"]},
+ ),
+ SYNTAX_ERROR_REGEX: (
+ __(
+ 'Please check your query for syntax errors near "%(server_error)s". '
+ "Then, try running your query again."
+ ),
+ SupersetErrorType.SYNTAX_ERROR,
+ {},
+ ),
+ }
+
+ @classmethod
+ def adjust_engine_params(
+ cls,
+ uri: URL,
+ connect_args: dict[str, Any],
+ catalog: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> tuple[URL, dict[str, Any]]:
+ database = uri.database
+ if schema and database:
+ schema = parse.quote(schema, safe="")
+ if "." in database:
+ database = database.split(".")[0] + "." + schema
+ else:
+ database = "internal." + schema
+ uri = uri.set(database=database)
+
+ return uri, connect_args
+
+ @classmethod
+ def get_schema_from_engine_params(
+ cls,
+ sqlalchemy_uri: URL,
+ connect_args: dict[str, Any],
+ ) -> Optional[str]:
+ """
+ Return the configured schema.
+
+ For doris the SQLAlchemy URI looks like this:
+
+ doris://localhost:9030/catalog.database
+
+ """
+ database = sqlalchemy_uri.database.strip("/")
+
+ if "." not in database:
+ return None
+
+ return parse.unquote(database.split(".")[1])
diff --git a/tests/unit_tests/db_engine_specs/test_doris.py b/tests/unit_tests/db_engine_specs/test_doris.py
new file mode 100644
index 0000000000..d7444f8d2d
--- /dev/null
+++ b/tests/unit_tests/db_engine_specs/test_doris.py
@@ -0,0 +1,147 @@
+# 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 typing import Any, Optional
+
+import pytest
+from sqlalchemy import JSON, types
+from sqlalchemy.engine.url import make_url
+
+from superset.db_engine_specs.doris import (
+ AggState,
+ ARRAY,
+ BITMAP,
+ DOUBLE,
+ HLL,
+ LARGEINT,
+ MAP,
+ QuantileState,
+ STRUCT,
+ TINYINT,
+)
+from superset.utils.core import GenericDataType
+from tests.unit_tests.db_engine_specs.utils import assert_column_spec
+
+
+@pytest.mark.parametrize(
+ "native_type,sqla_type,attrs,generic_type,is_dttm",
+ [
+ # Numeric
+ ("tinyint", TINYINT, None, GenericDataType.NUMERIC, False),
+ ("largeint", LARGEINT, None, GenericDataType.NUMERIC, False),
+ ("decimal(38,18)", types.DECIMAL, None, GenericDataType.NUMERIC, False),
+ ("decimalv3(38,18)", types.DECIMAL, None, GenericDataType.NUMERIC, False),
+ ("double", DOUBLE, None, GenericDataType.NUMERIC, False),
+ # String
+ ("char(10)", types.CHAR, None, GenericDataType.STRING, False),
+ ("varchar(65533)", types.VARCHAR, None, GenericDataType.STRING, False),
+ ("binary", types.BINARY, None, GenericDataType.STRING, False),
+ ("text", types.TEXT, None, GenericDataType.STRING, False),
+ ("string", types.String, None, GenericDataType.STRING, False),
+ # Date
+ ("datetimev2", types.DateTime, None, GenericDataType.STRING, False),
+ ("datev2", types.Date, None, GenericDataType.STRING, False),
+ # Complex type
+ ("array", ARRAY, None, GenericDataType.STRING, False),
+ ("map", MAP, None, GenericDataType.STRING, False),
+ ("struct", STRUCT, None, GenericDataType.STRING, False),
+ ("json", JSON, None, GenericDataType.STRING, False),
+ ("jsonb", JSON, None, GenericDataType.STRING, False),
+ ("bitmap", BITMAP, None, GenericDataType.STRING, False),
+ ("hll", HLL, None, GenericDataType.STRING, False),
+ ("quantile_state", QuantileState, None, GenericDataType.STRING, False),
+ ("agg_state", AggState, None, GenericDataType.STRING, False),
+ ],
+)
+def test_get_column_spec(
+ native_type: str,
+ sqla_type: type[types.TypeEngine],
+ attrs: Optional[dict[str, Any]],
+ generic_type: GenericDataType,
+ is_dttm: bool,
+) -> None:
+ from superset.db_engine_specs.doris import DorisEngineSpec as spec
+
+ assert_column_spec(spec, native_type, sqla_type, attrs, generic_type, is_dttm)
+
+
+@pytest.mark.parametrize(
+ "sqlalchemy_uri,connect_args,return_schema,return_connect_args",
+ [
+ (
+ "doris://user:password@host/db1",
+ {"param1": "some_value"},
+ "db1",
+ {"param1": "some_value"},
+ ),
+ (
+ "pydoris://user:password@host/db1",
+ {"param1": "some_value"},
+ "db1",
+ {"param1": "some_value"},
+ ),
+ (
+ "doris://user:password@host/catalog1.db1",
+ {"param1": "some_value"},
+ "catalog1.db1",
+ {"param1": "some_value"},
+ ),
+ (
+ "pydoris://user:password@host/catalog1.db1",
+ {"param1": "some_value"},
+ "catalog1.db1",
+ {"param1": "some_value"},
+ ),
+ ],
+)
+def test_adjust_engine_params(
+ sqlalchemy_uri: str,
+ connect_args: dict[str, Any],
+ return_schema: str,
+ return_connect_args: dict[str, Any],
+) -> None:
+ from superset.db_engine_specs.doris import DorisEngineSpec
+
+ url = make_url(sqlalchemy_uri)
+ returned_url, returned_connect_args = DorisEngineSpec.adjust_engine_params(
+ url, connect_args
+ )
+ assert returned_url.database == return_schema
+ assert returned_connect_args == return_connect_args
+
+
+def test_get_schema_from_engine_params() -> None:
+ """
+ Test the ``get_schema_from_engine_params`` method.
+ """
+ from superset.db_engine_specs.doris import DorisEngineSpec
+
+ assert (
+ DorisEngineSpec.get_schema_from_engine_params(
+ make_url("doris://localhost:9030/hive.test"),
+ {},
+ )
+ == "test"
+ )
+
+ assert (
+ DorisEngineSpec.get_schema_from_engine_params(
+ make_url("doris://localhost:9030/hive"),
+ {},
+ )
+ is None
+ )
From bd8951e9586fb3bb36c13f394bc257bda1a851e3 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Tue, 21 Nov 2023 13:26:02 -0800
Subject: [PATCH 056/119] fix: Optimize fetching samples logic (#26060)
---
superset/views/datasource/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/superset/views/datasource/utils.py b/superset/views/datasource/utils.py
index 9baabdcc54..afed5f7fd2 100644
--- a/superset/views/datasource/utils.py
+++ b/superset/views/datasource/utils.py
@@ -114,7 +114,7 @@ def get_samples( # pylint: disable=too-many-arguments,too-many-locals
sample_data = samples_instance.get_payload()["queries"][0]
if sample_data.get("status") == QueryStatus.FAILED:
- QueryCacheManager.delete(sample_data.get("cache_key"), CacheRegion.DATA)
+ QueryCacheManager.delete(count_star_data.get("cache_key"), CacheRegion.DATA)
raise DatasetSamplesFailedError(sample_data.get("error"))
sample_data["page"] = page
From 630734b90896bcf9879200eb9eb287b370668f4e Mon Sep 17 00:00:00 2001
From: Jack Fragassi
Date: Tue, 21 Nov 2023 15:39:42 -0800
Subject: [PATCH 057/119] fix: Prevent cached bootstrap data from leaking
between users w/ same first/last name (#26023)
---
superset/embedded/view.py | 4 ++--
superset/views/base.py | 16 +++++++++-------
superset/views/core.py | 6 +++---
superset/views/dashboard/views.py | 2 +-
4 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/superset/embedded/view.py b/superset/embedded/view.py
index e59a6ced90..462c6046fa 100644
--- a/superset/embedded/view.py
+++ b/superset/embedded/view.py
@@ -17,7 +17,7 @@
import json
from typing import Callable
-from flask import abort, g, request
+from flask import abort, request
from flask_appbuilder import expose
from flask_login import AnonymousUserMixin, login_user
from flask_wtf.csrf import same_origin
@@ -78,7 +78,7 @@ class EmbeddedView(BaseSupersetView):
)
bootstrap_data = {
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
"embedded": {
"dashboard_id": embedded.dashboard_id,
},
diff --git a/superset/views/base.py b/superset/views/base.py
index 8f8b4c1648..9149c7ad91 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -296,7 +296,7 @@ class BaseSupersetView(BaseView):
) -> FlaskResponse:
payload = {
"user": bootstrap_user_data(g.user, include_perms=True),
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
**(extra_bootstrap_data or {}),
}
return self.render_template(
@@ -380,7 +380,9 @@ def menu_data(user: User) -> dict[str, Any]:
@cache_manager.cache.memoize(timeout=60)
-def cached_common_bootstrap_data(user: User, locale: str) -> dict[str, Any]:
+def cached_common_bootstrap_data( # pylint: disable=unused-argument
+ user_id: int | None, locale: str
+) -> dict[str, Any]:
"""Common data always sent to the client
The function is memoized as the return value only changes when user permissions
@@ -417,15 +419,15 @@ def cached_common_bootstrap_data(user: User, locale: str) -> dict[str, Any]:
"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(user),
+ "menu_data": menu_data(g.user),
}
bootstrap_data.update(conf["COMMON_BOOTSTRAP_OVERRIDES_FUNC"](bootstrap_data))
return bootstrap_data
-def common_bootstrap_payload(user: User) -> dict[str, Any]:
+def common_bootstrap_payload() -> dict[str, Any]:
return {
- **cached_common_bootstrap_data(user, get_locale()),
+ **cached_common_bootstrap_data(utils.get_user_id(), get_locale()),
"flash_messages": get_flashed_messages(with_categories=True),
}
@@ -535,7 +537,7 @@ def show_unexpected_exception(ex: Exception) -> FlaskResponse:
def get_common_bootstrap_data() -> dict[str, Any]:
def serialize_bootstrap_data() -> str:
return json.dumps(
- {"common": common_bootstrap_payload(g.user)},
+ {"common": common_bootstrap_payload()},
default=utils.pessimistic_json_iso_dttm_ser,
)
@@ -553,7 +555,7 @@ class SupersetModelView(ModelView):
def render_app_template(self) -> FlaskResponse:
payload = {
"user": bootstrap_user_data(g.user, include_perms=True),
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
}
return self.render_template(
"superset/spa.html",
diff --git a/superset/views/core.py b/superset/views/core.py
index bb273eb53c..28d84d223f 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -604,7 +604,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
"force": force,
"user": bootstrap_user_data(g.user, include_perms=True),
"forced_height": request.args.get("height"),
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
}
if slc:
title = slc.slice_name
@@ -862,7 +862,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
bootstrap_data=json.dumps(
{
"user": bootstrap_user_data(g.user, include_perms=True),
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
},
default=utils.pessimistic_json_iso_dttm_ser,
),
@@ -953,7 +953,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
payload = {
"user": bootstrap_user_data(g.user, include_perms=True),
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
}
return self.render_template(
diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py
index ce5e8f1e07..0b41a67ee2 100644
--- a/superset/views/dashboard/views.py
+++ b/superset/views/dashboard/views.py
@@ -151,7 +151,7 @@ class Dashboard(BaseSupersetView):
)
bootstrap_data = {
- "common": common_bootstrap_payload(g.user),
+ "common": common_bootstrap_payload(),
"embedded": {"dashboard_id": dashboard_id_or_slug},
}
From 260d561b9a62332cb51d3b57c5a67ececcd7120e Mon Sep 17 00:00:00 2001
From: Daniel Vaz Gaspar
Date: Wed, 22 Nov 2023 10:31:32 +0000
Subject: [PATCH 058/119] docs: update security policy and contributing
(#25917)
Co-authored-by: Sam Firke
---
.github/SECURITY.md | 4 ++--
CONTRIBUTING.md | 45 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index f35b9c48f0..086ff8c0ca 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -12,8 +12,8 @@ Apache Software Foundation takes a rigorous standpoint in annihilating the secur
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
pertaining to its features and functionality.
If you have any concern or believe you have found a vulnerability in Apache Superset,
-please get in touch with the Apache Security Team privately at
-e-mail address [security@apache.org](mailto:security@apache.org).
+please get in touch with the Apache Superset Security Team privately at
+e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
More details can be found on the ASF website at
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d427ba393d..a955f123db 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -180,6 +180,51 @@ See [Translating](#translating) for more details.
There is a dedicated [`apache-superset` tag](https://stackoverflow.com/questions/tagged/apache-superset) on [StackOverflow](https://stackoverflow.com/). Please use it when asking questions.
+## Types of Contributors
+
+Following the project governance model of the Apache Software Foundation (ASF), Apache Superset has a specific set of contributor roles:
+
+### PMC Member
+
+A Project Management Committee (PMC) member is a person who has been elected by the PMC to help manage the project. PMC members are responsible for the overall health of the project, including community development, release management, and project governance. PMC members are also responsible for the technical direction of the project.
+
+For more information about Apache Project PMCs, please refer to https://www.apache.org/foundation/governance/pmcs.html
+
+### Committer
+
+A committer is a person who has been elected by the PMC to have write access (commit access) to the code repository. They can modify the code, documentation, and website and accept contributions from others.
+
+The official list of committers and PMC members can be found [here](https://projects.apache.org/committee.html?superset).
+
+### Contributor
+
+A contributor is a person who has contributed to the project in any way, including but not limited to code, tests, documentation, issues, and discussions.
+
+> You can also review the Superset project's guidelines for PMC member promotion here: https://github.com/apache/superset/wiki/Guidelines-for-promoting-Superset-Committers-to-the-Superset-PMC
+
+### Security Team
+
+The security team is a selected subset of PMC members, committers and non-committers who are responsible for handling security issues.
+
+New members of the security team are selected by the PMC members in a vote. You can request to be added to the team by sending a message to private@superset.apache.org. However, the team should be small and focused on solving security issues, so the requests will be evaluated on a case-by-case basis and the team size will be kept relatively small, limited to only actively security-focused contributors.
+
+This security team must follow the [ASF vulnerability handling process](https://apache.org/security/committers.html#asf-project-security-for-committers).
+
+Each new security issue is tracked as a JIRA ticket on the [ASF's JIRA Superset security project](https://issues.apache.org/jira/secure/RapidBoard.jspa?rapidView=588&projectKey=SUPERSETSEC)
+
+Security team members must:
+
+- Have an [ICLA](https://www.apache.org/licenses/contributor-agreements.html) signed with Apache Software Foundation.
+- Not reveal information about pending and unfixed security issues to anyone (including their employers) unless specifically authorised by the security team members, e.g., if the security team agrees that diagnosing and solving an issue requires the involvement of external experts.
+
+A release manager, the contributor overseeing the release of a specific version of Apache Superset, is by default a member of the security team. However, they are not expected to be active in assessing, discussing, and fixing security issues.
+
+Security team members should also follow these general expectations:
+
+- Actively participate in assessing, discussing, fixing, and releasing security issues in Superset.
+- Avoid discussing security fixes in public forums. Pull request (PR) descriptions should not contain any information about security issues. The corresponding JIRA ticket should contain a link to the PR.
+- Security team members who contribute to a fix may be listed as remediation developers in the CVE report, along with their job affiliation (if they choose to include it).
+
## Pull Request Guidelines
A philosophy we would like to strongly encourage is
From 843c7ab58a7a4ccc45c1fbdede6a1766d36b1c84 Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Wed, 22 Nov 2023 03:52:30 -0800
Subject: [PATCH 059/119] chore: Allow only iterables for BaseDAO.delete()
(#25844)
---
superset/daos/base.py | 11 +++++------
superset/dashboards/api.py | 3 +--
superset/dashboards/filter_sets/commands/delete.py | 2 +-
superset/databases/commands/delete.py | 2 +-
superset/databases/ssh_tunnel/commands/delete.py | 2 +-
superset/datasets/columns/commands/delete.py | 2 +-
superset/datasets/metrics/commands/delete.py | 2 +-
.../dashboards/security/security_dataset_tests.py | 2 +-
8 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/superset/daos/base.py b/superset/daos/base.py
index d2c1842c17..1133a76a1e 100644
--- a/superset/daos/base.py
+++ b/superset/daos/base.py
@@ -16,7 +16,7 @@
# under the License.
from __future__ import annotations
-from typing import Any, cast, Generic, get_args, TypeVar
+from typing import Any, Generic, get_args, TypeVar
from flask_appbuilder.models.filters import BaseFilter
from flask_appbuilder.models.sqla import Model
@@ -30,7 +30,6 @@ from superset.daos.exceptions import (
DAOUpdateFailedError,
)
from superset.extensions import db
-from superset.utils.core import as_list
T = TypeVar("T", bound=Model)
@@ -197,9 +196,9 @@ class BaseDAO(Generic[T]):
return item # type: ignore
@classmethod
- def delete(cls, item_or_items: T | list[T], commit: bool = True) -> None:
+ def delete(cls, items: list[T], commit: bool = True) -> None:
"""
- Delete the specified item(s) including their associated relationships.
+ Delete the specified items including their associated relationships.
Note that bulk deletion via `delete` is not invoked in the base class as this
does not dispatch the ORM `after_delete` event which may be required to augment
@@ -209,12 +208,12 @@ class BaseDAO(Generic[T]):
Subclasses may invoke bulk deletion but are responsible for instrumenting any
post-deletion logic.
- :param items: The item(s) to delete
+ :param items: The items to delete
:param commit: Whether to commit the transaction
:raises DAODeleteFailedError: If the deletion failed
:see: https://docs.sqlalchemy.org/en/latest/orm/queryguide/dml.html
"""
- items = cast(list[T], as_list(item_or_items))
+
try:
for item in items:
db.session.delete(item)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index b2aa43b0ee..b6ba689d83 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -1349,8 +1349,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
500:
$ref: '#/components/responses/500'
"""
- for embedded in dashboard.embedded:
- EmbeddedDashboardDAO.delete(embedded)
+ EmbeddedDashboardDAO.delete(dashboard.embedded)
return self.response(200, message="OK")
@expose("//copy/", methods=("POST",))
diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py
index edde4b9b45..8ac2107ca5 100644
--- a/superset/dashboards/filter_sets/commands/delete.py
+++ b/superset/dashboards/filter_sets/commands/delete.py
@@ -38,7 +38,7 @@ class DeleteFilterSetCommand(BaseFilterSetCommand):
assert self._filter_set
try:
- FilterSetDAO.delete(self._filter_set)
+ FilterSetDAO.delete([self._filter_set])
except DAODeleteFailedError as err:
raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err
diff --git a/superset/databases/commands/delete.py b/superset/databases/commands/delete.py
index 254380a906..59a247a506 100644
--- a/superset/databases/commands/delete.py
+++ b/superset/databases/commands/delete.py
@@ -44,7 +44,7 @@ class DeleteDatabaseCommand(BaseCommand):
assert self._model
try:
- DatabaseDAO.delete(self._model)
+ DatabaseDAO.delete([self._model])
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise DatabaseDeleteFailedError() from ex
diff --git a/superset/databases/ssh_tunnel/commands/delete.py b/superset/databases/ssh_tunnel/commands/delete.py
index 04d6e68338..70be55ce41 100644
--- a/superset/databases/ssh_tunnel/commands/delete.py
+++ b/superset/databases/ssh_tunnel/commands/delete.py
@@ -43,7 +43,7 @@ class DeleteSSHTunnelCommand(BaseCommand):
assert self._model
try:
- SSHTunnelDAO.delete(self._model)
+ SSHTunnelDAO.delete([self._model])
except DAODeleteFailedError as ex:
raise SSHTunnelDeleteFailedError() from ex
diff --git a/superset/datasets/columns/commands/delete.py b/superset/datasets/columns/commands/delete.py
index 23b0d93b6a..0eaa78b0d5 100644
--- a/superset/datasets/columns/commands/delete.py
+++ b/superset/datasets/columns/commands/delete.py
@@ -43,7 +43,7 @@ class DeleteDatasetColumnCommand(BaseCommand):
assert self._model
try:
- DatasetColumnDAO.delete(self._model)
+ DatasetColumnDAO.delete([self._model])
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise DatasetColumnDeleteFailedError() from ex
diff --git a/superset/datasets/metrics/commands/delete.py b/superset/datasets/metrics/commands/delete.py
index 8f27e98a3d..c19aff7aa5 100644
--- a/superset/datasets/metrics/commands/delete.py
+++ b/superset/datasets/metrics/commands/delete.py
@@ -43,7 +43,7 @@ class DeleteDatasetMetricCommand(BaseCommand):
assert self._model
try:
- DatasetMetricDAO.delete(self._model)
+ DatasetMetricDAO.delete([self._model])
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise DatasetMetricDeleteFailedError() from ex
diff --git a/tests/integration_tests/dashboards/security/security_dataset_tests.py b/tests/integration_tests/dashboards/security/security_dataset_tests.py
index 550d25ab59..4ccfa981b1 100644
--- a/tests/integration_tests/dashboards/security/security_dataset_tests.py
+++ b/tests/integration_tests/dashboards/security/security_dataset_tests.py
@@ -192,4 +192,4 @@ class TestDashboardDatasetSecurity(DashboardTestCase):
self.assert200(rv)
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(0, data["count"])
- DashboardDAO.delete(dashboard)
+ DashboardDAO.delete([dashboard])
From 2b88225ee113062ad1c108e28a8b41a7a04a0a1a Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Wed, 22 Nov 2023 09:11:09 -0300
Subject: [PATCH 060/119] fix: Flaky test_explore_json_async test (#26059)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
---
tests/integration_tests/core_tests.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 3157ddd649..f83d1b01ce 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -713,7 +713,8 @@ class TestCore(SupersetTestCase):
data = json.loads(rv.data.decode("utf-8"))
keys = list(data.keys())
- self.assertEqual(rv.status_code, 202)
+ # If chart is cached, it will return 200, otherwise 202
+ self.assertTrue(rv.status_code in {200, 202})
self.assertCountEqual(
keys, ["channel_id", "job_id", "user_id", "status", "errors", "result_url"]
)
From 63590867792a85a6e86eefaae7f6de89eb94c0b3 Mon Sep 17 00:00:00 2001
From: Rob Moore
Date: Wed, 22 Nov 2023 15:49:01 +0000
Subject: [PATCH 061/119] fix: move driver import to method (#26066)
---
superset/db_engine_specs/trino.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py
index d1c8e20bea..6e56dbfa24 100644
--- a/superset/db_engine_specs/trino.py
+++ b/superset/db_engine_specs/trino.py
@@ -27,7 +27,6 @@ from flask import current_app
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
from sqlalchemy.orm import Session
-from trino.sqlalchemy import datatype
from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY, USER_AGENT
from superset.databases.utils import make_url_safe
@@ -351,6 +350,9 @@ class TrinoEngineSpec(PrestoBaseEngineSpec):
the whole string they have to be quoted like "foo"."bar"."baz" and we then
alias them to the full dotted string for ease of reference.
"""
+ # pylint: disable=import-outside-toplevel
+ from trino.sqlalchemy import datatype
+
cols = [col]
col_type = col.get("type")
From fef82789b1c5a0f3f46c8aa8553cc795224353a7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 22 Nov 2023 09:48:01 -0800
Subject: [PATCH 062/119] build(deps): bump @types/lodash from 4.14.201 to
4.14.202 in /superset-websocket (#26063)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 2ced4f3b2f..815b80360a 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
- "@types/lodash": "^4.14.201",
+ "@types/lodash": "^4.14.202",
"cookie": "^0.6.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
@@ -1424,9 +1424,9 @@
}
},
"node_modules/@types/lodash": {
- "version": "4.14.201",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
- "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
+ "version": "4.14.202",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
+ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
},
"node_modules/@types/node": {
"version": "20.9.3",
@@ -7278,9 +7278,9 @@
}
},
"@types/lodash": {
- "version": "4.14.201",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
- "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
+ "version": "4.14.202",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
+ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
},
"@types/node": {
"version": "20.9.3",
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index 7ebac134f1..21e21a499e 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -16,7 +16,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@types/lodash": "^4.14.201",
+ "@types/lodash": "^4.14.202",
"cookie": "^0.6.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
From b1f521263dc4b055f4dd162b25a4a609c5ddf819 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 22 Nov 2023 09:48:25 -0800
Subject: [PATCH 063/119] build(deps-dev): bump @types/node from 20.9.3 to
20.9.4 in /superset-websocket (#26064)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
superset-websocket/package-lock.json | 14 +++++++-------
superset-websocket/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json
index 815b80360a..e1c6232e99 100644
--- a/superset-websocket/package-lock.json
+++ b/superset-websocket/package-lock.json
@@ -24,7 +24,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.5",
- "@types/node": "^20.9.3",
+ "@types/node": "^20.9.4",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
@@ -1429,9 +1429,9 @@
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
},
"node_modules/@types/node": {
- "version": "20.9.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz",
- "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==",
+ "version": "20.9.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
+ "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -7283,9 +7283,9 @@
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
},
"@types/node": {
- "version": "20.9.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz",
- "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==",
+ "version": "20.9.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
+ "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
diff --git a/superset-websocket/package.json b/superset-websocket/package.json
index 21e21a499e..d324dd4d46 100644
--- a/superset-websocket/package.json
+++ b/superset-websocket/package.json
@@ -31,7 +31,7 @@
"@types/ioredis": "^4.27.8",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^9.0.5",
- "@types/node": "^20.9.3",
+ "@types/node": "^20.9.4",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.61.0",
From 984c278c4c6da4ab5b235fc7efc7509c49de0b60 Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Wed, 22 Nov 2023 14:52:36 -0300
Subject: [PATCH 064/119] chore: Updates Announce template to include
CHANGELOG.md and UPDATING.md files (#26073)
---
RELEASING/email_templates/announce.j2 | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/RELEASING/email_templates/announce.j2 b/RELEASING/email_templates/announce.j2
index 4eb89701be..5e2318f792 100644
--- a/RELEASING/email_templates/announce.j2
+++ b/RELEASING/email_templates/announce.j2
@@ -35,6 +35,12 @@ The PyPI package:
https://pypi.org/project/apache-superset/
+The Change Log for the release:
+https://github.com/apache/{{ project_module }}/blob/{{ version }}/CHANGELOG.md
+
+The Updating instructions for the release:
+https://github.com/apache/{{ project_module }}/blob/{{ version }}/UPDATING.md
+
If you have any usage questions or have problems when upgrading or
find any issues with enhancements included in this release, please
don't hesitate to let us know by sending feedback to this mailing
From 07bcfa9b5f2f2c901cedd73f8326b97bf043c4de Mon Sep 17 00:00:00 2001
From: John Bodley <4567245+john-bodley@users.noreply.github.com>
Date: Wed, 22 Nov 2023 11:55:54 -0800
Subject: [PATCH 065/119] chore(command): Organize Commands according to SIP-92
(#25850)
---
superset/annotation_layers/annotations/api.py | 28 +++++------
superset/annotation_layers/api.py | 22 ++++-----
superset/charts/api.py | 30 ++++++------
superset/charts/data/api.py | 16 +++----
.../charts/data/query_context_cache_loader.py | 2 +-
superset/cli/importexport.py | 16 +++----
.../annotation_layer}/__init__.py | 0
.../annotation_layer/annotation}/__init__.py | 0
.../annotation_layer/annotation}/create.py | 6 +--
.../annotation_layer/annotation}/delete.py | 4 +-
.../annotation}/exceptions.py | 0
.../annotation_layer/annotation}/update.py | 6 +--
.../annotation_layer}/create.py | 4 +-
.../annotation_layer}/delete.py | 4 +-
.../annotation_layer}/exceptions.py | 0
.../annotation_layer}/update.py | 4 +-
.../commands => commands/chart}/__init__.py | 0
.../commands => commands/chart}/create.py | 4 +-
.../chart/data}/__init__.py | 0
.../chart/data}/create_async_job_command.py | 0
.../chart/data}/get_data_command.py | 4 +-
.../commands => commands/chart}/delete.py | 4 +-
.../commands => commands/chart}/exceptions.py | 0
.../commands => commands/chart}/export.py | 4 +-
.../chart/importers}/__init__.py | 0
.../chart}/importers/dispatcher.py | 2 +-
.../chart}/importers/v1/__init__.py | 8 ++--
.../chart}/importers/v1/utils.py | 0
.../commands => commands/chart}/update.py | 4 +-
.../chart}/warm_up_cache.py | 6 +--
.../commands => commands/css}/__init__.py | 0
.../commands => commands/css}/delete.py | 2 +-
.../commands => commands/css}/exceptions.py | 0
.../dashboard}/__init__.py | 0
.../commands => commands/dashboard}/create.py | 8 ++--
.../commands => commands/dashboard}/delete.py | 8 ++--
.../dashboard/embedded}/__init__.py | 0
.../dashboard/embedded}/exceptions.py | 0
.../dashboard}/exceptions.py | 0
.../commands => commands/dashboard}/export.py | 8 ++--
.../dashboard/filter_set}/__init__.py | 0
.../dashboard/filter_set}/base.py | 8 ++--
.../dashboard/filter_set}/create.py | 6 +--
.../dashboard/filter_set}/delete.py | 8 ++--
.../dashboard/filter_set}/exceptions.py | 0
.../dashboard/filter_set}/update.py | 6 +--
.../dashboard/filter_state}/__init__.py | 0
.../dashboard/filter_state}/create.py | 8 ++--
.../dashboard/filter_state}/delete.py | 10 ++--
.../dashboard/filter_state}/get.py | 6 +--
.../dashboard/filter_state}/update.py | 10 ++--
.../dashboard/filter_state}/utils.py | 6 +--
.../dashboard/importers}/__init__.py | 0
.../dashboard}/importers/dispatcher.py | 2 +-
.../dashboard}/importers/v0.py | 2 +-
.../dashboard}/importers/v1/__init__.py | 14 +++---
.../dashboard}/importers/v1/utils.py | 0
.../dashboard/permalink}/__init__.py | 0
.../dashboard/permalink}/base.py | 0
.../dashboard/permalink}/create.py | 4 +-
.../dashboard/permalink}/get.py | 6 +--
.../commands => commands/dashboard}/update.py | 8 ++--
.../database}/__init__.py | 0
.../commands => commands/database}/create.py | 12 ++---
.../commands => commands/database}/delete.py | 8 ++--
.../database}/exceptions.py | 0
.../commands => commands/database}/export.py | 2 +-
.../database/importers}/__init__.py | 0
.../database}/importers/dispatcher.py | 2 +-
.../database}/importers/v1/__init__.py | 6 +--
.../database}/importers/v1/utils.py | 0
.../database/ssh_tunnel}/__init__.py | 0
.../database/ssh_tunnel}/create.py | 6 +--
.../database/ssh_tunnel}/delete.py | 6 +--
.../database/ssh_tunnel}/exceptions.py | 0
.../database/ssh_tunnel}/update.py | 6 +--
.../commands => commands/database}/tables.py | 6 +--
.../database}/test_connection.py | 8 ++--
.../commands => commands/database}/update.py | 12 ++---
.../database}/validate.py | 4 +-
.../database}/validate_sql.py | 4 +-
.../commands => commands/dataset}/__init__.py | 0
.../dataset/columns}/__init__.py | 0
.../dataset/columns}/delete.py | 8 ++--
.../dataset/columns}/exceptions.py | 0
.../commands => commands/dataset}/create.py | 6 +--
.../commands => commands/dataset}/delete.py | 8 ++--
.../dataset}/duplicate.py | 10 ++--
.../dataset}/exceptions.py | 0
.../commands => commands/dataset}/export.py | 2 +-
.../dataset/importers}/__init__.py | 0
.../dataset}/importers/dispatcher.py | 2 +-
.../dataset}/importers/v0.py | 4 +-
.../dataset}/importers/v1/__init__.py | 6 +--
.../dataset}/importers/v1/utils.py | 2 +-
.../dataset/metrics}/__init__.py | 0
.../dataset/metrics}/delete.py | 8 ++--
.../dataset/metrics}/exceptions.py | 0
.../commands => commands/dataset}/refresh.py | 6 +--
.../commands => commands/dataset}/update.py | 8 ++--
.../dataset}/warm_up_cache.py | 8 ++--
.../commands => commands/explore}/__init__.py | 0
.../explore/form_data}/__init__.py | 0
.../explore/form_data}/create.py | 8 ++--
.../explore/form_data}/delete.py | 10 ++--
.../explore/form_data}/get.py | 8 ++--
.../explore/form_data}/parameters.py | 0
.../explore/form_data}/state.py | 0
.../explore/form_data}/update.py | 12 ++---
.../explore/form_data}/utils.py | 8 ++--
.../commands => commands/explore}/get.py | 12 ++---
.../explore}/parameters.py | 0
.../explore/permalink}/__init__.py | 0
.../explore/permalink}/base.py | 0
.../explore/permalink}/create.py | 4 +-
.../explore/permalink}/get.py | 6 +--
superset/commands/export/assets.py | 10 ++--
superset/commands/importers/v1/assets.py | 20 ++++----
superset/commands/importers/v1/examples.py | 22 ++++-----
.../key_value}/__init__.py | 0
.../commands => commands/key_value}/create.py | 0
.../commands => commands/key_value}/delete.py | 0
.../key_value}/delete_expired.py | 0
.../commands => commands/key_value}/get.py | 0
.../commands => commands/key_value}/update.py | 0
.../commands => commands/key_value}/upsert.py | 2 +-
.../commands => commands/query}/__init__.py | 0
.../commands => commands/query}/delete.py | 8 ++--
.../commands => commands/query}/exceptions.py | 0
.../commands => commands/query}/export.py | 2 +-
.../query}/importers/__init__.py | 0
.../query}/importers/dispatcher.py | 2 +-
.../query}/importers/v1/__init__.py | 8 ++--
.../query}/importers/v1/utils.py | 0
.../commands => commands/report}/__init__.py | 0
.../commands => commands/report}/alert.py | 2 +-
.../commands => commands/report}/base.py | 6 +--
.../commands => commands/report}/create.py | 10 ++--
.../commands => commands/report}/delete.py | 8 ++--
.../report}/exceptions.py | 0
.../commands => commands/report}/execute.py | 24 +++++-----
.../commands => commands/report}/log_prune.py | 2 +-
.../commands => commands/report}/update.py | 12 ++---
.../security}/__init__.py | 0
.../commands => commands/security}/create.py | 0
.../commands => commands/security}/delete.py | 8 ++--
.../security}/exceptions.py | 0
.../commands => commands/security}/update.py | 2 +-
.../commands => commands/sql_lab}/__init__.py | 0
.../commands => commands/sql_lab}/estimate.py | 0
.../commands => commands/sql_lab}/execute.py | 0
.../commands => commands/sql_lab}/export.py | 0
.../commands => commands/sql_lab}/results.py | 0
.../commands => commands/tag}/__init__.py | 0
.../{tags/commands => commands/tag}/create.py | 4 +-
.../{tags/commands => commands/tag}/delete.py | 8 ++--
.../commands => commands/tag}/exceptions.py | 0
.../{tags/commands => commands/tag}/update.py | 4 +-
.../{tags/commands => commands/tag}/utils.py | 0
.../temporary_cache}/__init__.py | 0
.../temporary_cache}/create.py | 4 +-
.../temporary_cache}/delete.py | 4 +-
.../temporary_cache}/entry.py | 0
.../temporary_cache}/exceptions.py | 0
.../temporary_cache}/get.py | 4 +-
.../temporary_cache}/parameters.py | 0
.../temporary_cache}/update.py | 4 +-
superset/common/query_context_processor.py | 4 +-
superset/connectors/sqla/models.py | 2 +-
superset/css_templates/api.py | 6 +--
.../{annotation.py => annotation_layer.py} | 0
superset/daos/dashboard.py | 4 +-
superset/daos/tag.py | 8 ++--
superset/dashboards/api.py | 20 ++++----
superset/dashboards/filter_sets/api.py | 14 +++---
superset/dashboards/filter_state/api.py | 8 ++--
superset/dashboards/permalink/api.py | 10 ++--
superset/databases/api.py | 46 +++++++++----------
superset/databases/schemas.py | 6 +--
superset/databases/utils.py | 2 +-
superset/datasets/api.py | 32 ++++++-------
superset/datasets/columns/api.py | 8 ++--
superset/datasets/metrics/api.py | 8 ++--
superset/embedded/api.py | 6 +--
superset/explore/api.py | 14 +++---
superset/explore/form_data/api.py | 18 ++++----
superset/explore/permalink/api.py | 10 ++--
superset/explore/utils.py | 10 ++--
superset/extensions/metastore_cache.py | 10 ++--
superset/jinja_context.py | 2 +-
superset/key_value/shared_entries.py | 4 +-
superset/models/core.py | 2 +-
superset/queries/saved_queries/api.py | 16 +++----
superset/reports/api.py | 16 +++----
superset/row_level_security/api.py | 8 ++--
superset/security/api.py | 2 +-
superset/security/manager.py | 4 +-
superset/sqllab/api.py | 8 ++--
superset/sqllab/query_render.py | 2 +-
superset/sqllab/validators.py | 2 +-
superset/tags/api.py | 16 +++----
superset/tasks/async_queries.py | 2 +-
superset/tasks/scheduler.py | 6 +--
superset/temporary_cache/api.py | 8 ++--
superset/utils/date_parser.py | 2 +-
superset/views/api.py | 2 +-
superset/views/core.py | 18 ++++----
superset/views/database/validators.py | 2 +-
superset/views/datasource/utils.py | 2 +-
superset/views/datasource/views.py | 8 ++--
tests/integration_tests/charts/api_tests.py | 4 +-
.../charts/commands_tests.py | 18 ++++----
.../charts/data/api_tests.py | 2 +-
tests/integration_tests/cli_tests.py | 18 ++++----
tests/integration_tests/core_tests.py | 6 +--
.../dashboards/commands_tests.py | 16 +++----
.../dashboards/filter_state/api_tests.py | 4 +-
.../dashboards/permalink/api_tests.py | 2 +-
.../security/security_rbac_tests.py | 2 +-
.../integration_tests/databases/api_tests.py | 36 +++++++--------
.../databases/commands_tests.py | 40 ++++++++--------
.../ssh_tunnel/commands/commands_tests.py | 10 ++--
tests/integration_tests/datasets/api_tests.py | 4 +-
.../datasets/commands_tests.py | 20 ++++----
tests/integration_tests/datasource_tests.py | 2 +-
tests/integration_tests/explore/api_tests.py | 2 +-
.../explore/form_data/api_tests.py | 4 +-
.../explore/form_data/commands_tests.py | 10 ++--
.../explore/permalink/commands_tests.py | 10 ++--
.../integration_tests/import_export_tests.py | 6 +--
.../importexport/commands_tests.py | 2 +-
.../key_value/commands/create_test.py | 8 ++--
.../key_value/commands/delete_test.py | 6 +--
.../key_value/commands/get_test.py | 10 ++--
.../key_value/commands/update_test.py | 6 +--
.../key_value/commands/upsert_test.py | 6 +--
.../queries/saved_queries/commands_tests.py | 8 ++--
.../integration_tests/reports/alert_tests.py | 22 ++++-----
.../commands/create_dashboard_report_tests.py | 4 +-
.../execute_dashboard_report_tests.py | 14 +++---
.../reports/commands_tests.py | 30 ++++++------
.../reports/scheduler_tests.py | 10 ++--
tests/integration_tests/sql_lab/api_tests.py | 12 ++---
.../sql_lab/commands_tests.py | 22 ++++-----
.../integration_tests/tags/commands_tests.py | 16 +++----
.../tasks/async_queries_tests.py | 4 +-
tests/integration_tests/utils_tests.py | 2 +-
.../commands/importers/v1/import_test.py | 6 +--
.../commands/importers/v1/utils_test.py | 2 +-
.../commands/importers/v1/import_test.py | 6 +--
.../commands/importers/v1/utils_test.py | 4 +-
tests/unit_tests/databases/api_test.py | 6 +--
.../commands/importers/v1/import_test.py | 8 ++--
.../commands/test_connection_test.py | 2 +-
.../ssh_tunnel/commands/create_test.py | 6 +--
.../ssh_tunnel/commands/delete_test.py | 4 +-
.../ssh_tunnel/commands/update_test.py | 6 +--
.../datasets/commands/export_test.py | 2 +-
.../commands/importers/v1/import_test.py | 18 ++++----
tests/unit_tests/explore/utils_test.py | 10 ++--
tests/unit_tests/jinja_context_test.py | 2 +-
tests/unit_tests/tags/commands/create_test.py | 4 +-
tests/unit_tests/tags/commands/update_test.py | 12 ++---
tests/unit_tests/tasks/test_async_queries.py | 2 +-
tests/unit_tests/utils/date_parser_tests.py | 2 +-
265 files changed, 786 insertions(+), 808 deletions(-)
rename superset/{annotation_layers/annotations/commands => commands/annotation_layer}/__init__.py (100%)
rename superset/{annotation_layers/commands => commands/annotation_layer/annotation}/__init__.py (100%)
rename superset/{annotation_layers/annotations/commands => commands/annotation_layer/annotation}/create.py (92%)
rename superset/{annotation_layers/annotations/commands => commands/annotation_layer/annotation}/delete.py (93%)
rename superset/{annotation_layers/annotations/commands => commands/annotation_layer/annotation}/exceptions.py (100%)
rename superset/{annotation_layers/annotations/commands => commands/annotation_layer/annotation}/update.py (93%)
rename superset/{annotation_layers/commands => commands/annotation_layer}/create.py (94%)
rename superset/{annotation_layers/commands => commands/annotation_layer}/delete.py (94%)
rename superset/{annotation_layers/commands => commands/annotation_layer}/exceptions.py (100%)
rename superset/{annotation_layers/commands => commands/annotation_layer}/update.py (95%)
rename superset/{charts/commands => commands/chart}/__init__.py (100%)
rename superset/{charts/commands => commands/chart}/create.py (98%)
rename superset/{charts/commands/importers => commands/chart/data}/__init__.py (100%)
rename superset/{charts/data/commands => commands/chart/data}/create_async_job_command.py (100%)
rename superset/{charts/data/commands => commands/chart/data}/get_data_command.py (97%)
rename superset/{charts/commands => commands/chart}/delete.py (98%)
rename superset/{charts/commands => commands/chart}/exceptions.py (100%)
rename superset/{charts/commands => commands/chart}/export.py (95%)
rename superset/{charts/data/commands => commands/chart/importers}/__init__.py (100%)
rename superset/{charts/commands => commands/chart}/importers/dispatcher.py (98%)
rename superset/{charts/commands => commands/chart}/importers/v1/__init__.py (93%)
rename superset/{charts/commands => commands/chart}/importers/v1/utils.py (100%)
rename superset/{charts/commands => commands/chart}/update.py (98%)
rename superset/{charts/commands => commands/chart}/warm_up_cache.py (96%)
rename superset/{css_templates/commands => commands/css}/__init__.py (100%)
rename superset/{css_templates/commands => commands/css}/delete.py (97%)
rename superset/{css_templates/commands => commands/css}/exceptions.py (100%)
rename superset/{dashboards/commands => commands/dashboard}/__init__.py (100%)
rename superset/{dashboards/commands => commands/dashboard}/create.py (98%)
rename superset/{dashboards/commands => commands/dashboard}/delete.py (98%)
rename superset/{dashboards/commands/importers => commands/dashboard/embedded}/__init__.py (100%)
rename superset/{embedded_dashboard/commands => commands/dashboard/embedded}/exceptions.py (100%)
rename superset/{dashboards/commands => commands/dashboard}/exceptions.py (100%)
rename superset/{dashboards/commands => commands/dashboard}/export.py (95%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/__init__.py (100%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/base.py (96%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/create.py (95%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/delete.py (93%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/exceptions.py (100%)
rename superset/{dashboards/filter_sets/commands => commands/dashboard/filter_set}/update.py (91%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/__init__.py (100%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/create.py (87%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/delete.py (84%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/get.py (89%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/update.py (87%)
rename superset/{dashboards/filter_state/commands => commands/dashboard/filter_state}/utils.py (91%)
rename superset/{dashboards/permalink/commands => commands/dashboard/importers}/__init__.py (100%)
rename superset/{dashboards/commands => commands/dashboard}/importers/dispatcher.py (97%)
rename superset/{dashboards/commands => commands/dashboard}/importers/v0.py (99%)
rename superset/{dashboards/commands => commands/dashboard}/importers/v1/__init__.py (94%)
rename superset/{dashboards/commands => commands/dashboard}/importers/v1/utils.py (100%)
rename superset/{databases/commands => commands/dashboard/permalink}/__init__.py (100%)
rename superset/{dashboards/permalink/commands => commands/dashboard/permalink}/base.py (100%)
rename superset/{dashboards/permalink/commands => commands/dashboard/permalink}/create.py (94%)
rename superset/{dashboards/permalink/commands => commands/dashboard/permalink}/get.py (91%)
rename superset/{dashboards/commands => commands/dashboard}/update.py (98%)
rename superset/{databases/commands/importers => commands/database}/__init__.py (100%)
rename superset/{databases/commands => commands/database}/create.py (95%)
rename superset/{databases/commands => commands/database}/delete.py (97%)
rename superset/{databases/commands => commands/database}/exceptions.py (100%)
rename superset/{databases/commands => commands/database}/export.py (98%)
rename superset/{databases/ssh_tunnel/commands => commands/database/importers}/__init__.py (100%)
rename superset/{databases/commands => commands/database}/importers/dispatcher.py (97%)
rename superset/{databases/commands => commands/database}/importers/v1/__init__.py (91%)
rename superset/{databases/commands => commands/database}/importers/v1/utils.py (100%)
rename superset/{datasets/columns/commands => commands/database/ssh_tunnel}/__init__.py (100%)
rename superset/{databases/ssh_tunnel/commands => commands/database/ssh_tunnel}/create.py (98%)
rename superset/{databases/ssh_tunnel/commands => commands/database/ssh_tunnel}/delete.py (96%)
rename superset/{databases/ssh_tunnel/commands => commands/database/ssh_tunnel}/exceptions.py (100%)
rename superset/{databases/ssh_tunnel/commands => commands/database/ssh_tunnel}/update.py (97%)
rename superset/{databases/commands => commands/database}/tables.py (98%)
rename superset/{databases/commands => commands/database}/test_connection.py (98%)
rename superset/{databases/commands => commands/database}/update.py (96%)
rename superset/{databases/commands => commands/database}/validate.py (98%)
rename superset/{databases/commands => commands/database}/validate_sql.py (98%)
rename superset/{datasets/commands => commands/dataset}/__init__.py (100%)
rename superset/{datasets/commands/importers => commands/dataset/columns}/__init__.py (100%)
rename superset/{datasets/columns/commands => commands/dataset/columns}/delete.py (97%)
rename superset/{datasets/columns/commands => commands/dataset/columns}/exceptions.py (100%)
rename superset/{datasets/commands => commands/dataset}/create.py (98%)
rename superset/{datasets/commands => commands/dataset}/delete.py (97%)
rename superset/{datasets/commands => commands/dataset}/duplicate.py (99%)
rename superset/{datasets/commands => commands/dataset}/exceptions.py (100%)
rename superset/{datasets/commands => commands/dataset}/export.py (98%)
rename superset/{datasets/metrics/commands => commands/dataset/importers}/__init__.py (100%)
rename superset/{datasets/commands => commands/dataset}/importers/dispatcher.py (97%)
rename superset/{datasets/commands => commands/dataset}/importers/v0.py (98%)
rename superset/{datasets/commands => commands/dataset}/importers/v1/__init__.py (92%)
rename superset/{datasets/commands => commands/dataset}/importers/v1/utils.py (99%)
rename superset/{embedded_dashboard/commands => commands/dataset/metrics}/__init__.py (100%)
rename superset/{datasets/metrics/commands => commands/dataset/metrics}/delete.py (97%)
rename superset/{datasets/metrics/commands => commands/dataset/metrics}/exceptions.py (100%)
rename superset/{datasets/commands => commands/dataset}/refresh.py (97%)
rename superset/{datasets/commands => commands/dataset}/update.py (99%)
rename superset/{datasets/commands => commands/dataset}/warm_up_cache.py (89%)
rename superset/{explore/commands => commands/explore}/__init__.py (100%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/__init__.py (100%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/create.py (91%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/delete.py (91%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/get.py (89%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/parameters.py (100%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/state.py (100%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/update.py (93%)
rename superset/{explore/form_data/commands => commands/explore/form_data}/utils.py (90%)
rename superset/{explore/commands => commands/explore}/get.py (96%)
rename superset/{explore/commands => commands/explore}/parameters.py (100%)
rename superset/{explore/permalink/commands => commands/explore/permalink}/__init__.py (100%)
rename superset/{explore/permalink/commands => commands/explore/permalink}/base.py (100%)
rename superset/{explore/permalink/commands => commands/explore/permalink}/create.py (95%)
rename superset/{explore/permalink/commands => commands/explore/permalink}/get.py (93%)
rename superset/{key_value/commands => commands/key_value}/__init__.py (100%)
rename superset/{key_value/commands => commands/key_value}/create.py (100%)
rename superset/{key_value/commands => commands/key_value}/delete.py (100%)
rename superset/{key_value/commands => commands/key_value}/delete_expired.py (100%)
rename superset/{key_value/commands => commands/key_value}/get.py (100%)
rename superset/{key_value/commands => commands/key_value}/update.py (100%)
rename superset/{key_value/commands => commands/key_value}/upsert.py (98%)
rename superset/{queries/saved_queries/commands => commands/query}/__init__.py (100%)
rename superset/{queries/saved_queries/commands => commands/query}/delete.py (96%)
rename superset/{queries/saved_queries/commands => commands/query}/exceptions.py (100%)
rename superset/{queries/saved_queries/commands => commands/query}/export.py (97%)
rename superset/{queries/saved_queries/commands => commands/query}/importers/__init__.py (100%)
rename superset/{queries/saved_queries/commands => commands/query}/importers/dispatcher.py (97%)
rename superset/{queries/saved_queries/commands => commands/query}/importers/v1/__init__.py (91%)
rename superset/{queries/saved_queries/commands => commands/query}/importers/v1/utils.py (100%)
rename superset/{reports/commands => commands/report}/__init__.py (100%)
rename superset/{reports/commands => commands/report}/alert.py (99%)
rename superset/{reports/commands => commands/report}/base.py (98%)
rename superset/{reports/commands => commands/report}/create.py (97%)
rename superset/{reports/commands => commands/report}/delete.py (97%)
rename superset/{reports/commands => commands/report}/exceptions.py (100%)
rename superset/{reports/commands => commands/report}/execute.py (99%)
rename superset/{reports/commands => commands/report}/log_prune.py (96%)
rename superset/{reports/commands => commands/report}/update.py (97%)
rename superset/{row_level_security/commands => commands/security}/__init__.py (100%)
rename superset/{row_level_security/commands => commands/security}/create.py (100%)
rename superset/{row_level_security/commands => commands/security}/delete.py (96%)
rename superset/{row_level_security/commands => commands/security}/exceptions.py (100%)
rename superset/{row_level_security/commands => commands/security}/update.py (96%)
rename superset/{sqllab/commands => commands/sql_lab}/__init__.py (100%)
rename superset/{sqllab/commands => commands/sql_lab}/estimate.py (100%)
rename superset/{sqllab/commands => commands/sql_lab}/execute.py (100%)
rename superset/{sqllab/commands => commands/sql_lab}/export.py (100%)
rename superset/{sqllab/commands => commands/sql_lab}/results.py (100%)
rename superset/{tags/commands => commands/tag}/__init__.py (100%)
rename superset/{tags/commands => commands/tag}/create.py (96%)
rename superset/{tags/commands => commands/tag}/delete.py (97%)
rename superset/{tags/commands => commands/tag}/exceptions.py (100%)
rename superset/{tags/commands => commands/tag}/update.py (94%)
rename superset/{tags/commands => commands/tag}/utils.py (100%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/__init__.py (100%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/create.py (92%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/delete.py (92%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/entry.py (100%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/exceptions.py (100%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/get.py (92%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/parameters.py (100%)
rename superset/{temporary_cache/commands => commands/temporary_cache}/update.py (92%)
rename superset/daos/{annotation.py => annotation_layer.py} (100%)
diff --git a/superset/annotation_layers/annotations/api.py b/superset/annotation_layers/annotations/api.py
index 4c95b3c105..0be6efbfa9 100644
--- a/superset/annotation_layers/annotations/api.py
+++ b/superset/annotation_layers/annotations/api.py
@@ -24,22 +24,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
from marshmallow import ValidationError
-from superset.annotation_layers.annotations.commands.create import (
- CreateAnnotationCommand,
-)
-from superset.annotation_layers.annotations.commands.delete import (
- DeleteAnnotationCommand,
-)
-from superset.annotation_layers.annotations.commands.exceptions import (
- AnnotationCreateFailedError,
- AnnotationDeleteFailedError,
- AnnotationInvalidError,
- AnnotationNotFoundError,
- AnnotationUpdateFailedError,
-)
-from superset.annotation_layers.annotations.commands.update import (
- UpdateAnnotationCommand,
-)
from superset.annotation_layers.annotations.filters import AnnotationAllTextFilter
from superset.annotation_layers.annotations.schemas import (
AnnotationPostSchema,
@@ -47,7 +31,17 @@ from superset.annotation_layers.annotations.schemas import (
get_delete_ids_schema,
openapi_spec_methods_override,
)
-from superset.annotation_layers.commands.exceptions import AnnotationLayerNotFoundError
+from superset.commands.annotation_layer.annotation.create import CreateAnnotationCommand
+from superset.commands.annotation_layer.annotation.delete import DeleteAnnotationCommand
+from superset.commands.annotation_layer.annotation.exceptions import (
+ AnnotationCreateFailedError,
+ AnnotationDeleteFailedError,
+ AnnotationInvalidError,
+ AnnotationNotFoundError,
+ AnnotationUpdateFailedError,
+)
+from superset.commands.annotation_layer.annotation.update import UpdateAnnotationCommand
+from superset.commands.annotation_layer.exceptions import AnnotationLayerNotFoundError
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.models.annotations import Annotation
from superset.views.base_api import (
diff --git a/superset/annotation_layers/api.py b/superset/annotation_layers/api.py
index b7a3b301bc..886c151a68 100644
--- a/superset/annotation_layers/api.py
+++ b/superset/annotation_layers/api.py
@@ -23,17 +23,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
from marshmallow import ValidationError
-from superset.annotation_layers.commands.create import CreateAnnotationLayerCommand
-from superset.annotation_layers.commands.delete import DeleteAnnotationLayerCommand
-from superset.annotation_layers.commands.exceptions import (
- AnnotationLayerCreateFailedError,
- AnnotationLayerDeleteFailedError,
- AnnotationLayerDeleteIntegrityError,
- AnnotationLayerInvalidError,
- AnnotationLayerNotFoundError,
- AnnotationLayerUpdateFailedError,
-)
-from superset.annotation_layers.commands.update import UpdateAnnotationLayerCommand
from superset.annotation_layers.filters import AnnotationLayerAllTextFilter
from superset.annotation_layers.schemas import (
AnnotationLayerPostSchema,
@@ -41,6 +30,17 @@ from superset.annotation_layers.schemas import (
get_delete_ids_schema,
openapi_spec_methods_override,
)
+from superset.commands.annotation_layer.create import CreateAnnotationLayerCommand
+from superset.commands.annotation_layer.delete import DeleteAnnotationLayerCommand
+from superset.commands.annotation_layer.exceptions import (
+ AnnotationLayerCreateFailedError,
+ AnnotationLayerDeleteFailedError,
+ AnnotationLayerDeleteIntegrityError,
+ AnnotationLayerInvalidError,
+ AnnotationLayerNotFoundError,
+ AnnotationLayerUpdateFailedError,
+)
+from superset.commands.annotation_layer.update import UpdateAnnotationLayerCommand
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.extensions import event_logger
from superset.models.annotations import AnnotationLayer
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 768d330291..ea705f0aa9 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -32,21 +32,6 @@ from werkzeug.wrappers import Response as WerkzeugResponse
from werkzeug.wsgi import FileWrapper
from superset import app, is_feature_enabled, thumbnail_cache
-from superset.charts.commands.create import CreateChartCommand
-from superset.charts.commands.delete import DeleteChartCommand
-from superset.charts.commands.exceptions import (
- ChartCreateFailedError,
- ChartDeleteFailedError,
- ChartForbiddenError,
- ChartInvalidError,
- ChartNotFoundError,
- ChartUpdateFailedError,
- DashboardsForbiddenError,
-)
-from superset.charts.commands.export import ExportChartsCommand
-from superset.charts.commands.importers.dispatcher import ImportChartsCommand
-from superset.charts.commands.update import UpdateChartCommand
-from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
from superset.charts.filters import (
ChartAllTextFilter,
ChartCertifiedFilter,
@@ -69,6 +54,21 @@ from superset.charts.schemas import (
screenshot_query_schema,
thumbnail_query_schema,
)
+from superset.commands.chart.create import CreateChartCommand
+from superset.commands.chart.delete import DeleteChartCommand
+from superset.commands.chart.exceptions import (
+ ChartCreateFailedError,
+ ChartDeleteFailedError,
+ ChartForbiddenError,
+ ChartInvalidError,
+ ChartNotFoundError,
+ ChartUpdateFailedError,
+ DashboardsForbiddenError,
+)
+from superset.commands.chart.export import ExportChartsCommand
+from superset.commands.chart.importers.dispatcher import ImportChartsCommand
+from superset.commands.chart.update import UpdateChartCommand
+from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
from superset.commands.exceptions import CommandException
from superset.commands.importers.exceptions import (
IncorrectFormatError,
diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py
index 885b6691b3..a62e6a2407 100644
--- a/superset/charts/data/api.py
+++ b/superset/charts/data/api.py
@@ -30,17 +30,17 @@ 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.commands.exceptions import (
- ChartDataCacheLoadError,
- ChartDataQueryFailedError,
-)
-from superset.charts.data.commands.create_async_job_command import (
- CreateAsyncChartDataJobCommand,
-)
-from superset.charts.data.commands.get_data_command import ChartDataCommand
from superset.charts.data.query_context_cache_loader import QueryContextCacheLoader
from superset.charts.post_processing import apply_post_process
from superset.charts.schemas import ChartDataQueryContextSchema
+from superset.commands.chart.data.create_async_job_command import (
+ CreateAsyncChartDataJobCommand,
+)
+from superset.commands.chart.data.get_data_command import ChartDataCommand
+from superset.commands.chart.exceptions import (
+ ChartDataCacheLoadError,
+ ChartDataQueryFailedError,
+)
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
from superset.connectors.sqla.models import BaseDatasource
from superset.daos.exceptions import DatasourceNotFound
diff --git a/superset/charts/data/query_context_cache_loader.py b/superset/charts/data/query_context_cache_loader.py
index 97fa733a3e..1bdabd33f4 100644
--- a/superset/charts/data/query_context_cache_loader.py
+++ b/superset/charts/data/query_context_cache_loader.py
@@ -17,7 +17,7 @@
from typing import Any
from superset import cache
-from superset.charts.commands.exceptions import ChartDataCacheLoadError
+from superset.commands.chart.exceptions import ChartDataCacheLoadError
class QueryContextCacheLoader: # pylint: disable=too-few-public-methods
diff --git a/superset/cli/importexport.py b/superset/cli/importexport.py
index 5dde06d01a..0d76e535e8 100755
--- a/superset/cli/importexport.py
+++ b/superset/cli/importexport.py
@@ -72,7 +72,7 @@ if feature_flags.get("VERSIONED_EXPORT"):
def export_dashboards(dashboard_file: Optional[str] = None) -> None:
"""Export dashboards to ZIP file"""
# pylint: disable=import-outside-toplevel
- from superset.dashboards.commands.export import ExportDashboardsCommand
+ from superset.commands.dashboard.export import ExportDashboardsCommand
from superset.models.dashboard import Dashboard
g.user = security_manager.find_user(username="admin")
@@ -106,8 +106,8 @@ if feature_flags.get("VERSIONED_EXPORT"):
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
- from superset.datasets.commands.export import ExportDatasetsCommand
g.user = security_manager.find_user(username="admin")
@@ -144,10 +144,10 @@ if feature_flags.get("VERSIONED_EXPORT"):
def import_dashboards(path: str, username: Optional[str]) -> None:
"""Import dashboards from ZIP file"""
# pylint: disable=import-outside-toplevel
- from superset.commands.importers.v1.utils import get_contents_from_bundle
- from superset.dashboards.commands.importers.dispatcher import (
+ 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)
@@ -176,10 +176,8 @@ if feature_flags.get("VERSIONED_EXPORT"):
def import_datasources(path: str) -> 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
- from superset.datasets.commands.importers.dispatcher import (
- ImportDatasetsCommand,
- )
if is_zipfile(path):
with ZipFile(path) as bundle:
@@ -304,7 +302,7 @@ else:
def import_dashboards(path: str, recursive: bool, username: str) -> None:
"""Import dashboards from JSON file"""
# pylint: disable=import-outside-toplevel
- from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
+ from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand
path_object = Path(path)
files: list[Path] = []
@@ -353,7 +351,7 @@ else:
def import_datasources(path: str, sync: str, recursive: bool) -> None:
"""Import datasources from YAML"""
# pylint: disable=import-outside-toplevel
- from superset.datasets.commands.importers.v0 import ImportDatasetsCommand
+ from superset.commands.dataset.importers.v0 import ImportDatasetsCommand
sync_array = sync.split(",")
sync_columns = "columns" in sync_array
diff --git a/superset/annotation_layers/annotations/commands/__init__.py b/superset/commands/annotation_layer/__init__.py
similarity index 100%
rename from superset/annotation_layers/annotations/commands/__init__.py
rename to superset/commands/annotation_layer/__init__.py
diff --git a/superset/annotation_layers/commands/__init__.py b/superset/commands/annotation_layer/annotation/__init__.py
similarity index 100%
rename from superset/annotation_layers/commands/__init__.py
rename to superset/commands/annotation_layer/annotation/__init__.py
diff --git a/superset/annotation_layers/annotations/commands/create.py b/superset/commands/annotation_layer/annotation/create.py
similarity index 92%
rename from superset/annotation_layers/annotations/commands/create.py
rename to superset/commands/annotation_layer/annotation/create.py
index 25317762da..feed6162ca 100644
--- a/superset/annotation_layers/annotations/commands/create.py
+++ b/superset/commands/annotation_layer/annotation/create.py
@@ -21,15 +21,15 @@ from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
-from superset.annotation_layers.annotations.commands.exceptions import (
+from superset.commands.annotation_layer.annotation.exceptions import (
AnnotationCreateFailedError,
AnnotationDatesValidationError,
AnnotationInvalidError,
AnnotationUniquenessValidationError,
)
-from superset.annotation_layers.commands.exceptions import AnnotationLayerNotFoundError
+from superset.commands.annotation_layer.exceptions import AnnotationLayerNotFoundError
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationDAO, AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
from superset.daos.exceptions import DAOCreateFailedError
logger = logging.getLogger(__name__)
diff --git a/superset/annotation_layers/annotations/commands/delete.py b/superset/commands/annotation_layer/annotation/delete.py
similarity index 93%
rename from superset/annotation_layers/annotations/commands/delete.py
rename to superset/commands/annotation_layer/annotation/delete.py
index 2850f8cb96..3f48ae2ceb 100644
--- a/superset/annotation_layers/annotations/commands/delete.py
+++ b/superset/commands/annotation_layer/annotation/delete.py
@@ -17,12 +17,12 @@
import logging
from typing import Optional
-from superset.annotation_layers.annotations.commands.exceptions import (
+from superset.commands.annotation_layer.annotation.exceptions import (
AnnotationDeleteFailedError,
AnnotationNotFoundError,
)
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationDAO
+from superset.daos.annotation_layer import AnnotationDAO
from superset.daos.exceptions import DAODeleteFailedError
from superset.models.annotations import Annotation
diff --git a/superset/annotation_layers/annotations/commands/exceptions.py b/superset/commands/annotation_layer/annotation/exceptions.py
similarity index 100%
rename from superset/annotation_layers/annotations/commands/exceptions.py
rename to superset/commands/annotation_layer/annotation/exceptions.py
diff --git a/superset/annotation_layers/annotations/commands/update.py b/superset/commands/annotation_layer/annotation/update.py
similarity index 93%
rename from superset/annotation_layers/annotations/commands/update.py
rename to superset/commands/annotation_layer/annotation/update.py
index 76287d24a9..9ba07fdcd6 100644
--- a/superset/annotation_layers/annotations/commands/update.py
+++ b/superset/commands/annotation_layer/annotation/update.py
@@ -21,16 +21,16 @@ from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
-from superset.annotation_layers.annotations.commands.exceptions import (
+from superset.commands.annotation_layer.annotation.exceptions import (
AnnotationDatesValidationError,
AnnotationInvalidError,
AnnotationNotFoundError,
AnnotationUniquenessValidationError,
AnnotationUpdateFailedError,
)
-from superset.annotation_layers.commands.exceptions import AnnotationLayerNotFoundError
+from superset.commands.annotation_layer.exceptions import AnnotationLayerNotFoundError
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationDAO, AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
from superset.daos.exceptions import DAOUpdateFailedError
from superset.models.annotations import Annotation
diff --git a/superset/annotation_layers/commands/create.py b/superset/commands/annotation_layer/create.py
similarity index 94%
rename from superset/annotation_layers/commands/create.py
rename to superset/commands/annotation_layer/create.py
index 39ce752d2a..6b87ad5703 100644
--- a/superset/annotation_layers/commands/create.py
+++ b/superset/commands/annotation_layer/create.py
@@ -20,13 +20,13 @@ from typing import Any
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
-from superset.annotation_layers.commands.exceptions import (
+from superset.commands.annotation_layer.exceptions import (
AnnotationLayerCreateFailedError,
AnnotationLayerInvalidError,
AnnotationLayerNameUniquenessValidationError,
)
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAOCreateFailedError
logger = logging.getLogger(__name__)
diff --git a/superset/annotation_layers/commands/delete.py b/superset/commands/annotation_layer/delete.py
similarity index 94%
rename from superset/annotation_layers/commands/delete.py
rename to superset/commands/annotation_layer/delete.py
index 41c727054b..a75ee42b77 100644
--- a/superset/annotation_layers/commands/delete.py
+++ b/superset/commands/annotation_layer/delete.py
@@ -17,13 +17,13 @@
import logging
from typing import Optional
-from superset.annotation_layers.commands.exceptions import (
+from superset.commands.annotation_layer.exceptions import (
AnnotationLayerDeleteFailedError,
AnnotationLayerDeleteIntegrityError,
AnnotationLayerNotFoundError,
)
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAODeleteFailedError
from superset.models.annotations import AnnotationLayer
diff --git a/superset/annotation_layers/commands/exceptions.py b/superset/commands/annotation_layer/exceptions.py
similarity index 100%
rename from superset/annotation_layers/commands/exceptions.py
rename to superset/commands/annotation_layer/exceptions.py
diff --git a/superset/annotation_layers/commands/update.py b/superset/commands/annotation_layer/update.py
similarity index 95%
rename from superset/annotation_layers/commands/update.py
rename to superset/commands/annotation_layer/update.py
index e7f6963e82..d15440882b 100644
--- a/superset/annotation_layers/commands/update.py
+++ b/superset/commands/annotation_layer/update.py
@@ -20,14 +20,14 @@ from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
-from superset.annotation_layers.commands.exceptions import (
+from superset.commands.annotation_layer.exceptions import (
AnnotationLayerInvalidError,
AnnotationLayerNameUniquenessValidationError,
AnnotationLayerNotFoundError,
AnnotationLayerUpdateFailedError,
)
from superset.commands.base import BaseCommand
-from superset.daos.annotation import AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAOUpdateFailedError
from superset.models.annotations import AnnotationLayer
diff --git a/superset/charts/commands/__init__.py b/superset/commands/chart/__init__.py
similarity index 100%
rename from superset/charts/commands/__init__.py
rename to superset/commands/chart/__init__.py
diff --git a/superset/charts/commands/create.py b/superset/commands/chart/create.py
similarity index 98%
rename from superset/charts/commands/create.py
rename to superset/commands/chart/create.py
index 876073e335..2b251029c3 100644
--- a/superset/charts/commands/create.py
+++ b/superset/commands/chart/create.py
@@ -23,13 +23,13 @@ from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from superset import security_manager
-from superset.charts.commands.exceptions import (
+from superset.commands.base import BaseCommand, CreateMixin
+from superset.commands.chart.exceptions import (
ChartCreateFailedError,
ChartInvalidError,
DashboardsForbiddenError,
DashboardsNotFoundValidationError,
)
-from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import get_datasource_by_id
from superset.daos.chart import ChartDAO
from superset.daos.dashboard import DashboardDAO
diff --git a/superset/charts/commands/importers/__init__.py b/superset/commands/chart/data/__init__.py
similarity index 100%
rename from superset/charts/commands/importers/__init__.py
rename to superset/commands/chart/data/__init__.py
diff --git a/superset/charts/data/commands/create_async_job_command.py b/superset/commands/chart/data/create_async_job_command.py
similarity index 100%
rename from superset/charts/data/commands/create_async_job_command.py
rename to superset/commands/chart/data/create_async_job_command.py
diff --git a/superset/charts/data/commands/get_data_command.py b/superset/commands/chart/data/get_data_command.py
similarity index 97%
rename from superset/charts/data/commands/get_data_command.py
rename to superset/commands/chart/data/get_data_command.py
index c791ace9de..971c343cba 100644
--- a/superset/charts/data/commands/get_data_command.py
+++ b/superset/commands/chart/data/get_data_command.py
@@ -19,11 +19,11 @@ from typing import Any
from flask_babel import gettext as _
-from superset.charts.commands.exceptions import (
+from superset.commands.base import BaseCommand
+from superset.commands.chart.exceptions import (
ChartDataCacheLoadError,
ChartDataQueryFailedError,
)
-from superset.commands.base import BaseCommand
from superset.common.query_context import QueryContext
from superset.exceptions import CacheLoadError
diff --git a/superset/charts/commands/delete.py b/superset/commands/chart/delete.py
similarity index 98%
rename from superset/charts/commands/delete.py
rename to superset/commands/chart/delete.py
index a31d22be3e..ee635f04af 100644
--- a/superset/charts/commands/delete.py
+++ b/superset/commands/chart/delete.py
@@ -20,13 +20,13 @@ from typing import Optional
from flask_babel import lazy_gettext as _
from superset import security_manager
-from superset.charts.commands.exceptions import (
+from superset.commands.base import BaseCommand
+from superset.commands.chart.exceptions import (
ChartDeleteFailedError,
ChartDeleteFailedReportsExistError,
ChartForbiddenError,
ChartNotFoundError,
)
-from superset.commands.base import BaseCommand
from superset.daos.chart import ChartDAO
from superset.daos.exceptions import DAODeleteFailedError
from superset.daos.report import ReportScheduleDAO
diff --git a/superset/charts/commands/exceptions.py b/superset/commands/chart/exceptions.py
similarity index 100%
rename from superset/charts/commands/exceptions.py
rename to superset/commands/chart/exceptions.py
diff --git a/superset/charts/commands/export.py b/superset/commands/chart/export.py
similarity index 95%
rename from superset/charts/commands/export.py
rename to superset/commands/chart/export.py
index c942aa96c9..fcb721c703 100644
--- a/superset/charts/commands/export.py
+++ b/superset/commands/chart/export.py
@@ -22,9 +22,9 @@ from collections.abc import Iterator
import yaml
-from superset.charts.commands.exceptions import ChartNotFoundError
+from superset.commands.chart.exceptions import ChartNotFoundError
from superset.daos.chart import ChartDAO
-from superset.datasets.commands.export import ExportDatasetsCommand
+from superset.commands.dataset.export import ExportDatasetsCommand
from superset.commands.export.models import ExportModelsCommand
from superset.models.slice import Slice
from superset.utils.dict_import_export import EXPORT_VERSION
diff --git a/superset/charts/data/commands/__init__.py b/superset/commands/chart/importers/__init__.py
similarity index 100%
rename from superset/charts/data/commands/__init__.py
rename to superset/commands/chart/importers/__init__.py
diff --git a/superset/charts/commands/importers/dispatcher.py b/superset/commands/chart/importers/dispatcher.py
similarity index 98%
rename from superset/charts/commands/importers/dispatcher.py
rename to superset/commands/chart/importers/dispatcher.py
index fb5007a50c..6d2d31ccf4 100644
--- a/superset/charts/commands/importers/dispatcher.py
+++ b/superset/commands/chart/importers/dispatcher.py
@@ -20,8 +20,8 @@ from typing import Any
from marshmallow.exceptions import ValidationError
-from superset.charts.commands.importers import v1
from superset.commands.base import BaseCommand
+from superset.commands.chart.importers import v1
from superset.commands.exceptions import CommandInvalidError
from superset.commands.importers.exceptions import IncorrectVersionError
diff --git a/superset/charts/commands/importers/v1/__init__.py b/superset/commands/chart/importers/v1/__init__.py
similarity index 93%
rename from superset/charts/commands/importers/v1/__init__.py
rename to superset/commands/chart/importers/v1/__init__.py
index 043018fa3b..783f300c07 100644
--- a/superset/charts/commands/importers/v1/__init__.py
+++ b/superset/commands/chart/importers/v1/__init__.py
@@ -20,15 +20,15 @@ from typing import Any
from marshmallow import Schema
from sqlalchemy.orm import Session
-from superset.charts.commands.exceptions import ChartImportError
-from superset.charts.commands.importers.v1.utils import import_chart
from superset.charts.schemas import ImportV1ChartSchema
+from superset.commands.chart.exceptions import ChartImportError
+from superset.commands.chart.importers.v1.utils import import_chart
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.importers.v1 import ImportModelsCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.chart import ChartDAO
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
diff --git a/superset/charts/commands/importers/v1/utils.py b/superset/commands/chart/importers/v1/utils.py
similarity index 100%
rename from superset/charts/commands/importers/v1/utils.py
rename to superset/commands/chart/importers/v1/utils.py
diff --git a/superset/charts/commands/update.py b/superset/commands/chart/update.py
similarity index 98%
rename from superset/charts/commands/update.py
rename to superset/commands/chart/update.py
index 32fd49e7cd..40b36ebcc5 100644
--- a/superset/charts/commands/update.py
+++ b/superset/commands/chart/update.py
@@ -23,7 +23,8 @@ from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from superset import security_manager
-from superset.charts.commands.exceptions import (
+from superset.commands.base import BaseCommand, UpdateMixin
+from superset.commands.chart.exceptions import (
ChartForbiddenError,
ChartInvalidError,
ChartNotFoundError,
@@ -31,7 +32,6 @@ from superset.charts.commands.exceptions import (
DashboardsNotFoundValidationError,
DatasourceTypeUpdateRequiredValidationError,
)
-from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import get_datasource_by_id
from superset.daos.chart import ChartDAO
from superset.daos.dashboard import DashboardDAO
diff --git a/superset/charts/commands/warm_up_cache.py b/superset/commands/chart/warm_up_cache.py
similarity index 96%
rename from superset/charts/commands/warm_up_cache.py
rename to superset/commands/chart/warm_up_cache.py
index a684ee5e77..2e5c0ac3a3 100644
--- a/superset/charts/commands/warm_up_cache.py
+++ b/superset/commands/chart/warm_up_cache.py
@@ -21,12 +21,12 @@ from typing import Any, Optional, Union
import simplejson as json
from flask import g
-from superset.charts.commands.exceptions import (
+from superset.commands.base import BaseCommand
+from superset.commands.chart.data.get_data_command import ChartDataCommand
+from superset.commands.chart.exceptions import (
ChartInvalidError,
WarmUpCacheChartNotFoundError,
)
-from superset.charts.data.commands.get_data_command import ChartDataCommand
-from superset.commands.base import BaseCommand
from superset.extensions import db
from superset.models.slice import Slice
from superset.utils.core import error_msg_from_exception
diff --git a/superset/css_templates/commands/__init__.py b/superset/commands/css/__init__.py
similarity index 100%
rename from superset/css_templates/commands/__init__.py
rename to superset/commands/css/__init__.py
diff --git a/superset/css_templates/commands/delete.py b/superset/commands/css/delete.py
similarity index 97%
rename from superset/css_templates/commands/delete.py
rename to superset/commands/css/delete.py
index 123658cb45..b8362f6b46 100644
--- a/superset/css_templates/commands/delete.py
+++ b/superset/commands/css/delete.py
@@ -18,7 +18,7 @@ import logging
from typing import Optional
from superset.commands.base import BaseCommand
-from superset.css_templates.commands.exceptions import (
+from superset.commands.css.exceptions import (
CssTemplateDeleteFailedError,
CssTemplateNotFoundError,
)
diff --git a/superset/css_templates/commands/exceptions.py b/superset/commands/css/exceptions.py
similarity index 100%
rename from superset/css_templates/commands/exceptions.py
rename to superset/commands/css/exceptions.py
diff --git a/superset/dashboards/commands/__init__.py b/superset/commands/dashboard/__init__.py
similarity index 100%
rename from superset/dashboards/commands/__init__.py
rename to superset/commands/dashboard/__init__.py
diff --git a/superset/dashboards/commands/create.py b/superset/commands/dashboard/create.py
similarity index 98%
rename from superset/dashboards/commands/create.py
rename to superset/commands/dashboard/create.py
index 4b5cd5fb04..1745391238 100644
--- a/superset/dashboards/commands/create.py
+++ b/superset/commands/dashboard/create.py
@@ -21,14 +21,14 @@ from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from superset.commands.base import BaseCommand, CreateMixin
-from superset.commands.utils import populate_roles
-from superset.daos.dashboard import DashboardDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardCreateFailedError,
DashboardInvalidError,
DashboardSlugExistsValidationError,
)
+from superset.commands.utils import populate_roles
+from superset.daos.dashboard import DashboardDAO
+from superset.daos.exceptions import DAOCreateFailedError
logger = logging.getLogger(__name__)
diff --git a/superset/dashboards/commands/delete.py b/superset/commands/dashboard/delete.py
similarity index 98%
rename from superset/dashboards/commands/delete.py
rename to superset/commands/dashboard/delete.py
index 7111758bb8..13ffcb443c 100644
--- a/superset/dashboards/commands/delete.py
+++ b/superset/commands/dashboard/delete.py
@@ -21,15 +21,15 @@ from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.daos.dashboard import DashboardDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardDeleteFailedError,
DashboardDeleteFailedReportsExistError,
DashboardForbiddenError,
DashboardNotFoundError,
)
+from superset.daos.dashboard import DashboardDAO
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.report import ReportScheduleDAO
from superset.exceptions import SupersetSecurityException
from superset.models.dashboard import Dashboard
diff --git a/superset/dashboards/commands/importers/__init__.py b/superset/commands/dashboard/embedded/__init__.py
similarity index 100%
rename from superset/dashboards/commands/importers/__init__.py
rename to superset/commands/dashboard/embedded/__init__.py
diff --git a/superset/embedded_dashboard/commands/exceptions.py b/superset/commands/dashboard/embedded/exceptions.py
similarity index 100%
rename from superset/embedded_dashboard/commands/exceptions.py
rename to superset/commands/dashboard/embedded/exceptions.py
diff --git a/superset/dashboards/commands/exceptions.py b/superset/commands/dashboard/exceptions.py
similarity index 100%
rename from superset/dashboards/commands/exceptions.py
rename to superset/commands/dashboard/exceptions.py
diff --git a/superset/dashboards/commands/export.py b/superset/commands/dashboard/export.py
similarity index 95%
rename from superset/dashboards/commands/export.py
rename to superset/commands/dashboard/export.py
index 4e25e5c1fc..fd06c60fa0 100644
--- a/superset/dashboards/commands/export.py
+++ b/superset/commands/dashboard/export.py
@@ -25,12 +25,12 @@ from collections.abc import Iterator
import yaml
-from superset.charts.commands.export import ExportChartsCommand
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.commands.importers.v1.utils import find_chart_uuids
+from superset.commands.chart.export import ExportChartsCommand
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.importers.v1.utils import find_chart_uuids
from superset.daos.dashboard import DashboardDAO
from superset.commands.export.models import ExportModelsCommand
-from superset.datasets.commands.export import ExportDatasetsCommand
+from superset.commands.dataset.export import ExportDatasetsCommand
from superset.daos.dataset import DatasetDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
diff --git a/superset/dashboards/filter_sets/commands/__init__.py b/superset/commands/dashboard/filter_set/__init__.py
similarity index 100%
rename from superset/dashboards/filter_sets/commands/__init__.py
rename to superset/commands/dashboard/filter_set/__init__.py
diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/commands/dashboard/filter_set/base.py
similarity index 96%
rename from superset/dashboards/filter_sets/commands/base.py
rename to superset/commands/dashboard/filter_set/base.py
index 8c53e8a818..24abe2509a 100644
--- a/superset/dashboards/filter_sets/commands/base.py
+++ b/superset/commands/dashboard/filter_set/base.py
@@ -20,13 +20,13 @@ from typing import cast, Optional
from flask_appbuilder.models.sqla import Model
from superset import security_manager
-from superset.common.not_authorized_object import NotAuthorizedException
-from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.filter_sets.commands.exceptions import (
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.filter_set.exceptions import (
FilterSetForbiddenError,
FilterSetNotFoundError,
)
+from superset.common.not_authorized_object import NotAuthorizedException
+from superset.daos.dashboard import DashboardDAO
from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE
from superset.models.dashboard import Dashboard
from superset.models.filter_set import FilterSet
diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/commands/dashboard/filter_set/create.py
similarity index 95%
rename from superset/dashboards/filter_sets/commands/create.py
rename to superset/commands/dashboard/filter_set/create.py
index d254e86d3c..49edb3172e 100644
--- a/superset/dashboards/filter_sets/commands/create.py
+++ b/superset/commands/dashboard/filter_set/create.py
@@ -20,13 +20,13 @@ from typing import Any
from flask_appbuilder.models.sqla import Model
from superset import security_manager
-from superset.daos.dashboard import FilterSetDAO
-from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
-from superset.dashboards.filter_sets.commands.exceptions import (
+from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
+from superset.commands.dashboard.filter_set.exceptions import (
DashboardIdInconsistencyError,
FilterSetCreateFailedError,
UserIsNotDashboardOwnerError,
)
+from superset.daos.dashboard import FilterSetDAO
from superset.dashboards.filter_sets.consts import (
DASHBOARD_ID_FIELD,
DASHBOARD_OWNER_TYPE,
diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/commands/dashboard/filter_set/delete.py
similarity index 93%
rename from superset/dashboards/filter_sets/commands/delete.py
rename to superset/commands/dashboard/filter_set/delete.py
index 8ac2107ca5..ce2bf6fce4 100644
--- a/superset/dashboards/filter_sets/commands/delete.py
+++ b/superset/commands/dashboard/filter_set/delete.py
@@ -16,14 +16,14 @@
# under the License.
import logging
-from superset.daos.dashboard import FilterSetDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
-from superset.dashboards.filter_sets.commands.exceptions import (
+from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
+from superset.commands.dashboard.filter_set.exceptions import (
FilterSetDeleteFailedError,
FilterSetForbiddenError,
FilterSetNotFoundError,
)
+from superset.daos.dashboard import FilterSetDAO
+from superset.daos.exceptions import DAODeleteFailedError
logger = logging.getLogger(__name__)
diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/commands/dashboard/filter_set/exceptions.py
similarity index 100%
rename from superset/dashboards/filter_sets/commands/exceptions.py
rename to superset/commands/dashboard/filter_set/exceptions.py
diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/commands/dashboard/filter_set/update.py
similarity index 91%
rename from superset/dashboards/filter_sets/commands/update.py
rename to superset/commands/dashboard/filter_set/update.py
index a63c8d46f2..5ce9f1fea6 100644
--- a/superset/dashboards/filter_sets/commands/update.py
+++ b/superset/commands/dashboard/filter_set/update.py
@@ -19,12 +19,10 @@ from typing import Any
from flask_appbuilder.models.sqla import Model
+from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
+from superset.commands.dashboard.filter_set.exceptions import FilterSetUpdateFailedError
from superset.daos.dashboard import FilterSetDAO
from superset.daos.exceptions import DAOUpdateFailedError
-from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
-from superset.dashboards.filter_sets.commands.exceptions import (
- FilterSetUpdateFailedError,
-)
from superset.dashboards.filter_sets.consts import OWNER_ID_FIELD, OWNER_TYPE_FIELD
logger = logging.getLogger(__name__)
diff --git a/superset/dashboards/filter_state/commands/__init__.py b/superset/commands/dashboard/filter_state/__init__.py
similarity index 100%
rename from superset/dashboards/filter_state/commands/__init__.py
rename to superset/commands/dashboard/filter_state/__init__.py
diff --git a/superset/dashboards/filter_state/commands/create.py b/superset/commands/dashboard/filter_state/create.py
similarity index 87%
rename from superset/dashboards/filter_state/commands/create.py
rename to superset/commands/dashboard/filter_state/create.py
index 48b5e4f5c2..1f105ac5c2 100644
--- a/superset/dashboards/filter_state/commands/create.py
+++ b/superset/commands/dashboard/filter_state/create.py
@@ -18,12 +18,12 @@ from typing import cast
from flask import session
-from superset.dashboards.filter_state.commands.utils import check_access
+from superset.commands.dashboard.filter_state.utils import check_access
+from superset.commands.temporary_cache.create import CreateTemporaryCacheCommand
+from superset.commands.temporary_cache.entry import Entry
+from superset.commands.temporary_cache.parameters import CommandParameters
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
-from superset.temporary_cache.commands.create import CreateTemporaryCacheCommand
-from superset.temporary_cache.commands.entry import Entry
-from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
diff --git a/superset/dashboards/filter_state/commands/delete.py b/superset/commands/dashboard/filter_state/delete.py
similarity index 84%
rename from superset/dashboards/filter_state/commands/delete.py
rename to superset/commands/dashboard/filter_state/delete.py
index 6086388a8c..8be7f44d98 100644
--- a/superset/dashboards/filter_state/commands/delete.py
+++ b/superset/commands/dashboard/filter_state/delete.py
@@ -16,12 +16,12 @@
# under the License.
from flask import session
-from superset.dashboards.filter_state.commands.utils import check_access
+from superset.commands.dashboard.filter_state.utils import check_access
+from superset.commands.temporary_cache.delete import DeleteTemporaryCacheCommand
+from superset.commands.temporary_cache.entry import Entry
+from superset.commands.temporary_cache.exceptions import TemporaryCacheAccessDeniedError
+from superset.commands.temporary_cache.parameters import CommandParameters
from superset.extensions import cache_manager
-from superset.temporary_cache.commands.delete import DeleteTemporaryCacheCommand
-from superset.temporary_cache.commands.entry import Entry
-from superset.temporary_cache.commands.exceptions import TemporaryCacheAccessDeniedError
-from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
diff --git a/superset/dashboards/filter_state/commands/get.py b/superset/commands/dashboard/filter_state/get.py
similarity index 89%
rename from superset/dashboards/filter_state/commands/get.py
rename to superset/commands/dashboard/filter_state/get.py
index ca7ffa9879..29104b5ee2 100644
--- a/superset/dashboards/filter_state/commands/get.py
+++ b/superset/commands/dashboard/filter_state/get.py
@@ -18,10 +18,10 @@ from typing import Optional
from flask import current_app as app
-from superset.dashboards.filter_state.commands.utils import check_access
+from superset.commands.dashboard.filter_state.utils import check_access
+from superset.commands.temporary_cache.get import GetTemporaryCacheCommand
+from superset.commands.temporary_cache.parameters import CommandParameters
from superset.extensions import cache_manager
-from superset.temporary_cache.commands.get import GetTemporaryCacheCommand
-from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key
diff --git a/superset/dashboards/filter_state/commands/update.py b/superset/commands/dashboard/filter_state/update.py
similarity index 87%
rename from superset/dashboards/filter_state/commands/update.py
rename to superset/commands/dashboard/filter_state/update.py
index c1dc529ccf..80b8c26ede 100644
--- a/superset/dashboards/filter_state/commands/update.py
+++ b/superset/commands/dashboard/filter_state/update.py
@@ -18,13 +18,13 @@ from typing import cast, Optional
from flask import session
-from superset.dashboards.filter_state.commands.utils import check_access
+from superset.commands.dashboard.filter_state.utils import check_access
+from superset.commands.temporary_cache.entry import Entry
+from superset.commands.temporary_cache.exceptions import TemporaryCacheAccessDeniedError
+from superset.commands.temporary_cache.parameters import CommandParameters
+from superset.commands.temporary_cache.update import UpdateTemporaryCacheCommand
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
-from superset.temporary_cache.commands.entry import Entry
-from superset.temporary_cache.commands.exceptions import TemporaryCacheAccessDeniedError
-from superset.temporary_cache.commands.parameters import CommandParameters
-from superset.temporary_cache.commands.update import UpdateTemporaryCacheCommand
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
diff --git a/superset/dashboards/filter_state/commands/utils.py b/superset/commands/dashboard/filter_state/utils.py
similarity index 91%
rename from superset/dashboards/filter_state/commands/utils.py
rename to superset/commands/dashboard/filter_state/utils.py
index 7e52518249..14f7eb7893 100644
--- a/superset/dashboards/filter_state/commands/utils.py
+++ b/superset/commands/dashboard/filter_state/utils.py
@@ -15,15 +15,15 @@
# specific language governing permissions and limitations
# under the License.
-from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardAccessDeniedError,
DashboardNotFoundError,
)
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheResourceNotFoundError,
)
+from superset.daos.dashboard import DashboardDAO
def check_access(resource_id: int) -> None:
diff --git a/superset/dashboards/permalink/commands/__init__.py b/superset/commands/dashboard/importers/__init__.py
similarity index 100%
rename from superset/dashboards/permalink/commands/__init__.py
rename to superset/commands/dashboard/importers/__init__.py
diff --git a/superset/dashboards/commands/importers/dispatcher.py b/superset/commands/dashboard/importers/dispatcher.py
similarity index 97%
rename from superset/dashboards/commands/importers/dispatcher.py
rename to superset/commands/dashboard/importers/dispatcher.py
index d5323b4fe4..061558cce9 100644
--- a/superset/dashboards/commands/importers/dispatcher.py
+++ b/superset/commands/dashboard/importers/dispatcher.py
@@ -21,9 +21,9 @@ from typing import Any
from marshmallow.exceptions import ValidationError
from superset.commands.base import BaseCommand
+from superset.commands.dashboard.importers import v0, v1
from superset.commands.exceptions import CommandInvalidError
from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.dashboards.commands.importers import v0, v1
logger = logging.getLogger(__name__)
diff --git a/superset/dashboards/commands/importers/v0.py b/superset/commands/dashboard/importers/v0.py
similarity index 99%
rename from superset/dashboards/commands/importers/v0.py
rename to superset/commands/dashboard/importers/v0.py
index 012dbbc5c9..4c2a18e5cc 100644
--- a/superset/dashboards/commands/importers/v0.py
+++ b/superset/commands/dashboard/importers/v0.py
@@ -26,8 +26,8 @@ from sqlalchemy.orm import make_transient, Session
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.datasets.commands.importers.v0 import import_dataset
from superset.exceptions import DashboardImportException
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
diff --git a/superset/dashboards/commands/importers/v1/__init__.py b/superset/commands/dashboard/importers/v1/__init__.py
similarity index 94%
rename from superset/dashboards/commands/importers/v1/__init__.py
rename to superset/commands/dashboard/importers/v1/__init__.py
index 30e63da4e4..2717650e9e 100644
--- a/superset/dashboards/commands/importers/v1/__init__.py
+++ b/superset/commands/dashboard/importers/v1/__init__.py
@@ -21,21 +21,21 @@ from marshmallow import Schema
from sqlalchemy.orm import Session
from sqlalchemy.sql import select
-from superset.charts.commands.importers.v1.utils import import_chart
from superset.charts.schemas import ImportV1ChartSchema
-from superset.commands.importers.v1 import ImportModelsCommand
-from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import DashboardImportError
-from superset.dashboards.commands.importers.v1.utils import (
+from superset.commands.chart.importers.v1.utils import import_chart
+from superset.commands.dashboard.exceptions import DashboardImportError
+from superset.commands.dashboard.importers.v1.utils import (
find_chart_uuids,
find_native_filter_datasets,
import_dashboard,
update_id_refs,
)
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.importers.v1.utils import import_dataset
+from superset.commands.importers.v1 import ImportModelsCommand
+from superset.daos.dashboard import DashboardDAO
from superset.dashboards.schemas import ImportV1DashboardSchema
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.dashboard import dashboard_slices
diff --git a/superset/dashboards/commands/importers/v1/utils.py b/superset/commands/dashboard/importers/v1/utils.py
similarity index 100%
rename from superset/dashboards/commands/importers/v1/utils.py
rename to superset/commands/dashboard/importers/v1/utils.py
diff --git a/superset/databases/commands/__init__.py b/superset/commands/dashboard/permalink/__init__.py
similarity index 100%
rename from superset/databases/commands/__init__.py
rename to superset/commands/dashboard/permalink/__init__.py
diff --git a/superset/dashboards/permalink/commands/base.py b/superset/commands/dashboard/permalink/base.py
similarity index 100%
rename from superset/dashboards/permalink/commands/base.py
rename to superset/commands/dashboard/permalink/base.py
diff --git a/superset/dashboards/permalink/commands/create.py b/superset/commands/dashboard/permalink/create.py
similarity index 94%
rename from superset/dashboards/permalink/commands/create.py
rename to superset/commands/dashboard/permalink/create.py
index 320003ff3d..3387d432d5 100644
--- a/superset/dashboards/permalink/commands/create.py
+++ b/superset/commands/dashboard/permalink/create.py
@@ -18,11 +18,11 @@ import logging
from sqlalchemy.exc import SQLAlchemyError
+from superset.commands.dashboard.permalink.base import BaseDashboardPermalinkCommand
+from superset.commands.key_value.upsert import UpsertKeyValueCommand
from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.permalink.commands.base import BaseDashboardPermalinkCommand
from superset.dashboards.permalink.exceptions import DashboardPermalinkCreateFailedError
from superset.dashboards.permalink.types import DashboardPermalinkState
-from superset.key_value.commands.upsert import UpsertKeyValueCommand
from superset.key_value.exceptions import KeyValueCodecEncodeException
from superset.key_value.utils import encode_permalink_key, get_deterministic_uuid
from superset.utils.core import get_user_id
diff --git a/superset/dashboards/permalink/commands/get.py b/superset/commands/dashboard/permalink/get.py
similarity index 91%
rename from superset/dashboards/permalink/commands/get.py
rename to superset/commands/dashboard/permalink/get.py
index 6b32a459a5..32efa68881 100644
--- a/superset/dashboards/permalink/commands/get.py
+++ b/superset/commands/dashboard/permalink/get.py
@@ -19,12 +19,12 @@ from typing import Optional
from sqlalchemy.exc import SQLAlchemyError
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.permalink.base import BaseDashboardPermalinkCommand
+from superset.commands.key_value.get import GetKeyValueCommand
from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.permalink.commands.base import BaseDashboardPermalinkCommand
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
from superset.dashboards.permalink.types import DashboardPermalinkValue
-from superset.key_value.commands.get import GetKeyValueCommand
from superset.key_value.exceptions import (
KeyValueCodecDecodeException,
KeyValueGetFailedError,
diff --git a/superset/dashboards/commands/update.py b/superset/commands/dashboard/update.py
similarity index 98%
rename from superset/dashboards/commands/update.py
rename to superset/commands/dashboard/update.py
index f9975c0dd2..22dcad4b2c 100644
--- a/superset/dashboards/commands/update.py
+++ b/superset/commands/dashboard/update.py
@@ -23,16 +23,16 @@ from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import BaseCommand, UpdateMixin
-from superset.commands.utils import populate_roles
-from superset.daos.dashboard import DashboardDAO
-from superset.daos.exceptions import DAOUpdateFailedError
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardForbiddenError,
DashboardInvalidError,
DashboardNotFoundError,
DashboardSlugExistsValidationError,
DashboardUpdateFailedError,
)
+from superset.commands.utils import populate_roles
+from superset.daos.dashboard import DashboardDAO
+from superset.daos.exceptions import DAOUpdateFailedError
from superset.exceptions import SupersetSecurityException
from superset.extensions import db
from superset.models.dashboard import Dashboard
diff --git a/superset/databases/commands/importers/__init__.py b/superset/commands/database/__init__.py
similarity index 100%
rename from superset/databases/commands/importers/__init__.py
rename to superset/commands/database/__init__.py
diff --git a/superset/databases/commands/create.py b/superset/commands/database/create.py
similarity index 95%
rename from superset/databases/commands/create.py
rename to superset/commands/database/create.py
index d3dfe59e5e..a012e9b2a5 100644
--- a/superset/databases/commands/create.py
+++ b/superset/commands/database/create.py
@@ -23,22 +23,22 @@ from marshmallow import ValidationError
from superset import is_feature_enabled
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseConnectionFailedError,
DatabaseCreateFailedError,
DatabaseExistsValidationError,
DatabaseInvalidError,
DatabaseRequiredFieldValidationError,
)
-from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
-from superset.databases.ssh_tunnel.commands.create import CreateSSHTunnelCommand
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelCreateFailedError,
SSHTunnelingNotEnabledError,
SSHTunnelInvalidError,
)
+from superset.commands.database.test_connection import TestConnectionDatabaseCommand
+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
diff --git a/superset/databases/commands/delete.py b/superset/commands/database/delete.py
similarity index 97%
rename from superset/databases/commands/delete.py
rename to superset/commands/database/delete.py
index 59a247a506..2db408c76e 100644
--- a/superset/databases/commands/delete.py
+++ b/superset/commands/database/delete.py
@@ -20,15 +20,15 @@ from typing import Optional
from flask_babel import lazy_gettext as _
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseDeleteDatasetsExistFailedError,
DatabaseDeleteFailedError,
DatabaseDeleteFailedReportsExistError,
DatabaseNotFoundError,
)
+from superset.daos.database import DatabaseDAO
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.report import ReportScheduleDAO
from superset.models.core import Database
logger = logging.getLogger(__name__)
diff --git a/superset/databases/commands/exceptions.py b/superset/commands/database/exceptions.py
similarity index 100%
rename from superset/databases/commands/exceptions.py
rename to superset/commands/database/exceptions.py
diff --git a/superset/databases/commands/export.py b/superset/commands/database/export.py
similarity index 98%
rename from superset/databases/commands/export.py
rename to superset/commands/database/export.py
index 71dc55a026..82c22ea801 100644
--- a/superset/databases/commands/export.py
+++ b/superset/commands/database/export.py
@@ -23,7 +23,7 @@ from collections.abc import Iterator
import yaml
-from superset.databases.commands.exceptions import DatabaseNotFoundError
+from superset.commands.database.exceptions import DatabaseNotFoundError
from superset.daos.database import DatabaseDAO
from superset.commands.export.models import ExportModelsCommand
from superset.models.core import Database
diff --git a/superset/databases/ssh_tunnel/commands/__init__.py b/superset/commands/database/importers/__init__.py
similarity index 100%
rename from superset/databases/ssh_tunnel/commands/__init__.py
rename to superset/commands/database/importers/__init__.py
diff --git a/superset/databases/commands/importers/dispatcher.py b/superset/commands/database/importers/dispatcher.py
similarity index 97%
rename from superset/databases/commands/importers/dispatcher.py
rename to superset/commands/database/importers/dispatcher.py
index 70031b09e4..bdf487a758 100644
--- a/superset/databases/commands/importers/dispatcher.py
+++ b/superset/commands/database/importers/dispatcher.py
@@ -21,9 +21,9 @@ from typing import Any
from marshmallow.exceptions import ValidationError
from superset.commands.base import BaseCommand
+from superset.commands.database.importers import v1
from superset.commands.exceptions import CommandInvalidError
from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.databases.commands.importers import v1
logger = logging.getLogger(__name__)
diff --git a/superset/databases/commands/importers/v1/__init__.py b/superset/commands/database/importers/v1/__init__.py
similarity index 91%
rename from superset/databases/commands/importers/v1/__init__.py
rename to superset/commands/database/importers/v1/__init__.py
index 585c2d54ca..73b1bca531 100644
--- a/superset/databases/commands/importers/v1/__init__.py
+++ b/superset/commands/database/importers/v1/__init__.py
@@ -20,12 +20,12 @@ from typing import Any
from marshmallow import Schema
from sqlalchemy.orm import Session
+from superset.commands.database.exceptions import DatabaseImportError
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.importers.v1 import ImportModelsCommand
from superset.daos.database import DatabaseDAO
-from superset.databases.commands.exceptions import DatabaseImportError
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
diff --git a/superset/databases/commands/importers/v1/utils.py b/superset/commands/database/importers/v1/utils.py
similarity index 100%
rename from superset/databases/commands/importers/v1/utils.py
rename to superset/commands/database/importers/v1/utils.py
diff --git a/superset/datasets/columns/commands/__init__.py b/superset/commands/database/ssh_tunnel/__init__.py
similarity index 100%
rename from superset/datasets/columns/commands/__init__.py
rename to superset/commands/database/ssh_tunnel/__init__.py
diff --git a/superset/databases/ssh_tunnel/commands/create.py b/superset/commands/database/ssh_tunnel/create.py
similarity index 98%
rename from superset/databases/ssh_tunnel/commands/create.py
rename to superset/commands/database/ssh_tunnel/create.py
index 36f33e46f9..07209f010b 100644
--- a/superset/databases/ssh_tunnel/commands/create.py
+++ b/superset/commands/database/ssh_tunnel/create.py
@@ -21,13 +21,13 @@ from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from superset.commands.base import BaseCommand
-from superset.daos.database import SSHTunnelDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelCreateFailedError,
SSHTunnelInvalidError,
SSHTunnelRequiredFieldValidationError,
)
+from superset.daos.database import SSHTunnelDAO
+from superset.daos.exceptions import DAOCreateFailedError
from superset.extensions import db, event_logger
logger = logging.getLogger(__name__)
diff --git a/superset/databases/ssh_tunnel/commands/delete.py b/superset/commands/database/ssh_tunnel/delete.py
similarity index 96%
rename from superset/databases/ssh_tunnel/commands/delete.py
rename to superset/commands/database/ssh_tunnel/delete.py
index 70be55ce41..b8919e6d7b 100644
--- a/superset/databases/ssh_tunnel/commands/delete.py
+++ b/superset/commands/database/ssh_tunnel/delete.py
@@ -19,13 +19,13 @@ from typing import Optional
from superset import is_feature_enabled
from superset.commands.base import BaseCommand
-from superset.daos.database import SSHTunnelDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelDeleteFailedError,
SSHTunnelingNotEnabledError,
SSHTunnelNotFoundError,
)
+from superset.daos.database import SSHTunnelDAO
+from superset.daos.exceptions import DAODeleteFailedError
from superset.databases.ssh_tunnel.models import SSHTunnel
logger = logging.getLogger(__name__)
diff --git a/superset/databases/ssh_tunnel/commands/exceptions.py b/superset/commands/database/ssh_tunnel/exceptions.py
similarity index 100%
rename from superset/databases/ssh_tunnel/commands/exceptions.py
rename to superset/commands/database/ssh_tunnel/exceptions.py
diff --git a/superset/databases/ssh_tunnel/commands/update.py b/superset/commands/database/ssh_tunnel/update.py
similarity index 97%
rename from superset/databases/ssh_tunnel/commands/update.py
rename to superset/commands/database/ssh_tunnel/update.py
index 4e4edcb664..ae7ee78afe 100644
--- a/superset/databases/ssh_tunnel/commands/update.py
+++ b/superset/commands/database/ssh_tunnel/update.py
@@ -20,14 +20,14 @@ from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from superset.commands.base import BaseCommand
-from superset.daos.database import SSHTunnelDAO
-from superset.daos.exceptions import DAOUpdateFailedError
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelInvalidError,
SSHTunnelNotFoundError,
SSHTunnelRequiredFieldValidationError,
SSHTunnelUpdateFailedError,
)
+from superset.daos.database import SSHTunnelDAO
+from superset.daos.exceptions import DAOUpdateFailedError
from superset.databases.ssh_tunnel.models import SSHTunnel
logger = logging.getLogger(__name__)
diff --git a/superset/databases/commands/tables.py b/superset/commands/database/tables.py
similarity index 98%
rename from superset/databases/commands/tables.py
rename to superset/commands/database/tables.py
index 6232470ece..fa98bcbc7e 100644
--- a/superset/databases/commands/tables.py
+++ b/superset/commands/database/tables.py
@@ -20,12 +20,12 @@ from typing import Any, cast
from sqlalchemy.orm import lazyload, load_only
from superset.commands.base import BaseCommand
-from superset.connectors.sqla.models import SqlaTable
-from superset.daos.database import DatabaseDAO
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseNotFoundError,
DatabaseTablesUnexpectedError,
)
+from superset.connectors.sqla.models import SqlaTable
+from superset.daos.database import DatabaseDAO
from superset.exceptions import SupersetException
from superset.extensions import db, security_manager
from superset.models.core import Database
diff --git a/superset/databases/commands/test_connection.py b/superset/commands/database/test_connection.py
similarity index 98%
rename from superset/databases/commands/test_connection.py
rename to superset/commands/database/test_connection.py
index 49c5340dd2..0ffdf3ddd9 100644
--- a/superset/databases/commands/test_connection.py
+++ b/superset/commands/database/test_connection.py
@@ -27,15 +27,13 @@ from sqlalchemy.exc import DBAPIError, NoSuchModuleError
from superset import is_feature_enabled
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO, SSHTunnelDAO
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseSecurityUnsafeError,
DatabaseTestConnectionDriverError,
DatabaseTestConnectionUnexpectedError,
)
-from superset.databases.ssh_tunnel.commands.exceptions import (
- SSHTunnelingNotEnabledError,
-)
+from superset.commands.database.ssh_tunnel.exceptions import SSHTunnelingNotEnabledError
+from superset.daos.database import DatabaseDAO, SSHTunnelDAO
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.databases.utils import make_url_safe
from superset.errors import ErrorLevel, SupersetErrorType
diff --git a/superset/databases/commands/update.py b/superset/commands/database/update.py
similarity index 96%
rename from superset/databases/commands/update.py
rename to superset/commands/database/update.py
index d8d86c6d2d..039d731d72 100644
--- a/superset/databases/commands/update.py
+++ b/superset/commands/database/update.py
@@ -22,23 +22,23 @@ from marshmallow import ValidationError
from superset import is_feature_enabled
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO
-from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseConnectionFailedError,
DatabaseExistsValidationError,
DatabaseInvalidError,
DatabaseNotFoundError,
DatabaseUpdateFailedError,
)
-from superset.databases.ssh_tunnel.commands.create import CreateSSHTunnelCommand
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelCreateFailedError,
SSHTunnelingNotEnabledError,
SSHTunnelInvalidError,
SSHTunnelUpdateFailedError,
)
-from superset.databases.ssh_tunnel.commands.update import UpdateSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.update import UpdateSSHTunnelCommand
+from superset.daos.database import DatabaseDAO
+from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError
from superset.extensions import db, security_manager
from superset.models.core import Database
from superset.utils.core import DatasourceType
diff --git a/superset/databases/commands/validate.py b/superset/commands/database/validate.py
similarity index 98%
rename from superset/databases/commands/validate.py
rename to superset/commands/database/validate.py
index 6ea412b490..83bbc4e90a 100644
--- a/superset/databases/commands/validate.py
+++ b/superset/commands/database/validate.py
@@ -21,13 +21,13 @@ from typing import Any, Optional
from flask_babel import gettext as __
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseOfflineError,
DatabaseTestConnectionFailedError,
InvalidEngineError,
InvalidParametersError,
)
+from superset.daos.database import DatabaseDAO
from superset.databases.utils import make_url_safe
from superset.db_engine_specs import get_engine_spec
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
diff --git a/superset/databases/commands/validate_sql.py b/superset/commands/database/validate_sql.py
similarity index 98%
rename from superset/databases/commands/validate_sql.py
rename to superset/commands/database/validate_sql.py
index 6fc0c3a398..9a00526bfa 100644
--- a/superset/databases/commands/validate_sql.py
+++ b/superset/commands/database/validate_sql.py
@@ -22,8 +22,7 @@ from flask import current_app
from flask_babel import gettext as __
from superset.commands.base import BaseCommand
-from superset.daos.database import DatabaseDAO
-from superset.databases.commands.exceptions import (
+from superset.commands.database.exceptions import (
DatabaseNotFoundError,
NoValidatorConfigFoundError,
NoValidatorFoundError,
@@ -31,6 +30,7 @@ from superset.databases.commands.exceptions import (
ValidatorSQLError,
ValidatorSQLUnexpectedError,
)
+from superset.daos.database import DatabaseDAO
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.models.core import Database
from superset.sql_validators import get_validator_by_name
diff --git a/superset/datasets/commands/__init__.py b/superset/commands/dataset/__init__.py
similarity index 100%
rename from superset/datasets/commands/__init__.py
rename to superset/commands/dataset/__init__.py
diff --git a/superset/datasets/commands/importers/__init__.py b/superset/commands/dataset/columns/__init__.py
similarity index 100%
rename from superset/datasets/commands/importers/__init__.py
rename to superset/commands/dataset/columns/__init__.py
diff --git a/superset/datasets/columns/commands/delete.py b/superset/commands/dataset/columns/delete.py
similarity index 97%
rename from superset/datasets/columns/commands/delete.py
rename to superset/commands/dataset/columns/delete.py
index 0eaa78b0d5..4739c2520f 100644
--- a/superset/datasets/columns/commands/delete.py
+++ b/superset/commands/dataset/columns/delete.py
@@ -19,14 +19,14 @@ from typing import Optional
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.connectors.sqla.models import TableColumn
-from superset.daos.dataset import DatasetColumnDAO, DatasetDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.datasets.columns.commands.exceptions import (
+from superset.commands.dataset.columns.exceptions import (
DatasetColumnDeleteFailedError,
DatasetColumnForbiddenError,
DatasetColumnNotFoundError,
)
+from superset.connectors.sqla.models import TableColumn
+from superset.daos.dataset import DatasetColumnDAO, DatasetDAO
+from superset.daos.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/columns/commands/exceptions.py b/superset/commands/dataset/columns/exceptions.py
similarity index 100%
rename from superset/datasets/columns/commands/exceptions.py
rename to superset/commands/dataset/columns/exceptions.py
diff --git a/superset/datasets/commands/create.py b/superset/commands/dataset/create.py
similarity index 98%
rename from superset/datasets/commands/create.py
rename to superset/commands/dataset/create.py
index 8f486b0c9a..1c354e835f 100644
--- a/superset/datasets/commands/create.py
+++ b/superset/commands/dataset/create.py
@@ -22,15 +22,15 @@ from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand, CreateMixin
-from superset.daos.dataset import DatasetDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatabaseNotFoundValidationError,
DatasetCreateFailedError,
DatasetExistsValidationError,
DatasetInvalidError,
TableNotFoundValidationError,
)
+from superset.daos.dataset import DatasetDAO
+from superset.daos.exceptions import DAOCreateFailedError
from superset.extensions import db
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/commands/delete.py b/superset/commands/dataset/delete.py
similarity index 97%
rename from superset/datasets/commands/delete.py
rename to superset/commands/dataset/delete.py
index 478267d01d..4b7e61ab4c 100644
--- a/superset/datasets/commands/delete.py
+++ b/superset/commands/dataset/delete.py
@@ -19,14 +19,14 @@ from typing import Optional
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.connectors.sqla.models import SqlaTable
-from superset.daos.dataset import DatasetDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetDeleteFailedError,
DatasetForbiddenError,
DatasetNotFoundError,
)
+from superset.connectors.sqla.models import SqlaTable
+from superset.daos.dataset import DatasetDAO
+from superset.daos.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/commands/duplicate.py b/superset/commands/dataset/duplicate.py
similarity index 99%
rename from superset/datasets/commands/duplicate.py
rename to superset/commands/dataset/duplicate.py
index 12ae96e0ae..0ae47c35bc 100644
--- a/superset/datasets/commands/duplicate.py
+++ b/superset/commands/dataset/duplicate.py
@@ -23,16 +23,16 @@ from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand, CreateMixin
-from superset.commands.exceptions import DatasourceTypeInvalidError
-from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
-from superset.daos.dataset import DatasetDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetDuplicateFailedError,
DatasetExistsValidationError,
DatasetInvalidError,
DatasetNotFoundError,
)
+from superset.commands.exceptions import DatasourceTypeInvalidError
+from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
+from superset.daos.dataset import DatasetDAO
+from superset.daos.exceptions import DAOCreateFailedError
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetErrorException
from superset.extensions import db
diff --git a/superset/datasets/commands/exceptions.py b/superset/commands/dataset/exceptions.py
similarity index 100%
rename from superset/datasets/commands/exceptions.py
rename to superset/commands/dataset/exceptions.py
diff --git a/superset/datasets/commands/export.py b/superset/commands/dataset/export.py
similarity index 98%
rename from superset/datasets/commands/export.py
rename to superset/commands/dataset/export.py
index 3922652322..afecdd2fea 100644
--- a/superset/datasets/commands/export.py
+++ b/superset/commands/dataset/export.py
@@ -25,7 +25,7 @@ import yaml
from superset.commands.export.models import ExportModelsCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.database import DatabaseDAO
-from superset.datasets.commands.exceptions import DatasetNotFoundError
+from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.daos.dataset import DatasetDAO
from superset.utils.dict_import_export import EXPORT_VERSION
from superset.utils.file import get_filename
diff --git a/superset/datasets/metrics/commands/__init__.py b/superset/commands/dataset/importers/__init__.py
similarity index 100%
rename from superset/datasets/metrics/commands/__init__.py
rename to superset/commands/dataset/importers/__init__.py
diff --git a/superset/datasets/commands/importers/dispatcher.py b/superset/commands/dataset/importers/dispatcher.py
similarity index 97%
rename from superset/datasets/commands/importers/dispatcher.py
rename to superset/commands/dataset/importers/dispatcher.py
index 6be8635da2..9138d4f971 100644
--- a/superset/datasets/commands/importers/dispatcher.py
+++ b/superset/commands/dataset/importers/dispatcher.py
@@ -21,9 +21,9 @@ from typing import Any
from marshmallow.exceptions import ValidationError
from superset.commands.base import BaseCommand
+from superset.commands.dataset.importers import v0, v1
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__)
diff --git a/superset/datasets/commands/importers/v0.py b/superset/commands/dataset/importers/v0.py
similarity index 98%
rename from superset/datasets/commands/importers/v0.py
rename to superset/commands/dataset/importers/v0.py
index 0647263085..d389a17651 100644
--- a/superset/datasets/commands/importers/v0.py
+++ b/superset/commands/dataset/importers/v0.py
@@ -25,6 +25,8 @@ 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
from superset.commands.importers.exceptions import IncorrectVersionError
from superset.connectors.sqla.models import (
BaseDatasource,
@@ -32,8 +34,6 @@ from superset.connectors.sqla.models import (
SqlMetric,
TableColumn,
)
-from superset.databases.commands.exceptions import DatabaseNotFoundError
-from superset.datasets.commands.exceptions import DatasetInvalidError
from superset.models.core import Database
from superset.utils.dict_import_export import DATABASES_KEY
diff --git a/superset/datasets/commands/importers/v1/__init__.py b/superset/commands/dataset/importers/v1/__init__.py
similarity index 92%
rename from superset/datasets/commands/importers/v1/__init__.py
rename to superset/commands/dataset/importers/v1/__init__.py
index f46c137b7e..600a39bf48 100644
--- a/superset/datasets/commands/importers/v1/__init__.py
+++ b/superset/commands/dataset/importers/v1/__init__.py
@@ -20,12 +20,12 @@ from typing import Any
from marshmallow import Schema
from sqlalchemy.orm import Session
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.exceptions import DatasetImportError
+from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.importers.v1 import ImportModelsCommand
from superset.daos.dataset import DatasetDAO
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.exceptions import DatasetImportError
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
diff --git a/superset/datasets/commands/importers/v1/utils.py b/superset/commands/dataset/importers/v1/utils.py
similarity index 99%
rename from superset/datasets/commands/importers/v1/utils.py
rename to superset/commands/dataset/importers/v1/utils.py
index c45f7a5655..c145cc50f9 100644
--- a/superset/datasets/commands/importers/v1/utils.py
+++ b/superset/commands/dataset/importers/v1/utils.py
@@ -29,9 +29,9 @@ from sqlalchemy.orm.exc import MultipleResultsFound
from sqlalchemy.sql.visitors import VisitableType
from superset import security_manager
+from superset.commands.dataset.exceptions import DatasetForbiddenDataURI
from superset.commands.exceptions import ImportFailedError
from superset.connectors.sqla.models import SqlaTable
-from superset.datasets.commands.exceptions import DatasetForbiddenDataURI
from superset.models.core import Database
logger = logging.getLogger(__name__)
diff --git a/superset/embedded_dashboard/commands/__init__.py b/superset/commands/dataset/metrics/__init__.py
similarity index 100%
rename from superset/embedded_dashboard/commands/__init__.py
rename to superset/commands/dataset/metrics/__init__.py
diff --git a/superset/datasets/metrics/commands/delete.py b/superset/commands/dataset/metrics/delete.py
similarity index 97%
rename from superset/datasets/metrics/commands/delete.py
rename to superset/commands/dataset/metrics/delete.py
index c19aff7aa5..b48668852c 100644
--- a/superset/datasets/metrics/commands/delete.py
+++ b/superset/commands/dataset/metrics/delete.py
@@ -19,14 +19,14 @@ from typing import Optional
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.connectors.sqla.models import SqlMetric
-from superset.daos.dataset import DatasetDAO, DatasetMetricDAO
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.datasets.metrics.commands.exceptions import (
+from superset.commands.dataset.metrics.exceptions import (
DatasetMetricDeleteFailedError,
DatasetMetricForbiddenError,
DatasetMetricNotFoundError,
)
+from superset.connectors.sqla.models import SqlMetric
+from superset.daos.dataset import DatasetDAO, DatasetMetricDAO
+from superset.daos.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/metrics/commands/exceptions.py b/superset/commands/dataset/metrics/exceptions.py
similarity index 100%
rename from superset/datasets/metrics/commands/exceptions.py
rename to superset/commands/dataset/metrics/exceptions.py
diff --git a/superset/datasets/commands/refresh.py b/superset/commands/dataset/refresh.py
similarity index 97%
rename from superset/datasets/commands/refresh.py
rename to superset/commands/dataset/refresh.py
index a25609636d..5976956d7c 100644
--- a/superset/datasets/commands/refresh.py
+++ b/superset/commands/dataset/refresh.py
@@ -21,13 +21,13 @@ from flask_appbuilder.models.sqla import Model
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.connectors.sqla.models import SqlaTable
-from superset.daos.dataset import DatasetDAO
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetForbiddenError,
DatasetNotFoundError,
DatasetRefreshFailedError,
)
+from superset.connectors.sqla.models import SqlaTable
+from superset.daos.dataset import DatasetDAO
from superset.exceptions import SupersetSecurityException
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/commands/update.py b/superset/commands/dataset/update.py
similarity index 99%
rename from superset/datasets/commands/update.py
rename to superset/commands/dataset/update.py
index 8dcc4dfd5f..8a72c24fd5 100644
--- a/superset/datasets/commands/update.py
+++ b/superset/commands/dataset/update.py
@@ -23,10 +23,7 @@ from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import BaseCommand, UpdateMixin
-from superset.connectors.sqla.models import SqlaTable
-from superset.daos.dataset import DatasetDAO
-from superset.daos.exceptions import DAOUpdateFailedError
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatabaseChangeValidationError,
DatasetColumnNotFoundValidationError,
DatasetColumnsDuplicateValidationError,
@@ -40,6 +37,9 @@ from superset.datasets.commands.exceptions import (
DatasetNotFoundError,
DatasetUpdateFailedError,
)
+from superset.connectors.sqla.models import SqlaTable
+from superset.daos.dataset import DatasetDAO
+from superset.daos.exceptions import DAOUpdateFailedError
from superset.exceptions import SupersetSecurityException
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/commands/warm_up_cache.py b/superset/commands/dataset/warm_up_cache.py
similarity index 89%
rename from superset/datasets/commands/warm_up_cache.py
rename to superset/commands/dataset/warm_up_cache.py
index 64becc9cd6..97b00c4772 100644
--- a/superset/datasets/commands/warm_up_cache.py
+++ b/superset/commands/dataset/warm_up_cache.py
@@ -18,10 +18,10 @@
from typing import Any, Optional
-from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
from superset.commands.base import BaseCommand
+from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
+from superset.commands.dataset.exceptions import WarmUpCacheTableNotFoundError
from superset.connectors.sqla.models import SqlaTable
-from superset.datasets.commands.exceptions import WarmUpCacheTableNotFoundError
from superset.extensions import db
from superset.models.core import Database
from superset.models.slice import Slice
@@ -45,7 +45,9 @@ class DatasetWarmUpCacheCommand(BaseCommand):
self.validate()
return [
ChartWarmUpCacheCommand(
- chart, self._dashboard_id, self._extra_filters
+ chart,
+ self._dashboard_id,
+ self._extra_filters,
).run()
for chart in self._charts
]
diff --git a/superset/explore/commands/__init__.py b/superset/commands/explore/__init__.py
similarity index 100%
rename from superset/explore/commands/__init__.py
rename to superset/commands/explore/__init__.py
diff --git a/superset/explore/form_data/commands/__init__.py b/superset/commands/explore/form_data/__init__.py
similarity index 100%
rename from superset/explore/form_data/commands/__init__.py
rename to superset/commands/explore/form_data/__init__.py
diff --git a/superset/explore/form_data/commands/create.py b/superset/commands/explore/form_data/create.py
similarity index 91%
rename from superset/explore/form_data/commands/create.py
rename to superset/commands/explore/form_data/create.py
index df0250f2ff..e85f840133 100644
--- a/superset/explore/form_data/commands/create.py
+++ b/superset/commands/explore/form_data/create.py
@@ -20,12 +20,12 @@ from flask import session
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.state import TemporaryExploreState
-from superset.explore.form_data.commands.utils import check_access
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.form_data.state import TemporaryExploreState
+from superset.commands.explore.form_data.utils import check_access
+from superset.commands.temporary_cache.exceptions import TemporaryCacheCreateFailedError
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
-from superset.temporary_cache.commands.exceptions import TemporaryCacheCreateFailedError
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType, get_user_id
from superset.utils.schema import validate_json
diff --git a/superset/explore/form_data/commands/delete.py b/superset/commands/explore/form_data/delete.py
similarity index 91%
rename from superset/explore/form_data/commands/delete.py
rename to superset/commands/explore/form_data/delete.py
index bce13b719a..d998f132d6 100644
--- a/superset/explore/form_data/commands/delete.py
+++ b/superset/commands/explore/form_data/delete.py
@@ -22,14 +22,14 @@ from flask import session
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.state import TemporaryExploreState
-from superset.explore.form_data.commands.utils import check_access
-from superset.extensions import cache_manager
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.form_data.state import TemporaryExploreState
+from superset.commands.explore.form_data.utils import check_access
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheDeleteFailedError,
)
+from superset.extensions import cache_manager
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType, get_user_id
diff --git a/superset/explore/form_data/commands/get.py b/superset/commands/explore/form_data/get.py
similarity index 89%
rename from superset/explore/form_data/commands/get.py
rename to superset/commands/explore/form_data/get.py
index 53fd6ea6a9..0153888d4e 100644
--- a/superset/explore/form_data/commands/get.py
+++ b/superset/commands/explore/form_data/get.py
@@ -22,11 +22,11 @@ from flask import current_app as app
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.state import TemporaryExploreState
-from superset.explore.form_data.commands.utils import check_access
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.form_data.state import TemporaryExploreState
+from superset.commands.explore.form_data.utils import check_access
+from superset.commands.temporary_cache.exceptions import TemporaryCacheGetFailedError
from superset.extensions import cache_manager
-from superset.temporary_cache.commands.exceptions import TemporaryCacheGetFailedError
from superset.utils.core import DatasourceType
logger = logging.getLogger(__name__)
diff --git a/superset/explore/form_data/commands/parameters.py b/superset/commands/explore/form_data/parameters.py
similarity index 100%
rename from superset/explore/form_data/commands/parameters.py
rename to superset/commands/explore/form_data/parameters.py
diff --git a/superset/explore/form_data/commands/state.py b/superset/commands/explore/form_data/state.py
similarity index 100%
rename from superset/explore/form_data/commands/state.py
rename to superset/commands/explore/form_data/state.py
diff --git a/superset/explore/form_data/commands/update.py b/superset/commands/explore/form_data/update.py
similarity index 93%
rename from superset/explore/form_data/commands/update.py
rename to superset/commands/explore/form_data/update.py
index ace57350c4..fbb6ee0719 100644
--- a/superset/explore/form_data/commands/update.py
+++ b/superset/commands/explore/form_data/update.py
@@ -22,15 +22,15 @@ from flask import session
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.state import TemporaryExploreState
-from superset.explore.form_data.commands.utils import check_access
-from superset.extensions import cache_manager
-from superset.key_value.utils import random_key
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.form_data.state import TemporaryExploreState
+from superset.commands.explore.form_data.utils import check_access
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheUpdateFailedError,
)
+from superset.extensions import cache_manager
+from superset.key_value.utils import random_key
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType, get_user_id
from superset.utils.schema import validate_json
diff --git a/superset/explore/form_data/commands/utils.py b/superset/commands/explore/form_data/utils.py
similarity index 90%
rename from superset/explore/form_data/commands/utils.py
rename to superset/commands/explore/form_data/utils.py
index e4a843dc62..45b46fb8b3 100644
--- a/superset/explore/form_data/commands/utils.py
+++ b/superset/commands/explore/form_data/utils.py
@@ -16,19 +16,19 @@
# under the License.
from typing import Optional
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
)
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetAccessDeniedError,
DatasetNotFoundError,
)
-from superset.explore.utils import check_access as explore_check_access
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheResourceNotFoundError,
)
+from superset.explore.utils import check_access as explore_check_access
from superset.utils.core import DatasourceType
diff --git a/superset/explore/commands/get.py b/superset/commands/explore/get.py
similarity index 96%
rename from superset/explore/commands/get.py
rename to superset/commands/explore/get.py
index 1994e7ad43..bb8f5a85e9 100644
--- a/superset/explore/commands/get.py
+++ b/superset/commands/explore/get.py
@@ -26,17 +26,17 @@ from sqlalchemy.exc import SQLAlchemyError
from superset import db
from superset.commands.base import BaseCommand
+from superset.commands.explore.form_data.get import GetFormDataCommand
+from superset.commands.explore.form_data.parameters import (
+ CommandParameters as FormDataCommandParameters,
+)
+from superset.commands.explore.parameters import CommandParameters
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.datasource import DatasourceDAO
from superset.daos.exceptions import DatasourceNotFound
from superset.exceptions import SupersetException
-from superset.explore.commands.parameters import CommandParameters
from superset.explore.exceptions import WrongEndpointError
-from superset.explore.form_data.commands.get import GetFormDataCommand
-from superset.explore.form_data.commands.parameters import (
- CommandParameters as FormDataCommandParameters,
-)
-from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.utils import core as utils
from superset.views.utils import (
diff --git a/superset/explore/commands/parameters.py b/superset/commands/explore/parameters.py
similarity index 100%
rename from superset/explore/commands/parameters.py
rename to superset/commands/explore/parameters.py
diff --git a/superset/explore/permalink/commands/__init__.py b/superset/commands/explore/permalink/__init__.py
similarity index 100%
rename from superset/explore/permalink/commands/__init__.py
rename to superset/commands/explore/permalink/__init__.py
diff --git a/superset/explore/permalink/commands/base.py b/superset/commands/explore/permalink/base.py
similarity index 100%
rename from superset/explore/permalink/commands/base.py
rename to superset/commands/explore/permalink/base.py
diff --git a/superset/explore/permalink/commands/create.py b/superset/commands/explore/permalink/create.py
similarity index 95%
rename from superset/explore/permalink/commands/create.py
rename to superset/commands/explore/permalink/create.py
index 97a8bcbf09..befb1d5a47 100644
--- a/superset/explore/permalink/commands/create.py
+++ b/superset/commands/explore/permalink/create.py
@@ -19,10 +19,10 @@ from typing import Any, Optional
from sqlalchemy.exc import SQLAlchemyError
-from superset.explore.permalink.commands.base import BaseExplorePermalinkCommand
+from superset.commands.explore.permalink.base import BaseExplorePermalinkCommand
+from superset.commands.key_value.create import CreateKeyValueCommand
from superset.explore.permalink.exceptions import ExplorePermalinkCreateFailedError
from superset.explore.utils import check_access as check_chart_access
-from superset.key_value.commands.create import CreateKeyValueCommand
from superset.key_value.exceptions import KeyValueCodecEncodeException
from superset.key_value.utils import encode_permalink_key
from superset.utils.core import DatasourceType
diff --git a/superset/explore/permalink/commands/get.py b/superset/commands/explore/permalink/get.py
similarity index 93%
rename from superset/explore/permalink/commands/get.py
rename to superset/commands/explore/permalink/get.py
index 1aa093b380..4c01db1cca 100644
--- a/superset/explore/permalink/commands/get.py
+++ b/superset/commands/explore/permalink/get.py
@@ -19,12 +19,12 @@ from typing import Optional
from sqlalchemy.exc import SQLAlchemyError
-from superset.datasets.commands.exceptions import DatasetNotFoundError
-from superset.explore.permalink.commands.base import BaseExplorePermalinkCommand
+from superset.commands.dataset.exceptions import DatasetNotFoundError
+from superset.commands.explore.permalink.base import BaseExplorePermalinkCommand
+from superset.commands.key_value.get import GetKeyValueCommand
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.explore.permalink.types import ExplorePermalinkValue
from superset.explore.utils import check_access as check_chart_access
-from superset.key_value.commands.get import GetKeyValueCommand
from superset.key_value.exceptions import (
KeyValueCodecDecodeException,
KeyValueGetFailedError,
diff --git a/superset/commands/export/assets.py b/superset/commands/export/assets.py
index 1bd2cf6d61..61d805acaf 100644
--- a/superset/commands/export/assets.py
+++ b/superset/commands/export/assets.py
@@ -20,12 +20,12 @@ from datetime import datetime, timezone
import yaml
-from superset.charts.commands.export import ExportChartsCommand
from superset.commands.base import BaseCommand
-from superset.dashboards.commands.export import ExportDashboardsCommand
-from superset.databases.commands.export import ExportDatabasesCommand
-from superset.datasets.commands.export import ExportDatasetsCommand
-from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand
+from superset.commands.chart.export import ExportChartsCommand
+from superset.commands.dashboard.export import ExportDashboardsCommand
+from superset.commands.database.export import ExportDatabasesCommand
+from superset.commands.dataset.export import ExportDatasetsCommand
+from superset.commands.query.export import ExportSavedQueriesCommand
from superset.utils.dict_import_export import EXPORT_VERSION
METADATA_FILE_NAME = "metadata.yaml"
diff --git a/superset/commands/importers/v1/assets.py b/superset/commands/importers/v1/assets.py
index 4c8971315c..b6bc29e0fa 100644
--- a/superset/commands/importers/v1/assets.py
+++ b/superset/commands/importers/v1/assets.py
@@ -22,29 +22,27 @@ from sqlalchemy.orm import Session
from sqlalchemy.sql import delete, insert
from superset import db
-from superset.charts.commands.importers.v1.utils import import_chart
from superset.charts.schemas import ImportV1ChartSchema
from superset.commands.base import BaseCommand
+from superset.commands.chart.importers.v1.utils import import_chart
+from superset.commands.dashboard.importers.v1.utils import (
+ find_chart_uuids,
+ import_dashboard,
+ update_id_refs,
+)
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.commands.exceptions import CommandInvalidError, ImportFailedError
from superset.commands.importers.v1.utils import (
load_configs,
load_metadata,
validate_metadata_type,
)
-from superset.dashboards.commands.importers.v1.utils import (
- find_chart_uuids,
- import_dashboard,
- update_id_refs,
-)
+from superset.commands.query.importers.v1.utils import import_saved_query
from superset.dashboards.schemas import ImportV1DashboardSchema
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.dashboard import dashboard_slices
-from superset.queries.saved_queries.commands.importers.v1.utils import (
- import_saved_query,
-)
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 737be25f8a..94194921ac 100644
--- a/superset/commands/importers/v1/examples.py
+++ b/superset/commands/importers/v1/examples.py
@@ -22,24 +22,24 @@ from sqlalchemy.orm.exc import MultipleResultsFound
from sqlalchemy.sql import select
from superset import db
-from superset.charts.commands.importers.v1 import ImportChartsCommand
-from superset.charts.commands.importers.v1.utils import import_chart
from superset.charts.schemas import ImportV1ChartSchema
-from superset.commands.exceptions import CommandException
-from superset.commands.importers.v1 import ImportModelsCommand
-from superset.daos.base import BaseDAO
-from superset.dashboards.commands.importers.v1 import ImportDashboardsCommand
-from superset.dashboards.commands.importers.v1.utils import (
+from superset.commands.chart.importers.v1 import ImportChartsCommand
+from superset.commands.chart.importers.v1.utils import import_chart
+from superset.commands.dashboard.importers.v1 import ImportDashboardsCommand
+from superset.commands.dashboard.importers.v1.utils import (
find_chart_uuids,
import_dashboard,
update_id_refs,
)
+from superset.commands.database.importers.v1 import ImportDatabasesCommand
+from superset.commands.database.importers.v1.utils import import_database
+from superset.commands.dataset.importers.v1 import ImportDatasetsCommand
+from superset.commands.dataset.importers.v1.utils import import_dataset
+from superset.commands.exceptions import CommandException
+from superset.commands.importers.v1 import ImportModelsCommand
+from superset.daos.base import BaseDAO
from superset.dashboards.schemas import ImportV1DashboardSchema
-from superset.databases.commands.importers.v1 import ImportDatabasesCommand
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.datasets.commands.importers.v1 import ImportDatasetsCommand
-from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.dashboard import dashboard_slices
from superset.utils.core import get_example_default_schema
diff --git a/superset/key_value/commands/__init__.py b/superset/commands/key_value/__init__.py
similarity index 100%
rename from superset/key_value/commands/__init__.py
rename to superset/commands/key_value/__init__.py
diff --git a/superset/key_value/commands/create.py b/superset/commands/key_value/create.py
similarity index 100%
rename from superset/key_value/commands/create.py
rename to superset/commands/key_value/create.py
diff --git a/superset/key_value/commands/delete.py b/superset/commands/key_value/delete.py
similarity index 100%
rename from superset/key_value/commands/delete.py
rename to superset/commands/key_value/delete.py
diff --git a/superset/key_value/commands/delete_expired.py b/superset/commands/key_value/delete_expired.py
similarity index 100%
rename from superset/key_value/commands/delete_expired.py
rename to superset/commands/key_value/delete_expired.py
diff --git a/superset/key_value/commands/get.py b/superset/commands/key_value/get.py
similarity index 100%
rename from superset/key_value/commands/get.py
rename to superset/commands/key_value/get.py
diff --git a/superset/key_value/commands/update.py b/superset/commands/key_value/update.py
similarity index 100%
rename from superset/key_value/commands/update.py
rename to superset/commands/key_value/update.py
diff --git a/superset/key_value/commands/upsert.py b/superset/commands/key_value/upsert.py
similarity index 98%
rename from superset/key_value/commands/upsert.py
rename to superset/commands/key_value/upsert.py
index 66d6785f2e..84f02cb9cd 100644
--- a/superset/key_value/commands/upsert.py
+++ b/superset/commands/key_value/upsert.py
@@ -24,7 +24,7 @@ from sqlalchemy.exc import SQLAlchemyError
from superset import db
from superset.commands.base import BaseCommand
-from superset.key_value.commands.create import CreateKeyValueCommand
+from superset.commands.key_value.create import CreateKeyValueCommand
from superset.key_value.exceptions import (
KeyValueCreateFailedError,
KeyValueUpsertFailedError,
diff --git a/superset/queries/saved_queries/commands/__init__.py b/superset/commands/query/__init__.py
similarity index 100%
rename from superset/queries/saved_queries/commands/__init__.py
rename to superset/commands/query/__init__.py
diff --git a/superset/queries/saved_queries/commands/delete.py b/superset/commands/query/delete.py
similarity index 96%
rename from superset/queries/saved_queries/commands/delete.py
rename to superset/commands/query/delete.py
index 40b73658e0..978f30c5c4 100644
--- a/superset/queries/saved_queries/commands/delete.py
+++ b/superset/commands/query/delete.py
@@ -18,13 +18,13 @@ import logging
from typing import Optional
from superset.commands.base import BaseCommand
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.query import SavedQueryDAO
-from superset.models.dashboard import Dashboard
-from superset.queries.saved_queries.commands.exceptions import (
+from superset.commands.query.exceptions import (
SavedQueryDeleteFailedError,
SavedQueryNotFoundError,
)
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.query import SavedQueryDAO
+from superset.models.dashboard import Dashboard
logger = logging.getLogger(__name__)
diff --git a/superset/queries/saved_queries/commands/exceptions.py b/superset/commands/query/exceptions.py
similarity index 100%
rename from superset/queries/saved_queries/commands/exceptions.py
rename to superset/commands/query/exceptions.py
diff --git a/superset/queries/saved_queries/commands/export.py b/superset/commands/query/export.py
similarity index 97%
rename from superset/queries/saved_queries/commands/export.py
rename to superset/commands/query/export.py
index 1b85cda796..a8fa8acbf0 100644
--- a/superset/queries/saved_queries/commands/export.py
+++ b/superset/commands/query/export.py
@@ -25,7 +25,7 @@ from werkzeug.utils import secure_filename
from superset.commands.export.models import ExportModelsCommand
from superset.models.sql_lab import SavedQuery
-from superset.queries.saved_queries.commands.exceptions import SavedQueryNotFoundError
+from superset.commands.query.exceptions import SavedQueryNotFoundError
from superset.daos.query import SavedQueryDAO
from superset.utils.dict_import_export import EXPORT_VERSION
diff --git a/superset/queries/saved_queries/commands/importers/__init__.py b/superset/commands/query/importers/__init__.py
similarity index 100%
rename from superset/queries/saved_queries/commands/importers/__init__.py
rename to superset/commands/query/importers/__init__.py
diff --git a/superset/queries/saved_queries/commands/importers/dispatcher.py b/superset/commands/query/importers/dispatcher.py
similarity index 97%
rename from superset/queries/saved_queries/commands/importers/dispatcher.py
rename to superset/commands/query/importers/dispatcher.py
index c2208f0e2a..438ea8351f 100644
--- a/superset/queries/saved_queries/commands/importers/dispatcher.py
+++ b/superset/commands/query/importers/dispatcher.py
@@ -23,7 +23,7 @@ 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.queries.saved_queries.commands.importers import v1
+from superset.commands.query.importers import v1
logger = logging.getLogger(__name__)
diff --git a/superset/queries/saved_queries/commands/importers/v1/__init__.py b/superset/commands/query/importers/v1/__init__.py
similarity index 91%
rename from superset/queries/saved_queries/commands/importers/v1/__init__.py
rename to superset/commands/query/importers/v1/__init__.py
index c8a159c7f5..fa1f21b6fc 100644
--- a/superset/queries/saved_queries/commands/importers/v1/__init__.py
+++ b/superset/commands/query/importers/v1/__init__.py
@@ -20,15 +20,13 @@ from typing import Any
from marshmallow import Schema
from sqlalchemy.orm import Session
+from superset.commands.database.importers.v1.utils import import_database
from superset.commands.importers.v1 import ImportModelsCommand
+from superset.commands.query.exceptions import SavedQueryImportError
+from superset.commands.query.importers.v1.utils import import_saved_query
from superset.connectors.sqla.models import SqlaTable
from superset.daos.query import SavedQueryDAO
-from superset.databases.commands.importers.v1.utils import import_database
from superset.databases.schemas import ImportV1DatabaseSchema
-from superset.queries.saved_queries.commands.exceptions import SavedQueryImportError
-from superset.queries.saved_queries.commands.importers.v1.utils import (
- import_saved_query,
-)
from superset.queries.saved_queries.schemas import ImportV1SavedQuerySchema
diff --git a/superset/queries/saved_queries/commands/importers/v1/utils.py b/superset/commands/query/importers/v1/utils.py
similarity index 100%
rename from superset/queries/saved_queries/commands/importers/v1/utils.py
rename to superset/commands/query/importers/v1/utils.py
diff --git a/superset/reports/commands/__init__.py b/superset/commands/report/__init__.py
similarity index 100%
rename from superset/reports/commands/__init__.py
rename to superset/commands/report/__init__.py
diff --git a/superset/reports/commands/alert.py b/superset/commands/report/alert.py
similarity index 99%
rename from superset/reports/commands/alert.py
rename to superset/commands/report/alert.py
index 2c36d3589c..68013a2c00 100644
--- a/superset/reports/commands/alert.py
+++ b/superset/commands/report/alert.py
@@ -29,7 +29,7 @@ from flask_babel import lazy_gettext as _
from superset import app, jinja_context, security_manager
from superset.commands.base import BaseCommand
-from superset.reports.commands.exceptions import (
+from superset.commands.report.exceptions import (
AlertQueryError,
AlertQueryInvalidTypeError,
AlertQueryMultipleColumnsError,
diff --git a/superset/reports/commands/base.py b/superset/commands/report/base.py
similarity index 98%
rename from superset/reports/commands/base.py
rename to superset/commands/report/base.py
index da871ef17c..3b2f280816 100644
--- a/superset/reports/commands/base.py
+++ b/superset/commands/report/base.py
@@ -20,9 +20,7 @@ from typing import Any
from marshmallow import ValidationError
from superset.commands.base import BaseCommand
-from superset.daos.chart import ChartDAO
-from superset.daos.dashboard import DashboardDAO
-from superset.reports.commands.exceptions import (
+from superset.commands.report.exceptions import (
ChartNotFoundValidationError,
ChartNotSavedValidationError,
DashboardNotFoundValidationError,
@@ -30,6 +28,8 @@ from superset.reports.commands.exceptions import (
ReportScheduleEitherChartOrDashboardError,
ReportScheduleOnlyChartOrDashboardError,
)
+from superset.daos.chart import ChartDAO
+from superset.daos.dashboard import DashboardDAO
from superset.reports.models import ReportCreationMethod
logger = logging.getLogger(__name__)
diff --git a/superset/reports/commands/create.py b/superset/commands/report/create.py
similarity index 97%
rename from superset/reports/commands/create.py
rename to superset/commands/report/create.py
index 177e01c33b..aa9bfefc6e 100644
--- a/superset/reports/commands/create.py
+++ b/superset/commands/report/create.py
@@ -22,11 +22,8 @@ from flask_babel import gettext as _
from marshmallow import ValidationError
from superset.commands.base import CreateMixin
-from superset.daos.database import DatabaseDAO
-from superset.daos.exceptions import DAOCreateFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.reports.commands.base import BaseReportScheduleCommand
-from superset.reports.commands.exceptions import (
+from superset.commands.report.base import BaseReportScheduleCommand
+from superset.commands.report.exceptions import (
DatabaseNotFoundValidationError,
ReportScheduleAlertRequiredDatabaseValidationError,
ReportScheduleCreateFailedError,
@@ -35,6 +32,9 @@ from superset.reports.commands.exceptions import (
ReportScheduleNameUniquenessValidationError,
ReportScheduleRequiredTypeValidationError,
)
+from superset.daos.database import DatabaseDAO
+from superset.daos.exceptions import DAOCreateFailedError
+from superset.daos.report import ReportScheduleDAO
from superset.reports.models import (
ReportCreationMethod,
ReportSchedule,
diff --git a/superset/reports/commands/delete.py b/superset/commands/report/delete.py
similarity index 97%
rename from superset/reports/commands/delete.py
rename to superset/commands/report/delete.py
index 2cdac17c4d..87ea4b99dd 100644
--- a/superset/reports/commands/delete.py
+++ b/superset/commands/report/delete.py
@@ -19,14 +19,14 @@ from typing import Optional
from superset import security_manager
from superset.commands.base import BaseCommand
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.exceptions import SupersetSecurityException
-from superset.reports.commands.exceptions import (
+from superset.commands.report.exceptions import (
ReportScheduleDeleteFailedError,
ReportScheduleForbiddenError,
ReportScheduleNotFoundError,
)
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.report import ReportScheduleDAO
+from superset.exceptions import SupersetSecurityException
from superset.reports.models import ReportSchedule
logger = logging.getLogger(__name__)
diff --git a/superset/reports/commands/exceptions.py b/superset/commands/report/exceptions.py
similarity index 100%
rename from superset/reports/commands/exceptions.py
rename to superset/commands/report/exceptions.py
diff --git a/superset/reports/commands/execute.py b/superset/commands/report/execute.py
similarity index 99%
rename from superset/reports/commands/execute.py
rename to superset/commands/report/execute.py
index 7cd8203a51..d4b53e30dd 100644
--- a/superset/reports/commands/execute.py
+++ b/superset/commands/report/execute.py
@@ -26,20 +26,10 @@ from sqlalchemy.orm import Session
from superset import app, security_manager
from superset.commands.base import BaseCommand
+from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
from superset.commands.exceptions import CommandException
-from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
-from superset.daos.report import (
- REPORT_SCHEDULE_ERROR_NOTIFICATION_MARKER,
- ReportScheduleDAO,
-)
-from superset.dashboards.permalink.commands.create import (
- CreateDashboardPermalinkCommand,
-)
-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.reports.commands.alert import AlertCommand
-from superset.reports.commands.exceptions import (
+from superset.commands.report.alert import AlertCommand
+from superset.commands.report.exceptions import (
ReportScheduleAlertGracePeriodError,
ReportScheduleClientErrorsException,
ReportScheduleCsvFailedError,
@@ -56,6 +46,14 @@ from superset.reports.commands.exceptions import (
ReportScheduleUnexpectedError,
ReportScheduleWorkingTimeoutError,
)
+from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
+from superset.daos.report import (
+ REPORT_SCHEDULE_ERROR_NOTIFICATION_MARKER,
+ ReportScheduleDAO,
+)
+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.reports.models import (
ReportDataFormat,
ReportExecutionLog,
diff --git a/superset/reports/commands/log_prune.py b/superset/commands/report/log_prune.py
similarity index 96%
rename from superset/reports/commands/log_prune.py
rename to superset/commands/report/log_prune.py
index 09d9995414..3a9883c9f1 100644
--- a/superset/reports/commands/log_prune.py
+++ b/superset/commands/report/log_prune.py
@@ -18,9 +18,9 @@ import logging
from datetime import datetime, timedelta
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.reports.commands.exceptions import ReportSchedulePruneLogError
from superset.reports.models import ReportSchedule
from superset.utils.celery import session_scope
diff --git a/superset/reports/commands/update.py b/superset/commands/report/update.py
similarity index 97%
rename from superset/reports/commands/update.py
rename to superset/commands/report/update.py
index 7c3351e5ec..a33ba6b59a 100644
--- a/superset/reports/commands/update.py
+++ b/superset/commands/report/update.py
@@ -23,12 +23,8 @@ from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import UpdateMixin
-from superset.daos.database import DatabaseDAO
-from superset.daos.exceptions import DAOUpdateFailedError
-from superset.daos.report import ReportScheduleDAO
-from superset.exceptions import SupersetSecurityException
-from superset.reports.commands.base import BaseReportScheduleCommand
-from superset.reports.commands.exceptions import (
+from superset.commands.report.base import BaseReportScheduleCommand
+from superset.commands.report.exceptions import (
DatabaseNotFoundValidationError,
ReportScheduleForbiddenError,
ReportScheduleInvalidError,
@@ -36,6 +32,10 @@ from superset.reports.commands.exceptions import (
ReportScheduleNotFoundError,
ReportScheduleUpdateFailedError,
)
+from superset.daos.database import DatabaseDAO
+from superset.daos.exceptions import DAOUpdateFailedError
+from superset.daos.report import ReportScheduleDAO
+from superset.exceptions import SupersetSecurityException
from superset.reports.models import ReportSchedule, ReportScheduleType, ReportState
logger = logging.getLogger(__name__)
diff --git a/superset/row_level_security/commands/__init__.py b/superset/commands/security/__init__.py
similarity index 100%
rename from superset/row_level_security/commands/__init__.py
rename to superset/commands/security/__init__.py
diff --git a/superset/row_level_security/commands/create.py b/superset/commands/security/create.py
similarity index 100%
rename from superset/row_level_security/commands/create.py
rename to superset/commands/security/create.py
diff --git a/superset/row_level_security/commands/delete.py b/superset/commands/security/delete.py
similarity index 96%
rename from superset/row_level_security/commands/delete.py
rename to superset/commands/security/delete.py
index d669f7d90f..2c19c5f89b 100644
--- a/superset/row_level_security/commands/delete.py
+++ b/superset/commands/security/delete.py
@@ -18,13 +18,13 @@
import logging
from superset.commands.base import BaseCommand
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.security import RLSDAO
-from superset.reports.models import ReportSchedule
-from superset.row_level_security.commands.exceptions import (
+from superset.commands.security.exceptions import (
RLSRuleNotFoundError,
RuleDeleteFailedError,
)
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.security import RLSDAO
+from superset.reports.models import ReportSchedule
logger = logging.getLogger(__name__)
diff --git a/superset/row_level_security/commands/exceptions.py b/superset/commands/security/exceptions.py
similarity index 100%
rename from superset/row_level_security/commands/exceptions.py
rename to superset/commands/security/exceptions.py
diff --git a/superset/row_level_security/commands/update.py b/superset/commands/security/update.py
similarity index 96%
rename from superset/row_level_security/commands/update.py
rename to superset/commands/security/update.py
index bc5ef368ba..f3a6cea607 100644
--- a/superset/row_level_security/commands/update.py
+++ b/superset/commands/security/update.py
@@ -21,12 +21,12 @@ from typing import Any, Optional
from superset.commands.base import BaseCommand
from superset.commands.exceptions import DatasourceNotFoundValidationError
+from superset.commands.security.exceptions import RLSRuleNotFoundError
from superset.commands.utils import populate_roles
from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
from superset.daos.exceptions import DAOUpdateFailedError
from superset.daos.security import RLSDAO
from superset.extensions import db
-from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
logger = logging.getLogger(__name__)
diff --git a/superset/sqllab/commands/__init__.py b/superset/commands/sql_lab/__init__.py
similarity index 100%
rename from superset/sqllab/commands/__init__.py
rename to superset/commands/sql_lab/__init__.py
diff --git a/superset/sqllab/commands/estimate.py b/superset/commands/sql_lab/estimate.py
similarity index 100%
rename from superset/sqllab/commands/estimate.py
rename to superset/commands/sql_lab/estimate.py
diff --git a/superset/sqllab/commands/execute.py b/superset/commands/sql_lab/execute.py
similarity index 100%
rename from superset/sqllab/commands/execute.py
rename to superset/commands/sql_lab/execute.py
diff --git a/superset/sqllab/commands/export.py b/superset/commands/sql_lab/export.py
similarity index 100%
rename from superset/sqllab/commands/export.py
rename to superset/commands/sql_lab/export.py
diff --git a/superset/sqllab/commands/results.py b/superset/commands/sql_lab/results.py
similarity index 100%
rename from superset/sqllab/commands/results.py
rename to superset/commands/sql_lab/results.py
diff --git a/superset/tags/commands/__init__.py b/superset/commands/tag/__init__.py
similarity index 100%
rename from superset/tags/commands/__init__.py
rename to superset/commands/tag/__init__.py
diff --git a/superset/tags/commands/create.py b/superset/commands/tag/create.py
similarity index 96%
rename from superset/tags/commands/create.py
rename to superset/commands/tag/create.py
index eb3d4458a2..ea23b8d59d 100644
--- a/superset/tags/commands/create.py
+++ b/superset/commands/tag/create.py
@@ -19,11 +19,11 @@ 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.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
-from superset.tags.commands.utils import to_object_model, to_object_type
from superset.tags.models import ObjectType, TagType
logger = logging.getLogger(__name__)
diff --git a/superset/tags/commands/delete.py b/superset/commands/tag/delete.py
similarity index 97%
rename from superset/tags/commands/delete.py
rename to superset/commands/tag/delete.py
index 5c10a934e3..c4f2239009 100644
--- a/superset/tags/commands/delete.py
+++ b/superset/commands/tag/delete.py
@@ -17,16 +17,16 @@
import logging
from superset.commands.base import BaseCommand
-from superset.daos.exceptions import DAODeleteFailedError
-from superset.daos.tag import TagDAO
-from superset.tags.commands.exceptions import (
+from superset.commands.tag.exceptions import (
TagDeleteFailedError,
TaggedObjectDeleteFailedError,
TaggedObjectNotFoundError,
TagInvalidError,
TagNotFoundError,
)
-from superset.tags.commands.utils import to_object_type
+from superset.commands.tag.utils import to_object_type
+from superset.daos.exceptions import DAODeleteFailedError
+from superset.daos.tag import TagDAO
from superset.tags.models import ObjectType
from superset.views.base import DeleteMixin
diff --git a/superset/tags/commands/exceptions.py b/superset/commands/tag/exceptions.py
similarity index 100%
rename from superset/tags/commands/exceptions.py
rename to superset/commands/tag/exceptions.py
diff --git a/superset/tags/commands/update.py b/superset/commands/tag/update.py
similarity index 94%
rename from superset/tags/commands/update.py
rename to superset/commands/tag/update.py
index 182376438b..431bf93c4d 100644
--- a/superset/tags/commands/update.py
+++ b/superset/commands/tag/update.py
@@ -21,9 +21,9 @@ 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.tags.commands.exceptions import TagInvalidError, TagNotFoundError
-from superset.tags.commands.utils import to_object_type
from superset.tags.models import Tag
logger = logging.getLogger(__name__)
diff --git a/superset/tags/commands/utils.py b/superset/commands/tag/utils.py
similarity index 100%
rename from superset/tags/commands/utils.py
rename to superset/commands/tag/utils.py
diff --git a/superset/temporary_cache/commands/__init__.py b/superset/commands/temporary_cache/__init__.py
similarity index 100%
rename from superset/temporary_cache/commands/__init__.py
rename to superset/commands/temporary_cache/__init__.py
diff --git a/superset/temporary_cache/commands/create.py b/superset/commands/temporary_cache/create.py
similarity index 92%
rename from superset/temporary_cache/commands/create.py
rename to superset/commands/temporary_cache/create.py
index af3b5350f6..e43d48e54c 100644
--- a/superset/temporary_cache/commands/create.py
+++ b/superset/commands/temporary_cache/create.py
@@ -20,8 +20,8 @@ from abc import ABC, abstractmethod
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.temporary_cache.commands.exceptions import TemporaryCacheCreateFailedError
-from superset.temporary_cache.commands.parameters import CommandParameters
+from superset.commands.temporary_cache.exceptions import TemporaryCacheCreateFailedError
+from superset.commands.temporary_cache.parameters import CommandParameters
logger = logging.getLogger(__name__)
diff --git a/superset/temporary_cache/commands/delete.py b/superset/commands/temporary_cache/delete.py
similarity index 92%
rename from superset/temporary_cache/commands/delete.py
rename to superset/commands/temporary_cache/delete.py
index 1281c8debf..d35b184d87 100644
--- a/superset/temporary_cache/commands/delete.py
+++ b/superset/commands/temporary_cache/delete.py
@@ -20,8 +20,8 @@ from abc import ABC, abstractmethod
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.temporary_cache.commands.exceptions import TemporaryCacheDeleteFailedError
-from superset.temporary_cache.commands.parameters import CommandParameters
+from superset.commands.temporary_cache.exceptions import TemporaryCacheDeleteFailedError
+from superset.commands.temporary_cache.parameters import CommandParameters
logger = logging.getLogger(__name__)
diff --git a/superset/temporary_cache/commands/entry.py b/superset/commands/temporary_cache/entry.py
similarity index 100%
rename from superset/temporary_cache/commands/entry.py
rename to superset/commands/temporary_cache/entry.py
diff --git a/superset/temporary_cache/commands/exceptions.py b/superset/commands/temporary_cache/exceptions.py
similarity index 100%
rename from superset/temporary_cache/commands/exceptions.py
rename to superset/commands/temporary_cache/exceptions.py
diff --git a/superset/temporary_cache/commands/get.py b/superset/commands/temporary_cache/get.py
similarity index 92%
rename from superset/temporary_cache/commands/get.py
rename to superset/commands/temporary_cache/get.py
index 8c220b9c04..fa16977a8e 100644
--- a/superset/temporary_cache/commands/get.py
+++ b/superset/commands/temporary_cache/get.py
@@ -21,8 +21,8 @@ from typing import Optional
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.temporary_cache.commands.exceptions import TemporaryCacheGetFailedError
-from superset.temporary_cache.commands.parameters import CommandParameters
+from superset.commands.temporary_cache.exceptions import TemporaryCacheGetFailedError
+from superset.commands.temporary_cache.parameters import CommandParameters
logger = logging.getLogger(__name__)
diff --git a/superset/temporary_cache/commands/parameters.py b/superset/commands/temporary_cache/parameters.py
similarity index 100%
rename from superset/temporary_cache/commands/parameters.py
rename to superset/commands/temporary_cache/parameters.py
diff --git a/superset/temporary_cache/commands/update.py b/superset/commands/temporary_cache/update.py
similarity index 92%
rename from superset/temporary_cache/commands/update.py
rename to superset/commands/temporary_cache/update.py
index 92af8c14f2..90b1c3d48f 100644
--- a/superset/temporary_cache/commands/update.py
+++ b/superset/commands/temporary_cache/update.py
@@ -21,8 +21,8 @@ from typing import Optional
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand
-from superset.temporary_cache.commands.exceptions import TemporaryCacheUpdateFailedError
-from superset.temporary_cache.commands.parameters import CommandParameters
+from superset.commands.temporary_cache.exceptions import TemporaryCacheUpdateFailedError
+from superset.commands.temporary_cache.parameters import CommandParameters
logger = logging.getLogger(__name__)
diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index 7967313cd7..5b1414d53b 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -38,7 +38,7 @@ from superset.common.utils.time_range_utils import (
)
from superset.connectors.sqla.models import BaseDatasource
from superset.constants import CacheRegion, TimeGrain
-from superset.daos.annotation import AnnotationLayerDAO
+from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.chart import ChartDAO
from superset.exceptions import (
InvalidPostProcessingError,
@@ -682,7 +682,7 @@ class QueryContextProcessor:
annotation_layer: dict[str, Any], force: bool
) -> dict[str, Any]:
# pylint: disable=import-outside-toplevel
- from superset.charts.data.commands.get_data_command import ChartDataCommand
+ from superset.commands.chart.data.get_data_command import ChartDataCommand
if not (chart := ChartDAO.find_by_id(annotation_layer["value"])):
raise QueryObjectValidationError(_("The chart does not exist"))
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 3d1435dc7b..598bc6741b 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -75,6 +75,7 @@ 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 (
get_columns_description,
@@ -82,7 +83,6 @@ from superset.connectors.sqla.utils import (
get_virtual_table_metadata,
)
from superset.constants import EMPTY_STRING, NULL_STRING
-from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.db_engine_specs.base import BaseEngineSpec, TimestampExpression
from superset.exceptions import (
ColumnNotFoundException,
diff --git a/superset/css_templates/api.py b/superset/css_templates/api.py
index ee5d5fac70..25f4d50f30 100644
--- a/superset/css_templates/api.py
+++ b/superset/css_templates/api.py
@@ -22,12 +22,12 @@ from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.css_templates.commands.delete import DeleteCssTemplateCommand
-from superset.css_templates.commands.exceptions import (
+from superset.commands.css.delete import DeleteCssTemplateCommand
+from superset.commands.css.exceptions import (
CssTemplateDeleteFailedError,
CssTemplateNotFoundError,
)
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.css_templates.filters import CssTemplateAllTextFilter
from superset.css_templates.schemas import (
get_delete_ids_schema,
diff --git a/superset/daos/annotation.py b/superset/daos/annotation_layer.py
similarity index 100%
rename from superset/daos/annotation.py
rename to superset/daos/annotation_layer.py
diff --git a/superset/daos/dashboard.py b/superset/daos/dashboard.py
index 77f2dd9f34..b98252070d 100644
--- a/superset/daos/dashboard.py
+++ b/superset/daos/dashboard.py
@@ -25,12 +25,12 @@ from flask import g
from flask_appbuilder.models.sqla.interface import SQLAInterface
from superset import is_feature_enabled, security_manager
-from superset.daos.base import BaseDAO
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardAccessDeniedError,
DashboardForbiddenError,
DashboardNotFoundError,
)
+from superset.daos.base import BaseDAO
from superset.dashboards.filter_sets.consts import (
DASHBOARD_ID_FIELD,
DESCRIPTION_FIELD,
diff --git a/superset/daos/tag.py b/superset/daos/tag.py
index ba5311ed44..fbc9aa229e 100644
--- a/superset/daos/tag.py
+++ b/superset/daos/tag.py
@@ -21,6 +21,8 @@ from typing import Any, Optional
from flask import g
from sqlalchemy.exc import SQLAlchemyError
+from superset.commands.tag.exceptions import TagNotFoundError
+from superset.commands.tag.utils import to_object_type
from superset.daos.base import BaseDAO
from superset.daos.exceptions import DAOCreateFailedError, DAODeleteFailedError
from superset.exceptions import MissingUserContextException
@@ -28,8 +30,6 @@ from superset.extensions import db
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
-from superset.tags.commands.exceptions import TagNotFoundError
-from superset.tags.commands.utils import to_object_type
from superset.tags.models import (
get_tag,
ObjectType,
@@ -409,7 +409,9 @@ class TagDAO(BaseDAO[Tag]):
for object_type, object_id in tagged_objects_to_delete:
# delete objects that were removed
TagDAO.delete_tagged_object(
- object_type, object_id, tag.name # type: ignore
+ object_type, # type: ignore
+ object_id,
+ tag.name,
)
db.session.add_all(tagged_objects)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index b6ba689d83..be773b83c3 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -35,13 +35,9 @@ from werkzeug.wsgi import FileWrapper
from superset import is_feature_enabled, thumbnail_cache
from superset.charts.schemas import ChartEntityResponseSchema
-from superset.commands.importers.exceptions import NoValidFilesFoundError
-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.dashboard import DashboardDAO, EmbeddedDashboardDAO
-from superset.dashboards.commands.create import CreateDashboardCommand
-from superset.dashboards.commands.delete import DeleteDashboardCommand
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.create import CreateDashboardCommand
+from superset.commands.dashboard.delete import DeleteDashboardCommand
+from superset.commands.dashboard.exceptions import (
DashboardAccessDeniedError,
DashboardCreateFailedError,
DashboardDeleteFailedError,
@@ -50,9 +46,13 @@ from superset.dashboards.commands.exceptions import (
DashboardNotFoundError,
DashboardUpdateFailedError,
)
-from superset.dashboards.commands.export import ExportDashboardsCommand
-from superset.dashboards.commands.importers.dispatcher import ImportDashboardsCommand
-from superset.dashboards.commands.update import UpdateDashboardCommand
+from superset.commands.dashboard.export import ExportDashboardsCommand
+from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCommand
+from superset.commands.dashboard.update import UpdateDashboardCommand
+from superset.commands.importers.exceptions import NoValidFilesFoundError
+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.dashboard import DashboardDAO, EmbeddedDashboardDAO
from superset.dashboards.filters import (
DashboardAccessFilter,
DashboardCertifiedFilter,
diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py
index 5a2bf01923..ee7297ef4c 100644
--- a/superset/dashboards/filter_sets/api.py
+++ b/superset/dashboards/filter_sets/api.py
@@ -29,12 +29,10 @@ from flask_appbuilder.api import (
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
-from superset.commands.exceptions import ObjectNotFoundError
-from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand
-from superset.dashboards.filter_sets.commands.delete import DeleteFilterSetCommand
-from superset.dashboards.filter_sets.commands.exceptions import (
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.filter_set.create import CreateFilterSetCommand
+from superset.commands.dashboard.filter_set.delete import DeleteFilterSetCommand
+from superset.commands.dashboard.filter_set.exceptions import (
FilterSetCreateFailedError,
FilterSetDeleteFailedError,
FilterSetForbiddenError,
@@ -42,7 +40,9 @@ from superset.dashboards.filter_sets.commands.exceptions import (
FilterSetUpdateFailedError,
UserIsNotDashboardOwnerError,
)
-from superset.dashboards.filter_sets.commands.update import UpdateFilterSetCommand
+from superset.commands.dashboard.filter_set.update import UpdateFilterSetCommand
+from superset.commands.exceptions import ObjectNotFoundError
+from superset.daos.dashboard import DashboardDAO
from superset.dashboards.filter_sets.consts import (
DASHBOARD_FIELD,
DASHBOARD_ID_FIELD,
diff --git a/superset/dashboards/filter_state/api.py b/superset/dashboards/filter_state/api.py
index 9e0720646a..d3b6ce8f7a 100644
--- a/superset/dashboards/filter_state/api.py
+++ b/superset/dashboards/filter_state/api.py
@@ -19,10 +19,10 @@ import logging
from flask import Response
from flask_appbuilder.api import expose, protect, safe
-from superset.dashboards.filter_state.commands.create import CreateFilterStateCommand
-from superset.dashboards.filter_state.commands.delete import DeleteFilterStateCommand
-from superset.dashboards.filter_state.commands.get import GetFilterStateCommand
-from superset.dashboards.filter_state.commands.update import UpdateFilterStateCommand
+from superset.commands.dashboard.filter_state.create import CreateFilterStateCommand
+from superset.commands.dashboard.filter_state.delete import DeleteFilterStateCommand
+from superset.commands.dashboard.filter_state.get import GetFilterStateCommand
+from superset.commands.dashboard.filter_state.update import UpdateFilterStateCommand
from superset.extensions import event_logger
from superset.temporary_cache.api import TemporaryCacheRestApi
diff --git a/superset/dashboards/permalink/api.py b/superset/dashboards/permalink/api.py
index 0a786d1def..a6ae2910f4 100644
--- a/superset/dashboards/permalink/api.py
+++ b/superset/dashboards/permalink/api.py
@@ -20,15 +20,13 @@ from flask import request, Response
from flask_appbuilder.api import expose, protect, safe
from marshmallow import ValidationError
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.dashboards.commands.exceptions import (
+from superset.commands.dashboard.exceptions import (
DashboardAccessDeniedError,
DashboardNotFoundError,
)
-from superset.dashboards.permalink.commands.create import (
- CreateDashboardPermalinkCommand,
-)
-from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
+from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
+from superset.commands.dashboard.permalink.get import GetDashboardPermalinkCommand
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.dashboards.permalink.exceptions import DashboardPermalinkInvalidStateError
from superset.dashboards.permalink.schemas import DashboardPermalinkStateSchema
from superset.extensions import event_logger
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 116e2ddb1f..df69d9ccd7 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -29,16 +29,9 @@ from marshmallow import ValidationError
from sqlalchemy.exc import NoSuchTableError, OperationalError, SQLAlchemyError
from superset import app, event_logger
-from superset.commands.importers.exceptions import (
- IncorrectFormatError,
- NoValidFilesFoundError,
-)
-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.database import DatabaseDAO
-from superset.databases.commands.create import CreateDatabaseCommand
-from superset.databases.commands.delete import DeleteDatabaseCommand
-from superset.databases.commands.exceptions import (
+from superset.commands.database.create import CreateDatabaseCommand
+from superset.commands.database.delete import DeleteDatabaseCommand
+from superset.commands.database.exceptions import (
DatabaseConnectionFailedError,
DatabaseCreateFailedError,
DatabaseDeleteDatasetsExistFailedError,
@@ -49,13 +42,26 @@ from superset.databases.commands.exceptions import (
DatabaseUpdateFailedError,
InvalidParametersError,
)
-from superset.databases.commands.export import ExportDatabasesCommand
-from superset.databases.commands.importers.dispatcher import ImportDatabasesCommand
-from superset.databases.commands.tables import TablesDatabaseCommand
-from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
-from superset.databases.commands.update import UpdateDatabaseCommand
-from superset.databases.commands.validate import ValidateDatabaseParametersCommand
-from superset.databases.commands.validate_sql import ValidateSQLCommand
+from superset.commands.database.export import ExportDatabasesCommand
+from superset.commands.database.importers.dispatcher import ImportDatabasesCommand
+from superset.commands.database.ssh_tunnel.delete import DeleteSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.exceptions import (
+ SSHTunnelDeleteFailedError,
+ SSHTunnelingNotEnabledError,
+ SSHTunnelNotFoundError,
+)
+from superset.commands.database.tables import TablesDatabaseCommand
+from superset.commands.database.test_connection import TestConnectionDatabaseCommand
+from superset.commands.database.update import UpdateDatabaseCommand
+from superset.commands.database.validate import ValidateDatabaseParametersCommand
+from superset.commands.database.validate_sql import ValidateSQLCommand
+from superset.commands.importers.exceptions import (
+ IncorrectFormatError,
+ NoValidFilesFoundError,
+)
+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.database import DatabaseDAO
from superset.databases.decorators import check_datasource_access
from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
from superset.databases.schemas import (
@@ -79,12 +85,6 @@ from superset.databases.schemas import (
ValidateSQLRequest,
ValidateSQLResponse,
)
-from superset.databases.ssh_tunnel.commands.delete import DeleteSSHTunnelCommand
-from superset.databases.ssh_tunnel.commands.exceptions import (
- SSHTunnelDeleteFailedError,
- SSHTunnelingNotEnabledError,
- SSHTunnelNotFoundError,
-)
from superset.databases.utils import get_table_metadata
from superset.db_engine_specs import get_available_engine_specs
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index abba9036a1..ac4d0e127d 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -28,13 +28,13 @@ from marshmallow.validate import Length, ValidationError
from sqlalchemy import MetaData
from superset import db, is_feature_enabled
-from superset.constants import PASSWORD_MASK
-from superset.databases.commands.exceptions import DatabaseInvalidError
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.exceptions import DatabaseInvalidError
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelingNotEnabledError,
SSHTunnelInvalidCredentials,
SSHTunnelMissingCredentials,
)
+from superset.constants import PASSWORD_MASK
from superset.databases.utils import make_url_safe
from superset.db_engine_specs import get_engine_spec
from superset.exceptions import CertificateException, SupersetSecurityException
diff --git a/superset/databases/utils.py b/superset/databases/utils.py
index fa163e4d9e..21abd7b9c2 100644
--- a/superset/databases/utils.py
+++ b/superset/databases/utils.py
@@ -18,7 +18,7 @@ from typing import Any, Optional, Union
from sqlalchemy.engine.url import make_url, URL
-from superset.databases.commands.exceptions import DatabaseInvalidError
+from superset.commands.database.exceptions import DatabaseInvalidError
def get_foreign_keys_metadata(
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index e1d7e5a09e..e256ff99d6 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -30,17 +30,10 @@ from flask_babel import ngettext
from marshmallow import ValidationError
from superset import event_logger, is_feature_enabled
-from superset.commands.exceptions import CommandException
-from superset.commands.importers.exceptions import NoValidFilesFoundError
-from superset.commands.importers.v1.utils import get_contents_from_bundle
-from superset.connectors.sqla.models import SqlaTable
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.daos.dataset import DatasetDAO
-from superset.databases.filters import DatabaseFilter
-from superset.datasets.commands.create import CreateDatasetCommand
-from superset.datasets.commands.delete import DeleteDatasetCommand
-from superset.datasets.commands.duplicate import DuplicateDatasetCommand
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.create import CreateDatasetCommand
+from superset.commands.dataset.delete import DeleteDatasetCommand
+from superset.commands.dataset.duplicate import DuplicateDatasetCommand
+from superset.commands.dataset.exceptions import (
DatasetCreateFailedError,
DatasetDeleteFailedError,
DatasetForbiddenError,
@@ -49,11 +42,18 @@ from superset.datasets.commands.exceptions import (
DatasetRefreshFailedError,
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.commands.warm_up_cache import DatasetWarmUpCacheCommand
+from superset.commands.dataset.export import ExportDatasetsCommand
+from superset.commands.dataset.importers.dispatcher import ImportDatasetsCommand
+from superset.commands.dataset.refresh import RefreshDatasetCommand
+from superset.commands.dataset.update import UpdateDatasetCommand
+from superset.commands.dataset.warm_up_cache import DatasetWarmUpCacheCommand
+from superset.commands.exceptions import CommandException
+from superset.commands.importers.exceptions import NoValidFilesFoundError
+from superset.commands.importers.v1.utils import get_contents_from_bundle
+from superset.connectors.sqla.models import SqlaTable
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
+from superset.daos.dataset import DatasetDAO
+from superset.databases.filters import DatabaseFilter
from superset.datasets.filters import DatasetCertifiedFilter, DatasetIsNullOrEmptyFilter
from superset.datasets.schemas import (
DatasetCacheWarmUpRequestSchema,
diff --git a/superset/datasets/columns/api.py b/superset/datasets/columns/api.py
index 0aafab5d39..90de0f7750 100644
--- a/superset/datasets/columns/api.py
+++ b/superset/datasets/columns/api.py
@@ -20,14 +20,14 @@ from flask import Response
from flask_appbuilder.api import expose, permission_name, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
-from superset.connectors.sqla.models import TableColumn
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.datasets.columns.commands.delete import DeleteDatasetColumnCommand
-from superset.datasets.columns.commands.exceptions import (
+from superset.commands.dataset.columns.delete import DeleteDatasetColumnCommand
+from superset.commands.dataset.columns.exceptions import (
DatasetColumnDeleteFailedError,
DatasetColumnForbiddenError,
DatasetColumnNotFoundError,
)
+from superset.connectors.sqla.models import TableColumn
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
logger = logging.getLogger(__name__)
diff --git a/superset/datasets/metrics/api.py b/superset/datasets/metrics/api.py
index 28ec9474e2..aa29254fc0 100644
--- a/superset/datasets/metrics/api.py
+++ b/superset/datasets/metrics/api.py
@@ -20,14 +20,14 @@ from flask import Response
from flask_appbuilder.api import expose, permission_name, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
-from superset.connectors.sqla.models import TableColumn
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.datasets.metrics.commands.delete import DeleteDatasetMetricCommand
-from superset.datasets.metrics.commands.exceptions import (
+from superset.commands.dataset.metrics.delete import DeleteDatasetMetricCommand
+from superset.commands.dataset.metrics.exceptions import (
DatasetMetricDeleteFailedError,
DatasetMetricForbiddenError,
DatasetMetricNotFoundError,
)
+from superset.connectors.sqla.models import TableColumn
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
logger = logging.getLogger(__name__)
diff --git a/superset/embedded/api.py b/superset/embedded/api.py
index ae800bf2b9..b907422bf5 100644
--- a/superset/embedded/api.py
+++ b/superset/embedded/api.py
@@ -23,12 +23,12 @@ 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.embedded_dashboard.commands.exceptions import (
- EmbeddedDashboardNotFoundError,
-)
from superset.extensions import event_logger
from superset.models.embedded_dashboard import EmbeddedDashboard
from superset.reports.logs.schemas import openapi_spec_methods_override
diff --git a/superset/explore/api.py b/superset/explore/api.py
index ebda161bea..faadbe8d9a 100644
--- a/superset/explore/api.py
+++ b/superset/explore/api.py
@@ -19,18 +19,18 @@ import logging
from flask import g, request, Response
from flask_appbuilder.api import expose, protect, safe
-from superset.charts.commands.exceptions import ChartNotFoundError
+from superset.commands.chart.exceptions import ChartNotFoundError
+from superset.commands.explore.get import GetExploreCommand
+from superset.commands.explore.parameters import CommandParameters
+from superset.commands.temporary_cache.exceptions import (
+ TemporaryCacheAccessDeniedError,
+ TemporaryCacheResourceNotFoundError,
+)
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.explore.commands.get import GetExploreCommand
-from superset.explore.commands.parameters import CommandParameters
from superset.explore.exceptions import DatasetAccessDeniedError, WrongEndpointError
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.explore.schemas import ExploreContextSchema
from superset.extensions import event_logger
-from superset.temporary_cache.commands.exceptions import (
- TemporaryCacheAccessDeniedError,
- TemporaryCacheResourceNotFoundError,
-)
from superset.views.base_api import BaseSupersetApi, statsd_metrics
logger = logging.getLogger(__name__)
diff --git a/superset/explore/form_data/api.py b/superset/explore/form_data/api.py
index 36489ca449..6c882d92a6 100644
--- a/superset/explore/form_data/api.py
+++ b/superset/explore/form_data/api.py
@@ -20,18 +20,18 @@ from flask import request, Response
from flask_appbuilder.api import expose, protect, safe
from marshmallow import ValidationError
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.explore.form_data.commands.create import CreateFormDataCommand
-from superset.explore.form_data.commands.delete import DeleteFormDataCommand
-from superset.explore.form_data.commands.get import GetFormDataCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.update import UpdateFormDataCommand
-from superset.explore.form_data.schemas import FormDataPostSchema, FormDataPutSchema
-from superset.extensions import event_logger
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.explore.form_data.create import CreateFormDataCommand
+from superset.commands.explore.form_data.delete import DeleteFormDataCommand
+from superset.commands.explore.form_data.get import GetFormDataCommand
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.form_data.update import UpdateFormDataCommand
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheResourceNotFoundError,
)
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
+from superset.explore.form_data.schemas import FormDataPostSchema, FormDataPutSchema
+from superset.extensions import event_logger
from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics
logger = logging.getLogger(__name__)
diff --git a/superset/explore/permalink/api.py b/superset/explore/permalink/api.py
index b249d4dee2..bc9bd1cf67 100644
--- a/superset/explore/permalink/api.py
+++ b/superset/explore/permalink/api.py
@@ -20,17 +20,17 @@ from flask import request, Response
from flask_appbuilder.api import expose, protect, safe
from marshmallow import ValidationError
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
)
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetAccessDeniedError,
DatasetNotFoundError,
)
-from superset.explore.permalink.commands.create import CreateExplorePermalinkCommand
-from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
+from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.explore.permalink.exceptions import ExplorePermalinkInvalidStateError
from superset.explore.permalink.schemas import ExplorePermalinkStateSchema
from superset.extensions import event_logger
diff --git a/superset/explore/utils.py b/superset/explore/utils.py
index ca73cb39fb..7d5c0d86be 100644
--- a/superset/explore/utils.py
+++ b/superset/explore/utils.py
@@ -17,10 +17,14 @@
from typing import Optional
from superset import security_manager
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
)
+from superset.commands.dataset.exceptions import (
+ DatasetAccessDeniedError,
+ DatasetNotFoundError,
+)
from superset.commands.exceptions import (
DatasourceNotFoundValidationError,
DatasourceTypeInvalidError,
@@ -29,10 +33,6 @@ from superset.commands.exceptions import (
from superset.daos.chart import ChartDAO
from superset.daos.dataset import DatasetDAO
from superset.daos.query import QueryDAO
-from superset.datasets.commands.exceptions import (
- DatasetAccessDeniedError,
- DatasetNotFoundError,
-)
from superset.utils.core import DatasourceType
diff --git a/superset/extensions/metastore_cache.py b/superset/extensions/metastore_cache.py
index b6effdfe91..435c38ced8 100644
--- a/superset/extensions/metastore_cache.py
+++ b/superset/extensions/metastore_cache.py
@@ -71,7 +71,7 @@ class SupersetMetastoreCache(BaseCache):
@staticmethod
def _prune() -> None:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.delete_expired import (
+ from superset.commands.key_value.delete_expired import (
DeleteExpiredKeyValueCommand,
)
@@ -85,7 +85,7 @@ class SupersetMetastoreCache(BaseCache):
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.upsert import UpsertKeyValueCommand
+ from superset.commands.key_value.upsert import UpsertKeyValueCommand
UpsertKeyValueCommand(
resource=RESOURCE,
@@ -98,7 +98,7 @@ class SupersetMetastoreCache(BaseCache):
def add(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
try:
CreateKeyValueCommand(
@@ -115,7 +115,7 @@ class SupersetMetastoreCache(BaseCache):
def get(self, key: str) -> Any:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
return GetKeyValueCommand(
resource=RESOURCE,
@@ -131,6 +131,6 @@ class SupersetMetastoreCache(BaseCache):
def delete(self, key: str) -> Any:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.delete import DeleteKeyValueCommand
+ from superset.commands.key_value.delete import DeleteKeyValueCommand
return DeleteKeyValueCommand(resource=RESOURCE, key=self.get_key(key)).run()
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 13a639df7b..3b046b732e 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -30,8 +30,8 @@ from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.sql.expression import bindparam
from sqlalchemy.types import String
+from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.constants import LRU_CACHE_MAX_SIZE
-from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.exceptions import SupersetTemplateException
from superset.extensions import feature_flag_manager
from superset.utils.core import (
diff --git a/superset/key_value/shared_entries.py b/superset/key_value/shared_entries.py
index 7895b75907..130313157a 100644
--- a/superset/key_value/shared_entries.py
+++ b/superset/key_value/shared_entries.py
@@ -28,7 +28,7 @@ CODEC = JsonKeyValueCodec()
def get_shared_value(key: SharedKey) -> Optional[Any]:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
uuid_key = uuid3(NAMESPACE, key)
return GetKeyValueCommand(RESOURCE, key=uuid_key, codec=CODEC).run()
@@ -36,7 +36,7 @@ def get_shared_value(key: SharedKey) -> Optional[Any]:
def set_shared_value(key: SharedKey, value: Any) -> None:
# pylint: disable=import-outside-toplevel
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
uuid_key = uuid3(NAMESPACE, key)
CreateKeyValueCommand(
diff --git a/superset/models/core.py b/superset/models/core.py
index d2b38ea806..eece661ec5 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -59,8 +59,8 @@ from sqlalchemy.schema import UniqueConstraint
from sqlalchemy.sql import ColumnElement, expression, Select
from superset import app, db_engine_specs
+from superset.commands.database.exceptions import DatabaseInvalidError
from superset.constants import LRU_CACHE_MAX_SIZE, PASSWORD_MASK
-from superset.databases.commands.exceptions import DatabaseInvalidError
from superset.databases.utils import make_url_safe
from superset.db_engine_specs.base import MetricType, TimeGrain
from superset.extensions import (
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index 69e1a6191b..25ac520e45 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -32,19 +32,17 @@ from superset.commands.importers.exceptions import (
NoValidFilesFoundError,
)
from superset.commands.importers.v1.utils import get_contents_from_bundle
+from superset.commands.query.delete import DeleteSavedQueryCommand
+from superset.commands.query.exceptions import (
+ SavedQueryDeleteFailedError,
+ SavedQueryNotFoundError,
+)
+from superset.commands.query.export import ExportSavedQueriesCommand
+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.models.sql_lab import SavedQuery
-from superset.queries.saved_queries.commands.delete import DeleteSavedQueryCommand
-from superset.queries.saved_queries.commands.exceptions import (
- SavedQueryDeleteFailedError,
- SavedQueryNotFoundError,
-)
-from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand
-from superset.queries.saved_queries.commands.importers.dispatcher import (
- ImportSavedQueriesCommand,
-)
from superset.queries.saved_queries.filters import (
SavedQueryAllTextFilter,
SavedQueryFavoriteFilter,
diff --git a/superset/reports/api.py b/superset/reports/api.py
index 3116aef3b8..ab4f80ae15 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -26,13 +26,9 @@ from marshmallow import ValidationError
from superset import is_feature_enabled
from superset.charts.filters import ChartFilter
-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.reports.commands.create import CreateReportScheduleCommand
-from superset.reports.commands.delete import DeleteReportScheduleCommand
-from superset.reports.commands.exceptions import (
+from superset.commands.report.create import CreateReportScheduleCommand
+from superset.commands.report.delete import DeleteReportScheduleCommand
+from superset.commands.report.exceptions import (
ReportScheduleCreateFailedError,
ReportScheduleDeleteFailedError,
ReportScheduleForbiddenError,
@@ -40,7 +36,11 @@ from superset.reports.commands.exceptions import (
ReportScheduleNotFoundError,
ReportScheduleUpdateFailedError,
)
-from superset.reports.commands.update import UpdateReportScheduleCommand
+from superset.commands.report.update import UpdateReportScheduleCommand
+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.reports.filters import ReportScheduleAllTextFilter, ReportScheduleFilter
from superset.reports.models import ReportSchedule
from superset.reports.schemas import (
diff --git a/superset/row_level_security/api.py b/superset/row_level_security/api.py
index 0a823f74d6..e7347f5280 100644
--- a/superset/row_level_security/api.py
+++ b/superset/row_level_security/api.py
@@ -28,14 +28,14 @@ from superset.commands.exceptions import (
DatasourceNotFoundValidationError,
RolesNotFoundValidationError,
)
+from superset.commands.security.create import CreateRLSRuleCommand
+from superset.commands.security.delete import DeleteRLSRuleCommand
+from superset.commands.security.exceptions import RLSRuleNotFoundError
+from superset.commands.security.update import UpdateRLSRuleCommand
from superset.connectors.sqla.models import RowLevelSecurityFilter
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError
from superset.extensions import event_logger
-from superset.row_level_security.commands.create import CreateRLSRuleCommand
-from superset.row_level_security.commands.delete import DeleteRLSRuleCommand
-from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
-from superset.row_level_security.commands.update import UpdateRLSRuleCommand
from superset.row_level_security.schemas import (
get_delete_ids_schema,
openapi_spec_methods_override,
diff --git a/superset/security/api.py b/superset/security/api.py
index b4a3069759..acafc32570 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -24,7 +24,7 @@ from flask_appbuilder.security.decorators import permission_name, protect
from flask_wtf.csrf import generate_csrf
from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
-from superset.embedded_dashboard.commands.exceptions import (
+from superset.commands.dashboard.embedded.exceptions import (
EmbeddedDashboardNotFoundError,
)
from superset.extensions import event_logger
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 03f0ee56cf..5eb1afdda9 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -2153,10 +2153,10 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
@staticmethod
def validate_guest_token_resources(resources: GuestTokenResources) -> None:
# pylint: disable=import-outside-toplevel
- from superset.daos.dashboard import EmbeddedDashboardDAO
- from superset.embedded_dashboard.commands.exceptions import (
+ from superset.commands.dashboard.embedded.exceptions import (
EmbeddedDashboardNotFoundError,
)
+ from superset.daos.dashboard import EmbeddedDashboardDAO
from superset.models.dashboard import Dashboard
for resource in resources:
diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py
index 16070b52cc..6be378a9b5 100644
--- a/superset/sqllab/api.py
+++ b/superset/sqllab/api.py
@@ -27,6 +27,10 @@ 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
+from superset.commands.sql_lab.results import SqlExecutionResultsCommand
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.daos.database import DatabaseDAO
from superset.daos.query import QueryDAO
@@ -35,10 +39,6 @@ from superset.jinja_context import get_template_processor
from superset.models.sql_lab import Query
from superset.sql_lab import get_sql_results
from superset.sqllab.command_status import SqlJsonExecutionStatus
-from superset.sqllab.commands.estimate import QueryEstimationCommand
-from superset.sqllab.commands.execute import CommandResult, ExecuteSqlCommand
-from superset.sqllab.commands.export import SqlResultExportCommand
-from superset.sqllab.commands.results import SqlExecutionResultsCommand
from superset.sqllab.exceptions import (
QueryIsForbiddenToAccessException,
SqlLabException,
diff --git a/superset/sqllab/query_render.py b/superset/sqllab/query_render.py
index 4fb64c8ce2..f4c1c26c6e 100644
--- a/superset/sqllab/query_render.py
+++ b/superset/sqllab/query_render.py
@@ -24,9 +24,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.sql_parse import ParsedQuery
-from superset.sqllab.commands.execute import SqlQueryRender
from superset.sqllab.exceptions import SqlLabException
from superset.utils import core as utils
diff --git a/superset/sqllab/validators.py b/superset/sqllab/validators.py
index 5bc8a62253..b79789da4c 100644
--- a/superset/sqllab/validators.py
+++ b/superset/sqllab/validators.py
@@ -20,7 +20,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from superset import security_manager
-from superset.sqllab.commands.execute import CanAccessQueryValidator
+from superset.commands.sql_lab.execute import CanAccessQueryValidator
if TYPE_CHECKING:
from superset.models.sql_lab import Query
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 8e01fd240f..a4fc185f29 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -22,16 +22,12 @@ from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.daos.tag import TagDAO
-from superset.exceptions import MissingUserContextException
-from superset.extensions import event_logger
-from superset.tags.commands.create import (
+from superset.commands.tag.create import (
CreateCustomTagCommand,
CreateCustomTagWithRelationshipsCommand,
)
-from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
-from superset.tags.commands.exceptions import (
+from superset.commands.tag.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
+from superset.commands.tag.exceptions import (
TagDeleteFailedError,
TaggedObjectDeleteFailedError,
TaggedObjectNotFoundError,
@@ -39,7 +35,11 @@ from superset.tags.commands.exceptions import (
TagNotFoundError,
TagUpdateFailedError,
)
-from superset.tags.commands.update import UpdateTagCommand
+from superset.commands.tag.update import UpdateTagCommand
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
+from superset.daos.tag import TagDAO
+from superset.exceptions import MissingUserContextException
+from superset.extensions import event_logger
from superset.tags.models import ObjectType, Tag
from superset.tags.schemas import (
delete_tags_schema,
diff --git a/superset/tasks/async_queries.py b/superset/tasks/async_queries.py
index 609af3bc8e..61970ca1f3 100644
--- a/superset/tasks/async_queries.py
+++ b/superset/tasks/async_queries.py
@@ -64,7 +64,7 @@ def load_chart_data_into_cache(
form_data: dict[str, Any],
) -> None:
# pylint: disable=import-outside-toplevel
- from superset.charts.data.commands.get_data_command import ChartDataCommand
+ from superset.commands.chart.data.get_data_command import ChartDataCommand
user = (
security_manager.get_user_by_id(job_metadata.get("user_id"))
diff --git a/superset/tasks/scheduler.py b/superset/tasks/scheduler.py
index f3cc270b86..7b1350a07d 100644
--- a/superset/tasks/scheduler.py
+++ b/superset/tasks/scheduler.py
@@ -22,11 +22,11 @@ from celery.exceptions import SoftTimeLimitExceeded
from superset import app, is_feature_enabled
from superset.commands.exceptions import CommandException
+from superset.commands.report.exceptions import ReportScheduleUnexpectedError
+from superset.commands.report.execute import AsyncExecuteReportScheduleCommand
+from superset.commands.report.log_prune import AsyncPruneReportScheduleLogCommand
from superset.daos.report import ReportScheduleDAO
from superset.extensions import celery_app
-from superset.reports.commands.exceptions import ReportScheduleUnexpectedError
-from superset.reports.commands.execute import AsyncExecuteReportScheduleCommand
-from superset.reports.commands.log_prune import AsyncPruneReportScheduleLogCommand
from superset.stats_logger import BaseStatsLogger
from superset.tasks.cron_util import cron_schedule_window
from superset.utils.celery import session_scope
diff --git a/superset/temporary_cache/api.py b/superset/temporary_cache/api.py
index 0ecab44bf1..5dc95c122a 100644
--- a/superset/temporary_cache/api.py
+++ b/superset/temporary_cache/api.py
@@ -24,13 +24,13 @@ from apispec.exceptions import DuplicateComponentNameError
from flask import request, Response
from marshmallow import ValidationError
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
-from superset.key_value.types import JsonKeyValueCodec
-from superset.temporary_cache.commands.exceptions import (
+from superset.commands.temporary_cache.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheResourceNotFoundError,
)
-from superset.temporary_cache.commands.parameters import CommandParameters
+from superset.commands.temporary_cache.parameters import CommandParameters
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
+from superset.key_value.types import JsonKeyValueCodec
from superset.temporary_cache.schemas import (
TemporaryCachePostSchema,
TemporaryCachePutSchema,
diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py
index 438e379a96..2d49424a82 100644
--- a/superset/utils/date_parser.py
+++ b/superset/utils/date_parser.py
@@ -41,7 +41,7 @@ from pyparsing import (
Suppress,
)
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
TimeDeltaAmbiguousError,
TimeRangeAmbiguousError,
TimeRangeParseFailError,
diff --git a/superset/views/api.py b/superset/views/api.py
index 312efb947e..eeedd7c641 100644
--- a/superset/views/api.py
+++ b/superset/views/api.py
@@ -26,7 +26,7 @@ from flask_appbuilder.security.decorators import has_access_api
from flask_babel import lazy_gettext as _
from superset import db, event_logger
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
TimeRangeAmbiguousError,
TimeRangeParseFailError,
)
diff --git a/superset/views/core.py b/superset/views/core.py
index 28d84d223f..9ad2f63fdc 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -44,26 +44,26 @@ from superset import (
security_manager,
)
from superset.async_events.async_query_manager import AsyncQueryTokenException
-from superset.charts.commands.exceptions import ChartNotFoundError
-from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
+from superset.commands.chart.exceptions import ChartNotFoundError
+from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
+from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand
+from superset.commands.dashboard.permalink.get import GetDashboardPermalinkCommand
+from superset.commands.dataset.exceptions import DatasetNotFoundError
+from superset.commands.explore.form_data.create import CreateFormDataCommand
+from superset.commands.explore.form_data.get import GetFormDataCommand
+from superset.commands.explore.form_data.parameters import CommandParameters
+from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.chart import ChartDAO
from superset.daos.datasource import DatasourceDAO
-from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
-from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
-from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.exceptions import (
CacheLoadError,
DatabaseNotFound,
SupersetException,
SupersetSecurityException,
)
-from superset.explore.form_data.commands.create import CreateFormDataCommand
-from superset.explore.form_data.commands.get import GetFormDataCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.extensions import async_query_manager, cache_manager
from superset.models.core import Database
diff --git a/superset/views/database/validators.py b/superset/views/database/validators.py
index 2ee49c8210..e4fef3446c 100644
--- a/superset/views/database/validators.py
+++ b/superset/views/database/validators.py
@@ -21,7 +21,7 @@ from flask_babel import lazy_gettext as _
from marshmallow import ValidationError
from superset import security_manager
-from superset.databases.commands.exceptions import DatabaseInvalidError
+from superset.commands.database.exceptions import DatabaseInvalidError
from superset.databases.utils import make_url_safe
from superset.models.core import Database
diff --git a/superset/views/datasource/utils.py b/superset/views/datasource/utils.py
index afed5f7fd2..61b7cc85bc 100644
--- a/superset/views/datasource/utils.py
+++ b/superset/views/datasource/utils.py
@@ -17,12 +17,12 @@
from typing import Any, Optional
from superset import app, db
+from superset.commands.dataset.exceptions import DatasetSamplesFailedError
from superset.common.chart_data import ChartDataResultType
from superset.common.query_context_factory import QueryContextFactory
from superset.common.utils.query_cache_manager import QueryCacheManager
from superset.constants import CacheRegion
from superset.daos.datasource import DatasourceDAO
-from superset.datasets.commands.exceptions import DatasetSamplesFailedError
from superset.utils.core import QueryStatus
from superset.views.datasource.schemas import SamplesPayloadSchema
diff --git a/superset/views/datasource/views.py b/superset/views/datasource/views.py
index 56acbd8580..a4c158a11f 100644
--- a/superset/views/datasource/views.py
+++ b/superset/views/datasource/views.py
@@ -28,14 +28,14 @@ from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.orm.exc import NoResultFound
from superset import db, event_logger, security_manager
+from superset.commands.dataset.exceptions import (
+ DatasetForbiddenError,
+ DatasetNotFoundError,
+)
from superset.commands.utils import populate_owners
from superset.connectors.sqla.models import SqlaTable
from superset.connectors.sqla.utils import get_physical_table_metadata
from superset.daos.datasource import DatasourceDAO
-from superset.datasets.commands.exceptions import (
- DatasetForbiddenError,
- DatasetNotFoundError,
-)
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.models.core import Database
from superset.superset_typing import FlaskResponse
diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py
index ae64eba807..fe489a0f36 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -28,8 +28,8 @@ from parameterized import parameterized
from sqlalchemy import and_
from sqlalchemy.sql import func
-from superset.charts.commands.exceptions import ChartDataQueryFailedError
-from superset.charts.data.commands.get_data_command import ChartDataCommand
+from superset.commands.chart.data.get_data_command import ChartDataCommand
+from superset.commands.chart.exceptions import ChartDataQueryFailedError
from superset.connectors.sqla.models import SqlaTable
from superset.extensions import cache_manager, db, security_manager
from superset.models.core import Database, FavStar, FavStarClassName
diff --git a/tests/integration_tests/charts/commands_tests.py b/tests/integration_tests/charts/commands_tests.py
index f9785a4dd6..87c7823ae5 100644
--- a/tests/integration_tests/charts/commands_tests.py
+++ b/tests/integration_tests/charts/commands_tests.py
@@ -22,15 +22,15 @@ import yaml
from flask import g
from superset import db, security_manager
-from superset.charts.commands.create import CreateChartCommand
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.create import CreateChartCommand
+from superset.commands.chart.exceptions import (
ChartNotFoundError,
WarmUpCacheChartNotFoundError,
)
-from superset.charts.commands.export import ExportChartsCommand
-from superset.charts.commands.importers.v1 import ImportChartsCommand
-from superset.charts.commands.update import UpdateChartCommand
-from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
+from superset.commands.chart.export import ExportChartsCommand
+from superset.commands.chart.importers.v1 import ImportChartsCommand
+from superset.commands.chart.update import UpdateChartCommand
+from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
from superset.commands.exceptions import CommandInvalidError
from superset.commands.importers.exceptions import IncorrectVersionError
from superset.connectors.sqla.models import SqlaTable
@@ -171,7 +171,7 @@ class TestExportChartsCommand(SupersetTestCase):
class TestImportChartsCommand(SupersetTestCase):
- @patch("superset.charts.commands.importers.v1.utils.g")
+ @patch("superset.commands.chart.importers.v1.utils.g")
@patch("superset.security.manager.g")
def test_import_v1_chart(self, sm_g, utils_g):
"""Test that we can import a chart"""
@@ -324,7 +324,7 @@ class TestImportChartsCommand(SupersetTestCase):
class TestChartsCreateCommand(SupersetTestCase):
@patch("superset.utils.core.g")
- @patch("superset.charts.commands.create.g")
+ @patch("superset.commands.chart.create.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_create_v1_response(self, mock_sm_g, mock_c_g, mock_u_g):
@@ -354,7 +354,7 @@ class TestChartsCreateCommand(SupersetTestCase):
class TestChartsUpdateCommand(SupersetTestCase):
- @patch("superset.charts.commands.update.g")
+ @patch("superset.commands.chart.update.g")
@patch("superset.utils.core.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py
index 5a62ce0a82..4def03ff4e 100644
--- a/tests/integration_tests/charts/data/api_tests.py
+++ b/tests/integration_tests/charts/data/api_tests.py
@@ -42,7 +42,7 @@ from tests.integration_tests.fixtures.energy_dashboard import (
import pytest
from superset.models.slice import Slice
-from superset.charts.data.commands.get_data_command import ChartDataCommand
+from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.connectors.sqla.models import TableColumn, SqlaTable
from superset.errors import SupersetErrorType
from superset.extensions import async_query_manager_factory, db
diff --git a/tests/integration_tests/cli_tests.py b/tests/integration_tests/cli_tests.py
index f9195a6c26..55557ab32d 100644
--- a/tests/integration_tests/cli_tests.py
+++ b/tests/integration_tests/cli_tests.py
@@ -137,7 +137,7 @@ def test_export_dashboards_versioned_export(app_context, fs):
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
@mock.patch(
- "superset.dashboards.commands.export.ExportDashboardsCommand.run",
+ "superset.commands.dashboard.export.ExportDashboardsCommand.run",
side_effect=Exception(),
)
def test_failing_export_dashboards_versioned_export(
@@ -191,7 +191,7 @@ def test_export_datasources_versioned_export(app_context, fs):
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
@mock.patch(
- "superset.dashboards.commands.export.ExportDatasetsCommand.run",
+ "superset.commands.dashboard.export.ExportDatasetsCommand.run",
side_effect=Exception(),
)
def test_failing_export_datasources_versioned_export(
@@ -217,7 +217,7 @@ def test_failing_export_datasources_versioned_export(
@mock.patch.dict(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
-@mock.patch("superset.dashboards.commands.importers.dispatcher.ImportDashboardsCommand")
+@mock.patch("superset.commands.dashboard.importers.dispatcher.ImportDashboardsCommand")
def test_import_dashboards_versioned_export(import_dashboards_command, app_context, fs):
"""
Test that both ZIP and JSON can be imported.
@@ -261,7 +261,7 @@ def test_import_dashboards_versioned_export(import_dashboards_command, app_conte
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
@mock.patch(
- "superset.dashboards.commands.importers.dispatcher.ImportDashboardsCommand.run",
+ "superset.commands.dashboard.importers.dispatcher.ImportDashboardsCommand.run",
side_effect=Exception(),
)
def test_failing_import_dashboards_versioned_export(
@@ -304,7 +304,7 @@ def test_failing_import_dashboards_versioned_export(
@mock.patch.dict(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
-@mock.patch("superset.datasets.commands.importers.dispatcher.ImportDatasetsCommand")
+@mock.patch("superset.commands.dataset.importers.dispatcher.ImportDatasetsCommand")
def test_import_datasets_versioned_export(import_datasets_command, app_context, fs):
"""
Test that both ZIP and YAML can be imported.
@@ -347,7 +347,7 @@ def test_import_datasets_versioned_export(import_datasets_command, app_context,
@mock.patch.dict(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True
)
-@mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand")
+@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand")
def test_import_datasets_sync_argument_columns_metrics(
import_datasets_command, app_context, fs
):
@@ -384,7 +384,7 @@ def test_import_datasets_sync_argument_columns_metrics(
@mock.patch.dict(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True
)
-@mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand")
+@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand")
def test_import_datasets_sync_argument_columns(
import_datasets_command, app_context, fs
):
@@ -421,7 +421,7 @@ def test_import_datasets_sync_argument_columns(
@mock.patch.dict(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True
)
-@mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand")
+@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand")
def test_import_datasets_sync_argument_metrics(
import_datasets_command, app_context, fs
):
@@ -459,7 +459,7 @@ def test_import_datasets_sync_argument_metrics(
"superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True
)
@mock.patch(
- "superset.datasets.commands.importers.dispatcher.ImportDatasetsCommand.run",
+ "superset.commands.dataset.importers.dispatcher.ImportDatasetsCommand.run",
side_effect=Exception(),
)
def test_failing_import_datasets_versioned_export(
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index f83d1b01ce..25f8e624ec 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -35,8 +35,8 @@ from sqlalchemy.exc import SQLAlchemyError
import superset.utils.database
import superset.views.utils
from superset import dataframe, db, security_manager, sql_lab
-from superset.charts.commands.exceptions import ChartDataQueryFailedError
-from superset.charts.data.commands.get_data_command import ChartDataCommand
+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
from superset.connectors.sqla.models import SqlaTable
from superset.db_engine_specs.base import BaseEngineSpec
@@ -1165,7 +1165,7 @@ class TestCore(SupersetTestCase):
self.assertIn("Error message", data)
@pytest.mark.usefixtures("load_energy_table_with_slice")
- @mock.patch("superset.explore.form_data.commands.create.CreateFormDataCommand.run")
+ @mock.patch("superset.commands.explore.form_data.create.CreateFormDataCommand.run")
def test_explore_redirect(self, mock_command: mock.Mock):
self.login(username="admin")
random_key = "random_key"
diff --git a/tests/integration_tests/dashboards/commands_tests.py b/tests/integration_tests/dashboards/commands_tests.py
index 75bdd17bcf..cfc992b95f 100644
--- a/tests/integration_tests/dashboards/commands_tests.py
+++ b/tests/integration_tests/dashboards/commands_tests.py
@@ -23,16 +23,16 @@ import yaml
from werkzeug.utils import secure_filename
from superset import db, security_manager
-from superset.commands.exceptions import CommandInvalidError
-from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.connectors.sqla.models import SqlaTable
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.commands.export import (
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.export import (
append_charts,
ExportDashboardsCommand,
get_default_position,
)
-from superset.dashboards.commands.importers import v0, v1
+from superset.commands.dashboard.importers import v0, v1
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
@@ -292,7 +292,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
]
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
- @patch("superset.dashboards.commands.export.suffix")
+ @patch("superset.commands.dashboard.export.suffix")
def test_append_charts(self, mock_suffix):
"""Test that orphaned charts are added to the dashboard position"""
# return deterministic IDs
@@ -490,7 +490,7 @@ class TestImportDashboardsCommand(SupersetTestCase):
db.session.delete(dataset)
db.session.commit()
- @patch("superset.dashboards.commands.importers.v1.utils.g")
+ @patch("superset.commands.dashboard.importers.v1.utils.g")
@patch("superset.security.manager.g")
def test_import_v1_dashboard(self, sm_g, utils_g):
"""Test that we can import a dashboard"""
diff --git a/tests/integration_tests/dashboards/filter_state/api_tests.py b/tests/integration_tests/dashboards/filter_state/api_tests.py
index 15b479686a..3538e14012 100644
--- a/tests/integration_tests/dashboards/filter_state/api_tests.py
+++ b/tests/integration_tests/dashboards/filter_state/api_tests.py
@@ -22,10 +22,10 @@ from flask.ctx import AppContext
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.orm import Session
-from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
+from superset.commands.dashboard.exceptions import DashboardAccessDeniedError
+from superset.commands.temporary_cache.entry import Entry
from superset.extensions import cache_manager
from superset.models.dashboard import Dashboard
-from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.utils import cache_key
from tests.integration_tests.fixtures.world_bank_dashboard import (
load_world_bank_dashboard_with_slices,
diff --git a/tests/integration_tests/dashboards/permalink/api_tests.py b/tests/integration_tests/dashboards/permalink/api_tests.py
index 3c560a4469..a49f1e6f4c 100644
--- a/tests/integration_tests/dashboards/permalink/api_tests.py
+++ b/tests/integration_tests/dashboards/permalink/api_tests.py
@@ -23,7 +23,7 @@ from flask_appbuilder.security.sqla.models import User
from sqlalchemy.orm import Session
from superset import db
-from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
+from superset.commands.dashboard.exceptions import DashboardAccessDeniedError
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_rbac_tests.py b/tests/integration_tests/dashboards/security/security_rbac_tests.py
index 8b7f2ad1ef..792c9d1716 100644
--- a/tests/integration_tests/dashboards/security/security_rbac_tests.py
+++ b/tests/integration_tests/dashboards/security/security_rbac_tests.py
@@ -21,8 +21,8 @@ from unittest.mock import patch
import pytest
+from superset.commands.dashboard.exceptions import DashboardForbiddenError
from superset.daos.dashboard import DashboardDAO
-from superset.dashboards.commands.exceptions import DashboardForbiddenError
from superset.utils.core import backend, override_user
from tests.integration_tests.conftest import with_feature_flags
from tests.integration_tests.dashboards.dashboard_test_utils import *
diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py
index cbdacc8f34..6e6f6e15b8 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -288,9 +288,9 @@ class TestDatabaseApi(SupersetTestCase):
db.session.commit()
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
@@ -336,10 +336,10 @@ class TestDatabaseApi(SupersetTestCase):
db.session.commit()
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
- @mock.patch("superset.databases.commands.update.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.update.is_feature_enabled")
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
@@ -397,10 +397,10 @@ class TestDatabaseApi(SupersetTestCase):
db.session.commit()
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
- @mock.patch("superset.databases.commands.update.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.update.is_feature_enabled")
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
@@ -477,12 +477,12 @@ class TestDatabaseApi(SupersetTestCase):
db.session.commit()
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
def test_cascade_delete_ssh_tunnel(
self,
mock_test_connection_database_command_run,
@@ -531,9 +531,9 @@ class TestDatabaseApi(SupersetTestCase):
assert model_ssh_tunnel is None
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
@@ -582,9 +582,9 @@ class TestDatabaseApi(SupersetTestCase):
assert model is None
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
- @mock.patch("superset.databases.commands.create.is_feature_enabled")
+ @mock.patch("superset.commands.database.create.is_feature_enabled")
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
)
@@ -637,7 +637,7 @@ class TestDatabaseApi(SupersetTestCase):
db.session.commit()
@mock.patch(
- "superset.databases.commands.test_connection.TestConnectionDatabaseCommand.run",
+ "superset.commands.database.test_connection.TestConnectionDatabaseCommand.run",
)
@mock.patch(
"superset.models.core.Database.get_all_schema_names",
@@ -2005,10 +2005,10 @@ class TestDatabaseApi(SupersetTestCase):
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = False
@mock.patch(
- "superset.databases.commands.test_connection.DatabaseDAO.build_db_for_connection_test",
+ "superset.commands.database.test_connection.DatabaseDAO.build_db_for_connection_test",
)
@mock.patch(
- "superset.databases.commands.test_connection.event_logger",
+ "superset.commands.database.test_connection.event_logger",
)
def test_test_connection_failed_invalid_hostname(
self, mock_event_logger, mock_build_db
@@ -3748,7 +3748,7 @@ class TestDatabaseApi(SupersetTestCase):
},
)
- @patch("superset.databases.commands.validate_sql.get_validator_by_name")
+ @patch("superset.commands.database.validate_sql.get_validator_by_name")
@patch.dict(
"superset.config.SQL_VALIDATORS_BY_ENGINE",
PRESTO_SQL_VALIDATORS_BY_ENGINE,
diff --git a/tests/integration_tests/databases/commands_tests.py b/tests/integration_tests/databases/commands_tests.py
index d5946d8b6d..b46e1b7ea3 100644
--- a/tests/integration_tests/databases/commands_tests.py
+++ b/tests/integration_tests/databases/commands_tests.py
@@ -23,11 +23,8 @@ from func_timeout import FunctionTimedOut
from sqlalchemy.exc import DBAPIError
from superset import db, event_logger, security_manager
-from superset.commands.exceptions import CommandInvalidError
-from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.connectors.sqla.models import SqlaTable
-from superset.databases.commands.create import CreateDatabaseCommand
-from superset.databases.commands.exceptions import (
+from superset.commands.database.create import CreateDatabaseCommand
+from superset.commands.database.exceptions import (
DatabaseInvalidError,
DatabaseNotFoundError,
DatabaseSecurityUnsafeError,
@@ -35,11 +32,14 @@ from superset.databases.commands.exceptions import (
DatabaseTestConnectionDriverError,
DatabaseTestConnectionUnexpectedError,
)
-from superset.databases.commands.export import ExportDatabasesCommand
-from superset.databases.commands.importers.v1 import ImportDatabasesCommand
-from superset.databases.commands.tables import TablesDatabaseCommand
-from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
-from superset.databases.commands.validate import ValidateDatabaseParametersCommand
+from superset.commands.database.export import ExportDatabasesCommand
+from superset.commands.database.importers.v1 import ImportDatabasesCommand
+from superset.commands.database.tables import TablesDatabaseCommand
+from superset.commands.database.test_connection import TestConnectionDatabaseCommand
+from superset.commands.database.validate import ValidateDatabaseParametersCommand
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.connectors.sqla.models import SqlaTable
from superset.databases.schemas import DatabaseTestConnectionSchema
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
@@ -75,7 +75,7 @@ from tests.integration_tests.fixtures.importexport import (
class TestCreateDatabaseCommand(SupersetTestCase):
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_create_duplicate_error(self, mock_g, mock_logger):
example_db = get_example_database()
@@ -94,7 +94,7 @@ class TestCreateDatabaseCommand(SupersetTestCase):
"DatabaseRequiredFieldValidationError"
)
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_multiple_error_logging(self, mock_g, mock_logger):
mock_g.user = security_manager.find_user("admin")
@@ -834,7 +834,7 @@ class TestImportDatabasesCommand(SupersetTestCase):
}
}
- @patch("superset.databases.commands.importers.v1.import_dataset")
+ @patch("superset.commands.database.importers.v1.import_dataset")
def test_import_v1_rollback(self, mock_import_dataset):
"""Test than on an exception everything is rolled back"""
num_databases = db.session.query(Database).count()
@@ -860,7 +860,7 @@ class TestImportDatabasesCommand(SupersetTestCase):
class TestTestConnectionDatabaseCommand(SupersetTestCase):
@patch("superset.daos.database.Database._get_sqla_engine")
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_connection_db_exception(
self, mock_g, mock_event_logger, mock_get_sqla_engine
@@ -881,7 +881,7 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
mock_event_logger.assert_called()
@patch("superset.daos.database.Database._get_sqla_engine")
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_connection_do_ping_exception(
self, mock_g, mock_event_logger, mock_get_sqla_engine
@@ -903,8 +903,8 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
== SupersetErrorType.GENERIC_DB_ENGINE_ERROR
)
- @patch("superset.databases.commands.test_connection.func_timeout")
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.func_timeout")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_connection_do_ping_timeout(
self, mock_g, mock_event_logger, mock_func_timeout
@@ -926,7 +926,7 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
)
@patch("superset.daos.database.Database._get_sqla_engine")
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_connection_superset_security_connection(
self, mock_g, mock_event_logger, mock_get_sqla_engine
@@ -949,7 +949,7 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
mock_event_logger.assert_called()
@patch("superset.daos.database.Database._get_sqla_engine")
- @patch("superset.databases.commands.test_connection.event_logger.log_with_context")
+ @patch("superset.commands.database.test_connection.event_logger.log_with_context")
@patch("superset.utils.core.g")
def test_connection_db_api_exc(
self, mock_g, mock_event_logger, mock_get_sqla_engine
@@ -975,7 +975,7 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@patch("superset.db_engine_specs.base.is_hostname_valid")
@patch("superset.db_engine_specs.base.is_port_open")
-@patch("superset.databases.commands.validate.DatabaseDAO")
+@patch("superset.commands.database.validate.DatabaseDAO")
def test_validate(DatabaseDAO, is_port_open, is_hostname_valid, app_context):
"""
Test parameter validation.
diff --git a/tests/integration_tests/databases/ssh_tunnel/commands/commands_tests.py b/tests/integration_tests/databases/ssh_tunnel/commands/commands_tests.py
index 64bc0d8572..1cd9afcc80 100644
--- a/tests/integration_tests/databases/ssh_tunnel/commands/commands_tests.py
+++ b/tests/integration_tests/databases/ssh_tunnel/commands/commands_tests.py
@@ -20,13 +20,13 @@ from unittest.mock import patch
import pytest
from superset import security_manager
-from superset.databases.ssh_tunnel.commands.create import CreateSSHTunnelCommand
-from superset.databases.ssh_tunnel.commands.delete import DeleteSSHTunnelCommand
-from superset.databases.ssh_tunnel.commands.exceptions import (
+from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.delete import DeleteSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelInvalidError,
SSHTunnelNotFoundError,
)
-from superset.databases.ssh_tunnel.commands.update import UpdateSSHTunnelCommand
+from superset.commands.database.ssh_tunnel.update import UpdateSSHTunnelCommand
from tests.integration_tests.base_tests import SupersetTestCase
@@ -67,7 +67,7 @@ class TestUpdateSSHTunnelCommand(SupersetTestCase):
class TestDeleteSSHTunnelCommand(SupersetTestCase):
@mock.patch("superset.utils.core.g")
- @mock.patch("superset.databases.ssh_tunnel.commands.delete.is_feature_enabled")
+ @mock.patch("superset.commands.database.ssh_tunnel.delete.is_feature_enabled")
def test_delete_ssh_tunnel_not_found(self, mock_g, mock_delete_is_feature_enabled):
mock_g.user = security_manager.find_user("admin")
mock_delete_is_feature_enabled.return_value = True
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index f060d36739..d969895489 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -30,13 +30,13 @@ from sqlalchemy.orm import joinedload
from sqlalchemy.sql import func
from superset import app
+from superset.commands.dataset.exceptions import DatasetCreateFailedError
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.daos.exceptions import (
DAOCreateFailedError,
DAODeleteFailedError,
DAOUpdateFailedError,
)
-from superset.datasets.commands.exceptions import DatasetCreateFailedError
from superset.datasets.models import Dataset
from superset.extensions import db, security_manager
from superset.models.core import Database
@@ -2458,7 +2458,7 @@ class TestDatasetApi(SupersetTestCase):
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual(response["message"], {"database": ["Database does not exist"]})
- @patch("superset.datasets.commands.create.CreateDatasetCommand.run")
+ @patch("superset.commands.dataset.create.CreateDatasetCommand.run")
def test_get_or_create_dataset_create_fails(self, command_run_mock):
"""
Dataset API: Test get or create endpoint when create fails
diff --git a/tests/integration_tests/datasets/commands_tests.py b/tests/integration_tests/datasets/commands_tests.py
index a718c81e29..1ea554a818 100644
--- a/tests/integration_tests/datasets/commands_tests.py
+++ b/tests/integration_tests/datasets/commands_tests.py
@@ -23,19 +23,19 @@ import yaml
from sqlalchemy.exc import SQLAlchemyError
from superset import db, security_manager
-from superset.commands.exceptions import CommandInvalidError
-from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.connectors.sqla.models import SqlaTable
-from superset.databases.commands.importers.v1 import ImportDatabasesCommand
-from superset.datasets.commands.create import CreateDatasetCommand
-from superset.datasets.commands.exceptions import (
+from superset.commands.database.importers.v1 import ImportDatabasesCommand
+from superset.commands.dataset.create import CreateDatasetCommand
+from superset.commands.dataset.exceptions import (
DatasetInvalidError,
DatasetNotFoundError,
WarmUpCacheTableNotFoundError,
)
-from superset.datasets.commands.export import ExportDatasetsCommand
-from superset.datasets.commands.importers import v0, v1
-from superset.datasets.commands.warm_up_cache import DatasetWarmUpCacheCommand
+from superset.commands.dataset.export import ExportDatasetsCommand
+from superset.commands.dataset.importers import v0, v1
+from superset.commands.dataset.warm_up_cache import DatasetWarmUpCacheCommand
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.slice import Slice
from superset.utils.core import get_example_default_schema
@@ -339,7 +339,7 @@ class TestImportDatasetsCommand(SupersetTestCase):
db.session.delete(dataset)
db.session.commit()
- @patch("superset.datasets.commands.importers.v1.utils.g")
+ @patch("superset.commands.dataset.importers.v1.utils.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_import_v1_dataset(self, sm_g, utils_g):
diff --git a/tests/integration_tests/datasource_tests.py b/tests/integration_tests/datasource_tests.py
index 802c67e852..5ab81b58d1 100644
--- a/tests/integration_tests/datasource_tests.py
+++ b/tests/integration_tests/datasource_tests.py
@@ -24,11 +24,11 @@ import prison
import pytest
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 SqlaTable, SqlMetric, TableColumn
from superset.constants import CacheRegion
from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError
-from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.exceptions import SupersetGenericDBErrorException
from superset.models.core import Database
from superset.utils.core import backend, get_example_default_schema
diff --git a/tests/integration_tests/explore/api_tests.py b/tests/integration_tests/explore/api_tests.py
index 50606257c2..e37200e310 100644
--- a/tests/integration_tests/explore/api_tests.py
+++ b/tests/integration_tests/explore/api_tests.py
@@ -21,9 +21,9 @@ import pytest
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.orm import Session
+from superset.commands.explore.form_data.state import TemporaryExploreState
from superset.connectors.sqla.models import SqlaTable
from superset.explore.exceptions import DatasetAccessDeniedError
-from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.extensions import cache_manager
from superset.models.slice import Slice
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 0e73d0b516..5dbd67d4f5 100644
--- a/tests/integration_tests/explore/form_data/api_tests.py
+++ b/tests/integration_tests/explore/form_data/api_tests.py
@@ -21,9 +21,9 @@ import pytest
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.orm import Session
+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.datasets.commands.exceptions import DatasetAccessDeniedError
-from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.extensions import cache_manager
from superset.models.slice import Slice
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 18dd8415f6..781c4fdbb2 100644
--- a/tests/integration_tests/explore/form_data/commands_tests.py
+++ b/tests/integration_tests/explore/form_data/commands_tests.py
@@ -22,12 +22,12 @@ import pytest
from superset import app, db, security, 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
+from superset.commands.explore.form_data.get import GetFormDataCommand
+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.explore.form_data.commands.create import CreateFormDataCommand
-from superset.explore.form_data.commands.delete import DeleteFormDataCommand
-from superset.explore.form_data.commands.get import GetFormDataCommand
-from superset.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.form_data.commands.update import UpdateFormDataCommand
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/explore/permalink/commands_tests.py b/tests/integration_tests/explore/permalink/commands_tests.py
index eace978d78..5402a419bc 100644
--- a/tests/integration_tests/explore/permalink/commands_tests.py
+++ b/tests/integration_tests/explore/permalink/commands_tests.py
@@ -21,10 +21,10 @@ import pytest
from superset import app, db, security, security_manager
from superset.commands.exceptions import DatasourceTypeInvalidError
+from superset.commands.explore.form_data.parameters import CommandParameters
+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.explore.form_data.commands.parameters import CommandParameters
-from superset.explore.permalink.commands.create import CreateExplorePermalinkCommand
-from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
from superset.key_value.utils import decode_permalink_id
from superset.models.slice import Slice
from superset.models.sql_lab import Query
@@ -138,8 +138,8 @@ class TestCreatePermalinkDataCommand(SupersetTestCase):
assert cache_data.get("datasource") == datasource
@patch("superset.security.manager.g")
- @patch("superset.key_value.commands.get.GetKeyValueCommand.run")
- @patch("superset.explore.permalink.commands.get.decode_permalink_id")
+ @patch("superset.commands.key_value.get.GetKeyValueCommand.run")
+ @patch("superset.commands.explore.permalink.get.decode_permalink_id")
@pytest.mark.usefixtures("create_dataset", "create_slice")
def test_get_permalink_command_with_old_dataset_key(
self, decode_id_mock, get_kv_command_mock, mock_g
diff --git a/tests/integration_tests/import_export_tests.py b/tests/integration_tests/import_export_tests.py
index 5dc8143f77..c195e3a4cb 100644
--- a/tests/integration_tests/import_export_tests.py
+++ b/tests/integration_tests/import_export_tests.py
@@ -32,12 +32,12 @@ from tests.integration_tests.fixtures.energy_dashboard import (
load_energy_table_data,
)
from tests.integration_tests.test_app import app
-from superset.dashboards.commands.importers.v0 import decode_dashboards
+from superset.commands.dashboard.importers.v0 import decode_dashboards
from superset import db, security_manager
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
-from superset.dashboards.commands.importers.v0 import import_chart, import_dashboard
-from superset.datasets.commands.importers.v0 import import_dataset
+from superset.commands.dashboard.importers.v0 import import_chart, import_dashboard
+from superset.commands.dataset.importers.v0 import import_dataset
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils.core import DatasourceType, get_example_default_schema
diff --git a/tests/integration_tests/importexport/commands_tests.py b/tests/integration_tests/importexport/commands_tests.py
index ceaf097565..9e8f790260 100644
--- a/tests/integration_tests/importexport/commands_tests.py
+++ b/tests/integration_tests/importexport/commands_tests.py
@@ -21,7 +21,7 @@ import yaml
from freezegun import freeze_time
from superset import security_manager
-from superset.databases.commands.export import ExportDatabasesCommand
+from superset.commands.database.export import ExportDatabasesCommand
from superset.utils.database import get_example_database
from tests.integration_tests.base_tests import SupersetTestCase
diff --git a/tests/integration_tests/key_value/commands/create_test.py b/tests/integration_tests/key_value/commands/create_test.py
index c7ba076b5f..494456fa0c 100644
--- a/tests/integration_tests/key_value/commands/create_test.py
+++ b/tests/integration_tests/key_value/commands/create_test.py
@@ -37,7 +37,7 @@ from tests.integration_tests.key_value.commands.fixtures import (
def test_create_id_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -54,7 +54,7 @@ def test_create_id_entry(app_context: AppContext, admin: User) -> None:
def test_create_uuid_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -69,7 +69,7 @@ def test_create_uuid_entry(app_context: AppContext, admin: User) -> None:
def test_create_fail_json_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
with pytest.raises(KeyValueCreateFailedError):
CreateKeyValueCommand(
@@ -80,7 +80,7 @@ def test_create_fail_json_entry(app_context: AppContext, admin: User) -> None:
def test_create_pickle_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.create import CreateKeyValueCommand
+ from superset.commands.key_value.create import CreateKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
diff --git a/tests/integration_tests/key_value/commands/delete_test.py b/tests/integration_tests/key_value/commands/delete_test.py
index 3c4892faa6..706aab8880 100644
--- a/tests/integration_tests/key_value/commands/delete_test.py
+++ b/tests/integration_tests/key_value/commands/delete_test.py
@@ -58,7 +58,7 @@ def test_delete_id_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.delete import DeleteKeyValueCommand
+ from superset.commands.key_value.delete import DeleteKeyValueCommand
assert DeleteKeyValueCommand(resource=RESOURCE, key=ID_KEY).run() is True
@@ -68,7 +68,7 @@ def test_delete_uuid_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.delete import DeleteKeyValueCommand
+ from superset.commands.key_value.delete import DeleteKeyValueCommand
assert DeleteKeyValueCommand(resource=RESOURCE, key=UUID_KEY).run() is True
@@ -78,6 +78,6 @@ def test_delete_entry_missing(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.delete import DeleteKeyValueCommand
+ from superset.commands.key_value.delete import DeleteKeyValueCommand
assert DeleteKeyValueCommand(resource=RESOURCE, key=456).run() is False
diff --git a/tests/integration_tests/key_value/commands/get_test.py b/tests/integration_tests/key_value/commands/get_test.py
index 28a6dd73d5..b14c64f752 100644
--- a/tests/integration_tests/key_value/commands/get_test.py
+++ b/tests/integration_tests/key_value/commands/get_test.py
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
def test_get_id_entry(app_context: AppContext, key_value_entry: KeyValueEntry) -> None:
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
value = GetKeyValueCommand(resource=RESOURCE, key=ID_KEY, codec=JSON_CODEC).run()
assert value == JSON_VALUE
@@ -47,7 +47,7 @@ def test_get_id_entry(app_context: AppContext, key_value_entry: KeyValueEntry) -
def test_get_uuid_entry(
app_context: AppContext, key_value_entry: KeyValueEntry
) -> None:
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
value = GetKeyValueCommand(resource=RESOURCE, key=UUID_KEY, codec=JSON_CODEC).run()
assert value == JSON_VALUE
@@ -57,14 +57,14 @@ def test_get_id_entry_missing(
app_context: AppContext,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
value = GetKeyValueCommand(resource=RESOURCE, key=456, codec=JSON_CODEC).run()
assert value is None
def test_get_expired_entry(app_context: AppContext) -> None:
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
from superset.key_value.models import KeyValueEntry
entry = KeyValueEntry(
@@ -83,7 +83,7 @@ def test_get_expired_entry(app_context: AppContext) -> None:
def test_get_future_expiring_entry(app_context: AppContext) -> None:
- from superset.key_value.commands.get import GetKeyValueCommand
+ from superset.commands.key_value.get import GetKeyValueCommand
from superset.key_value.models import KeyValueEntry
id_ = 789
diff --git a/tests/integration_tests/key_value/commands/update_test.py b/tests/integration_tests/key_value/commands/update_test.py
index 816a6f857a..62d118b197 100644
--- a/tests/integration_tests/key_value/commands/update_test.py
+++ b/tests/integration_tests/key_value/commands/update_test.py
@@ -45,7 +45,7 @@ def test_update_id_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.update import UpdateKeyValueCommand
+ from superset.commands.key_value.update import UpdateKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -67,7 +67,7 @@ def test_update_uuid_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.update import UpdateKeyValueCommand
+ from superset.commands.key_value.update import UpdateKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -85,7 +85,7 @@ def test_update_uuid_entry(
def test_update_missing_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.update import UpdateKeyValueCommand
+ from superset.commands.key_value.update import UpdateKeyValueCommand
with override_user(admin):
key = UpdateKeyValueCommand(
diff --git a/tests/integration_tests/key_value/commands/upsert_test.py b/tests/integration_tests/key_value/commands/upsert_test.py
index 9b094ef65e..b23ddaee97 100644
--- a/tests/integration_tests/key_value/commands/upsert_test.py
+++ b/tests/integration_tests/key_value/commands/upsert_test.py
@@ -45,7 +45,7 @@ def test_upsert_id_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.upsert import UpsertKeyValueCommand
+ from superset.commands.key_value.upsert import UpsertKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -67,7 +67,7 @@ def test_upsert_uuid_entry(
admin: User,
key_value_entry: KeyValueEntry,
) -> None:
- from superset.key_value.commands.upsert import UpsertKeyValueCommand
+ from superset.commands.key_value.upsert import UpsertKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
@@ -85,7 +85,7 @@ def test_upsert_uuid_entry(
def test_upsert_missing_entry(app_context: AppContext, admin: User) -> None:
- from superset.key_value.commands.upsert import UpsertKeyValueCommand
+ from superset.commands.key_value.upsert import UpsertKeyValueCommand
from superset.key_value.models import KeyValueEntry
with override_user(admin):
diff --git a/tests/integration_tests/queries/saved_queries/commands_tests.py b/tests/integration_tests/queries/saved_queries/commands_tests.py
index 5c7b862209..cccc409985 100644
--- a/tests/integration_tests/queries/saved_queries/commands_tests.py
+++ b/tests/integration_tests/queries/saved_queries/commands_tests.py
@@ -23,13 +23,11 @@ import yaml
from superset import db, 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.models.core import Database
from superset.models.sql_lab import SavedQuery
-from superset.queries.saved_queries.commands.exceptions import SavedQueryNotFoundError
-from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand
-from superset.queries.saved_queries.commands.importers.v1 import (
- ImportSavedQueriesCommand,
-)
from superset.utils.database import get_example_database
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.importexport import (
diff --git a/tests/integration_tests/reports/alert_tests.py b/tests/integration_tests/reports/alert_tests.py
index 76890a19e2..6664d65a9b 100644
--- a/tests/integration_tests/reports/alert_tests.py
+++ b/tests/integration_tests/reports/alert_tests.py
@@ -22,7 +22,7 @@ import pandas as pd
import pytest
from pytest_mock import MockFixture
-from superset.reports.commands.exceptions import AlertQueryError
+from superset.commands.report.exceptions import AlertQueryError
from superset.reports.models import ReportCreationMethod, ReportScheduleType
from superset.tasks.types import ExecutorType
from superset.utils.database import get_example_database
@@ -64,7 +64,7 @@ def test_execute_query_as_report_executor(
app_context: None,
get_user,
) -> None:
- from superset.reports.commands.alert import AlertCommand
+ from superset.commands.report.alert import AlertCommand
from superset.reports.models import ReportSchedule
with app.app_context():
@@ -86,7 +86,7 @@ def test_execute_query_as_report_executor(
)
command = AlertCommand(report_schedule=report_schedule)
override_user_mock = mocker.patch(
- "superset.reports.commands.alert.override_user"
+ "superset.commands.report.alert.override_user"
)
cm = (
pytest.raises(type(expected_result))
@@ -103,10 +103,10 @@ def test_execute_query_as_report_executor(
def test_execute_query_succeeded_no_retry(
mocker: MockFixture, app_context: None
) -> None:
- from superset.reports.commands.alert import AlertCommand
+ from superset.commands.report.alert import AlertCommand
execute_query_mock = mocker.patch(
- "superset.reports.commands.alert.AlertCommand._execute_query",
+ "superset.commands.report.alert.AlertCommand._execute_query",
side_effect=lambda: pd.DataFrame([{"sample_col": 0}]),
)
@@ -120,10 +120,10 @@ def test_execute_query_succeeded_no_retry(
def test_execute_query_succeeded_with_retries(
mocker: MockFixture, app_context: None
) -> None:
- from superset.reports.commands.alert import AlertCommand, AlertQueryError
+ from superset.commands.report.alert import AlertCommand, AlertQueryError
execute_query_mock = mocker.patch(
- "superset.reports.commands.alert.AlertCommand._execute_query"
+ "superset.commands.report.alert.AlertCommand._execute_query"
)
query_executed_count = 0
@@ -150,10 +150,10 @@ def test_execute_query_succeeded_with_retries(
def test_execute_query_failed_no_retry(mocker: MockFixture, app_context: None) -> None:
- from superset.reports.commands.alert import AlertCommand, AlertQueryTimeout
+ from superset.commands.report.alert import AlertCommand, AlertQueryTimeout
execute_query_mock = mocker.patch(
- "superset.reports.commands.alert.AlertCommand._execute_query"
+ "superset.commands.report.alert.AlertCommand._execute_query"
)
def _mocked_execute_query() -> None:
@@ -172,10 +172,10 @@ def test_execute_query_failed_no_retry(mocker: MockFixture, app_context: None) -
def test_execute_query_failed_max_retries(
mocker: MockFixture, app_context: None
) -> None:
- from superset.reports.commands.alert import AlertCommand, AlertQueryError
+ from superset.commands.report.alert import AlertCommand, AlertQueryError
execute_query_mock = mocker.patch(
- "superset.reports.commands.alert.AlertCommand._execute_query"
+ "superset.commands.report.alert.AlertCommand._execute_query"
)
def _mocked_execute_query() -> None:
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 81945c18a9..a7f3001aa8 100644
--- a/tests/integration_tests/reports/commands/create_dashboard_report_tests.py
+++ b/tests/integration_tests/reports/commands/create_dashboard_report_tests.py
@@ -18,9 +18,9 @@
import pytest
from superset import db
+from superset.commands.report.create import CreateReportScheduleCommand
+from superset.commands.report.exceptions import ReportScheduleInvalidError
from superset.models.dashboard import Dashboard
-from superset.reports.commands.create import CreateReportScheduleCommand
-from superset.reports.commands.exceptions import ReportScheduleInvalidError
from superset.reports.models import (
ReportCreationMethod,
ReportRecipientType,
diff --git a/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py b/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py
index fe20365765..68150a9c3c 100644
--- a/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py
+++ b/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py
@@ -20,11 +20,9 @@ from uuid import uuid4
from flask import current_app
-from superset.dashboards.permalink.commands.create import (
- CreateDashboardPermalinkCommand,
-)
+from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
+from superset.commands.report.execute import AsyncExecuteReportScheduleCommand
from superset.models.dashboard import Dashboard
-from superset.reports.commands.execute import AsyncExecuteReportScheduleCommand
from superset.reports.models import ReportSourceFormat
from tests.integration_tests.fixtures.tabbed_dashboard import tabbed_dashboard
from tests.integration_tests.reports.utils import create_dashboard_report
@@ -32,10 +30,10 @@ from tests.integration_tests.reports.utils import create_dashboard_report
@patch("superset.reports.notifications.email.send_email_smtp")
@patch(
- "superset.reports.commands.execute.DashboardScreenshot",
+ "superset.commands.report.execute.DashboardScreenshot",
)
@patch(
- "superset.dashboards.permalink.commands.create.CreateDashboardPermalinkCommand.run"
+ "superset.commands.dashboard.permalink.create.CreateDashboardPermalinkCommand.run"
)
def test_report_for_dashboard_with_tabs(
create_dashboard_permalink_mock: MagicMock,
@@ -70,10 +68,10 @@ def test_report_for_dashboard_with_tabs(
@patch("superset.reports.notifications.email.send_email_smtp")
@patch(
- "superset.reports.commands.execute.DashboardScreenshot",
+ "superset.commands.report.execute.DashboardScreenshot",
)
@patch(
- "superset.dashboards.permalink.commands.create.CreateDashboardPermalinkCommand.run"
+ "superset.commands.dashboard.permalink.create.CreateDashboardPermalinkCommand.run"
)
def test_report_with_header_data(
create_dashboard_permalink_mock: MagicMock,
diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py
index 11ec170121..939c9c0cfa 100644
--- a/tests/integration_tests/reports/commands_tests.py
+++ b/tests/integration_tests/reports/commands_tests.py
@@ -39,11 +39,7 @@ from slack_sdk.errors import (
from sqlalchemy.sql import func
from superset import db
-from superset.exceptions import SupersetException
-from superset.models.core import Database
-from superset.models.dashboard import Dashboard
-from superset.models.slice import Slice
-from superset.reports.commands.exceptions import (
+from superset.commands.report.exceptions import (
AlertQueryError,
AlertQueryInvalidTypeError,
AlertQueryMultipleColumnsError,
@@ -58,11 +54,15 @@ from superset.reports.commands.exceptions import (
ReportScheduleSystemErrorsException,
ReportScheduleWorkingTimeoutError,
)
-from superset.reports.commands.execute import (
+from superset.commands.report.execute import (
AsyncExecuteReportScheduleCommand,
BaseReportState,
)
-from superset.reports.commands.log_prune import AsyncPruneReportScheduleLogCommand
+from superset.commands.report.log_prune import AsyncPruneReportScheduleLogCommand
+from superset.exceptions import SupersetException
+from superset.models.core import Database
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
from superset.reports.models import (
ReportDataFormat,
ReportExecutionLog,
@@ -1607,7 +1607,7 @@ def test_soft_timeout_alert(email_mock, create_alert_email_chart):
"""
from celery.exceptions import SoftTimeLimitExceeded
- from superset.reports.commands.exceptions import AlertQueryTimeout
+ from superset.commands.report.exceptions import AlertQueryTimeout
with patch.object(
create_alert_email_chart.database.db_engine_spec, "execute", return_value=None
@@ -1748,7 +1748,7 @@ def test_fail_screenshot(screenshot_mock, email_mock, create_report_email_chart)
"""
from celery.exceptions import SoftTimeLimitExceeded
- from superset.reports.commands.exceptions import AlertQueryTimeout
+ from superset.commands.report.exceptions import AlertQueryTimeout
screenshot_mock.side_effect = Exception("Unexpected error")
with pytest.raises(ReportScheduleScreenshotFailedError):
@@ -1963,8 +1963,8 @@ def test_prune_log_soft_time_out(bulk_delete_logs, create_report_email_dashboard
assert str(excinfo.value) == "SoftTimeLimitExceeded()"
-@patch("superset.reports.commands.execute.logger")
-@patch("superset.reports.commands.execute.create_notification")
+@patch("superset.commands.report.execute.logger")
+@patch("superset.commands.report.execute.create_notification")
def test__send_with_client_errors(notification_mock, logger_mock):
notification_content = "I am some content"
recipients = ["test@foo.com"]
@@ -1978,8 +1978,8 @@ def test__send_with_client_errors(notification_mock, logger_mock):
)
-@patch("superset.reports.commands.execute.logger")
-@patch("superset.reports.commands.execute.create_notification")
+@patch("superset.commands.report.execute.logger")
+@patch("superset.commands.report.execute.create_notification")
def test__send_with_multiple_errors(notification_mock, logger_mock):
notification_content = "I am some content"
recipients = ["test@foo.com", "test2@bar.com"]
@@ -2005,8 +2005,8 @@ def test__send_with_multiple_errors(notification_mock, logger_mock):
)
-@patch("superset.reports.commands.execute.logger")
-@patch("superset.reports.commands.execute.create_notification")
+@patch("superset.commands.report.execute.logger")
+@patch("superset.commands.report.execute.create_notification")
def test__send_with_server_errors(notification_mock, logger_mock):
notification_content = "I am some content"
recipients = ["test@foo.com"]
diff --git a/tests/integration_tests/reports/scheduler_tests.py b/tests/integration_tests/reports/scheduler_tests.py
index 29dd58273a..ee93ef48a4 100644
--- a/tests/integration_tests/reports/scheduler_tests.py
+++ b/tests/integration_tests/reports/scheduler_tests.py
@@ -154,11 +154,11 @@ def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled, owners):
@pytest.mark.usefixtures("owners")
-@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.__init__")
-@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.run")
+@patch("superset.commands.report.execute.AsyncExecuteReportScheduleCommand.__init__")
+@patch("superset.commands.report.execute.AsyncExecuteReportScheduleCommand.run")
@patch("superset.tasks.scheduler.execute.update_state")
def test_execute_task(update_state_mock, command_mock, init_mock, owners):
- from superset.reports.commands.exceptions import ReportScheduleUnexpectedError
+ from superset.commands.report.exceptions import ReportScheduleUnexpectedError
with app.app_context():
report_schedule = insert_report_schedule(
@@ -179,8 +179,8 @@ def test_execute_task(update_state_mock, command_mock, init_mock, owners):
@pytest.mark.usefixtures("owners")
-@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.__init__")
-@patch("superset.reports.commands.execute.AsyncExecuteReportScheduleCommand.run")
+@patch("superset.commands.report.execute.AsyncExecuteReportScheduleCommand.__init__")
+@patch("superset.commands.report.execute.AsyncExecuteReportScheduleCommand.run")
@patch("superset.tasks.scheduler.execute.update_state")
@patch("superset.utils.log.logger")
def test_execute_task_with_command_exception(
diff --git a/tests/integration_tests/sql_lab/api_tests.py b/tests/integration_tests/sql_lab/api_tests.py
index 49dd4ea32e..da050c2363 100644
--- a/tests/integration_tests/sql_lab/api_tests.py
+++ b/tests/integration_tests/sql_lab/api_tests.py
@@ -209,7 +209,7 @@ class TestSqlLabApi(SupersetTestCase):
return_value=formatter_response
)
- with mock.patch("superset.sqllab.commands.estimate.db") as mock_superset_db:
+ with mock.patch("superset.commands.sql_lab.estimate.db") as mock_superset_db:
mock_superset_db.session.query().get.return_value = db_mock
data = {"database_id": 1, "sql": "SELECT 1"}
@@ -236,7 +236,7 @@ class TestSqlLabApi(SupersetTestCase):
self.assertDictEqual(resp_data, success_resp)
self.assertEqual(rv.status_code, 200)
- @mock.patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @mock.patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_execute_required_params(self):
self.login()
client_id = f"{random.getrandbits(64)}"[:10]
@@ -276,7 +276,7 @@ class TestSqlLabApi(SupersetTestCase):
self.assertDictEqual(resp_data, failed_resp)
self.assertEqual(rv.status_code, 400)
- @mock.patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @mock.patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_execute_valid_request(self) -> None:
from superset import sql_lab as core
@@ -320,9 +320,9 @@ class TestSqlLabApi(SupersetTestCase):
self.delete_fake_db_for_macros()
- @mock.patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @mock.patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_get_results_with_display_limit(self):
- from superset.sqllab.commands import results as command
+ from superset.commands.sql_lab import results as command
command.results_backend = mock.Mock()
self.login()
@@ -355,7 +355,7 @@ class TestSqlLabApi(SupersetTestCase):
compressed = utils.zlib_compress(serialized_payload)
command.results_backend.get.return_value = compressed
- with mock.patch("superset.sqllab.commands.results.db") as mock_superset_db:
+ with mock.patch("superset.commands.sql_lab.results.db") as mock_superset_db:
mock_superset_db.session.query().filter_by().one_or_none.return_value = (
query_mock
)
diff --git a/tests/integration_tests/sql_lab/commands_tests.py b/tests/integration_tests/sql_lab/commands_tests.py
index d76924a8fb..11eb5de0c9 100644
--- a/tests/integration_tests/sql_lab/commands_tests.py
+++ b/tests/integration_tests/sql_lab/commands_tests.py
@@ -22,6 +22,7 @@ import pytest
from flask_babel import gettext as __
from superset import app, db, 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
from superset.exceptions import (
@@ -32,7 +33,6 @@ from superset.exceptions import (
)
from superset.models.core import Database
from superset.models.sql_lab import Query
-from superset.sqllab.commands import estimate, export, results
from superset.sqllab.limiting_factor import LimitingFactor
from superset.sqllab.schemas import EstimateQueryCostSchema
from superset.utils import core as utils
@@ -47,7 +47,7 @@ class TestQueryEstimationCommand(SupersetTestCase):
data: EstimateQueryCostSchema = schema.dump(params)
command = estimate.QueryEstimationCommand(data)
- with mock.patch("superset.sqllab.commands.estimate.db") as mock_superset_db:
+ with mock.patch("superset.commands.sql_lab.estimate.db") as mock_superset_db:
mock_superset_db.session.query().get.return_value = None
with pytest.raises(SupersetErrorException) as ex_info:
command.validate()
@@ -79,7 +79,7 @@ class TestQueryEstimationCommand(SupersetTestCase):
db_mock.db_engine_spec.query_cost_formatter = mock.Mock(return_value=None)
is_feature_enabled.return_value = False
- with mock.patch("superset.sqllab.commands.estimate.db") as mock_superset_db:
+ with mock.patch("superset.commands.sql_lab.estimate.db") as mock_superset_db:
mock_superset_db.session.query().get.return_value = db_mock
with pytest.raises(SupersetErrorException) as ex_info:
command.run()
@@ -105,7 +105,7 @@ class TestQueryEstimationCommand(SupersetTestCase):
db_mock.db_engine_spec.estimate_query_cost = mock.Mock(return_value=100)
db_mock.db_engine_spec.query_cost_formatter = mock.Mock(return_value=payload)
- with mock.patch("superset.sqllab.commands.estimate.db") as mock_superset_db:
+ with mock.patch("superset.commands.sql_lab.estimate.db") as mock_superset_db:
mock_superset_db.session.query().get.return_value = db_mock
result = command.run()
assert result == payload
@@ -223,7 +223,7 @@ class TestSqlResultExportCommand(SupersetTestCase):
@pytest.mark.usefixtures("create_database_and_query")
@patch("superset.models.sql_lab.Query.raise_for_access", lambda _: None)
- @patch("superset.sqllab.commands.export.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.export.results_backend_use_msgpack", False)
def test_run_with_results_backend(self) -> None:
command = export.SqlResultExportCommand("test")
@@ -273,8 +273,8 @@ class TestSqlExecutionResultsCommand(SupersetTestCase):
db.session.delete(query_obj)
db.session.commit()
- @patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
- @patch("superset.sqllab.commands.results.results_backend", None)
+ @patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.results.results_backend", None)
def test_validation_no_results_backend(self) -> None:
command = results.SqlExecutionResultsCommand("test", 1000)
@@ -285,7 +285,7 @@ class TestSqlExecutionResultsCommand(SupersetTestCase):
== SupersetErrorType.RESULTS_BACKEND_NOT_CONFIGURED_ERROR
)
- @patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_validation_data_cannot_be_retrieved(self) -> None:
results.results_backend = mock.Mock()
results.results_backend.get.return_value = None
@@ -296,7 +296,7 @@ class TestSqlExecutionResultsCommand(SupersetTestCase):
command.run()
assert ex_info.value.error.error_type == SupersetErrorType.RESULTS_BACKEND_ERROR
- @patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_validation_data_not_found(self) -> None:
data = [{"col_0": i} for i in range(100)]
payload = {
@@ -317,7 +317,7 @@ class TestSqlExecutionResultsCommand(SupersetTestCase):
assert ex_info.value.error.error_type == SupersetErrorType.RESULTS_BACKEND_ERROR
@pytest.mark.usefixtures("create_database_and_query")
- @patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_validation_query_not_found(self) -> None:
data = [{"col_0": i} for i in range(104)]
payload = {
@@ -344,7 +344,7 @@ class TestSqlExecutionResultsCommand(SupersetTestCase):
)
@pytest.mark.usefixtures("create_database_and_query")
- @patch("superset.sqllab.commands.results.results_backend_use_msgpack", False)
+ @patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False)
def test_run_succeeds(self) -> None:
data = [{"col_0": i} for i in range(104)]
payload = {
diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py
index 057d28abe0..48abfd31b4 100644
--- a/tests/integration_tests/tags/commands_tests.py
+++ b/tests/integration_tests/tags/commands_tests.py
@@ -22,21 +22,21 @@ import yaml
from werkzeug.utils import secure_filename
from superset import db, security_manager
-from superset.commands.exceptions import CommandInvalidError
-from superset.commands.importers.exceptions import IncorrectVersionError
-from superset.connectors.sqla.models import SqlaTable
-from superset.dashboards.commands.exceptions import DashboardNotFoundError
-from superset.dashboards.commands.export import (
+from superset.commands.dashboard.exceptions import DashboardNotFoundError
+from superset.commands.dashboard.export import (
append_charts,
ExportDashboardsCommand,
get_default_position,
)
-from superset.dashboards.commands.importers import v0, v1
+from superset.commands.dashboard.importers import v0, v1
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.commands.tag.create import CreateCustomTagCommand
+from superset.commands.tag.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
+from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
-from superset.tags.commands.create import CreateCustomTagCommand
-from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
from superset.tags.models import ObjectType, Tag, TaggedObject, TagType
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.importexport import (
diff --git a/tests/integration_tests/tasks/async_queries_tests.py b/tests/integration_tests/tasks/async_queries_tests.py
index 8e6e595757..01880b7a62 100644
--- a/tests/integration_tests/tasks/async_queries_tests.py
+++ b/tests/integration_tests/tasks/async_queries_tests.py
@@ -21,8 +21,8 @@ from uuid import uuid4
import pytest
from celery.exceptions import SoftTimeLimitExceeded
-from superset.charts.commands.exceptions import ChartDataQueryFailedError
-from superset.charts.data.commands.get_data_command import ChartDataCommand
+from superset.commands.chart.data.get_data_command import ChartDataCommand
+from superset.commands.chart.exceptions import ChartDataQueryFailedError
from superset.exceptions import SupersetException
from superset.extensions import async_query_manager, security_manager
from tests.integration_tests.base_tests import SupersetTestCase
diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py
index 6f8a7ed457..405040e3d0 100644
--- a/tests/integration_tests/utils_tests.py
+++ b/tests/integration_tests/utils_tests.py
@@ -24,7 +24,7 @@ import re
from typing import Any, Optional
from unittest.mock import Mock, patch
-from superset.databases.commands.exceptions import DatabaseInvalidError
+from superset.commands.database.exceptions import DatabaseInvalidError
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices,
load_birth_names_data,
diff --git a/tests/unit_tests/charts/commands/importers/v1/import_test.py b/tests/unit_tests/charts/commands/importers/v1/import_test.py
index 06e0063fe9..f0d142644d 100644
--- a/tests/unit_tests/charts/commands/importers/v1/import_test.py
+++ b/tests/unit_tests/charts/commands/importers/v1/import_test.py
@@ -30,7 +30,7 @@ def test_import_chart(mocker: MockFixture, session: Session) -> None:
Test importing a chart.
"""
from superset import security_manager
- from superset.charts.commands.importers.v1.utils import import_chart
+ from superset.commands.chart.importers.v1.utils import import_chart
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.slice import Slice
@@ -57,7 +57,7 @@ def test_import_chart_managed_externally(mocker: MockFixture, session: Session)
Test importing a chart that is managed externally.
"""
from superset import security_manager
- from superset.charts.commands.importers.v1.utils import import_chart
+ from superset.commands.chart.importers.v1.utils import import_chart
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.slice import Slice
@@ -87,7 +87,7 @@ def test_import_chart_without_permission(
Test importing a chart when a user doesn't have permissions to create.
"""
from superset import security_manager
- from superset.charts.commands.importers.v1.utils import import_chart
+ from superset.commands.chart.importers.v1.utils import import_chart
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.models.slice import Slice
diff --git a/tests/unit_tests/charts/commands/importers/v1/utils_test.py b/tests/unit_tests/charts/commands/importers/v1/utils_test.py
index 2addfa3016..de3f805d8b 100644
--- a/tests/unit_tests/charts/commands/importers/v1/utils_test.py
+++ b/tests/unit_tests/charts/commands/importers/v1/utils_test.py
@@ -17,7 +17,7 @@
import json
-from superset.charts.commands.importers.v1.utils import migrate_chart
+from superset.commands.chart.importers.v1.utils import migrate_chart
def test_migrate_chart_area() -> None:
diff --git a/tests/unit_tests/dashboards/commands/importers/v1/import_test.py b/tests/unit_tests/dashboards/commands/importers/v1/import_test.py
index e07a23f6bf..67e0897755 100644
--- a/tests/unit_tests/dashboards/commands/importers/v1/import_test.py
+++ b/tests/unit_tests/dashboards/commands/importers/v1/import_test.py
@@ -30,8 +30,8 @@ def test_import_dashboard(mocker: MockFixture, session: Session) -> None:
Test importing a dashboard.
"""
from superset import security_manager
+ from superset.commands.dashboard.importers.v1.utils import import_dashboard
from superset.connectors.sqla.models import SqlaTable
- from superset.dashboards.commands.importers.v1.utils import import_dashboard
from superset.models.core import Database
from superset.models.slice import Slice
from tests.integration_tests.fixtures.importexport import dashboard_config
@@ -58,8 +58,8 @@ def test_import_dashboard_managed_externally(
Test importing a dashboard that is managed externally.
"""
from superset import security_manager
+ from superset.commands.dashboard.importers.v1.utils import import_dashboard
from superset.connectors.sqla.models import SqlaTable
- from superset.dashboards.commands.importers.v1.utils import import_dashboard
from superset.models.core import Database
from superset.models.slice import Slice
from tests.integration_tests.fixtures.importexport import dashboard_config
@@ -86,8 +86,8 @@ def test_import_dashboard_without_permission(
Test importing a dashboard when a user doesn't have permissions to create.
"""
from superset import security_manager
+ from superset.commands.dashboard.importers.v1.utils import import_dashboard
from superset.connectors.sqla.models import SqlaTable
- from superset.dashboards.commands.importers.v1.utils import import_dashboard
from superset.models.core import Database
from superset.models.slice import Slice
from tests.integration_tests.fixtures.importexport import dashboard_config
diff --git a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
index 60a659159a..0e84362957 100644
--- a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
+++ b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
@@ -29,7 +29,7 @@ def test_update_id_refs_immune_missing( # pylint: disable=invalid-name
immune to filters. The missing chart ID should be simply ignored when the
dashboard is imported.
"""
- from superset.dashboards.commands.importers.v1.utils import update_id_refs
+ from superset.commands.dashboard.importers.v1.utils import update_id_refs
config = {
"position": {
@@ -83,7 +83,7 @@ def test_update_id_refs_immune_missing( # pylint: disable=invalid-name
def test_update_native_filter_config_scope_excluded():
- from superset.dashboards.commands.importers.v1.utils import update_id_refs
+ from superset.commands.dashboard.importers.v1.utils import update_id_refs
config = {
"position": {
diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py
index aa15645ddb..28ca123ec6 100644
--- a/tests/unit_tests/databases/api_test.py
+++ b/tests/unit_tests/databases/api_test.py
@@ -396,7 +396,7 @@ def test_delete_ssh_tunnel(
mocker.patch("sqlalchemy.engine.URL.get_driver_name", return_value="gsheets")
mocker.patch("superset.utils.log.DBEventLogger.log")
mocker.patch(
- "superset.databases.ssh_tunnel.commands.delete.is_feature_enabled",
+ "superset.commands.database.ssh_tunnel.delete.is_feature_enabled",
return_value=True,
)
@@ -472,7 +472,7 @@ def test_delete_ssh_tunnel_not_found(
mocker.patch("sqlalchemy.engine.URL.get_driver_name", return_value="gsheets")
mocker.patch("superset.utils.log.DBEventLogger.log")
mocker.patch(
- "superset.databases.ssh_tunnel.commands.delete.is_feature_enabled",
+ "superset.commands.database.ssh_tunnel.delete.is_feature_enabled",
return_value=True,
)
@@ -559,7 +559,7 @@ def test_apply_dynamic_database_filter(
mocker.patch("sqlalchemy.engine.URL.get_driver_name", return_value="gsheets")
mocker.patch("superset.utils.log.DBEventLogger.log")
mocker.patch(
- "superset.databases.ssh_tunnel.commands.delete.is_feature_enabled",
+ "superset.commands.database.ssh_tunnel.delete.is_feature_enabled",
return_value=False,
)
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 b8bd24d94d..dcd093a9cf 100644
--- a/tests/unit_tests/databases/commands/importers/v1/import_test.py
+++ b/tests/unit_tests/databases/commands/importers/v1/import_test.py
@@ -30,7 +30,7 @@ def test_import_database(mocker: MockFixture, session: Session) -> None:
Test importing a database.
"""
from superset import security_manager
- from superset.databases.commands.importers.v1.utils import import_database
+ 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
@@ -70,7 +70,7 @@ def test_import_database_sqlite_invalid(mocker: MockFixture, session: Session) -
Test importing a database.
"""
from superset import app, security_manager
- from superset.databases.commands.importers.v1.utils import import_database
+ 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
@@ -99,7 +99,7 @@ def test_import_database_managed_externally(
Test importing a database that is managed externally.
"""
from superset import security_manager
- from superset.databases.commands.importers.v1.utils import import_database
+ 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
@@ -125,7 +125,7 @@ def test_import_database_without_permission(
Test importing a database when a user doesn't have permissions to create.
"""
from superset import security_manager
- from superset.databases.commands.importers.v1.utils import import_database
+ 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
diff --git a/tests/unit_tests/databases/commands/test_connection_test.py b/tests/unit_tests/databases/commands/test_connection_test.py
index 8e86cfd1cf..66efa7d717 100644
--- a/tests/unit_tests/databases/commands/test_connection_test.py
+++ b/tests/unit_tests/databases/commands/test_connection_test.py
@@ -17,7 +17,7 @@
from parameterized import parameterized
-from superset.databases.commands.test_connection import get_log_connection_action
+from superset.commands.database.test_connection import get_log_connection_action
from superset.databases.ssh_tunnel.models import SSHTunnel
diff --git a/tests/unit_tests/databases/ssh_tunnel/commands/create_test.py b/tests/unit_tests/databases/ssh_tunnel/commands/create_test.py
index fbad104c1d..bd891b64f0 100644
--- a/tests/unit_tests/databases/ssh_tunnel/commands/create_test.py
+++ b/tests/unit_tests/databases/ssh_tunnel/commands/create_test.py
@@ -19,11 +19,11 @@
import pytest
from sqlalchemy.orm.session import Session
-from superset.databases.ssh_tunnel.commands.exceptions import SSHTunnelInvalidError
+from superset.commands.database.ssh_tunnel.exceptions import SSHTunnelInvalidError
def test_create_ssh_tunnel_command() -> None:
- from superset.databases.ssh_tunnel.commands.create import CreateSSHTunnelCommand
+ from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.models.core import Database
@@ -44,7 +44,7 @@ def test_create_ssh_tunnel_command() -> None:
def test_create_ssh_tunnel_command_invalid_params() -> None:
- from superset.databases.ssh_tunnel.commands.create import CreateSSHTunnelCommand
+ from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.models.core import Database
diff --git a/tests/unit_tests/databases/ssh_tunnel/commands/delete_test.py b/tests/unit_tests/databases/ssh_tunnel/commands/delete_test.py
index 641e34d347..14838ddc58 100644
--- a/tests/unit_tests/databases/ssh_tunnel/commands/delete_test.py
+++ b/tests/unit_tests/databases/ssh_tunnel/commands/delete_test.py
@@ -54,8 +54,8 @@ def session_with_data(session: Session) -> Iterator[Session]:
def test_delete_ssh_tunnel_command(
mocker: MockFixture, session_with_data: Session
) -> None:
+ from superset.commands.database.ssh_tunnel.delete import DeleteSSHTunnelCommand
from superset.daos.database import DatabaseDAO
- from superset.databases.ssh_tunnel.commands.delete import DeleteSSHTunnelCommand
from superset.databases.ssh_tunnel.models import SSHTunnel
result = DatabaseDAO.get_ssh_tunnel(1)
@@ -64,7 +64,7 @@ def test_delete_ssh_tunnel_command(
assert isinstance(result, SSHTunnel)
assert 1 == result.database_id
mocker.patch(
- "superset.databases.ssh_tunnel.commands.delete.is_feature_enabled",
+ "superset.commands.database.ssh_tunnel.delete.is_feature_enabled",
return_value=True,
)
DeleteSSHTunnelCommand(1).run()
diff --git a/tests/unit_tests/databases/ssh_tunnel/commands/update_test.py b/tests/unit_tests/databases/ssh_tunnel/commands/update_test.py
index d4a5faba8b..5c3907b016 100644
--- a/tests/unit_tests/databases/ssh_tunnel/commands/update_test.py
+++ b/tests/unit_tests/databases/ssh_tunnel/commands/update_test.py
@@ -20,7 +20,7 @@ from collections.abc import Iterator
import pytest
from sqlalchemy.orm.session import Session
-from superset.databases.ssh_tunnel.commands.exceptions import SSHTunnelInvalidError
+from superset.commands.database.ssh_tunnel.exceptions import SSHTunnelInvalidError
@pytest.fixture
@@ -50,8 +50,8 @@ def session_with_data(session: Session) -> Iterator[Session]:
def test_update_shh_tunnel_command(session_with_data: Session) -> None:
+ from superset.commands.database.ssh_tunnel.update import UpdateSSHTunnelCommand
from superset.daos.database import DatabaseDAO
- from superset.databases.ssh_tunnel.commands.update import UpdateSSHTunnelCommand
from superset.databases.ssh_tunnel.models import SSHTunnel
result = DatabaseDAO.get_ssh_tunnel(1)
@@ -72,8 +72,8 @@ def test_update_shh_tunnel_command(session_with_data: Session) -> None:
def test_update_shh_tunnel_invalid_params(session_with_data: Session) -> None:
+ from superset.commands.database.ssh_tunnel.update import UpdateSSHTunnelCommand
from superset.daos.database import DatabaseDAO
- from superset.databases.ssh_tunnel.commands.update import UpdateSSHTunnelCommand
from superset.databases.ssh_tunnel.models import SSHTunnel
result = DatabaseDAO.get_ssh_tunnel(1)
diff --git a/tests/unit_tests/datasets/commands/export_test.py b/tests/unit_tests/datasets/commands/export_test.py
index be6a637f8f..20565da5bc 100644
--- a/tests/unit_tests/datasets/commands/export_test.py
+++ b/tests/unit_tests/datasets/commands/export_test.py
@@ -25,8 +25,8 @@ def test_export(session: Session) -> None:
"""
Test exporting a dataset.
"""
+ from superset.commands.dataset.export import ExportDatasetsCommand
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
- from superset.datasets.commands.export import ExportDatasetsCommand
from superset.models.core import Database
engine = session.get_bind()
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 e8e8c8e7c5..5089838e69 100644
--- a/tests/unit_tests/datasets/commands/importers/v1/import_test.py
+++ b/tests/unit_tests/datasets/commands/importers/v1/import_test.py
@@ -28,11 +28,11 @@ from flask import current_app
from pytest_mock import MockFixture
from sqlalchemy.orm.session import Session
-from superset.datasets.commands.exceptions import (
+from superset.commands.dataset.exceptions import (
DatasetForbiddenDataURI,
ImportFailedError,
)
-from superset.datasets.commands.importers.v1.utils import validate_data_uri
+from superset.commands.dataset.importers.v1.utils import validate_data_uri
def test_import_dataset(mocker: MockFixture, session: Session) -> None:
@@ -40,8 +40,8 @@ def test_import_dataset(mocker: MockFixture, session: Session) -> None:
Test importing a dataset.
"""
from superset import security_manager
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.models.core import Database
mocker.patch.object(security_manager, "can_access", return_value=True)
@@ -156,8 +156,8 @@ def test_import_dataset_duplicate_column(mocker: MockFixture, session: Session)
"""
from superset import security_manager
from superset.columns.models import Column as NewColumn
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable, TableColumn
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.models.core import Database
mocker.patch.object(security_manager, "can_access", return_value=True)
@@ -281,8 +281,8 @@ def test_import_column_extra_is_string(mocker: MockFixture, session: Session) ->
Test importing a dataset when the column extra is a string.
"""
from superset import security_manager
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.core import Database
@@ -366,8 +366,8 @@ def test_import_dataset_extra_empty_string(
Test importing a dataset when the extra field is an empty string.
"""
from superset import security_manager
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.core import Database
@@ -422,7 +422,7 @@ def test_import_dataset_extra_empty_string(
assert sqla_table.extra == None
-@patch("superset.datasets.commands.importers.v1.utils.request")
+@patch("superset.commands.dataset.importers.v1.utils.request")
def test_import_column_allowed_data_url(
request: Mock,
mocker: MockFixture,
@@ -434,8 +434,8 @@ def test_import_column_allowed_data_url(
import io
from superset import security_manager
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.datasets.schemas import ImportV1DatasetSchema
from superset.models.core import Database
@@ -510,8 +510,8 @@ def test_import_dataset_managed_externally(
Test importing a dataset that is managed externally.
"""
from superset import security_manager
+ from superset.commands.dataset.importers.v1.utils import import_dataset
from superset.connectors.sqla.models import SqlaTable
- from superset.datasets.commands.importers.v1.utils import import_dataset
from superset.models.core import Database
from tests.integration_tests.fixtures.importexport import dataset_config
diff --git a/tests/unit_tests/explore/utils_test.py b/tests/unit_tests/explore/utils_test.py
index de39187ec7..fa99091f09 100644
--- a/tests/unit_tests/explore/utils_test.py
+++ b/tests/unit_tests/explore/utils_test.py
@@ -18,20 +18,20 @@ from flask_appbuilder.security.sqla.models import User
from pytest import raises
from pytest_mock import MockFixture
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
)
+from superset.commands.dataset.exceptions import (
+ DatasetAccessDeniedError,
+ DatasetNotFoundError,
+)
from superset.commands.exceptions import (
DatasourceNotFoundValidationError,
DatasourceTypeInvalidError,
OwnersNotFoundValidationError,
QueryNotFoundValidationError,
)
-from superset.datasets.commands.exceptions import (
- DatasetAccessDeniedError,
- DatasetNotFoundError,
-)
from superset.exceptions import SupersetSecurityException
from superset.utils.core import DatasourceType, override_user
diff --git a/tests/unit_tests/jinja_context_test.py b/tests/unit_tests/jinja_context_test.py
index 114f046300..e2a5e8cd49 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -22,7 +22,7 @@ import pytest
from pytest_mock import MockFixture
from sqlalchemy.dialects import mysql
-from superset.datasets.commands.exceptions import DatasetNotFoundError
+from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.jinja_context import dataset_macro, WhereInMacro
diff --git a/tests/unit_tests/tags/commands/create_test.py b/tests/unit_tests/tags/commands/create_test.py
index 39f0e3c4eb..ca31e44566 100644
--- a/tests/unit_tests/tags/commands/create_test.py
+++ b/tests/unit_tests/tags/commands/create_test.py
@@ -49,12 +49,12 @@ def session_with_data(session: Session):
def test_create_command_success(session_with_data: Session, mocker: MockFixture):
+ from superset.commands.tag.create import CreateCustomTagWithRelationshipsCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query, SavedQuery
- from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
from superset.tags.models import ObjectType, TaggedObject
# Define a list of objects to tag
@@ -92,12 +92,12 @@ def test_create_command_success(session_with_data: Session, mocker: MockFixture)
def test_create_command_success_clear(session_with_data: Session, mocker: MockFixture):
+ from superset.commands.tag.create import CreateCustomTagWithRelationshipsCommand
from superset.connectors.sqla.models import SqlaTable
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query, SavedQuery
- from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
from superset.tags.models import ObjectType, TaggedObject
# Define a list of objects to tag
diff --git a/tests/unit_tests/tags/commands/update_test.py b/tests/unit_tests/tags/commands/update_test.py
index 6d0a99b670..47ef16e4e7 100644
--- a/tests/unit_tests/tags/commands/update_test.py
+++ b/tests/unit_tests/tags/commands/update_test.py
@@ -58,9 +58,9 @@ def session_with_data(session: Session):
def test_update_command_success(session_with_data: Session, mocker: MockFixture):
+ from superset.commands.tag.update import UpdateTagCommand
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
- from superset.tags.commands.update import UpdateTagCommand
from superset.tags.models import ObjectType, TaggedObject
dashboard = session_with_data.query(Dashboard).first()
@@ -94,11 +94,11 @@ def test_update_command_success(session_with_data: Session, mocker: MockFixture)
def test_update_command_success_duplicates(
session_with_data: Session, mocker: MockFixture
):
+ from superset.commands.tag.create import CreateCustomTagWithRelationshipsCommand
+ from superset.commands.tag.update import UpdateTagCommand
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
- from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
- from superset.tags.commands.update import UpdateTagCommand
from superset.tags.models import ObjectType, TaggedObject
dashboard = session_with_data.query(Dashboard).first()
@@ -144,12 +144,12 @@ def test_update_command_success_duplicates(
def test_update_command_failed_validation(
session_with_data: Session, mocker: MockFixture
):
+ from superset.commands.tag.create import CreateCustomTagWithRelationshipsCommand
+ from superset.commands.tag.exceptions import TagInvalidError
+ from superset.commands.tag.update import UpdateTagCommand
from superset.daos.tag import TagDAO
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
- from superset.tags.commands.create import CreateCustomTagWithRelationshipsCommand
- from superset.tags.commands.exceptions import TagInvalidError
- from superset.tags.commands.update import UpdateTagCommand
from superset.tags.models import ObjectType
dashboard = session_with_data.query(Dashboard).first()
diff --git a/tests/unit_tests/tasks/test_async_queries.py b/tests/unit_tests/tasks/test_async_queries.py
index 5787bbdc8b..1e14d742da 100644
--- a/tests/unit_tests/tasks/test_async_queries.py
+++ b/tests/unit_tests/tasks/test_async_queries.py
@@ -3,7 +3,7 @@ from unittest import mock
import pytest
from flask_babel import lazy_gettext as _
-from superset.charts.commands.exceptions import ChartDataQueryFailedError
+from superset.commands.chart.exceptions import ChartDataQueryFailedError
@mock.patch("superset.tasks.async_queries.security_manager")
diff --git a/tests/unit_tests/utils/date_parser_tests.py b/tests/unit_tests/utils/date_parser_tests.py
index a2ec20901a..0311377237 100644
--- a/tests/unit_tests/utils/date_parser_tests.py
+++ b/tests/unit_tests/utils/date_parser_tests.py
@@ -22,7 +22,7 @@ from unittest.mock import Mock, patch
import pytest
from dateutil.relativedelta import relativedelta
-from superset.charts.commands.exceptions import (
+from superset.commands.chart.exceptions import (
TimeRangeAmbiguousError,
TimeRangeParseFailError,
)
From 57d61df44d51812920513f43282ca1984d0b9b19 Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Wed, 22 Nov 2023 17:25:14 -0300
Subject: [PATCH 066/119] chore: Adds 3.0.2 data to CHANGELOG.md (#26075)
---
CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 54 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 170824f6f2..5a296ac4b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ under the License.
## Change Log
+- [3.0.2](#302-mon-nov-20-073838-2023--0500)
- [3.0.1](#301-tue-oct-13-103221-2023--0700)
- [3.0.0](#300-thu-aug-24-133627-2023--0600)
- [2.1.1](#211-sun-apr-23-154421-2023-0100)
@@ -32,6 +33,59 @@ under the License.
- [1.4.2](#142-sat-mar-19-000806-2022-0200)
- [1.4.1](#141)
+### 3.0.2 (Mon Nov 20 07:38:38 2023 -0500)
+
+**Fixes**
+
+- [#26037](https://github.com/apache/superset/pull/26037) fix: update FAB to 4.3.10, Azure user info fix (@dpgaspar)
+- [#25901](https://github.com/apache/superset/pull/25901) fix(native filters): rendering performance improvement by reduce overrendering (@justinpark)
+- [#25985](https://github.com/apache/superset/pull/25985) fix(explore): redandant force param (@justinpark)
+- [#25993](https://github.com/apache/superset/pull/25993) fix: Make Select component fire onChange listener when a selection is pasted in (@jfrag1)
+- [#25997](https://github.com/apache/superset/pull/25997) fix(rls): Update text from tables to datasets in RLS modal (@yousoph)
+- [#25703](https://github.com/apache/superset/pull/25703) fix(helm): Restart all related deployments when bootstrap script changed (@josedev-union)
+- [#25973](https://github.com/apache/superset/pull/25973) fix: naming denomalized to denormalized in helpers.py (@hughhhh)
+- [#25919](https://github.com/apache/superset/pull/25919) fix: always denorm column value before querying values (@hughhhh)
+- [#25947](https://github.com/apache/superset/pull/25947) fix: update flask-caching to avoid breaking redis cache, solves #25339 (@ggbaro)
+- [#25903](https://github.com/apache/superset/pull/25903) fix(sqllab): invalid sanitization on comparison symbol (@justinpark)
+- [#25857](https://github.com/apache/superset/pull/25857) fix(table): Double percenting ad-hoc percentage metrics (@john-bodley)
+- [#25872](https://github.com/apache/superset/pull/25872) fix(trino): allow impersonate_user flag to be imported (@FGrobelny)
+- [#25897](https://github.com/apache/superset/pull/25897) fix: trino cursor (@betodealmeida)
+- [#25898](https://github.com/apache/superset/pull/25898) fix: database version field (@betodealmeida)
+- [#25877](https://github.com/apache/superset/pull/25877) fix: Saving Mixed Chart with dashboard filter applied breaks adhoc_filter_b (@kgabryje)
+- [#25842](https://github.com/apache/superset/pull/25842) fix(charts): Time grain is None when dataset uses Jinja (@Antonio-RiveroMartnez)
+- [#25843](https://github.com/apache/superset/pull/25843) fix: remove `update_charts_owners` (@betodealmeida)
+- [#25707](https://github.com/apache/superset/pull/25707) fix(table chart): Show Cell Bars correctly #25625 (@SA-Ark)
+- [#25429](https://github.com/apache/superset/pull/25429) fix: the temporal x-axis results in a none time_range. (@mapledan)
+- [#25853](https://github.com/apache/superset/pull/25853) fix: Fires onChange when clearing all values of single select (@michael-s-molina)
+- [#25814](https://github.com/apache/superset/pull/25814) fix(sqllab): infinite fetching status after results are landed (@justinpark)
+- [#25768](https://github.com/apache/superset/pull/25768) fix(SQL field in edit dataset modal): display full sql query (@rtexelm)
+- [#25804](https://github.com/apache/superset/pull/25804) fix: Resolve issue #24195 (@john-bodley)
+- [#25801](https://github.com/apache/superset/pull/25801) fix: Revert "fix: Apply normalization to all dttm columns (#25147)" (@john-bodley)
+- [#25779](https://github.com/apache/superset/pull/25779) fix: DB-specific quoting in Jinja macro (@betodealmeida)
+- [#25640](https://github.com/apache/superset/pull/25640) fix: allow for backward compatible errors (@eschutho)
+- [#25741](https://github.com/apache/superset/pull/25741) fix(sqllab): slow pop datasource query (@justinpark)
+- [#25756](https://github.com/apache/superset/pull/25756) fix: dataset update uniqueness (@betodealmeida)
+- [#25753](https://github.com/apache/superset/pull/25753) fix: Revert "fix(Charts): Set max row limit + removed the option to use an empty row limit value" (@geido)
+- [#25732](https://github.com/apache/superset/pull/25732) fix(horizontal filter label): show full tooltip with ellipsis (@rtexelm)
+- [#25712](https://github.com/apache/superset/pull/25712) fix: bump to FAB 4.3.9 remove CSP exception (@dpgaspar)
+- [#24709](https://github.com/apache/superset/pull/24709) fix(chore): dashboard requests to database equal the number of slices it has (@Always-prog)
+- [#25679](https://github.com/apache/superset/pull/25679) fix: remove unnecessary redirect (@Khrol)
+- [#25680](https://github.com/apache/superset/pull/25680) fix(sqllab): reinstate "Force trino client async execution" (@giftig)
+- [#25657](https://github.com/apache/superset/pull/25657) fix(dremio): Fixes issue with Dremio SQL generation for Charts with Series Limit (@OskarNS)
+- [#23638](https://github.com/apache/superset/pull/23638) fix: warning of nth-child (@justinpark)
+- [#25658](https://github.com/apache/superset/pull/25658) fix: improve upload ZIP file validation (@dpgaspar)
+- [#25495](https://github.com/apache/superset/pull/25495) fix(header navlinks): link navlinks to path prefix (@fisjac)
+- [#25112](https://github.com/apache/superset/pull/25112) fix: permalink save/overwrites in explore (@hughhhh)
+- [#25493](https://github.com/apache/superset/pull/25493) fix(import): Make sure query context is overwritten for overwriting imports (@jfrag1)
+- [#25553](https://github.com/apache/superset/pull/25553) fix: avoid 500 errors with SQLLAB_BACKEND_PERSISTENCE (@Khrol)
+- [#25626](https://github.com/apache/superset/pull/25626) fix(sqllab): template validation error within comments (@justinpark)
+- [#25523](https://github.com/apache/superset/pull/25523) fix(sqllab): Mistitled for new tab after rename (@justinpark)
+
+**Others**
+
+- [#25995](https://github.com/apache/superset/pull/25995) chore: Optimize fetching samples logic (@john-bodley)
+- [#23619](https://github.com/apache/superset/pull/23619) chore(colors): Updating Airbnb brand colors (@john-bodley)
+
### 3.0.1 (Tue Oct 13 10:32:21 2023 -0700)
**Database Migrations**
From aad67e43dbabadad9a5e4accb29ecefb39315f6e Mon Sep 17 00:00:00 2001
From: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
Date: Fri, 24 Nov 2023 07:36:32 -0800
Subject: [PATCH 067/119] fix(plugin-chart-echarts): support numerical x-axis
(#26087)
---
.../src/Timeseries/transformProps.ts | 15 ++++++++-------
.../plugin-chart-echarts/src/utils/series.ts | 9 ++++++---
.../test/utils/series.test.ts | 9 +++++++++
3 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index d44ae93580..c85dc8db00 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -20,9 +20,13 @@
import { invert } from 'lodash';
import {
AnnotationLayer,
+ AxisType,
+ buildCustomFormatters,
CategoricalColorNamespace,
+ CurrencyFormatter,
ensureIsArray,
GenericDataType,
+ getCustomFormatter,
getMetricLabel,
getNumberFormatter,
getXAxisLabel,
@@ -34,9 +38,6 @@ import {
isTimeseriesAnnotationLayer,
t,
TimeseriesChartDataResponseResult,
- buildCustomFormatters,
- getCustomFormatter,
- CurrencyFormatter,
} from '@superset-ui/core';
import {
extractExtraMetrics,
@@ -48,8 +49,8 @@ import { ZRLineType } from 'echarts/types/src/util/types';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
- TimeseriesChartTransformedProps,
OrientationType,
+ TimeseriesChartTransformedProps,
} from './types';
import { DEFAULT_FORM_DATA } from './constants';
import { ForecastSeriesEnum, ForecastValue, Refs } from '../types';
@@ -88,8 +89,8 @@ import {
} from './transformers';
import {
StackControlsValue,
- TIMESERIES_CONSTANTS,
TIMEGRAIN_TO_TIMESTAMP,
+ TIMESERIES_CONSTANTS,
} from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import {
@@ -448,13 +449,13 @@ export default function transformProps(
rotate: xAxisLabelRotation,
},
minInterval:
- xAxisType === 'time' && timeGrainSqla
+ xAxisType === AxisType.time && timeGrainSqla
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
: 0,
};
let yAxis: any = {
...defaultYAxis,
- type: logAxis ? 'log' : 'value',
+ type: logAxis ? AxisType.log : AxisType.value,
min,
max,
minorTick: { show: true },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 663548f25d..bd4e329d0b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -25,12 +25,12 @@ import {
DTTM_ALIAS,
ensureIsArray,
GenericDataType,
+ LegendState,
+ normalizeTimestamp,
NumberFormats,
NumberFormatter,
- TimeFormatter,
SupersetTheme,
- normalizeTimestamp,
- LegendState,
+ TimeFormatter,
ValueFormatter,
} from '@superset-ui/core';
import { SortSeriesType } from '@superset-ui/chart-controls';
@@ -512,6 +512,9 @@ export function getAxisType(dataType?: GenericDataType): AxisType {
if (dataType === GenericDataType.TEMPORAL) {
return AxisType.time;
}
+ if (dataType === GenericDataType.NUMERIC) {
+ return AxisType.value;
+ }
return AxisType.category;
}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
index 75faee93e5..927ee49e8c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
@@ -18,6 +18,7 @@
*/
import { SortSeriesType } from '@superset-ui/chart-controls';
import {
+ AxisType,
DataRecord,
GenericDataType,
getNumberFormatter,
@@ -31,6 +32,7 @@ import {
extractSeries,
extractShowValueIndexes,
formatSeriesName,
+ getAxisType,
getChartPadding,
getLegendProps,
getOverMaxHiddenFormatter,
@@ -870,3 +872,10 @@ test('calculateLowerLogTick', () => {
expect(calculateLowerLogTick(2)).toEqual(1);
expect(calculateLowerLogTick(0.005)).toEqual(0.001);
});
+
+test('getAxisType', () => {
+ expect(getAxisType(GenericDataType.TEMPORAL)).toEqual(AxisType.time);
+ expect(getAxisType(GenericDataType.NUMERIC)).toEqual(AxisType.value);
+ expect(getAxisType(GenericDataType.BOOLEAN)).toEqual(AxisType.category);
+ expect(getAxisType(GenericDataType.STRING)).toEqual(AxisType.category);
+});
From 4fc2758e6a542382ee8e36bcc70dd57942038566 Mon Sep 17 00:00:00 2001
From: Daniel Vaz Gaspar
Date: Mon, 27 Nov 2023 09:23:52 +0000
Subject: [PATCH 068/119] fix: bump node-fetch to 2.6.7 (#26091)
---
superset-frontend/package-lock.json | 2 +-
superset-frontend/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 6affdb893a..c4e771b259 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -260,7 +260,7 @@
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.7.6",
"mock-socket": "^9.0.3",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.7",
"prettier": "^2.4.1",
"prettier-plugin-packagejson": "^2.2.15",
"process": "^0.11.10",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 5f6a2e7e2d..3b6310fa71 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -325,7 +325,7 @@
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.7.6",
"mock-socket": "^9.0.3",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.7",
"prettier": "^2.4.1",
"prettier-plugin-packagejson": "^2.2.15",
"process": "^0.11.10",
From d20f96f8d0c020e2085e8561f89f799c7b919b15 Mon Sep 17 00:00:00 2001
From: Amrit Raj <122262283+raamri@users.noreply.github.com>
Date: Tue, 28 Nov 2023 01:58:49 +0530
Subject: [PATCH 069/119] docs(databases): Update pinot.mdx to incorporate
username and password based connection. (#26000)
---
docs/docs/databases/pinot.mdx | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/docs/databases/pinot.mdx b/docs/docs/databases/pinot.mdx
index 8d5b8c2062..e6add897ba 100644
--- a/docs/docs/databases/pinot.mdx
+++ b/docs/docs/databases/pinot.mdx
@@ -14,3 +14,9 @@ The expected connection string is formatted as follows:
```
pinot+http://:/query?controller=http://:/``
```
+
+The expected connection string using username and password is formatted as follows:
+
+```
+pinot://:@:/query/sql?controller=http://:/verify_ssl=true``
+```
From 91a8b69d365789833b6b9698df3b3ae95b34629e Mon Sep 17 00:00:00 2001
From: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
Date: Tue, 28 Nov 2023 05:21:13 -0800
Subject: [PATCH 070/119] fix: flaky test_explore_json_async test v2 (#26106)
---
tests/integration_tests/core_tests.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 25f8e624ec..c4a0897332 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -714,10 +714,16 @@ class TestCore(SupersetTestCase):
keys = list(data.keys())
# If chart is cached, it will return 200, otherwise 202
- self.assertTrue(rv.status_code in {200, 202})
- self.assertCountEqual(
- keys, ["channel_id", "job_id", "user_id", "status", "errors", "result_url"]
- )
+ assert rv.status_code in {200, 202}
+ if rv.status_code == 202:
+ assert keys == [
+ "channel_id",
+ "job_id",
+ "user_id",
+ "status",
+ "errors",
+ "result_url",
+ ]
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
From 849ca64ba83c12a9caf9e8578f54d7c5c6193f11 Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Tue, 28 Nov 2023 11:31:48 -0300
Subject: [PATCH 071/119] chore: Adds the 3.1.0 Release Notes (#26058)
---
RELEASING/README.md | 1 +
RELEASING/release-notes-3-1/README.md | 166 ++++++++++++++++++
.../release-notes-3-1/media/bubble_chart.png | Bin 0 -> 430546 bytes
RELEASING/release-notes-3-1/media/contour.png | Bin 0 -> 1035330 bytes
.../release-notes-3-1/media/databend.png | Bin 0 -> 7318 bytes
.../media/dataset_selector.png | Bin 0 -> 420784 bytes
RELEASING/release-notes-3-1/media/doris.png | Bin 0 -> 17856 bytes
RELEASING/release-notes-3-1/media/france.png | Bin 0 -> 107828 bytes
.../release-notes-3-1/media/kazakhstan.png | Bin 0 -> 263828 bytes
.../media/keyboard_shortcuts.png | Bin 0 -> 305917 bytes
.../release-notes-3-1/media/kyrgyzstan.png | Bin 0 -> 183302 bytes
.../media/sql_formatting.png | Bin 0 -> 473810 bytes
.../release-notes-3-1/media/tajikistan.png | Bin 0 -> 204499 bytes
.../release-notes-3-1/media/turkmenistan.png | Bin 0 -> 178477 bytes
.../release-notes-3-1/media/uzbekistan.png | Bin 0 -> 160917 bytes
.../media/waterfall_chart.png | Bin 0 -> 326362 bytes
16 files changed, 167 insertions(+)
create mode 100644 RELEASING/release-notes-3-1/README.md
create mode 100644 RELEASING/release-notes-3-1/media/bubble_chart.png
create mode 100644 RELEASING/release-notes-3-1/media/contour.png
create mode 100644 RELEASING/release-notes-3-1/media/databend.png
create mode 100644 RELEASING/release-notes-3-1/media/dataset_selector.png
create mode 100644 RELEASING/release-notes-3-1/media/doris.png
create mode 100644 RELEASING/release-notes-3-1/media/france.png
create mode 100644 RELEASING/release-notes-3-1/media/kazakhstan.png
create mode 100644 RELEASING/release-notes-3-1/media/keyboard_shortcuts.png
create mode 100644 RELEASING/release-notes-3-1/media/kyrgyzstan.png
create mode 100644 RELEASING/release-notes-3-1/media/sql_formatting.png
create mode 100644 RELEASING/release-notes-3-1/media/tajikistan.png
create mode 100644 RELEASING/release-notes-3-1/media/turkmenistan.png
create mode 100644 RELEASING/release-notes-3-1/media/uzbekistan.png
create mode 100644 RELEASING/release-notes-3-1/media/waterfall_chart.png
diff --git a/RELEASING/README.md b/RELEASING/README.md
index 8b23dafbf1..b007a89170 100644
--- a/RELEASING/README.md
+++ b/RELEASING/README.md
@@ -30,6 +30,7 @@ partaking in the process should join the channel.
## Release notes for recent releases
+- [3.1](release-notes-3-1/README.md)
- [2.0](release-notes-2-0/README.md)
- [1.5](release-notes-1-5/README.md)
- [1.4](release-notes-1-4/README.md)
diff --git a/RELEASING/release-notes-3-1/README.md b/RELEASING/release-notes-3-1/README.md
new file mode 100644
index 0000000000..97635139b1
--- /dev/null
+++ b/RELEASING/release-notes-3-1/README.md
@@ -0,0 +1,166 @@
+
+
+# Release Notes for Superset 3.1.0
+
+Superset 3.1.0 brings a range of new features and quality of life improvements. This release is a minor version, meaning it doesn't include any breaking changes to ensure a seamless transition for our users. Here are some of the highlights of this release.
+
+### Waterfall chart
+
+The new [Waterfall chart](https://github.com/apache/superset/pull/25557) visualization provides a visual representation of how a value changes over time or across different categories. They are very helpful to show the cumulative effect of positive and negative changes from a starting value. Superset's Waterfall chart supports Breakdowns which can be used to analyze the contribution of different dimensions or factors to a specific metric. By breaking down the data into various categories or dimensions, you can identify the individual components that contribute to the overall variation or change in the metric.
+
+The chart example below displays the total sales grouped by year and broken down by product line.
+
+![Waterfall](media/waterfall_chart.png)
+
+### Bubble Chart ECharts version
+
+The new ECharts [Bubble chart](https://github.com/apache/superset/pull/22107) offers feature parity with the previous NVD3 version which should be removed in the next major release. This work is part of the [ECharts migration effort](https://github.com/apache/superset/issues/10418) to increase consistency and quality of our plugins. We'll add a migration to the new plugin soon which you'll be able to execute using the new CLI command.
+
+![Bubble](media/bubble_chart.png)
+
+### Improved Dataset selectors
+
+The [dataset selectors](https://github.com/apache/superset/pull/25569) have been improved to also display the database and schema names which will help users locate the correct dataset, particularly when there are multiple tables/datasets with the same name that could benefit from disambiguation.
+
+![Dataset](media/dataset_selector.png)
+
+### SQL Lab improvements
+
+SQL Lab received many user experience and performance improvements in this release. We’ll continue to improve the capabilities of SQL Lab with feedback from the community.
+
+Now users can [automatically format](https://github.com/apache/superset/pull/25344) their SQL queries using the `Ctrl+Shift+F` shortcut or the Format SQL menu option available in the SQL configuration panel. Another improvement is that the results panel now shows the [executed query](https://github.com/apache/superset/pull/24787) which is very helpful when your SQL Lab editor has multiple queries.
+
+![SQL Formatting](media/sql_formatting.png)
+
+In the SQL panel configurations, there's a menu option to show the [keyboard shortcuts](https://github.com/apache/superset/pull/25542) a user has access to.
+
+![Keyboard Shortcuts](media/keyboard_shortcuts.png)
+
+SQL Lab has launched a non-blocking persistence mode, as outlined in [SIP-93](https://github.com/apache/superset/issues/21385). This enhancement ensures that your SQL editor content is preserved, even if your internet or service goes offline. Moreover, it improves user interaction by saving changes in a non-blocking way, similar to how Google Docs does.
+
+Finally, the [SQL Lab module was moved to the Single Page Application](https://github.com/apache/superset/pull/25151) context. This means that both navigation and loading time of that module is significantly faster than previous versions (particularly when navigating to and from this page from other pages in Superset). This also reduces the number of requests to the server and pays some of our technical debt. Try it out! The difference is quite impressive!
+
+### Country Map improvements
+
+The Country Map visualization received some improvements in this release. The community added [France's regions](https://github.com/apache/superset/pull/25676) in addition to its departments and also many [Central Asia countries](https://github.com/apache/superset/pull/24870).
+
+
+
+
France's regions
+
Kazakhstan
+
Kyrgyzstan
+
+
+
+
+
+
+
+
Tajikistan
+
Turkmenistan
+
Uzbekistan
+
+
+
+
+
+
+
+
+### Deck.gl ContourLayer
+
+We [added](https://github.com/apache/superset/pull/24154) the Deck.gl [ContourLayer](https://deck.gl/docs/api-reference/aggregation-layers/contour-layer) which aggregates data into Isolines or Isobands for a given threshold and cell size. By expanding the range of available [Deck.gl](https://deck.gl/) visualization layers, users will have more options to choose from when creating their visualizations. This will allow them to tailor their visualizations to their specific needs and explore their data in different ways.
+
+![Contour](media/contour.png)
+
+### New Databases
+
+Superset has added support for two new databases:
+
+- [Databend](https://databend.rs/), an open-source, elastic, and workload-aware cloud data warehouse built in Rust. You can see the PR [here](https://github.com/apache/superset/pull/23308), and the updated documentation [here](https://superset.apache.org/docs/databases/databend).
+- [Apache Doris](https://doris.apache.org/), which is based on the MySQL protocol and introduces the concept of Multi Catalog. You can see the PR [here](https://github.com/apache/superset/pull/24714/) and the updated documentation [here](https://superset.apache.org/docs/databases/doris).
+
+
+
+
+
+
+
+
+### CLI command to execute viz migrations
+
+A new [CLI command](https://github.com/apache/superset/pull/25304) called viz-migrations was added to allow users to migrate charts of a specific type. This command is particularly helpful to migrate visualizations to their latest version and at the same time disable their legacy versions with the `VIZ_TYPE_DENYLIST` configuration. The main advantage of this command is that you can migrate your visualizations without needing to wait for a major release, where we generally remove the legacy plugins.
+
+Currently, you can use the command to migrate Area, Bubble, Line, and Sunburst chart types but we'll add more as the ECharts migrations continue. Note that migrations for deprecated charts may be forced in upcoming major versions when the code is removed. Running migrations earlier will allow you to de-risk future upgrades while improving user experience.
+
+```bash
+Usage: superset viz-migrations [OPTIONS] COMMAND [ARGS]...
+
+ Migrates a viz from one type to another.
+
+Commands:
+ downgrade Downgrades a viz to the previous version.
+ upgrade Upgrade a viz to the latest version.
+```
+
+Note: When migrating dashboards from one Superset instance to another (using import/export features or the Superset CLI), or restoring a backup of prior charts and dashboards, Superset will apply the existing migrations that are used during version upgrades. This will ensure that your charts and dashboards are using the latest and greatest charts that Superset officially supports.
+
+### Database engine spec improvements
+
+Many database engine improvements were added in this release. Some highlights:
+
+- [feat: improve SQLite DB engine spec](https://github.com/apache/superset/pull/24909)
+- [feat: add MotherDuck DB engine spec](https://github.com/apache/superset/pull/24934)
+- [feat: Add week time grain for Elasticsearch datasets](https://github.com/apache/superset/pull/25683)
+- [feat: method for dynamic allows_alias_in_select](https://github.com/apache/superset/pull/25882)
+
+We even added a new [CLI command](https://github.com/apache/superset/pull/24918) to test DB engine specs, SQLAlchemy dialects, and database connections.
+
+```bash
+Usage: superset test-db [OPTIONS] SQLALCHEMY_URI
+
+ Run a series of tests against an analytical database.
+
+ This command tests:
+ 1. The Superset DB engine spec.
+ 2. The SQLAlchemy dialect.
+ 3. The database connectivity and performance.
+
+ It's useful for people developing DB engine specs and/or SQLAlchemy
+ dialects, and also to test new versions of DB API 2.0 drivers.
+
+Options:
+ -c, --connect-args TEXT Connect args as JSON or YAML
+ --help Show this message and exit.
+```
+
+### Playwright as an alternative to Selenium
+
+Per [SIP-98](https://github.com/apache/superset/issues/24948), we [introduced Playwright](https://github.com/apache/superset/pull/25247) for rendering charts in Superset reports. [Playwright](https://playwright.dev/) is an open-source library for automating web browsers, similar to Selenium but with better support for modern browser features and improved performance. By using Playwright, we aim to provide a more stable and accurate chart rendering experience in Superset reports, especially for [Deck.gl](https://deck.gl/) charts.
+
+Since configuring Playwright requires installing additional dependencies, in order to prevent breaking changes in existing deployments, we put the new flow behind a feature flag called `PLAYWRIGHT_REPORTS_AND_THUMBNAILS`. Users that don't enable the feature flag will be unaffected by the changes.
+
+### Pandas upgraded to v2
+
+We [upgraded Pandas to v2](https://github.com/apache/superset/pull/24705) and [added performance dependencies](https://github.com/apache/superset/pull/24768) to provide speed improvements, especially when working with large data sets. For the full list of changes, check [Pandas 2.0.0 Release Notes](https://pandas.pydata.org/docs/dev/whatsnew/v2.0.0.html).
+
+### Tags
+
+Tags evolved a lot since 3.0, with many PRs that further improved the feature. During this phase, the community also made [great suggestions](https://github.com/apache/superset/discussions/25918) to make sure the feature is scalable, adhere to our security model, and offer a consistent design. We're still working on this feedback and new improvements will follow. For that reason, we're keeping the feature as beta behind the `TAGGING_SYSTEM` feature flag.
diff --git a/RELEASING/release-notes-3-1/media/bubble_chart.png b/RELEASING/release-notes-3-1/media/bubble_chart.png
new file mode 100644
index 0000000000000000000000000000000000000000..505913ed2cf6aa705599e11959c1fe5c6c7fa9f4
GIT binary patch
literal 430546
zcma%i1z1#T*Y*Gcf*^__(jX1e-6GwHbW8Wp9U=&VQqo=04BZ{l-CYtx4K;-Hzd7gV
z_x|61o!^)1nwibsPp)S@Yu)Q!&kj$^G<4HC;bNj1xDPm$7m**fF1RduKnHjn
zHu{Wi;?Hj#Zap>-U#a|%qQEP`2F3r-Hm^y=W#eQ_2_gX*y$4|{qC6!Ql4yJQ-~nQ;
z>`Zgtr41-#ZZ2lE_U8KbmaZ4^-9r)(4aPc!@I#>|>rZl?m6;uv+8&f@fH1_T*u+UrVHO-I
z)}-y0qHVMG^nruti){4-9LD14m+K`tan#90^fF;zFW-LDcDwm7-W^9f_(UvF)=u7C
zblDL@VlTG0ZTD5Dy
zvQhlHUw0^rDSPfmc0FLbTDwVVxPLyYee9d0OF$*m&aV4v`YF=GZbZCXX@n>D(wiF)
zx^WnLiI(nD%Y0?R$@;dzIpsm)5p>HIhj|Cv@#okycIF;8E7qrh!$WIZ7a*>GiL9)J-CCH&I
zIIf5!k3g@5DLx^=5lFs$kgb4!)JIcC3l^rG235V6E_twvjN1Cx7K8o6Gh1S9#Hkj!
zVPxVD#KTy+AKo1he|i1LKQQ+Zrc$6I8F@Ei*+=zozBMFd;T1)~sQX#33?xw)d{u?B
z6!DbNV7?Y;EugOG=0~#_S2xgRxrw8}Ac+jo2+;VP_nq)NyZ$V5HA`dMyMC^hI0YXoTk7;{
zr`4^{oyn@6YlSX^2?RBMg0-C*F0I;fqYU1I`op_ARz__<+gaN2*`Cxm)W~a-*xU>2
zDp+MWu=%OTkCpnFsT15z(wW+^)ONNigx!5l=L17)>?4IoPteiU{YCuQ{I&cM+GySc
znTa}*1z;n8_x$1hL(qWj5T!JgTUv=KK73v>;v4&@v=VY&sAI(Or{fq|s-4K>9*6E1
zN9gv*=QQW6wXs)08LwG#Y-!M*;u#aIgVqVx1=ks(r8%Y)c>~
ze27L=6HY79PM1y>UWsX=7t6?qc%fz2WZ-L1`@^n-AXr3Nl+utg6kEYOUMYH%HmhEm
zyqDdWU|n%tcD=GUFshhFNUgQtdBM-z`^v$}r}^57TbX3+Nce%BIgkukrp~vMF+0a6
zys;_bOj#u9F2jK;D#|dGOcmTb)w~o{*gH}TLzLKvR`nt7&P@}~lym_<0TFJ)fiHD<
z)h3>bW6E&lC;8F269rCrUaF?~m$^9wU}dnfPoeJ!^Hf}BsiI*{r&tJ?TwQhnm0r0h
zJZ=MtLA_6#UfZ&CQWvtLNj}Rv>phDNv!2DR2y}eTo5E|(o9HySiLpKExZosKm*_OK
z-K3w-I@6C~f?OWLQ<#$lEv6ou%bZH*$_HnVWa;JV72i(3H)>&(
z^Sxy9!1wXUC*vrKiizBXO~2A0yDx_ptKzFrDhn#@NW@6ZNQrFaw^(CVV-{n4CfsZ>
z8g(-d%M&+BYW>{u-J;JiA56E6yf!Lr&v2k}&W6h#_HW0#Fwn+W_3`&<_UY0GsD!Je
zt7J|TXato1Fttva5Zp3{Kz@L3AP%rKCe=#a@oywZgTE}CG6_X
zX-z+Xxy&v(r19M2D|9w<`nkQiM&nfIK<8V*7rhrL7l{Pv1m6fo3o3Y?c)j(s^a{Kt
zyn1$-4DY%?y#OCK_f2dZ)pE?QfO6hvAz1rHwR~#nZOIYd5FSn2(C6kRb$xq~`6%;|
zounbn{q+DFbT2{^>=>fc9m4sf5*`(Yl#g@%OlaV+G_jYTxSt*hj?&wEt+msP(P$`J
zRv0+Gc9c5~e7+x77MC)uQ7U6t++qGb=tshj!^4ro%TKgjhkY#K4DV`Qu)61
zqv~aHp_oQ8_5-QXZ`5;?9x|sT?22_lsJ-#AV^J~^sF^#gZ)dQ|7WvN_@*Bb)Wa1L*
zeQS_x*mQ4lyGgk6MO^xjifi}`j$6$*qE%)uwXbwKD$ti^EY+tOOYo9ObFsM%+|e5S
zEqaUIhrWXGCB0`gor#J=g!4#nMia@EcP@h}P0X_;9ax?FR9GB|n2>W}saCo5KsB~-
z&xF#>=+P*X<+)|?jS>89hl1hG*D0q$7wu*TBpW|hku&kI5Y)b?KJ?xTPGqpfbpO0|
z+4({YvQ|;lT`3x}f6_h4sbSB5BnU=r
zPA!9<7H}1?(mCFll3kNM_!c2YGf^i|;l|=*$M;!dO#WDY&QvN{GA%EgbJg;0H&0J(
zI-C}Isfk-5*zm|>MSd=+D)h52E|cC-qhosVg7<0J+qnf;qv5s}ahAU*HuKgsk
zndi#6ti3dIC15NhJ8_UJk9u5?{LvwtEGDv;yR=us>b;(AdMwsdwBmL$}4j4O~S
zNKZ0MleHSDvs;j>XY}=;ch)20f5(FV|6JLIXMs=aD5+i4*?&93|t`qA3+3y-`C;@G$5qE?jwRgf##rl|CEsj
ze((OmfzMr?zkiXw27pk3|FD6NYZ~IO(hv00kbYew4+GCY!f!>Sq=4VIhW5tBHV$TB
zM-{obYv2Z&t)zwn2=t8d?t>tuL~#JLKW(n8?x-#&%WDX>W_)J^HZW#%wYI%$2gL8n
z3tU>
zCFQp_GT~Jc75}F?@Sng7Ge<{TUM40N7Z*ksHb$_$DH9714-XSFD-$a#15kp&!Oh0;
zohyTl1Nq;L{Ax$k*ul`=+}637>eRsOrXyo%wN8p(o4=;Ql15a<<1N>o_c6=8P))%EGg
zWY=NjBGNs{P2We~{Mdz&9~U=$PwnS-cfz&oj<-z9DkH^`#BAcQcpsODXrFpC^`(y$2!QC%yX`clX64;EX$q
zOG?t-*^ed=1tHvf^yHC49-vU#V07fu1f;wbP{(npTUsuPK@)c-~UXb^9c@DMrAm;%u
z=R(3icKx@YNNs^hAej#J|60CRuYf_IMy2-rx01BT1tf`ov8>MdOOneFV0pmETVcOv
z_Wz%bBJSP1J8}jEe$OKSXu%ufzZPR|9WaLc#1W)lv;KeA=GT*d(~r;+pb2t}LC4>M
zaC!{rsZPqhoqwzHzlWDAb*B)cV+N34ZM??DZtDM
zpbqa;e$DJpOFN+jDB_8?VBy1GfbgoW$VuQvZ6&1FOq+cXkr2uR6_i#jph4jk6Hv@56k-fD)Wa?C>Gq=xBRZ2f!_tA?`Gmgule7O
zf>a;qvx>s?w~gJ7yHmB(mRg-(!}`74|Dv6#z|2q4$o+ngfkWZW+IW~0KL54I|5Ec;
z52Oco*5(#N9pRS}JbDBO2jQKm(ythd)
zkcmw)rl<7LsoPyDQ+a3vi^@pSrloyZctu)qT>nbZEZ6^gr=)9U0QN{jylAC0d1*4A
zq_Q+Y^AKzZt;)z$ayDfkqZrSv-e~{Es)w&eFIYPGiig3FTv42qf<`o>{lzz3(Ig6S
zYDsvFs8rrkna&N!1yN+oMM1ih`Jo{~
z$y#e%*-#Iw)i~M-a4XaA3bv>feD?8`7@|NVZ*NdfW0sU`W^e+^6))hg`9A>sbNA7D
zU*DIfj~|msNlA@nh&|YvE;Wqb*^@UiHtvbl^ET&1d-yP>kFj3%c*1S2)*;99umg86
zhpy7+sE1m{-rhdGYQOO;g^=HMnBQmCJV$CQn`3p3*VxAY$^CJAUmSYX?&9VfxPJ>RKreo88ms^{kVa=oy8@TgbiKx1WF~P7vGwL;GwBbnw472Mm&?e^TyJ~8R9HfU
z`N;Op+ef2A|F~KRL-(rW5?!
zO2xo7JAQ}4`b(*GQf+g3k*C)N8RZ{A@@!2as6tIr?pNw0Qw>gHHZ
z*zR1lLbiOeML>OWF%$|-H1xWJRS)J6U3L+OyVp$YFI_K+34+-0ME`SPWc9M+H3)%CiaqR4`xy8M0)fp@G+`1lq9Bal|_t3!#}!%hN7JGve!#&c3|(|LC00+Gx7
z%Fh>}tKU8oVMg5ET<$X#*`oUf16xVWO7y`W?O56i->VA8nX6u{uhpE8jBM>$a(9k{Zd=cJ%G{H+cpQ7$QesYO$`mqgx1Xc=7&~
z{=JIcW1ky1$;cTFCf-ggQ^jb7>g?1RA|$SOBJE_qu&E1!n7?NHHDtL|fL}X5VocZ#
z!3xt@57@dH6U8Uj^365rI5#H(rl-3zCehZA{)IvfwvphG3oVf2*jZLQTi=aEGOnKI
z8)w)oWvs4yo*5+=DtF22qhYka*l2$;q1N}KxY2r!bEE~X=W}iR
zFkSE>#oPz0oUr0edjO_bf$WZlvDlm+mY~^USXDVe3~tLLT+?ZOAzP)BqtJV~1)=tA
z(PMyVOC1IQok~$BQ(Rxh-dZz4enM`=OURak0Fa_EzWHazO!#b?52`S8uq&hVBO`o%
zMRj*V)t59=U+>78OR~}M5@qdKtWi-!PhYmw8>u(1HUJNI#qbFG=?h#Z;uksu)$lWA
z72;Hob97Iyv+YBDCiRk61_lQG;vF3wWzI0ocp;xAPhdlY_U7V$8>7oP)lcI%>W!vf
z?`Iog$Ko){^OYca+a3}T5usXcugB4yOZI(ZAQ7-mS4uW~JFnloiNM<%d-RPXRrhk)
z59^!hP>L#>?&DXauMjou+&72QGkh1LqMowd*Dls=R4N!bOH|W!6YC5k0SAqSy@@-72#7%qK}uG415J_=>kSbtc5<`&*s}p
zA-1IJz50FWX76h&H5rm^T=|NxRVbmAtj<$KIx<|$p~sD*ltehPJ&w;hHDIF-bh@rz6?u{P@R*A@w|X%W-uyrnq_wXlH}mKq
z5LhIiTvn=IAmZPE*f@@_&X`Z6+^QagzPit=?y;F0-03Ux<~=S3PapG})Agg3;C(M4
zX7U$`n8=KquKC9n_S{%%L@|<}IaG!822d4z7(atp(6gpct9p?C8Zdf2wa}EE`R}E-
z<8Kvuw7`wVs;*ue@uo)OxoX3;WjUHga*^
zdvw3>eG^v0&BcyM7WB>mXjWNi?0q=iFAfMF;(-#n0Sit9E4$rXKH`eDj}=ul-`;51)U2Rv4X68^>u%(SI2Dy4SV2;}CREg%RNEVJa@l-J2Z81xrRc|G<*@N
z6{=2Y1@RQH+nS2jBV>4-i;NiS%+si;(nj?j=0}p>d#}VP0iso%kS+;fU&xZdEyphK
za)%gr{0O2B7L0o$&)tgg*kXO6u<#|kApo6zHXpyG305_?mI!WQDbTZ=m}N2S>~>l5
zqSkz;7S`U@Mwjq)=RPSgWG{u`vk!oCE>|kstDifTAf&Xo%7S-30
zO9C
zt|?1BKxgakcilV!A`&c>&fiGzU!1w`EMs_T45Q_qr=m3a1ItT@y^jiz>&J|CR=82i
zP?}XvSDQ$*=@|^ig(^g3e9g#r$wo!wP`|^VHoF|!30;(`pX-6H{JIopKvnWifa~m2
z)AKSOyZ}6~4B)4wA^L+!Joe}7v5C+W&m$>3POCVxN-Iph0Du2as=jiPk~9-t-ipy6
zXB(R$LYH|vlMc%QgS{}J8yc=f*G!&5)dYd_wMf`o+b_pz3|K-}C0_tULXEInYS`5aXaQ{(
zp~IK+8{(%=l}+3cr;R1KSN?av_HO|Nps$vl^VDvZV;0w5_I@h3pXB8toDE4J?p<`p
z&xOEcR)dRgJ`%HwrJdfd_$2c+(L+y}!R8YHl88lA5U0V7Co%jd<8L<#0lZmgw;*bpUmU2G>T^>&?=+=j;-Zs?Lcp<`
zo4yN}JszMT=uZgiL`fNDDgdTz7pAO$AWVH9GVH(8INhonkJLNuR63z|6+=wDfJu
z;zre*lVTq!N5_gQKrOLxaMsOCVd{DbBWD}($-F^L&9j(X*0V<5Zz#uqK3gp_?kg->
zZ`$^_5X03m3fMY%z;nvwWz}~o~xHu#s!W$4D!}&HF9Jj-5g6dmG^EV*PtNE1EYz@RFuWIN3D50Wf*MYN9
zs5x^1;6$gww6wGq3_(j33rqqFWHrmN_p31sTHid*_A-2BU9QnP*izVXoRI$K=^S`{QVYxx2I#RH
z9=~hJ>g;5Cgd3Fu?7q9ZN{k-o9)#!7HZ>VfV`yjB-R5JG)Xm#Bt`dN^_3sJA=ya
zh$CAudl(Etyuo?S=UmuuzLB`GB!Ma5QK!*rs0TO%OffOB43+FkgJ$m_k>*zh);US`
z!Fa>YXM)C+5LwV=^^!Lmy|v5Eq@3W*Ps4(OxhX$Xtl+AtvuZc^hLpB;(#)cL3F0)H
z>0lH9_N94U_N*2^!_&5lZlFmHq5L;LSC6LjeB1zeKe`5@jcEZ|bKrHkJ(LZmxq>Xlr&_aV^{KPMJn>YGyLK1snhB^n?#T+QbI1kvLPwQ!h
z;A_~m(^bTkUGZs@7>ePz4?p#tr5|R?%f#4sl@|FncSd{OaZE1M@-!LnaUA!|RK}C}
z-O7%)C;C{T0_Zt%^KkUTcs8ELb4P#&xoxL~m+zyi`zTzV?K9okx-K>?V11BFl5sOs
zk~xz2t!LTK0TV(=P2E+Pf7^;mQgdDQDfCS|s7iGnh`Y)u>FA_4hEkbSv%H}k(`OJt
z%?_WQuau#EF@88LjI^{YQzdq*;%xJfbnAuV29zGa>`ym;-u4yGj{u=WPm%GPPx?0~NUDzx5I%CM
zi`=tJ`0qmSr6Vni!Z(YO`wty9ZW**vi1+P_x;cABcdH1*E~93@6t<_3a(O{mTp|1I!4aMD)nKLuocMAnn(^}CS`7?3Af_;dkB3i|{6vX@
zrz!8dzgUm>DzV!BBF&sVTcbwkTvN)$J*jX<3$wxrTWi9FR~3lv7fp~(cdnY`p~%na
ztz`BFpBHWFZP+%ZQUi}T6;h?#R#i5x;+kR;Iit;Y!Z?A=oTdqE=G&VyAvXX93}_^a
z4B)$`liC{HYr46x#1n9@prD|L)9XKQRHb*_1xN&VLRneaT2cKTHY7tY
zo(Zf>^*%X1o)-Vr6b$g#cl-#HR%g#LYxi=meh7odGRwxYoUjm7l~S!%H|bDBXXx=3
zL5to$)vgmSEwqG-)@>wq*p&Pg)evCr|lhx4v%{dU~Hnvng|LZ_h$d^T~dpq>t$zC@%H+2&OzJL
zy7prN#2ak-P{cZC(0P0%5yGYMttUP9a@*=F6q{je#=l)01)PX;(0J
z0ZeMN@$k(?&ESyHX_eT~!o>_D6n^}TcYX{iUA^El!>;DNz&~V6ppY8W*P3r@uV_z#
z_tl<8P4%OVprter;zX&S>EA`^tl&ldpwFKpXUa`KtC~*0uFsZY14+DVMhngl+OgI*
zGsB!`npgoxtOjs3Ta1_Wym<)2mPNIl6O@Bw@86Rkv$C?T`9G0ore__BV-ZPUH~-;W
z1z6<-oShTESMu<@c+qB6Jdr06f={W*0ALg!XTHu90grwr|IO#Ks+Y>fV?OFA4+&|~
z)6-2lEG#jLI&%R;bBRgJuRc09X1qIHY7#y(p*{#D23+X~`n)F3#bFnTi?%&Z%j6{y
zv;O;Y%BPs2U0q#w5PZPHXScL#4S)(9mmUDvsbH
zraLJ9K&*h1Pn@DUo2~L^gSG%uj?aDZ5th9?x45G<39x?|n>+T<@(b
zhqTJHIfXdn0l-eQ6SOrVr#a6e{jElQRANcX1hRPSB-4Fpf)K+?-BPdZH{cUIS@vlqVPPw^Qa
z<(rpvGm&YLo+y{}Ky%x5dE^uR}}
zVM4tjT#Gbhoq-n}pK%j~EnwB&*Y4-r1;rGcoT^1NKhOo%?dj?0SPS%^Yir%$=4&c|
zsb-wp>H)lFR~QKsQ{(Yhz)7i4$t6%kL?(Axb(v05#0{SOIAo4jtlZKi~~*3R69;MpL)UK&|2^#A@9F+m<_L
zUuadc=DQKRo5d;UJ-*M0jut@48IyLv%oA7pHuw}qKh~L>MxAa~YEs&k;WcAig!=KL
zw4`H2)KeUphzPk-aO>lhjjImv$L&8FJTHw$4%-tsSf;#~$@>aqA%|y19Cmx@$2w7{U@;>h
zZ#ANIGg~U#B!4+DQf#M;RJ*k)65ZS^ok->oqVMdc<^Onk@;tGVJ*LuwGhTQfo0hh?
zc4jiNvMO&%yJ*ufV7GgYwwUqed!aid(yU_-^-{-ju$saWqJdUF-xKnnefHs3;vm=n
z^OXmE6Q|$Edga-;gvhSTaZ|s#)+hezWv>~R<%6MfV{zpDsuRuUQ2X^6B0i|bm^
z)r@LTfLAQ~(8xM?e?ZL`ehkf_bEvWOklFv7<))ocPvu=N4p`m_K(~Ev&Y|{a3oePb
z%x1aZ?eVBIXEFB(7#1~UH2;ichdis-v8ushaflYat
z2rEEprGNdKhK7dyh!}2AQ_fv5n)Q0LqqCYofoJ{cOoLl_lFOpo#^sj+x_&n|w>$bZ
zF}i@`Q(>#A<(#SKrT~DT*IRs%Ou}n{7-up|sTg1LX2|D;+nORa>2H)O0}}D{UWHJ%8E)Ta2Z^8i2EX
ztOnoj0~Brh?P^aX$l2SwDamakeuHh~%-Jifcvl=$U#3(67Y9O5F8AXnB7Hvb;iZ#gzNZi{-y4!56$JcBNyx8#@$x(}qUy+my%>am=h0w|0sO-G--o^OQR_4GSaW(=?i~u3NSv
zPWO0ls3yXlH@;(Kk$PRPREfXmJvliPm*mr)3~7)iKUggj|WB+~ywtP8M0%5BGmC)qd
zqBYz-CXQmKG?i8`%&ydlrr9Qiy4fmWhueAauv4NS6wA^h15a`)yt$JFIdWp
zqJCa;)nG0@%SQ)!IVoS$MFM$IJb+Z;IzVGGhb?)X>1<6Ervjm9S8ebq5O`Xk+;v-MXzoTZvi_$aP|!IEs)F@yZRyOe+UtV%F%0u;d4l%hv*4qs{E`h
zvj+k)BDspTN|fpo0h#7C7P}v!Bim<#bC_K7q(axnk>9ejgS~|_0a9x)Rt!2EUK3Es
z;Sc1u0)=E_Eq)H&?Vnlg0*Rf*o$PdzMqH@HsOGws$l%4?y^7gm(^M}Ll>C8*w^wiO
zxOs>5-snzTF+bTc^RevcZE&ND#eJ_ff3(ndcQF-ps%P2x$2cIJ$uqb!Xc#6yb(cTY
z_^4H2r)Et4UI&L^*8RUspuf_=p61>&o)Iw2UINePfcUoZA1mFiJ)zvm95RAJ14ZHo
zKcfeQM2vGiTpym)F*OoBCC+Dt$>~84VV9~Pk24n`DWpvI>VP=-RJN3zO_7zyxm>wh
z_#wfx*1TJRs0)n3;OV1&HpDft&N_oSm&ceUmN+{hXF?|aomi-@iD9)G1|Gx=b9#xS
z9zG1P#Uo!n$~r?2hur}Me)`(au`!yey$=irn12R%+*sbL=Zc%E*sekutHiDeXs=Z0
zX?-2EH`-Ge03q9jbh4dxP3r6Wm~&WsQxj3pxJiCFZO5IvKcQbWs2aV4Q$M=yr?SLT
zWWnradt`SsGn=~JcIs!WWs}`tpd4DoiHWr2dHm5tUBhoqmB@sig5SKO+Wt)-naRhk
zt{eBh`>wGMd$m^C!>nIUISWK7ykw+}G5wN1ID>jr*!TA?%D~AoY|mI;zg9SF7)1k(~jxQpEs?)aDTIj$v7Lljz=AvcNT#5@3mqzw!*`r}yIVHZi4r2w1R
z^n`|v?yCfemok+?iq%7W^Q-(ARfwik&7rl>^<|!~=>o6gCh3J(=*7uSV%MqA@&3U8
z*V0R5WaN!3Af+V(kZ;Bv!8m1>Q;cx!NGiGh_ob>ORp5)TfYYh)a_@A}xlJEU9n+Xbu;X8D1|1supkQE{=jG)O-?KfUt%MH3ZBOu!(y7j6rlst1_ec
zL=4x9$!k4Yt=GTZ3Bv_p7x1Qv#IDN-4*5-9mkF)rmbzXL3y>BP42Wkqmx?)ti_&df
z5_z{{tqOsN6bLzFtgH&1X4iKBFblqUS<}!hb#ERxDC6wcPWh+s^wlznFNfh?BtN
z>HMkRY#EZUAS@oeUrkwjplFjH`^Mb8o@+CR7Q$8pKK_I~}cdbE>kk>&;#mp$#S=d-%)^L{9ptOfdfYB5z|Su3X3tU;;^
zliNd@zH{5f`}CSCw`F~dQYKirr
zs(|jhqYyluT=YOR1a30r3wtvGI?WLKymkd2av^SmHC+qLpzGJej)yvc24>#kB
z5fp&2xx)B;a_nEpRGbGd>wmb@&X-sJ2+*yv1%!w`_y#uDvysy&wb$0JVh4L$<-Pqb
z$ZX~{r@(2&8iT-7ij(YOFHfi;z?HLs)2-Y#6xXexJ1cV7SpyEYkwE;R;R8joXWmg<
ze`iNkGw@Z%8J2e$PP2vH5a&8^odS5a12Vu9`9kKB6@WKk
zy7sS5S02?;BMGtYRN5E3_FzqMNKL0j5-WIqTj!Q?Rc2n~mY4IGS#SV@O5v+PX4Q#+
zR}ni>&F%Fn$Lgk{yd*ulWtIu1tb+P!*31w(nu7LWh(?y1Q8gSmP$dGdtJklDyU)Ws
z>Qo_~5S$XHb*us4H78We_@(*P*BS|x^4{}512I_k&VW!}7OC>U8Qv*D|G8j&t}jRW
zVv%7(X;A7qmO^wgk}JlWiYTO|4uD{mvz@q!I4y$e+B6rbD(hNZliTZhS=Utf-PPa}
z!6%p8i-cWxzQ>ng&69~_{ChH>IYVCFyVHlhPo|rFnl&ElwGUrVVqL!$SK)%VHoC!0
z%H5ssvM_eQxrTgp*`h;wL%{A+j*N_$a9k^{&IL(WjO8z1gvBiede3Av44*`3V!=rj
zpWK~R{hrgA@TUXF%aRXX3WEKko<6(q(gZB`S{F%pHzz~}xj=^@cLK>pO1Uf3sBaod
zY1;atJc!FnXW(6OIpQ?mVhV+a_s(7rrrL1x9H#}LWjC|bI
zq*HdAM6K1;xLXr-)$N6Ct{k@bb9wb^vpd91aI04)>xS0Ps2w;oqyh<)k`1#l$+CpF
zLJ~tWSFLZB9KP0@1-Z%J4s&d3vWGXpI;OI0_tTfy_sy2Oh#{Pkd@FP#qeMCKYGkfO!Ns#QEWCPg>>=
z-@|TCVtA7ASiy$M%_%}Xg`d2n3%ps=+`^QiTj=W<)nE;WwR86yBtD9F_k9+M0nYS|
zGM6>YJ09D*`K(u$nGA_bL6(%n9r+t&Pwdooa(8PF!kO8|b=0*f%ssdg;V5`Q53F}8
z!*2${IFWuve$l_?T}^;*Y_Zu9x6fUNr#*7}@jbDTs5A5CUWtG~=96ECHx%J_3w~wV
zP4p-0@ctJ0S+Y~aEj5b?U>I!NiXSuXjY?HDa|)l{@RrY?RN!6a`d6D#0Ri3XBQ>IY
z^&Pn-dumojAa8)lv7dM%5uY7I&KQLvBF|Fr21p;OC2GJfqM`lU)83L
ztutlt@?wge)u916?T`>{YsBNS(>kpIQs+^e*yqGx0^(*FTT%aSO@|+%o+DpR>N-S5
z5%Lqpg_%t#eI=`|xGyVX^Q<(z#FKJ1a~hlIfI{xG_Dx1)xeb1OxUKpYc(f8%~!!2lWYIUKW`w~W~Ay_@>n6XS_J
zOcUm5z!>PBKnfQdN348wsvWX4WDQ4xr|}6-o#BoUw1`92{H_lBw9PdBi>X9;GB`WA
zkzkkhWL{5K&*4B_Uq9r*z;bOh!1O^+bz`MHT?-rPempEj^&CazNj~=JUfn8zxF!07*Esk;!uw6
zBzvzz$%y~^2!Evna5xy|7Bi3bYo-4!H}g{=jaRwF#<}kVd#X5W_v%=t=VGnT9?W?X
za)^yhK)(e*B&5XZ1X#nC)}CnR$6&;1hP@^^gE}YLw-K{~8*lcTECMp8w7rv`?hEPb
zhMLW2YYLrwdRizGi+LvSVny!jw*d!*?xrQvzN*-zsFLrU8NR!_LL0r4P`!xhl!dJG
zI4z3H;`6eT+SuK6q0e06;n@LiODLzs#ssX2f5b9a`c7a|$HNms+
zF~6Xu_sntZRh4o2y#4XVviV#Kh_BUx0f6d57h`+8Gu@93Z6Jh>AxRH-u*y-^yn4l&k~9Q5VSyg2!I)S7Q@
zGOaT;Uil_JX=?AOMy*GqtS-+$qOodzaX>4_Q}2?R;h%V}V~!QP?iaF!H%+cth*eBa
z@qWKQ@hVLkNURln7c=}5hRw}H>?^9#ML*j;xm>6(c)tMP#jKH9)854;lb%zxMf=*A
z@SogLUQS6G>uv)oT-^DuJjSNhMQ@V|Y2GKh&WMfbAM}*14xAYc4K>#h(oqQND(MkW
zf8=xNsUrY0O;D|$=TH>diDdpbp$QT?eJ<5-WiWHvW?2S4O*N_K91>(Cv$GTL*h!?^
z3jQ3DKTX|JrM4L5)l7tr6^p-{>bJaU7PMy@CF*2GeTWqL1k%DlNm;fvQu?*pGpm46
zH+k%kulYJy&5)mUhogt{L(R=}#df)>&y8gl>x_Z2@}&Ne1HcFoW?od<;NP2kbujor
z;OMFii%@2`cT#hOm(8ad4{|3?!72$E
z*S~Gg-)33jm43(@IbN`J!jh>yz@hEU3u`XA1mY>n*TaZud2Dk5W-g=f=A2%&uQTu#
zTC*mn*G05H(r{MO4{$G*E4Bua*82<4@%YJ~@90enPXKq=8ZZ;%!-K=OLW5|N9
zyRhl%rWZ^E<@n@jy5E=eox9u$$}9E7f|Q9Mx(EUh7HycHg&WY)%ro64o;l~`_oxzh
z$uYN0RUg-MV_1>U@S6EoF0mC@^HrCg0_51~R7p)h2ChjVJDp^8r0GET`HJG@W=gDd
z<0o~uiTk|Zfq{zbmnZl}g-ZK*-l9ovy7WGl$hK;UT!Q$Yru5+%{SzCSIfXa6s`x5B
z5V?X7l7*T^jV#Ye)%^k|t<{(_e8Q&`XJ?J#$0tF}RFb}LFc)SU+^8m>FE&;EeMV^w
z#!P%d{T}w1Ao*A7z&C*jcxPfmNM!Hd&O-f8fn70`*vKePx?ShM%?B9TnP$6kE6-&XiNrV?
zoo^ir)$ur`t6@9z=ky`V=RR1dLzV9oU2DD*FQ~L7az-S-4FQ$Dfz7&ohAoIBgf&SW
zO%>8}R5=iUPYEz~nIPgJ$0qmV|2{tP@eX0}P|&E`>~p*}Xv^rQI<=U