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 0610c48 [query] Migrate api v1 query to new location (#9479)
0610c48 is described below
commit 0610c489bbc3f77ff473cb47cf1072980c6afadd
Author: Daniel Vaz Gaspar <[email protected]>
AuthorDate: Thu Apr 9 09:05:49 2020 +0100
[query] Migrate api v1 query to new location (#9479)
* [query] Migrate api v1 query to new location
* Improved errors and tests
* [query] nit and revert undesired change
* [query] lint
* [query] revert nan change
* Update superset/queries/api.py
Co-Authored-By: Ville Brofeldt <[email protected]>
* Update tests/queries/api_tests.py
Co-Authored-By: Ville Brofeldt <[email protected]>
* change endpoint location to charts
Co-authored-by: Ville Brofeldt <[email protected]>
---
superset/charts/api.py | 117 ++++++++++++++++++++++++++++++++++++++++++++-
superset/views/base_api.py | 1 +
tests/charts/api_tests.py | 40 +++++++++++++++-
tests/queries/api_tests.py | 2 +-
4 files changed, 157 insertions(+), 3 deletions(-)
diff --git a/superset/charts/api.py b/superset/charts/api.py
index fb31064..fb78cd9 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -17,7 +17,8 @@
import logging
from typing import Any
-from flask import g, request, Response
+import simplejson
+from flask import g, make_response, request, Response
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
@@ -41,8 +42,12 @@ from superset.charts.schemas import (
ChartPutSchema,
get_delete_ids_schema,
)
+from superset.common.query_context import QueryContext
from superset.constants import RouteMethod
+from superset.exceptions import SupersetSecurityException
+from superset.extensions import event_logger, security_manager
from superset.models.slice import Slice
+from superset.utils.core import json_int_dttm_ser
from superset.views.base_api import BaseSupersetModelRestApi,
RelatedFieldFilter
from superset.views.filters import FilterRelatedOwners
@@ -59,6 +64,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
RouteMethod.EXPORT,
RouteMethod.RELATED,
"bulk_delete", # not using RouteMethod since locally defined
+ "data",
}
class_permission_name = "SliceModelView"
show_columns = [
@@ -348,3 +354,112 @@ class ChartRestApi(BaseSupersetModelRestApi):
return self.response_403()
except ChartBulkDeleteFailedError as ex:
return self.response_422(message=str(ex))
+
+ @expose("/data", methods=["POST"])
+ @event_logger.log_this
+ @protect()
+ @safe
+ def data(self) -> Response:
+ """
+ Takes a query context constructed in the client and returns payload
+ data response for the given query.
+ ---
+ post:
+ description: >-
+ Takes a query context constructed in the client and returns
payload data
+ response for the given query.
+ requestBody:
+ description: Query context schema
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ datasource:
+ type: object
+ description: The datasource where the query will run
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ queries:
+ type: array
+ items:
+ type: object
+ properties:
+ granularity:
+ type: string
+ groupby:
+ type: array
+ items:
+ type: string
+ metrics:
+ type: array
+ items:
+ type: object
+ filters:
+ type: array
+ items:
+ type: string
+ row_limit:
+ type: integer
+ responses:
+ 200:
+ description: Query result
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ cache_key:
+ type: string
+ cached_dttm:
+ type: string
+ cache_timeout:
+ type: integer
+ error:
+ type: string
+ is_cached:
+ type: boolean
+ query:
+ type: string
+ status:
+ type: string
+ stacktrace:
+ type: string
+ rowcount:
+ type: integer
+ data:
+ type: array
+ items:
+ type: object
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ if not request.is_json:
+ return self.response_400(message="Request is not JSON")
+ try:
+ query_context = QueryContext(**request.json)
+ except KeyError:
+ return self.response_400(message="Request is incorrect")
+ try:
+ security_manager.assert_query_context_permission(query_context)
+ except SupersetSecurityException:
+ return self.response_401()
+ payload_json = query_context.get_payload()
+ response_data = simplejson.dumps(
+ payload_json, default=json_int_dttm_ser, ignore_nan=True
+ )
+ resp = make_response(response_data, 200)
+ resp.headers["Content-Type"] = "application/json; charset=utf-8"
+ return resp
diff --git a/superset/views/base_api.py b/superset/views/base_api.py
index 4faad91..d0c027d 100644
--- a/superset/views/base_api.py
+++ b/superset/views/base_api.py
@@ -84,6 +84,7 @@ class BaseSupersetModelRestApi(ModelRestApi):
"info": "list",
"related": "list",
"refresh": "edit",
+ "data": "list",
}
order_rel_fields: Dict[str, Tuple[str, str]] = {}
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index d885e0b..450e993 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -16,7 +16,7 @@
# under the License.
"""Unit tests for Superset"""
import json
-from typing import List, Optional
+from typing import Any, Dict, List, Optional
import prison
from sqlalchemy.sql import func
@@ -69,6 +69,22 @@ class ChartApiTests(SupersetTestCase,
ApiOwnersTestCaseMixin):
db.session.commit()
return slice
+ def _get_query_context(self) -> Dict[str, Any]:
+ self.login(username="admin")
+ slc = self.get_slice("Girl Name Cloud", db.session)
+ return {
+ "datasource": {"id": slc.datasource_id, "type":
slc.datasource_type},
+ "queries": [
+ {
+ "granularity": "ds",
+ "groupby": ["name"],
+ "metrics": [{"label": "sum__num"}],
+ "filters": [],
+ "row_limit": 100,
+ }
+ ],
+ }
+
def test_delete_chart(self):
"""
Chart API: Test delete
@@ -580,3 +596,25 @@ class ChartApiTests(SupersetTestCase,
ApiOwnersTestCaseMixin):
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(data["count"], 0)
+
+ def test_chart_data(self):
+ """
+ Query API: Test chart data query
+ """
+ self.login(username="admin")
+ query_context = self._get_query_context()
+ uri = "api/v1/chart/data"
+ rv = self.client.post(uri, json=query_context)
+ self.assertEqual(rv.status_code, 200)
+ data = json.loads(rv.data.decode("utf-8"))
+ self.assertEqual(data[0]["rowcount"], 100)
+
+ def test_query_exec_not_allowed(self):
+ """
+ Query API: Test chart data query not allowed
+ """
+ self.login(username="gamma")
+ query_context = self._get_query_context()
+ uri = "api/v1/chart/data"
+ rv = self.client.post(uri, json=query_context)
+ self.assertEqual(rv.status_code, 401)
diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py
index 616178f..4f43c77 100644
--- a/tests/queries/api_tests.py
+++ b/tests/queries/api_tests.py
@@ -17,9 +17,9 @@
# isort:skip_file
"""Unit tests for Superset"""
import json
-import uuid
import random
import string
+from typing import Dict, Any
import prison
from sqlalchemy.sql import func