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