You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by dp...@apache.org on 2020/05/05 13:42:29 UTC

[incubator-superset] branch master updated: docs(api): improve openapi documentation for dash, charts and queries (#9724)

This is an automated email from the ASF dual-hosted git repository.

dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 0d85d25  docs(api): improve openapi documentation for dash, charts and queries (#9724)
0d85d25 is described below

commit 0d85d2531456ceebb8d40364102bca62e43a1140
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Tue May 5 14:42:18 2020 +0100

    docs(api): improve openapi documentation for dash, charts and queries (#9724)
---
 superset/charts/api.py         |  27 ++++----
 superset/charts/schemas.py     | 136 +++++++++++++++++++++++++++++++++++------
 superset/dashboards/api.py     |  16 +++--
 superset/dashboards/schemas.py |  96 +++++++++++++++++++++++++----
 superset/queries/api.py        |   2 +
 superset/queries/schemas.py    |  28 +++++++++
 superset/views/base_api.py     |  15 +++++
 tests/charts/api_tests.py      |   8 +--
 8 files changed, 272 insertions(+), 56 deletions(-)

diff --git a/superset/charts/api.py b/superset/charts/api.py
index d3c1d33..19de171 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -18,7 +18,6 @@ import logging
 from typing import Any, Dict
 
 import simplejson
-from apispec import APISpec
 from flask import g, make_response, redirect, request, Response, url_for
 from flask_appbuilder.api import expose, protect, rison, safe
 from flask_appbuilder.models.sqla.interface import SQLAInterface
@@ -48,6 +47,7 @@ from superset.charts.schemas import (
     ChartPostSchema,
     ChartPutSchema,
     get_delete_ids_schema,
+    openapi_spec_methods_override,
     thumbnail_query_schema,
 )
 from superset.constants import RouteMethod
@@ -145,14 +145,21 @@ class ChartRestApi(BaseSupersetModelRestApi):
     edit_model_schema = ChartPutSchema()
 
     openapi_spec_tag = "Charts"
+    """ Override the name set for this collection of endpoints """
+    openapi_spec_component_schemas = CHART_DATA_SCHEMAS
+    """ Add extra schemas to the OpenAPI components schema section """
+    openapi_spec_methods = openapi_spec_methods_override
+    """ Overrides GET methods OpenApi descriptions """
 
     order_rel_fields = {
         "slices": ("slice_name", "asc"),
         "owners": ("first_name", "asc"),
     }
+
     related_field_filters = {
         "owners": RelatedFieldFilter("first_name", FilterRelatedOwners)
     }
+
     allowed_rel_fields = {"owners"}
 
     def __init__(self) -> None:
@@ -169,7 +176,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         ---
         post:
           description: >-
-            Create a new Chart
+            Create a new Chart.
           requestBody:
             description: Chart schema
             required: true
@@ -224,7 +231,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         ---
         put:
           description: >-
-            Changes a Chart
+            Changes a Chart.
           parameters:
           - in: path
             schema:
@@ -290,7 +297,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         ---
         delete:
           description: >-
-            Deletes a Chart
+            Deletes a Chart.
           parameters:
           - in: path
             schema:
@@ -340,7 +347,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         ---
         delete:
           description: >-
-            Deletes multiple Charts in a bulk operation
+            Deletes multiple Charts in a bulk operation.
           parameters:
           - in: query
             name: q
@@ -457,7 +464,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         """Get Chart thumbnail
         ---
         get:
-          description: Compute or get already computed chart thumbnail from cache
+          description: Compute or get already computed chart thumbnail from cache.
           parameters:
           - in: path
             schema:
@@ -509,13 +516,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
             FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
         )
 
-    def add_apispec_components(self, api_spec: APISpec) -> None:
-        for chart_type in CHART_DATA_SCHEMAS:
-            api_spec.components.schema(
-                chart_type.__name__, schema=chart_type,
-            )
-        super().add_apispec_components(api_spec)
-
     @expose("/datasources", methods=["GET"])
     @protect()
     @safe
@@ -523,6 +523,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         """Get available datasources
         ---
         get:
+          description: Get available datasources.
           responses:
             200:
               description: charts unique datasource data
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 5bb3fe6..8302f0c 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -23,12 +23,69 @@ from superset.common.query_context import QueryContext
 from superset.exceptions import SupersetException
 from superset.utils import core as utils
 
+#
+# RISON/JSON schemas for query parameters
+#
 get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
 thumbnail_query_schema = {
     "type": "object",
     "properties": {"force": {"type": "boolean"}},
 }
 
+#
+# Column schema descriptions
+#
+slice_name_description = "The name of the chart."
+description_description = "A description of the chart propose."
+viz_type_description = "The type of chart visualization used."
+owners_description = (
+    "Owner are users ids allowed to delete or change this chart. "
+    "If left empty you will be one of the owners of the chart."
+)
+params_description = (
+    "Parameters are generated dynamically when clicking the save "
+    "or overwrite button in the explore view. "
+    "This JSON object for power users who may want to alter specific parameters."
+)
+cache_timeout_description = (
+    "Duration (in seconds) of the caching timeout "
+    "for this chart. Note this defaults to the datasource/table"
+    " timeout if undefined."
+)
+datasource_id_description = (
+    "The id of the dataset/datasource this new chart will use. "
+    "A complete datasource identification needs `datasouce_id` "
+    "and `datasource_type`."
+)
+datasource_type_description = (
+    "The type of dataset/datasource identified on `datasource_id`."
+)
+datasource_name_description = "The datasource name."
+dashboards_description = "A list of dashboards to include this new chart to."
+
+#
+# OpenAPI method specification overrides
+#
+openapi_spec_methods_override = {
+    "get": {"get": {"description": "Get a chart detail information."}},
+    "get_list": {
+        "get": {
+            "description": "Get a list of charts, use Rison or JSON query "
+            "parameters for filtering, sorting, pagination and "
+            " for selecting specific columns and metadata.",
+        }
+    },
+    "info": {
+        "get": {
+            "description": "Several metadata information about chart API endpoints.",
+        }
+    },
+    "related": {
+        "get": {"description": "Get a list of all possible owners for a chart."}
+    },
+}
+""" Overrides GET methods OpenApi descriptions """
+
 
 def validate_json(value: Union[bytes, bytearray, str]) -> None:
     try:
@@ -38,35 +95,74 @@ def validate_json(value: Union[bytes, bytearray, str]) -> None:
 
 
 class ChartPostSchema(Schema):
-    slice_name = fields.String(required=True, validate=Length(1, 250))
-    description = fields.String(allow_none=True)
-    viz_type = fields.String(allow_none=True, validate=Length(0, 250))
-    owners = fields.List(fields.Integer())
-    params = fields.String(allow_none=True, validate=validate_json)
-    cache_timeout = fields.Integer(allow_none=True)
-    datasource_id = fields.Integer(required=True)
-    datasource_type = fields.String(required=True)
-    datasource_name = fields.String(allow_none=True)
-    dashboards = fields.List(fields.Integer())
+    """
+    Schema to add a new chart.
+    """
+
+    slice_name = fields.String(
+        description=slice_name_description, required=True, validate=Length(1, 250)
+    )
+    description = fields.String(description=description_description, allow_none=True)
+    viz_type = fields.String(
+        description=viz_type_description,
+        validate=Length(0, 250),
+        example=["bar", "line_multi", "area", "table"],
+    )
+    owners = fields.List(fields.Integer(description=owners_description))
+    params = fields.String(
+        description=params_description, allow_none=True, validate=validate_json
+    )
+    cache_timeout = fields.Integer(
+        description=cache_timeout_description, allow_none=True
+    )
+    datasource_id = fields.Integer(description=datasource_id_description, required=True)
+    datasource_type = fields.String(
+        description=datasource_type_description,
+        validate=validate.OneOf(choices=("druid", "table", "view")),
+        required=True,
+    )
+    datasource_name = fields.String(
+        description=datasource_name_description, allow_none=True
+    )
+    dashboards = fields.List(fields.Integer(description=dashboards_description))
 
 
 class ChartPutSchema(Schema):
-    slice_name = fields.String(allow_none=True, validate=Length(0, 250))
-    description = fields.String(allow_none=True)
-    viz_type = fields.String(allow_none=True, validate=Length(0, 250))
-    owners = fields.List(fields.Integer())
-    params = fields.String(allow_none=True)
-    cache_timeout = fields.Integer(allow_none=True)
-    datasource_id = fields.Integer(allow_none=True)
-    datasource_type = fields.String(allow_none=True)
-    dashboards = fields.List(fields.Integer())
+    """
+    Schema to update or patch a chart
+    """
+
+    slice_name = fields.String(
+        description=slice_name_description, allow_none=True, validate=Length(0, 250)
+    )
+    description = fields.String(description=description_description, allow_none=True)
+    viz_type = fields.String(
+        description=viz_type_description,
+        allow_none=True,
+        validate=Length(0, 250),
+        example=["bar", "line_multi", "area", "table"],
+    )
+    owners = fields.List(fields.Integer(description=owners_description))
+    params = fields.String(description=params_description, allow_none=True)
+    cache_timeout = fields.Integer(
+        description=cache_timeout_description, allow_none=True
+    )
+    datasource_id = fields.Integer(
+        description=datasource_id_description, allow_none=True
+    )
+    datasource_type = fields.String(
+        description=datasource_type_description,
+        validate=validate.OneOf(choices=("druid", "table", "view")),
+        allow_none=True,
+    )
+    dashboards = fields.List(fields.Integer(description=dashboards_description))
 
 
 class ChartDataColumnSchema(Schema):
     column_name = fields.String(
         description="The name of the target column", example="mycol",
     )
-    type = fields.String(description="Type of target column", example="BIGINT",)
+    type = fields.String(description="Type of target column", example="BIGINT")
 
 
 class ChartDataAdhocMetricSchema(Schema):
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 3806bbf..21d4e79 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -45,6 +45,7 @@ from superset.dashboards.schemas import (
     DashboardPutSchema,
     get_delete_ids_schema,
     get_export_ids_schema,
+    openapi_spec_methods_override,
     thumbnail_query_schema,
 )
 from superset.models.dashboard import Dashboard
@@ -145,6 +146,9 @@ class DashboardRestApi(BaseSupersetModelRestApi):
     }
     allowed_rel_fields = {"owners"}
 
+    openapi_spec_methods = openapi_spec_methods_override
+    """ Overrides GET methods OpenApi descriptions """
+
     def __init__(self) -> None:
         if is_feature_enabled("THUMBNAILS"):
             self.include_route_methods = self.include_route_methods | {"thumbnail"}
@@ -159,7 +163,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         post:
           description: >-
-            Create a new Dashboard
+            Create a new Dashboard.
           requestBody:
             description: Dashboard schema
             required: true
@@ -216,7 +220,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         put:
           description: >-
-            Changes a Dashboard
+            Changes a Dashboard.
           parameters:
           - in: path
             schema:
@@ -282,7 +286,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         delete:
           description: >-
-            Deletes a Dashboard
+            Deletes a Dashboard.
           parameters:
           - in: path
             schema:
@@ -332,7 +336,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         delete:
           description: >-
-            Deletes multiple Dashboards in a bulk operation
+            Deletes multiple Dashboards in a bulk operation.
           parameters:
           - in: query
             name: q
@@ -391,7 +395,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         get:
           description: >-
-            Exports multiple Dashboards and downloads them as YAML files
+            Exports multiple Dashboards and downloads them as YAML files.
           parameters:
           - in: query
             name: q
@@ -444,7 +448,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         ---
         get:
           description: >-
-            Compute async or get already computed dashboard thumbnail from cache
+            Compute async or get already computed dashboard thumbnail from cache.
           parameters:
           - in: path
             schema:
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 201c4ca..0d8c60a 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -31,6 +31,52 @@ thumbnail_query_schema = {
     "properties": {"force": {"type": "boolean"}},
 }
 
+dashboard_title_description = "A title for the dashboard."
+slug_description = "Unique identifying part for the web address of the dashboard."
+owners_description = (
+    "Owner are users ids allowed to delete or change this dashboard. "
+    "If left empty you will be one of the owners of the dashboard."
+)
+position_json_description = (
+    "This json object describes the positioning of the widgets "
+    "in the dashboard. It is dynamically generated when "
+    "adjusting the widgets size and positions by using "
+    "drag & drop in the dashboard view"
+)
+css_description = "Override CSS for the dashboard."
+json_metadata_description = (
+    "This JSON object is generated dynamically when clicking "
+    "the save or overwrite button in the dashboard view. "
+    "It is exposed here for reference and for power users who may want to alter "
+    " specific parameters."
+)
+published_description = (
+    "Determines whether or not this dashboard is visible in "
+    "the list of all dashboards."
+)
+
+
+openapi_spec_methods_override = {
+    "get": {"get": {"description": "Get a dashboard detail information."}},
+    "get_list": {
+        "get": {
+            "description": "Get a list of dashboards, use Rison or JSON query "
+            "parameters for filtering, sorting, pagination and "
+            " for selecting specific columns and metadata.",
+        }
+    },
+    "info": {
+        "get": {
+            "description": "Several metadata information about dashboard API "
+            "endpoints.",
+        }
+    },
+    "related": {
+        "get": {"description": "Get a list of all possible owners for a dashboard."}
+    },
+}
+""" Overrides GET methods OpenApi descriptions """
+
 
 def validate_json(value: Union[bytes, bytearray, str]) -> None:
     try:
@@ -73,20 +119,44 @@ class BaseDashboardSchema(Schema):
 
 
 class DashboardPostSchema(BaseDashboardSchema):
-    dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
-    slug = fields.String(allow_none=True, validate=[Length(1, 255)])
-    owners = fields.List(fields.Integer())
-    position_json = fields.String(validate=validate_json)
+    dashboard_title = fields.String(
+        description=dashboard_title_description,
+        allow_none=True,
+        validate=Length(0, 500),
+    )
+    slug = fields.String(
+        description=slug_description, allow_none=True, validate=[Length(1, 255)]
+    )
+    owners = fields.List(fields.Integer(description=owners_description))
+    position_json = fields.String(
+        description=position_json_description, validate=validate_json
+    )
     css = fields.String()
-    json_metadata = fields.String(validate=validate_json_metadata)
-    published = fields.Boolean()
+    json_metadata = fields.String(
+        description=json_metadata_description, validate=validate_json_metadata
+    )
+    published = fields.Boolean(description=published_description)
 
 
 class DashboardPutSchema(BaseDashboardSchema):
-    dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
-    slug = fields.String(allow_none=True, validate=Length(0, 255))
-    owners = fields.List(fields.Integer(allow_none=True))
-    position_json = fields.String(allow_none=True, validate=validate_json)
-    css = fields.String(allow_none=True)
-    json_metadata = fields.String(allow_none=True, validate=validate_json_metadata)
-    published = fields.Boolean(allow_none=True)
+    dashboard_title = fields.String(
+        description=dashboard_title_description,
+        allow_none=True,
+        validate=Length(0, 500),
+    )
+    slug = fields.String(
+        description=slug_description, allow_none=True, validate=Length(0, 255)
+    )
+    owners = fields.List(
+        fields.Integer(description=owners_description, allow_none=True)
+    )
+    position_json = fields.String(
+        description=position_json_description, allow_none=True, validate=validate_json
+    )
+    css = fields.String(description=css_description, allow_none=True)
+    json_metadata = fields.String(
+        description=json_metadata_description,
+        allow_none=True,
+        validate=validate_json_metadata,
+    )
+    published = fields.Boolean(description=published_description, allow_none=True)
diff --git a/superset/queries/api.py b/superset/queries/api.py
index 092989f..0d49bc3 100644
--- a/superset/queries/api.py
+++ b/superset/queries/api.py
@@ -21,6 +21,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
 from superset.constants import RouteMethod
 from superset.models.sql_lab import Query
 from superset.queries.filters import QueryFilter
+from superset.queries.schemas import openapi_spec_methods_override
 from superset.views.base_api import BaseSupersetModelRestApi
 
 logger = logging.getLogger(__name__)
@@ -70,3 +71,4 @@ class QueryRestApi(BaseSupersetModelRestApi):
     base_order = ("changed_on", "desc")
 
     openapi_spec_tag = "Queries"
+    openapi_spec_methods = openapi_spec_methods_override
diff --git a/superset/queries/schemas.py b/superset/queries/schemas.py
new file mode 100644
index 0000000..04c003e
--- /dev/null
+++ b/superset/queries/schemas.py
@@ -0,0 +1,28 @@
+# 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.
+
+openapi_spec_methods_override = {
+    "get": {"get": {"description": "Get query detail information."}},
+    "get_list": {
+        "get": {
+            "description": "Get a list of queries, use Rison or JSON query "
+            "parameters for filtering, sorting, pagination and "
+            " for selecting specific columns and metadata.",
+        }
+    },
+}
+""" Overrides GET methods OpenApi descriptions """
diff --git a/superset/views/base_api.py b/superset/views/base_api.py
index 60f9d29..5675506 100644
--- a/superset/views/base_api.py
+++ b/superset/views/base_api.py
@@ -18,11 +18,13 @@ import functools
 import logging
 from typing import Any, cast, Dict, Optional, Set, Tuple, Type, Union
 
+from apispec import APISpec
 from flask import Response
 from flask_appbuilder import ModelRestApi
 from flask_appbuilder.api import expose, protect, rison, safe
 from flask_appbuilder.models.filters import BaseFilter, Filters
 from flask_appbuilder.models.sqla.filters import FilterStartsWith
+from marshmallow import Schema
 
 from superset.stats_logger import BaseStatsLogger
 from superset.utils.core import time_function
@@ -109,10 +111,23 @@ class BaseSupersetModelRestApi(ModelRestApi):
     """  # pylint: disable=pointless-string-statement
     allowed_rel_fields: Set[str] = set()
 
+    openapi_spec_component_schemas: Tuple[Schema, ...] = tuple()
+    """
+    Add extra schemas to the OpenAPI component schemas section
+    """  # pylint: disable=pointless-string-statement
+
     def __init__(self) -> None:
         super().__init__()
         self.stats_logger = BaseStatsLogger()
 
+    def add_apispec_components(self, api_spec: APISpec) -> None:
+
+        for schema in self.openapi_spec_component_schemas:
+            api_spec.components.schema(
+                schema.__name__, schema=schema,
+            )
+        super().add_apispec_components(api_spec)
+
     def create_blueprint(self, appbuilder, *args, **kwargs):
         self.stats_logger = self.appbuilder.get_app.config["STATS_LOGGER"]
         return super().create_blueprint(appbuilder, *args, **kwargs)
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index 558e2e9..3e9769c 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -333,10 +333,10 @@ class ChartApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
         }
         uri = f"api/v1/chart/"
         rv = self.post_assert_metric(uri, chart_data, "post")
-        self.assertEqual(rv.status_code, 422)
+        self.assertEqual(rv.status_code, 400)
         response = json.loads(rv.data.decode("utf-8"))
         self.assertEqual(
-            response, {"message": {"datasource_id": ["Datasource does not exist"]}}
+            response, {"message": {"datasource_type": ["Not a valid choice."]}}
         )
         chart_data = {
             "slice_name": "title1",
@@ -439,10 +439,10 @@ class ChartApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
         chart_data = {"datasource_id": 1, "datasource_type": "unknown"}
         uri = f"api/v1/chart/{chart.id}"
         rv = self.put_assert_metric(uri, chart_data, "put")
-        self.assertEqual(rv.status_code, 422)
+        self.assertEqual(rv.status_code, 400)
         response = json.loads(rv.data.decode("utf-8"))
         self.assertEqual(
-            response, {"message": {"datasource_id": ["Datasource does not exist"]}}
+            response, {"message": {"datasource_type": ["Not a valid choice."]}}
         )
         chart_data = {"datasource_id": 0, "datasource_type": "table"}
         uri = f"api/v1/chart/{chart.id}"