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",