# pylint: disable=C,R,W import json from past.builtins import basestring from sqlalchemy import ( and_, Boolean, Column, Integer, String, Text, ) from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import foreign, relationship from superset import utils from superset.models.core import Slice from superset.models.helpers import AuditMixinNullable, ImportMixin class BaseDatasource(AuditMixinNullable, ImportMixin): """A common interface to objects that are queryable (tables and datasources)""" # --------------------------------------------------------------- # class attributes to define when deriving BaseDatasource # --------------------------------------------------------------- __tablename__ = None # {connector_name}_datasource type = None # datasoure type, str to be defined when deriving this class baselink = None # url portion pointing to ModelView endpoint column_class = None # link to derivative of BaseColumn metric_class = None # link to derivative of BaseMetric # Used to do code highlighting when displaying the query in the UI query_language = None name = None # can be a Column or a property pointing to one # --------------------------------------------------------------- # Columns id = Column(Integer, primary_key=True) description = Column(Text) default_endpoint = Column(Text) is_featured = Column(Boolean, default=False) # TODO deprecating filter_select_enabled = Column(Boolean, default=False) offset = Column(Integer, default=0) cache_timeout = Column(Integer) params = Column(String(1000)) perm = Column(String(1000)) sql = None owner = None update_from_object_fields = None @declared_attr def slices(self): return relationship( 'Slice', primaryjoin=lambda: and_( foreign(Slice.datasource_id) == self.id, foreign(Slice.datasource_type) == self.type, ), ) # placeholder for a relationship to a derivative of BaseColumn columns = [] # placeholder for a relationship to a derivative of BaseMetric metrics = [] @property def uid(self): """Unique id across datasource types""" return '{self.id}__{self.type}'.format(**locals()) @property def column_names(self): return sorted([c.column_name for c in self.columns]) @property def columns_types(self): return {c.column_name: c.type for c in self.columns} @property def main_dttm_col(self): return 'timestamp' @property def datasource_name(self): raise NotImplementedError() @property def connection(self): """String representing the context of the Datasource""" return None @property def schema(self): """String representing the schema of the Datasource (if it applies)""" return None @property def groupby_column_names(self): return sorted([c.column_name for c in self.columns if c.groupby]) @property def filterable_column_names(self): return sorted([c.column_name for c in self.columns if c.filterable]) @property def dttm_cols(self): return [] @property def url(self): return '/{}/edit/{}'.format(self.baselink, self.id) @property def explore_url(self): if self.default_endpoint: return self.default_endpoint else: return '/superset/explore/{obj.type}/{obj.id}/'.format(obj=self) @property def column_formats(self): return { m.metric_name: m.d3format for m in self.metrics if m.d3format } def add_missing_metrics(self, metrics): exisiting_metrics = {m.metric_name for m in self.metrics} for metric in metrics: if metric.metric_name not in exisiting_metrics: metric.table_id = self.id self.metrics += [metric] @property def metrics_combo(self): return sorted( [ (m.metric_name, m.verbose_name or m.metric_name or '') for m in self.metrics], key=lambda x: x[1]) @property def short_data(self): """Data representation of the datasource sent to the frontend""" return { 'edit_url': self.url, 'id': self.id, 'uid': self.uid, 'schema': self.schema, 'name': self.name, 'type': self.type, 'connection': self.connection, 'creator': str(self.created_by), } @property def select_star(self): pass @property def data(self): """Data representation of the datasource sent to the frontend""" order_by_choices = [] for s in sorted(self.column_names): order_by_choices.append((json.dumps([s, True]), s + ' [asc]')) order_by_choices.append((json.dumps([s, False]), s + ' [desc]')) verbose_map = {'__timestamp': 'Time'} verbose_map.update({ o.metric_name: o.verbose_name or o.metric_name for o in self.metrics }) verbose_map.update({ o.column_name: o.verbose_name or o.column_name for o in self.columns }) return { # simple fields 'id': self.id, 'column_formats': self.column_formats, 'description': self.description, 'database': self.database.data, # pylint: disable=no-member 'default_endpoint': self.default_endpoint, 'filter_select': self.filter_select_enabled, # TODO deprecate 'filter_select_enabled': self.filter_select_enabled, 'name': self.name, 'datasource_name': self.datasource_name, 'type': self.type, 'schema': self.schema, 'offset': self.offset, 'cache_timeout': self.cache_timeout, 'params': self.params, 'perm': self.perm, # sqla-specific 'sql': self.sql, # computed fields 'all_cols': utils.choicify(self.column_names), 'columns': [o.data for o in self.columns], 'edit_url': self.url, 'filterable_cols': utils.choicify(self.filterable_column_names), 'gb_cols': utils.choicify(self.groupby_column_names), 'metrics': [o.data for o in self.metrics], 'metrics_combo': self.metrics_combo, 'order_by_choices': order_by_choices, 'owner': self.owner.id if self.owner else None, 'verbose_map': verbose_map, 'select_star': self.select_star, } @staticmethod def filter_values_handler( values, target_column_is_numeric=False, is_list_target=False): def handle_single_value(v): # backward compatibility with previous