Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] New, REST API #8917

Merged
merged 27 commits into from Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a955d45
[charts] New REST API
dpgaspar Jan 3, 2020
00000eb
[charts] Small improvements
dpgaspar Jan 6, 2020
1044bb8
[charts] Fix, lint
dpgaspar Jan 6, 2020
d06421f
[charts] Tests and datasource validation
dpgaspar Jan 6, 2020
08969bf
[charts] Fix, lint
dpgaspar Jan 6, 2020
c90010d
[charts] DRY post schemas
dpgaspar Jan 6, 2020
c20a158
[charts] lint and improve type declarations
dpgaspar Jan 6, 2020
7735459
Merge remote-tracking branch 'upstream/master' into feature/charts-api
dpgaspar Jan 6, 2020
4e8691f
[charts] merge master and resolve conflicts
dpgaspar Jan 6, 2020
98d841a
[charts] DRY owned REST APIs
dpgaspar Jan 6, 2020
f33eae7
[charts] Small fixes
dpgaspar Jan 6, 2020
712436b
[charts] More tests
dpgaspar Jan 6, 2020
17d7f96
[charts] Tests and DRY
dpgaspar Jan 7, 2020
beecb25
[charts] Tests for update
dpgaspar Jan 7, 2020
d8f0c31
[charts] More tests
dpgaspar Jan 7, 2020
fb6bb5e
[charts] Fix, isort
dpgaspar Jan 7, 2020
1342b58
[charts] DRY and improve quality
dpgaspar Jan 8, 2020
c626051
[charts] DRY and more tests
dpgaspar Jan 8, 2020
3c262f4
[charts] Refactor base for api and schemas
dpgaspar Jan 8, 2020
b93595e
[charts] Fix bug on partial updates for dashboards
dpgaspar Jan 8, 2020
c8841e7
[charts] Fix missing apache license
dpgaspar Jan 8, 2020
a8b77ea
Merge remote-tracking branch 'upstream/master' into feature/charts-api
dpgaspar Jan 9, 2020
c9f333f
black app.py after merge
dpgaspar Jan 9, 2020
7b8528f
Merge master solve conflicts
dpgaspar Jan 17, 2020
8bb704f
[charts] Fix, missing imports and black
dpgaspar Jan 17, 2020
6a83c7d
[api] Log on sqlalchemy error
dpgaspar Jan 21, 2020
32e28da
[api] isort
dpgaspar Jan 21, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions superset/app.py
Expand Up @@ -142,15 +142,14 @@ def init_views(self) -> None:
from superset.views.api import Api
from superset.views.core import (
AccessRequestsModelView,
SliceModelView,
SliceAsync,
SliceAddView,
KV,
R,
Superset,
CssTemplateModelView,
CssTemplateAsyncModelView,
)
from superset.views.chart.api import ChartRestApi
from superset.views.chart.views import SliceModelView, SliceAsync, SliceAddView
from superset.views.dashboard.api import DashboardRestApi
from superset.views.dashboard.views import (
DashboardModelView,
Expand Down Expand Up @@ -185,6 +184,7 @@ def init_views(self) -> None:
#
# Setup API views
#
appbuilder.add_api(ChartRestApi)
appbuilder.add_api(DashboardRestApi)
appbuilder.add_api(DatabaseRestApi)

Expand Down
10 changes: 6 additions & 4 deletions superset/views/api.py
Expand Up @@ -25,10 +25,12 @@
from superset.legacy import update_time_range
from superset.models.slice import Slice
from superset.utils import core as utils

from .base import api, BaseSupersetView, handle_api_exception
from .dashboard import api as dashboard_api # pylint: disable=unused-import
from .database import api as database_api # pylint: disable=unused-import
from superset.views.base import api, BaseSupersetView, handle_api_exception
from superset.views.chart import api as chart_api # pylint: disable=unused-import
from superset.views.dashboard import ( # pylint: disable=unused-import
api as dashboard_api,
)
from superset.views.database import api as database_api # pylint: disable=unused-import


class Api(BaseSupersetView):
Expand Down
169 changes: 2 additions & 167 deletions superset/views/base.py
Expand Up @@ -18,21 +18,18 @@
import logging
import traceback
from datetime import datetime
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Optional

import simplejson as json
import yaml
from flask import abort, flash, g, get_flashed_messages, redirect, Response, session
from flask_appbuilder import BaseView, Model, ModelRestApi, ModelView
from flask_appbuilder import BaseView, ModelView
from flask_appbuilder.actions import action
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.forms import DynamicForm
from flask_appbuilder.models.filters import Filters
from flask_appbuilder.models.sqla.filters import BaseFilter
from flask_appbuilder.widgets import ListWidget
from flask_babel import get_locale, gettext as __, lazy_gettext as _
from flask_wtf.form import FlaskForm
from marshmallow import Schema
from sqlalchemy import or_
from werkzeug.exceptions import HTTPException
from wtforms.fields.core import Field, UnboundField
Expand Down Expand Up @@ -155,26 +152,6 @@ def wraps(self, *args, **kwargs):
return functools.update_wrapper(wraps, f)


def check_ownership_and_item_exists(f):
"""
A Decorator that checks if an object exists and is owned by the current user
"""

def wraps(self, pk): # pylint: disable=invalid-name
item = self.datamodel.get(
pk, self._base_filters # pylint: disable=protected-access
)
if not item:
return self.response_404()
try:
check_ownership(item)
except SupersetSecurityException as e:
return self.response(403, message=str(e))
return f(self, item)

return functools.update_wrapper(wraps, f)


def get_datasource_exist_error_msg(full_name):
return __("Datasource %(name)s already exists", name=full_name)

Expand Down Expand Up @@ -378,148 +355,6 @@ def apply(self, query, value):
)


class BaseSupersetSchema(Schema):
"""
Extends Marshmallow schema so that we can pass a Model to load
(following marshamallow-sqlalchemy pattern). This is useful
to perform partial model merges on HTTP PUT
"""

def __init__(self, **kwargs):
self.instance = None
super().__init__(**kwargs)

def load(
self, data, many=None, partial=None, instance: Model = None, **kwargs
): # pylint: disable=arguments-differ
self.instance = instance
return super().load(data, many=many, partial=partial, **kwargs)


get_related_schema = {
"type": "object",
"properties": {
"page_size": {"type": "integer"},
"page": {"type": "integer"},
"filter": {"type": "string"},
},
}


class BaseSupersetModelRestApi(ModelRestApi):
"""
Extends FAB's ModelResApi to implement specific superset generic functionality
"""

order_rel_fields: Dict[str, Tuple[str, str]] = {}
"""
Impose ordering on related fields query::

order_rel_fields = {
"<RELATED_FIELD>": ("<RELATED_FIELD_FIELD>", "<asc|desc>"),
...
}
""" # pylint: disable=pointless-string-statement
filter_rel_fields_field: Dict[str, str] = {}
"""
Declare the related field field for filtering::

filter_rel_fields_field = {
"<RELATED_FIELD>": "<RELATED_FIELD_FIELD>", "<asc|desc>")
}
""" # pylint: disable=pointless-string-statement

def _get_related_filter(self, datamodel, column_name: str, value: str) -> Filters:
filter_field = self.filter_rel_fields_field.get(column_name)
filters = datamodel.get_filters([filter_field])
if value:
filters.rest_add_filters(
[{"opr": "sw", "col": filter_field, "value": value}]
)
return filters

@expose("/related/<column_name>", methods=["GET"])
@protect()
@safe
@rison(get_related_schema)
def related(self, column_name: str, **kwargs):
"""Get related fields data
---
get:
parameters:
- in: path
schema:
type: string
name: column_name
- in: query
name: q
content:
application/json:
schema:
type: object
properties:
page_size:
type: integer
page:
type: integer
filter:
type: string
responses:
200:
description: Related column data
content:
application/json:
schema:
type: object
properties:
count:
type: integer
result:
type: object
properties:
value:
type: integer
text:
type: string
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
404:
$ref: '#/components/responses/404'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
"""
args = kwargs.get("rison", {})
# handle pagination
page, page_size = self._handle_page_args(args)
try:
datamodel = self.datamodel.get_related_interface(column_name)
except KeyError:
return self.response_404()
page, page_size = self._sanitize_page_args(page, page_size)
# handle ordering
order_field = self.order_rel_fields.get(column_name)
if order_field:
order_column, order_direction = order_field
else:
order_column, order_direction = "", ""
# handle filters
filters = self._get_related_filter(datamodel, column_name, args.get("filter"))
# Make the query
count, values = datamodel.query(
filters, order_column, order_direction, page=page, page_size=page_size
)
# produce response
result = [
{"value": datamodel.get_pk_value(value), "text": str(value)}
for value in values
]
return self.response(200, count=count, result=result)


class CsvResponse(Response): # pylint: disable=too-many-ancestors
"""
Override Response to take into account csv encoding from config.py
Expand Down