models.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. # pylint: disable=C,R,W
  2. import json
  3. from past.builtins import basestring
  4. from sqlalchemy import (
  5. and_, Boolean, Column, Integer, String, Text,
  6. )
  7. from sqlalchemy.ext.declarative import declared_attr
  8. from sqlalchemy.orm import foreign, relationship
  9. from superset import utils
  10. from superset.models.core import Slice
  11. from superset.models.helpers import AuditMixinNullable, ImportMixin
  12. class BaseDatasource(AuditMixinNullable, ImportMixin):
  13. """A common interface to objects that are queryable
  14. (tables and datasources)"""
  15. # ---------------------------------------------------------------
  16. # class attributes to define when deriving BaseDatasource
  17. # ---------------------------------------------------------------
  18. __tablename__ = None # {connector_name}_datasource
  19. type = None # datasoure type, str to be defined when deriving this class
  20. baselink = None # url portion pointing to ModelView endpoint
  21. column_class = None # link to derivative of BaseColumn
  22. metric_class = None # link to derivative of BaseMetric
  23. # Used to do code highlighting when displaying the query in the UI
  24. query_language = None
  25. name = None # can be a Column or a property pointing to one
  26. # ---------------------------------------------------------------
  27. # Columns
  28. id = Column(Integer, primary_key=True)
  29. description = Column(Text)
  30. default_endpoint = Column(Text)
  31. is_featured = Column(Boolean, default=False) # TODO deprecating
  32. filter_select_enabled = Column(Boolean, default=False)
  33. offset = Column(Integer, default=0)
  34. cache_timeout = Column(Integer)
  35. params = Column(String(1000))
  36. perm = Column(String(1000))
  37. sql = None
  38. owner = None
  39. update_from_object_fields = None
  40. @declared_attr
  41. def slices(self):
  42. return relationship(
  43. 'Slice',
  44. primaryjoin=lambda: and_(
  45. foreign(Slice.datasource_id) == self.id,
  46. foreign(Slice.datasource_type) == self.type,
  47. ),
  48. )
  49. # placeholder for a relationship to a derivative of BaseColumn
  50. columns = []
  51. # placeholder for a relationship to a derivative of BaseMetric
  52. metrics = []
  53. @property
  54. def uid(self):
  55. """Unique id across datasource types"""
  56. return '{self.id}__{self.type}'.format(**locals())
  57. @property
  58. def column_names(self):
  59. return sorted([c.column_name for c in self.columns])
  60. @property
  61. def columns_types(self):
  62. return {c.column_name: c.type for c in self.columns}
  63. @property
  64. def main_dttm_col(self):
  65. return 'timestamp'
  66. @property
  67. def datasource_name(self):
  68. raise NotImplementedError()
  69. @property
  70. def connection(self):
  71. """String representing the context of the Datasource"""
  72. return None
  73. @property
  74. def schema(self):
  75. """String representing the schema of the Datasource (if it applies)"""
  76. return None
  77. @property
  78. def groupby_column_names(self):
  79. return sorted([c.column_name for c in self.columns if c.groupby])
  80. @property
  81. def filterable_column_names(self):
  82. return sorted([c.column_name for c in self.columns if c.filterable])
  83. @property
  84. def dttm_cols(self):
  85. return []
  86. @property
  87. def url(self):
  88. return '/{}/edit/{}'.format(self.baselink, self.id)
  89. @property
  90. def explore_url(self):
  91. if self.default_endpoint:
  92. return self.default_endpoint
  93. else:
  94. return '/superset/explore/{obj.type}/{obj.id}/'.format(obj=self)
  95. @property
  96. def column_formats(self):
  97. return {
  98. m.metric_name: m.d3format
  99. for m in self.metrics
  100. if m.d3format
  101. }
  102. def add_missing_metrics(self, metrics):
  103. exisiting_metrics = {m.metric_name for m in self.metrics}
  104. for metric in metrics:
  105. if metric.metric_name not in exisiting_metrics:
  106. metric.table_id = self.id
  107. self.metrics += [metric]
  108. @property
  109. def metrics_combo(self):
  110. return sorted(
  111. [
  112. (m.metric_name, m.verbose_name or m.metric_name or '')
  113. for m in self.metrics],
  114. key=lambda x: x[1])
  115. @property
  116. def short_data(self):
  117. """Data representation of the datasource sent to the frontend"""
  118. return {
  119. 'edit_url': self.url,
  120. 'id': self.id,
  121. 'uid': self.uid,
  122. 'schema': self.schema,
  123. 'name': self.name,
  124. 'type': self.type,
  125. 'connection': self.connection,
  126. 'creator': str(self.created_by),
  127. }
  128. @property
  129. def select_star(self):
  130. pass
  131. @property
  132. def data(self):
  133. """Data representation of the datasource sent to the frontend"""
  134. order_by_choices = []
  135. for s in sorted(self.column_names):
  136. order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
  137. order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
  138. verbose_map = {'__timestamp': 'Time'}
  139. verbose_map.update({
  140. o.metric_name: o.verbose_name or o.metric_name
  141. for o in self.metrics
  142. })
  143. verbose_map.update({
  144. o.column_name: o.verbose_name or o.column_name
  145. for o in self.columns
  146. })
  147. return {
  148. # simple fields
  149. 'id': self.id,
  150. 'column_formats': self.column_formats,
  151. 'description': self.description,
  152. 'database': self.database.data, # pylint: disable=no-member
  153. 'default_endpoint': self.default_endpoint,
  154. 'filter_select': self.filter_select_enabled, # TODO deprecate
  155. 'filter_select_enabled': self.filter_select_enabled,
  156. 'name': self.name,
  157. 'datasource_name': self.datasource_name,
  158. 'type': self.type,
  159. 'schema': self.schema,
  160. 'offset': self.offset,
  161. 'cache_timeout': self.cache_timeout,
  162. 'params': self.params,
  163. 'perm': self.perm,
  164. # sqla-specific
  165. 'sql': self.sql,
  166. # computed fields
  167. 'all_cols': utils.choicify(self.column_names),
  168. 'columns': [o.data for o in self.columns],
  169. 'edit_url': self.url,
  170. 'filterable_cols': utils.choicify(self.filterable_column_names),
  171. 'gb_cols': utils.choicify(self.groupby_column_names),
  172. 'metrics': [o.data for o in self.metrics],
  173. 'metrics_combo': self.metrics_combo,
  174. 'order_by_choices': order_by_choices,
  175. 'owner': self.owner.id if self.owner else None,
  176. 'verbose_map': verbose_map,
  177. 'select_star': self.select_star,
  178. }
  179. @staticmethod
  180. def filter_values_handler(
  181. values, target_column_is_numeric=False, is_list_target=False):
  182. def handle_single_value(v):
  183. # backward compatibility with previous <select> components
  184. if isinstance(v, basestring):
  185. v = v.strip('\t\n \'"')
  186. if target_column_is_numeric:
  187. # For backwards compatibility and edge cases
  188. # where a column data type might have changed
  189. v = utils.string_to_num(v)
  190. if v == '<NULL>':
  191. return None
  192. elif v == '<empty string>':
  193. return ''
  194. return v
  195. if isinstance(values, (list, tuple)):
  196. values = [handle_single_value(v) for v in values]
  197. else:
  198. values = handle_single_value(values)
  199. if is_list_target and not isinstance(values, (tuple, list)):
  200. values = [values]
  201. elif not is_list_target and isinstance(values, (tuple, list)):
  202. if len(values) > 0:
  203. values = values[0]
  204. else:
  205. values = None
  206. return values
  207. def external_metadata(self):
  208. """Returns column information from the external system"""
  209. raise NotImplementedError()
  210. def get_query_str(self, query_obj):
  211. """Returns a query as a string
  212. This is used to be displayed to the user so that she/he can
  213. understand what is taking place behind the scene"""
  214. raise NotImplementedError()
  215. def query(self, query_obj):
  216. """Executes the query and returns a dataframe
  217. query_obj is a dictionary representing Superset's query interface.
  218. Should return a ``superset.models.helpers.QueryResult``
  219. """
  220. raise NotImplementedError()
  221. def values_for_column(self, column_name, limit=10000):
  222. """Given a column, returns an iterable of distinct values
  223. This is used to populate the dropdown showing a list of
  224. values in filters in the explore view"""
  225. raise NotImplementedError()
  226. @staticmethod
  227. def default_query(qry):
  228. return qry
  229. def get_column(self, column_name):
  230. for col in self.columns:
  231. if col.column_name == column_name:
  232. return col
  233. def get_fk_many_from_list(
  234. self, object_list, fkmany, fkmany_class, key_attr):
  235. """Update ORM one-to-many list from object list
  236. Used for syncing metrics and columns using the same code"""
  237. object_dict = {o.get(key_attr): o for o in object_list}
  238. object_keys = [o.get(key_attr) for o in object_list]
  239. # delete fks that have been removed
  240. fkmany = [o for o in fkmany if getattr(o, key_attr) in object_keys]
  241. # sync existing fks
  242. for fk in fkmany:
  243. obj = object_dict.get(getattr(fk, key_attr))
  244. for attr in fkmany_class.update_from_object_fields:
  245. setattr(fk, attr, obj.get(attr))
  246. # create new fks
  247. new_fks = []
  248. orm_keys = [getattr(o, key_attr) for o in fkmany]
  249. for obj in object_list:
  250. key = obj.get(key_attr)
  251. if key not in orm_keys:
  252. del obj['id']
  253. orm_kwargs = {}
  254. for k in obj:
  255. if (
  256. k in fkmany_class.update_from_object_fields and
  257. k in obj
  258. ):
  259. orm_kwargs[k] = obj[k]
  260. new_obj = fkmany_class(**orm_kwargs)
  261. new_fks.append(new_obj)
  262. fkmany += new_fks
  263. return fkmany
  264. def update_from_object(self, obj):
  265. """Update datasource from a data structure
  266. The UI's table editor crafts a complex data structure that
  267. contains most of the datasource's properties as well as
  268. an array of metrics and columns objects. This method
  269. receives the object from the UI and syncs the datasource to
  270. match it. Since the fields are different for the different
  271. connectors, the implementation uses ``update_from_object_fields``
  272. which can be defined for each connector and
  273. defines which fields should be synced"""
  274. for attr in self.update_from_object_fields:
  275. setattr(self, attr, obj.get(attr))
  276. self.user_id = obj.get('owner')
  277. # Syncing metrics
  278. metrics = self.get_fk_many_from_list(
  279. obj.get('metrics'), self.metrics, self.metric_class, 'metric_name')
  280. self.metrics = metrics
  281. # Syncing columns
  282. self.columns = self.get_fk_many_from_list(
  283. obj.get('columns'), self.columns, self.column_class, 'column_name')
  284. class BaseColumn(AuditMixinNullable, ImportMixin):
  285. """Interface for column"""
  286. __tablename__ = None # {connector_name}_column
  287. id = Column(Integer, primary_key=True)
  288. column_name = Column(String(255))
  289. verbose_name = Column(String(1024))
  290. is_active = Column(Boolean, default=True)
  291. type = Column(String(32))
  292. groupby = Column(Boolean, default=False)
  293. count_distinct = Column(Boolean, default=False)
  294. sum = Column(Boolean, default=False)
  295. avg = Column(Boolean, default=False)
  296. max = Column(Boolean, default=False)
  297. min = Column(Boolean, default=False)
  298. filterable = Column(Boolean, default=False)
  299. description = Column(Text)
  300. is_dttm = None
  301. # [optional] Set this to support import/export functionality
  302. export_fields = []
  303. def __repr__(self):
  304. return self.column_name
  305. num_types = (
  306. 'DOUBLE', 'FLOAT', 'INT', 'BIGINT',
  307. 'LONG', 'REAL', 'NUMERIC', 'DECIMAL', 'MONEY',
  308. )
  309. date_types = ('DATE', 'TIME', 'DATETIME')
  310. str_types = ('VARCHAR', 'STRING', 'CHAR')
  311. @property
  312. def is_num(self):
  313. return (
  314. self.type and
  315. any([t in self.type.upper() for t in self.num_types])
  316. )
  317. @property
  318. def is_time(self):
  319. return (
  320. self.type and
  321. any([t in self.type.upper() for t in self.date_types])
  322. )
  323. @property
  324. def is_string(self):
  325. return (
  326. self.type and
  327. any([t in self.type.upper() for t in self.str_types])
  328. )
  329. @property
  330. def expression(self):
  331. raise NotImplementedError()
  332. @property
  333. def data(self):
  334. attrs = (
  335. 'id', 'column_name', 'verbose_name', 'description', 'expression',
  336. 'filterable', 'groupby', 'is_dttm', 'type',
  337. 'database_expression', 'python_date_format',
  338. )
  339. return {s: getattr(self, s) for s in attrs if hasattr(self, s)}
  340. class BaseMetric(AuditMixinNullable, ImportMixin):
  341. """Interface for Metrics"""
  342. __tablename__ = None # {connector_name}_metric
  343. id = Column(Integer, primary_key=True)
  344. metric_name = Column(String(512))
  345. verbose_name = Column(String(1024))
  346. metric_type = Column(String(32))
  347. description = Column(Text)
  348. is_restricted = Column(Boolean, default=False, nullable=True)
  349. d3format = Column(String(128))
  350. warning_text = Column(Text)
  351. """
  352. The interface should also declare a datasource relationship pointing
  353. to a derivative of BaseDatasource, along with a FK
  354. datasource_name = Column(
  355. String(255),
  356. ForeignKey('datasources.datasource_name'))
  357. datasource = relationship(
  358. # needs to be altered to point to {Connector}Datasource
  359. 'BaseDatasource',
  360. backref=backref('metrics', cascade='all, delete-orphan'),
  361. enable_typechecks=False)
  362. """
  363. @property
  364. def perm(self):
  365. raise NotImplementedError()
  366. @property
  367. def expression(self):
  368. raise NotImplementedError()
  369. @property
  370. def data(self):
  371. attrs = (
  372. 'id', 'metric_name', 'verbose_name', 'description', 'expression',
  373. 'warning_text', 'd3format')
  374. return {s: getattr(self, s) for s in attrs}