Skip to content

Commit

Permalink
feat: add GET /api/v1/chart/{chart_id}/data/?format{format} API (#1…
Browse files Browse the repository at this point in the history
…5827)

* feat: add `GET /api/v1/chart/{chart_id}/data/?format{format}` API

* Fix test
  • Loading branch information
betodealmeida committed Jul 22, 2021
1 parent 3441182 commit f104fba
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 54 deletions.
140 changes: 118 additions & 22 deletions superset/charts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
RouteMethod.IMPORT,
RouteMethod.RELATED,
"bulk_delete", # not using RouteMethod since locally defined
"data",
"post_data",
"get_data",
"data_from_cache",
"viz_types",
"favorite_status",
Expand Down Expand Up @@ -516,14 +517,103 @@ def get_data_response(

return self.send_chart_response(result)

@expose("/<int:pk>/data/", methods=["GET"])
@protect()
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.data",
log_to_statsd=False,
)
def get_data(self, pk: int) -> Response:
"""
Takes a chart ID and uses the query context stored when the chart was saved
to return payload data response.
---
get:
description: >-
Takes a chart ID and uses the query context stored when the chart was saved
to return payload data response.
parameters:
- in: path
schema:
type: integer
name: pk
description: The chart ID
- in: query
name: format
description: The format in which the data should be returned
schema:
type: string
responses:
200:
description: Query result
content:
application/json:
schema:
$ref: "#/components/schemas/ChartDataResponseSchema"
202:
description: Async job details
content:
application/json:
schema:
$ref: "#/components/schemas/ChartDataAsyncResponseSchema"
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
"""
chart = self.datamodel.get(pk, self._base_filters)
if not chart:
return self.response_404()

try:
json_body = json.loads(chart.query_context)
except (TypeError, json.decoder.JSONDecodeError):
json_body = None

if json_body is None:
return self.response_400(
message=_(
"Chart has no query context saved. Please save the chart again."
)
)

json_body["result_format"] = request.args.get(
"format", ChartDataResultFormat.JSON
)
try:
command = ChartDataCommand()
query_context = command.set_query_context(json_body)
command.validate()
except QueryObjectValidationError as error:
return self.response_400(message=error.message)
except ValidationError as error:
return self.response_400(
message=_(
"Request is incorrect: %(error)s", error=error.normalized_messages()
)
)

# TODO: support CSV, SQL query and other non-JSON types
if (
is_feature_enabled("GLOBAL_ASYNC_QUERIES")
and query_context.result_format == ChartDataResultFormat.JSON
and query_context.result_type == ChartDataResultType.FULL
):
return self._run_async(command)

return self.get_data_response(command)

@expose("/data", methods=["POST"])
@protect()
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.data",
log_to_statsd=False,
)
def data(self) -> Response:
def post_data(self) -> Response:
"""
Takes a query context constructed in the client and returns payload
data response for the given query.
Expand Down Expand Up @@ -593,31 +683,37 @@ def data(self) -> Response:
and query_context.result_format == ChartDataResultFormat.JSON
and query_context.result_type == ChartDataResultType.FULL
):
# First, look for the chart query results in the cache.
try:
result = command.run(force_cached=True)
except ChartDataCacheLoadError:
result = None # type: ignore
return self._run_async(command)

return self.get_data_response(command)

already_cached_result = result is not None
def _run_async(self, command: ChartDataCommand) -> Response:
"""
Execute command as an async query.
"""
# First, look for the chart query results in the cache.
try:
result = command.run(force_cached=True)
except ChartDataCacheLoadError:
result = None # type: ignore

# If the chart query has already been cached, return it immediately.
if already_cached_result:
return self.send_chart_response(result)
already_cached_result = result is not None

# Otherwise, kick off a background job to run the chart query.
# Clients will either poll or be notified of query completion,
# at which point they will call the /data/<cache_key> endpoint
# to retrieve the results.
try:
command.validate_async_request(request)
except AsyncQueryTokenException:
return self.response_401()
# If the chart query has already been cached, return it immediately.
if already_cached_result:
return self.send_chart_response(result)

result = command.run_async(g.user.get_id())
return self.response(202, **result)
# Otherwise, kick off a background job to run the chart query.
# Clients will either poll or be notified of query completion,
# at which point they will call the /data/<cache_key> endpoint
# to retrieve the results.
try:
command.validate_async_request(request)
except AsyncQueryTokenException:
return self.response_401()

return self.get_data_response(command)
result = command.run_async(g.user.get_id())
return self.response(202, **result)

@expose("/data/<cache_key>", methods=["GET"])
@protect()
Expand Down
Loading

0 comments on commit f104fba

Please sign in to comment.