From aca8db83019d474d9ba98bbd305a2ff456e7e5b2 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Mon, 6 Apr 2020 16:52:40 +0100 Subject: [PATCH 1/8] [query] Migrate api v1 query to new location --- superset/queries/api.py | 85 +++++++++++++++++++++++++++++++++++++- superset/views/api.py | 2 +- tests/queries/api_tests.py | 30 +++++++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index 092989f5eff0..f2a2ad8a356d 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -14,13 +14,19 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import json import logging +from flask import make_response, request, Response +from flask_appbuilder.api import expose, protect, safe from flask_appbuilder.models.sqla.interface import SQLAInterface +from superset.common.query_context import QueryContext from superset.constants import RouteMethod +from superset.extensions import security_manager from superset.models.sql_lab import Query from superset.queries.filters import QueryFilter +from superset.utils.core import json_int_dttm_ser from superset.views.base_api import BaseSupersetModelRestApi logger = logging.getLogger(__name__) @@ -31,7 +37,7 @@ class QueryRestApi(BaseSupersetModelRestApi): resource_name = "query" allow_browser_login = True - include_route_methods = {RouteMethod.GET, RouteMethod.GET_LIST} + include_route_methods = {RouteMethod.GET, RouteMethod.GET_LIST, "exec"} class_permission_name = "QueryView" list_columns = [ @@ -70,3 +76,80 @@ class QueryRestApi(BaseSupersetModelRestApi): base_order = ("changed_on", "desc") openapi_spec_tag = "Queries" + + @expose("/exec", methods=["POST"]) + @protect() + @safe + def exec(self) -> Response: + """ + Takes a query_obj constructed in the client and returns payload data response + for the given query_obj. + --- + post: + description: >- + Takes a query_obj constructed in the client and returns payload data + response for the given query_obj + requestBody: + description: Query object 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: 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") + query_context = QueryContext(**json.loads(request.json)) + security_manager.assert_query_context_permission(query_context) + payload_json = query_context.get_payload() + response_data = json.dumps( + payload_json, default=json_int_dttm_ser, allow_nan=False + ) + resp = make_response(response_data, 200) + resp.headers["Content-Type"] = "application/json; charset=utf-8" + return resp diff --git a/superset/views/api.py b/superset/views/api.py index e82aa86dbc91..4dfeb22410d4 100644 --- a/superset/views/api.py +++ b/superset/views/api.py @@ -44,7 +44,7 @@ def query(self): security_manager.assert_query_context_permission(query_context) payload_json = query_context.get_payload() return json.dumps( - payload_json, default=utils.json_int_dttm_ser, ignore_nan=True + payload_json, default=utils.json_int_dttm_ser, allow_nan=False ) @event_logger.log_this diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py index 616178fd07ee..1e554a97fe27 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 @@ -67,6 +67,22 @@ def insert_query( db.session.commit() return query + def _get_query_context_dict(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, + } + ], + } + @staticmethod def get_random_string(length: int = 10): letters = string.ascii_letters @@ -245,3 +261,15 @@ def test_get_queries_no_data_access(self): # rollback changes db.session.delete(query) db.session.commit() + + def test_query_exec(self): + """ + Query API: Test exec query + """ + self.login(username="admin") + qc_dict = self._get_query_context_dict() + data = json.dumps(qc_dict) + uri = "api/v1/query/exec" + rv = self.client.post(uri, json=data) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(data[0]["rowcount"], 100) From 9691720e52592edf1dd745eb5a17f1ca39457cbb Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Tue, 7 Apr 2020 11:48:15 +0100 Subject: [PATCH 2/8] Improved errors and tests --- superset/queries/api.py | 41 ++++++++++++++++++++++++++++++++++---- tests/queries/api_tests.py | 14 +++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index f2a2ad8a356d..cf2733ba3918 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -23,7 +23,8 @@ from superset.common.query_context import QueryContext from superset.constants import RouteMethod -from superset.extensions import security_manager +from superset.exceptions import SupersetSecurityException +from superset.extensions import event_logger, security_manager from superset.models.sql_lab import Query from superset.queries.filters import QueryFilter from superset.utils.core import json_int_dttm_ser @@ -78,6 +79,7 @@ class QueryRestApi(BaseSupersetModelRestApi): openapi_spec_tag = "Queries" @expose("/exec", methods=["POST"]) + @event_logger.log_this @protect() @safe def exec(self) -> Response: @@ -132,7 +134,32 @@ def exec(self) -> Response: content: application/json: schema: - type: object + 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: @@ -144,8 +171,14 @@ def exec(self) -> Response: """ if not request.is_json: return self.response_400(message="Request is not JSON") - query_context = QueryContext(**json.loads(request.json)) - security_manager.assert_query_context_permission(query_context) + 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 = json.dumps( payload_json, default=json_int_dttm_ser, allow_nan=False diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py index 1e554a97fe27..fee4aa39e1e1 100644 --- a/tests/queries/api_tests.py +++ b/tests/queries/api_tests.py @@ -268,8 +268,18 @@ def test_query_exec(self): """ self.login(username="admin") qc_dict = self._get_query_context_dict() - data = json.dumps(qc_dict) uri = "api/v1/query/exec" - rv = self.client.post(uri, json=data) + rv = self.client.post(uri, json=qc_dict) + 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_alloed(self): + """ + Query API: Test exec query not allowed + """ + self.login(username="gamma") + qc_dict = self._get_query_context_dict() + uri = "api/v1/query/exec" + rv = self.client.post(uri, json=qc_dict) + self.assertEqual(rv.status_code, 401) From 70b9202f7b22886dce7255232673e3733d3009f9 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Tue, 7 Apr 2020 16:07:23 +0100 Subject: [PATCH 3/8] [query] nit and revert undesired change --- superset/queries/api.py | 12 ++++++------ superset/views/api.py | 2 +- tests/queries/api_tests.py | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index cf2733ba3918..a80933967366 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -import json +import simplejson import logging from flask import make_response, request, Response @@ -84,13 +84,13 @@ class QueryRestApi(BaseSupersetModelRestApi): @safe def exec(self) -> Response: """ - Takes a query_obj constructed in the client and returns payload data response - for the given query_obj. + Takes a query context constructed in the client and returns payload + data response for the given query. --- post: description: >- - Takes a query_obj constructed in the client and returns payload data - response for the given query_obj + Takes a query context constructed in the client and returns payload data + response for the given query. requestBody: description: Query object schema required: true @@ -180,7 +180,7 @@ def exec(self) -> Response: except SupersetSecurityException: return self.response_401() payload_json = query_context.get_payload() - response_data = json.dumps( + response_data = simplejson.dumps( payload_json, default=json_int_dttm_ser, allow_nan=False ) resp = make_response(response_data, 200) diff --git a/superset/views/api.py b/superset/views/api.py index 4dfeb22410d4..e82aa86dbc91 100644 --- a/superset/views/api.py +++ b/superset/views/api.py @@ -44,7 +44,7 @@ def query(self): security_manager.assert_query_context_permission(query_context) payload_json = query_context.get_payload() return json.dumps( - payload_json, default=utils.json_int_dttm_ser, allow_nan=False + payload_json, default=utils.json_int_dttm_ser, ignore_nan=True ) @event_logger.log_this diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py index fee4aa39e1e1..94059d86f8be 100644 --- a/tests/queries/api_tests.py +++ b/tests/queries/api_tests.py @@ -67,7 +67,7 @@ def insert_query( db.session.commit() return query - def _get_query_context_dict(self) -> Dict[str, Any]: + def _get_query_context(self) -> Dict[str, Any]: self.login(username="admin") slc = self.get_slice("Girl Name Cloud", db.session) return { @@ -267,9 +267,9 @@ def test_query_exec(self): Query API: Test exec query """ self.login(username="admin") - qc_dict = self._get_query_context_dict() + query_context = self._get_query_context() uri = "api/v1/query/exec" - rv = self.client.post(uri, json=qc_dict) + 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) @@ -279,7 +279,7 @@ def test_query_exec_not_alloed(self): Query API: Test exec query not allowed """ self.login(username="gamma") - qc_dict = self._get_query_context_dict() + query_context = self._get_query_context() uri = "api/v1/query/exec" - rv = self.client.post(uri, json=qc_dict) + rv = self.client.post(uri, json=query_context) self.assertEqual(rv.status_code, 401) From 6636afb66694031feb16972f32677faa865773f2 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Tue, 7 Apr 2020 16:09:54 +0100 Subject: [PATCH 4/8] [query] lint --- superset/queries/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index a80933967366..b05f46920b82 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -14,9 +14,9 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -import simplejson import logging +import simplejson from flask import make_response, request, Response from flask_appbuilder.api import expose, protect, safe from flask_appbuilder.models.sqla.interface import SQLAInterface From 909cc48aec43a5afac61dca9dfcd25cf76f1b9da Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Tue, 7 Apr 2020 16:12:26 +0100 Subject: [PATCH 5/8] [query] revert nan change --- superset/queries/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index b05f46920b82..b33634813c29 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -181,7 +181,7 @@ def exec(self) -> Response: return self.response_401() payload_json = query_context.get_payload() response_data = simplejson.dumps( - payload_json, default=json_int_dttm_ser, allow_nan=False + 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" From 40d86faf60a62150c950731a54d9d6211c6682b7 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 7 Apr 2020 16:39:44 +0100 Subject: [PATCH 6/8] Update superset/queries/api.py Co-Authored-By: Ville Brofeldt <33317356+villebro@users.noreply.github.com> --- superset/queries/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/queries/api.py b/superset/queries/api.py index b33634813c29..9263f91ce336 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -92,7 +92,7 @@ def exec(self) -> Response: Takes a query context constructed in the client and returns payload data response for the given query. requestBody: - description: Query object schema + description: Query context schema required: true content: application/json: From 06dbae99ddfae6b7d234de8d23831297d208d9e0 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 7 Apr 2020 16:40:03 +0100 Subject: [PATCH 7/8] Update tests/queries/api_tests.py Co-Authored-By: Ville Brofeldt <33317356+villebro@users.noreply.github.com> --- tests/queries/api_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/queries/api_tests.py b/tests/queries/api_tests.py index 94059d86f8be..8e8d3e02ddc1 100644 --- a/tests/queries/api_tests.py +++ b/tests/queries/api_tests.py @@ -274,7 +274,7 @@ def test_query_exec(self): data = json.loads(rv.data.decode("utf-8")) self.assertEqual(data[0]["rowcount"], 100) - def test_query_exec_not_alloed(self): + def test_query_exec_not_allowed(self): """ Query API: Test exec query not allowed """ From bb2149f6414b374819e13045e9927c4d4772862f Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Wed, 8 Apr 2020 09:36:32 +0100 Subject: [PATCH 8/8] change endpoint location to charts --- superset/charts/api.py | 117 +++++++++++++++++++++++++++++++++++- superset/queries/api.py | 118 +------------------------------------ superset/views/base_api.py | 1 + tests/charts/api_tests.py | 40 ++++++++++++- tests/queries/api_tests.py | 38 ------------ 5 files changed, 157 insertions(+), 157 deletions(-) diff --git a/superset/charts/api.py b/superset/charts/api.py index 761de3c33e93..1cdcd6d0f22d 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 @@ 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 @@ def bulk_delete( return self.response_403() except ChartBulkDeleteFailedError as e: return self.response_422(message=str(e)) + + @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/queries/api.py b/superset/queries/api.py index 9263f91ce336..092989f5eff0 100644 --- a/superset/queries/api.py +++ b/superset/queries/api.py @@ -16,18 +16,11 @@ # under the License. import logging -import simplejson -from flask import make_response, request, Response -from flask_appbuilder.api import expose, protect, safe from flask_appbuilder.models.sqla.interface import SQLAInterface -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.sql_lab import Query from superset.queries.filters import QueryFilter -from superset.utils.core import json_int_dttm_ser from superset.views.base_api import BaseSupersetModelRestApi logger = logging.getLogger(__name__) @@ -38,7 +31,7 @@ class QueryRestApi(BaseSupersetModelRestApi): resource_name = "query" allow_browser_login = True - include_route_methods = {RouteMethod.GET, RouteMethod.GET_LIST, "exec"} + include_route_methods = {RouteMethod.GET, RouteMethod.GET_LIST} class_permission_name = "QueryView" list_columns = [ @@ -77,112 +70,3 @@ class QueryRestApi(BaseSupersetModelRestApi): base_order = ("changed_on", "desc") openapi_spec_tag = "Queries" - - @expose("/exec", methods=["POST"]) - @event_logger.log_this - @protect() - @safe - def exec(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 5f49780f90f2..9e4a48b1bff7 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 d885e0b8321d..450e99379b0b 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 @@ def insert_chart( 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 @@ def test_get_charts_no_data_access(self): 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 8e8d3e02ddc1..4f43c7733500 100644 --- a/tests/queries/api_tests.py +++ b/tests/queries/api_tests.py @@ -67,22 +67,6 @@ def insert_query( db.session.commit() return query - 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, - } - ], - } - @staticmethod def get_random_string(length: int = 10): letters = string.ascii_letters @@ -261,25 +245,3 @@ def test_get_queries_no_data_access(self): # rollback changes db.session.delete(query) db.session.commit() - - def test_query_exec(self): - """ - Query API: Test exec query - """ - self.login(username="admin") - query_context = self._get_query_context() - uri = "api/v1/query/exec" - 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 exec query not allowed - """ - self.login(username="gamma") - query_context = self._get_query_context() - uri = "api/v1/query/exec" - rv = self.client.post(uri, json=query_context) - self.assertEqual(rv.status_code, 401)