You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2025/08/18 20:05:22 UTC
(superset) branch master updated: feat(api): Added uuid filed support to dataset, chart, dashboard API (#29573)
This is an automated email from the ASF dual-hosted git repository.
maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 31e2143c84 feat(api): Added uuid filed support to dataset, chart, dashboard API (#29573)
31e2143c84 is described below
commit 31e2143c84a40731e6a66df10d46217ae33111f4
Author: Danylo Korostil <te...@gmail.com>
AuthorDate: Mon Aug 18 23:05:16 2025 +0300
feat(api): Added uuid filed support to dataset, chart, dashboard API (#29573)
---
superset/charts/api.py | 77 ++++++++++++++++---------
superset/charts/schemas.py | 47 +++++++++++++++
superset/daos/chart.py | 15 ++++-
superset/dashboards/api.py | 2 +
superset/dashboards/schemas.py | 3 +
superset/datasets/api.py | 5 ++
superset/datasets/schemas.py | 2 +
superset/models/slice.py | 11 +++-
superset/tasks/thumbnails.py | 2 +-
tests/integration_tests/charts/api_tests.py | 6 ++
tests/integration_tests/dashboards/api_tests.py | 1 +
tests/integration_tests/datasets/api_tests.py | 1 +
12 files changed, 141 insertions(+), 31 deletions(-)
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 5abead3f6a..a33c0bc943 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -45,6 +45,7 @@ from superset.charts.filters import (
from superset.charts.schemas import (
CHART_SCHEMAS,
ChartCacheWarmUpRequestSchema,
+ ChartGetResponseSchema,
ChartPostSchema,
ChartPutSchema,
get_delete_ids_schema,
@@ -131,34 +132,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
}
class_permission_name = "Chart"
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
- show_columns = [
- "cache_timeout",
- "certified_by",
- "certification_details",
- "changed_on_delta_humanized",
- "dashboards.dashboard_title",
- "dashboards.id",
- "dashboards.json_metadata",
- "description",
- "id",
- "owners.first_name",
- "owners.id",
- "owners.last_name",
- "dashboards.id",
- "dashboards.dashboard_title",
- "params",
- "slice_name",
- "thumbnail_url",
- "url",
- "viz_type",
- "query_context",
- "is_managed_externally",
- "tags.id",
- "tags.name",
- "tags.type",
- ]
- show_select_columns = show_columns + ["table.id"]
list_columns = [
"is_managed_externally",
"certified_by",
@@ -230,6 +204,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
"datasource_type",
"description",
"id",
+ "uuid",
"owners",
"dashboards",
"slice_name",
@@ -255,6 +230,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
add_model_schema = ChartPostSchema()
edit_model_schema = ChartPutSchema()
+ chart_get_response_schema = ChartGetResponseSchema()
openapi_spec_tag = "Charts"
""" Override the name set for this collection of endpoints """
@@ -287,6 +263,53 @@ class ChartRestApi(BaseSupersetModelRestApi):
allowed_rel_fields = {"owners", "created_by", "changed_by"}
+ @expose("/<id_or_uuid>", methods=["GET"])
+ @protect()
+ @safe
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
+ log_to_statsd=False,
+ )
+ def get(self, id_or_uuid: str) -> Response:
+ """Gets a chart
+ ---
+ get:
+ description: >-
+ Get a chart
+ parameters:
+ - in: path
+ schema:
+ type: string
+ name: id_or_uuid
+ description: Either the id of the chart, or its uuid
+ responses:
+ 200:
+ description: Chart
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ result:
+ $ref: '#/components/schemas/ChartGetResponseSchema'
+ 302:
+ description: Redirects to the current digest
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ """
+ # pylint: disable=arguments-differ
+ try:
+ dash = ChartDAO.get_by_id_or_uuid(id_or_uuid)
+ result = self.chart_get_response_schema.dump(dash)
+ return self.response(200, result=result)
+ except ChartNotFoundError:
+ return self.response_404()
+
@expose("/", methods=("POST",))
@protect()
@safe
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index f1f7a7a19e..3a05c3ff17 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -27,6 +27,7 @@ from marshmallow.validate import Length, Range
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
from superset.db_engine_specs.base import builtin_time_grains
+from superset.tags.models import TagType
from superset.utils import pandas_postprocessing, schema as utils
from superset.utils.core import (
AnnotationType,
@@ -241,6 +242,7 @@ class ChartPostSchema(Schema):
)
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
external_url = fields.String(allow_none=True)
+ uuid = fields.UUID(allow_none=True)
class ChartPutSchema(Schema):
@@ -297,6 +299,7 @@ class ChartPutSchema(Schema):
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
external_url = fields.String(allow_none=True)
tags = fields.List(fields.Integer(metadata={"description": tags_description}))
+ uuid = fields.UUID(allow_none=True)
class ChartGetDatasourceObjectDataResponseSchema(Schema):
@@ -1617,6 +1620,49 @@ class ChartCacheWarmUpResponseSchema(Schema):
)
+class TagSchema(Schema):
+ id = fields.Int()
+ name = fields.String()
+ type = fields.Enum(TagType, by_value=True)
+
+
+class UserSchema(Schema):
+ id = fields.Int()
+ first_name = fields.String()
+ last_name = fields.String()
+
+
+class DashboardSchema(Schema):
+ id = fields.Int()
+ dashboard_title = fields.String()
+ json_metadata = fields.String()
+
+
+class ChartGetResponseSchema(Schema):
+ id = fields.Int(description=id_description)
+ url = fields.String()
+ cache_timeout = fields.String()
+ certified_by = fields.String()
+ certification_details = fields.String()
+ changed_on_humanized = fields.String(data_key="changed_on_delta_humanized")
+ description = fields.String()
+ params = fields.String()
+ slice_name = fields.String()
+ thumbnail_url = fields.String()
+ viz_type = fields.String()
+ query_context = fields.String()
+ is_managed_externally = fields.Boolean()
+ tags = fields.Nested(TagSchema, many=True)
+ owners = fields.List(fields.Nested(UserSchema))
+ dashboards = fields.List(fields.Nested(DashboardSchema))
+ uuid = fields.UUID()
+ datasource_id = fields.Int()
+ datasource_name_text = fields.Function(lambda obj: obj.datasource_name_text())
+ datasource_type = fields.String()
+ datasource_url = fields.Function(lambda obj: obj.datasource_url())
+ datasource_uuid = fields.UUID(attribute="table.uuid")
+
+
CHART_SCHEMAS = (
ChartCacheWarmUpRequestSchema,
ChartCacheWarmUpResponseSchema,
@@ -1640,6 +1686,7 @@ CHART_SCHEMAS = (
ChartDataGeodeticParseOptionsSchema,
ChartEntityResponseSchema,
ChartGetDatasourceResponseSchema,
+ ChartGetResponseSchema,
ChartCacheScreenshotResponseSchema,
GetFavStarIdsSchema,
)
diff --git a/superset/daos/chart.py b/superset/daos/chart.py
index 35afb7f7a9..adca95b8a6 100644
--- a/superset/daos/chart.py
+++ b/superset/daos/chart.py
@@ -20,11 +20,14 @@ import logging
from datetime import datetime
from typing import TYPE_CHECKING
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+
from superset.charts.filters import ChartFilter
+from superset.commands.chart.exceptions import ChartNotFoundError
from superset.daos.base import BaseDAO
from superset.extensions import db
from superset.models.core import FavStar, FavStarClassName
-from superset.models.slice import Slice
+from superset.models.slice import id_or_uuid_filter, Slice
from superset.utils.core import get_user_id
if TYPE_CHECKING:
@@ -36,6 +39,16 @@ logger = logging.getLogger(__name__)
class ChartDAO(BaseDAO[Slice]):
base_filter = ChartFilter
+ @staticmethod
+ def get_by_id_or_uuid(id_or_uuid: str) -> Slice:
+ query = db.session.query(Slice).filter(id_or_uuid_filter(id_or_uuid))
+ # Apply chart base filters
+ query = ChartFilter("id", SQLAInterface(Slice, db.session)).apply(query, None)
+ chart = query.one_or_none()
+ if not chart:
+ raise ChartNotFoundError()
+ return chart
+
@staticmethod
def favorited_ids(charts: list[Slice]) -> list[FavStar]:
ids = [chart.id for chart in charts]
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 6d8fd0da55..54e05df8b7 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -191,6 +191,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
list_columns = [
"id",
+ "uuid",
"published",
"status",
"slug",
@@ -251,6 +252,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"changed_by",
"dashboard_title",
"id",
+ "uuid",
"owners",
"published",
"roles",
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index bdfcb94fc7..df6c1dd04b 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -238,6 +238,7 @@ class DashboardGetResponseSchema(Schema):
changed_on_humanized = fields.String(data_key="changed_on_delta_humanized")
created_on_humanized = fields.String(data_key="created_on_delta_humanized")
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
+ uuid = fields.UUID(allow_none=True)
# pylint: disable=unused-argument
@post_dump()
@@ -365,6 +366,7 @@ class DashboardPostSchema(BaseDashboardSchema):
)
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
external_url = fields.String(allow_none=True)
+ uuid = fields.UUID(allow_none=True)
class DashboardCopySchema(Schema):
@@ -431,6 +433,7 @@ class DashboardPutSchema(BaseDashboardSchema):
tags = fields.List(
fields.Integer(metadata={"description": tags_description}, allow_none=True)
)
+ uuid = fields.UUID(allow_none=True)
class DashboardNativeFiltersConfigUpdateSchema(BaseDashboardSchema):
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 0af6ecca4b..2afd0411c2 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -117,8 +117,10 @@ class DatasetRestApi(BaseSupersetModelRestApi):
}
list_columns = [
"id",
+ "uuid",
"database.id",
"database.database_name",
+ "database.uuid",
"changed_by_name",
"changed_by.first_name",
"changed_by.last_name",
@@ -153,6 +155,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"id",
"database.database_name",
"database.id",
+ "database.uuid",
"table_name",
"sql",
"filter_select_enabled",
@@ -222,6 +225,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"columns.advanced_data_type",
"is_managed_externally",
"uid",
+ "uuid",
"datasource_name",
"name",
"column_formats",
@@ -273,6 +277,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
}
search_columns = [
"id",
+ "uuid",
"database",
"owners",
"catalog",
diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py
index 3b781f4b1c..a0d5747dd7 100644
--- a/superset/datasets/schemas.py
+++ b/superset/datasets/schemas.py
@@ -150,6 +150,7 @@ class DatasetPostSchema(Schema):
normalize_columns = fields.Boolean(load_default=False)
always_filter_main_dttm = fields.Boolean(load_default=False)
template_params = fields.String(allow_none=True)
+ uuid = fields.UUID(allow_none=True)
class DatasetPutSchema(Schema):
@@ -176,6 +177,7 @@ class DatasetPutSchema(Schema):
extra = fields.String(allow_none=True)
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
external_url = fields.String(allow_none=True)
+ uuid = fields.UUID(allow_none=True)
def handle_error(
self,
diff --git a/superset/models/slice.py b/superset/models/slice.py
index 2469db90ad..f47f424fc3 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -37,6 +37,7 @@ from sqlalchemy import (
from sqlalchemy.engine.base import Connection
from sqlalchemy.orm import relationship
from sqlalchemy.orm.mapper import Mapper
+from sqlalchemy.sql.elements import BinaryExpression
from superset import db, is_feature_enabled, security_manager
from superset.legacy import update_time_range
@@ -361,11 +362,17 @@ class Slice( # pylint: disable=too-many-public-methods
return self.query_context_factory
@classmethod
- def get(cls, id_: int) -> Slice:
- qry = db.session.query(Slice).filter_by(id=id_)
+ def get(cls, id_or_uuid: str) -> Slice:
+ qry = db.session.query(Slice).filter_by(id_or_uuid_filter(id_or_uuid))
return qry.one_or_none()
+def id_or_uuid_filter(id_or_uuid: str) -> BinaryExpression:
+ if id_or_uuid.isdigit():
+ return Slice.id == int(id_or_uuid)
+ return Slice.uuid == id_or_uuid
+
+
def set_related_perm(_mapper: Mapper, _connection: Connection, target: Slice) -> None:
src_class = target.cls_model
if id_ := target.datasource_id:
diff --git a/superset/tasks/thumbnails.py b/superset/tasks/thumbnails.py
index 5c3e2e412d..9ff5ccfedf 100644
--- a/superset/tasks/thumbnails.py
+++ b/superset/tasks/thumbnails.py
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
@celery_app.task(name="cache_chart_thumbnail", soft_time_limit=300)
def cache_chart_thumbnail(
current_user: Optional[str],
- chart_id: int,
+ chart_id: str,
force: bool,
window_size: Optional[WindowSize] = None,
thumb_size: Optional[WindowSize] = None,
diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py
index 89fb23fc39..1b95ed1f63 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -1055,6 +1055,12 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase):
"id",
"thumbnail_url",
"url",
+ "uuid",
+ "datasource_id",
+ "datasource_name_text",
+ "datasource_type",
+ "datasource_url",
+ "datasource_uuid",
):
assert value == expected_result[key]
db.session.delete(chart)
diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py
index 45aad249ec..43da49abbb 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -530,6 +530,7 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
"last_name": "user",
},
"id": dashboard.id,
+ "uuid": str(dashboard.uuid),
"css": "",
"dashboard_title": "title",
"datasources": [],
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index 91bada2275..2316414a12 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -400,6 +400,7 @@ class TestDatasetApi(SupersetTestCase):
"backend": main_db.backend,
"database_name": "examples",
"id": 1,
+ "uuid": ANY,
},
"default_endpoint": None,
"description": "Energy consumption",