docs(api): improve openapi documentation for dash, charts and queries (#9724)

This commit is contained in:
Daniel Vaz Gaspar
2020-05-05 14:42:18 +01:00
committed by GitHub
parent 911f117673
commit 0d85d25314
8 changed files with 272 additions and 56 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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)

View File

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