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

Reply via email to