1
1
import logging
2
2
from collections import namedtuple
3
+ from typing import Union
3
4
import urllib
4
5
5
6
from sqlalchemy import create_engine , MetaData , Column , Table , select , asc , desc , and_
6
7
from sqlalchemy import engine
7
8
from sqlalchemy .sql import Select
8
9
from sqlalchemy .sql .functions import Function
10
+ from sqlalchemy .sql .expression import BinaryExpression
9
11
from sqlalchemy .engine import reflection
10
12
from grice .complex_filter import ComplexFilter , get_column
11
13
from grice .errors import ConfigurationError , NotFoundError , JoinError
15
17
DEFAULT_PAGE = 0
16
18
DEFAULT_PER_PAGE = 50
17
19
SORT_DIRECTIONS = ['asc' , 'desc' ]
18
- SUPPORTED_FUNCS = ['avg' , 'count' , 'min' , 'max' , 'sum' ]
20
+ SUPPORTED_FUNCS = ['avg' , 'count' , 'min' , 'max' , 'sum' , 'stddev_pop' ]
19
21
ColumnSort = namedtuple ('ColumnSort' , ['table_name' , 'column_name' , 'direction' ])
20
22
ColumnPair = namedtuple ('ColumnPair' , ['from_column' , 'to_column' ])
21
23
TableJoin = namedtuple ('TableJoin' , ['table_name' , 'column_pairs' , 'outer_join' ])
@@ -48,15 +50,30 @@ def init_database(db_config):
48
50
return create_engine (eng_url )
49
51
50
52
51
- def function_to_dict (func : Function ):
52
- data = {
53
- 'name' : str (func ),
54
- 'primary_key' : func .primary_key ,
55
- 'table' : '<Function {}>' .format (func .name ),
56
- }
53
+ def computed_column_to_dict (column : Union [Function , BinaryExpression ]):
54
+ """
55
+ Converts a SqlAlchemy object for a column that contains a computed value to a dict so we can return JSON.
56
+
57
+ :param column: a SqlAlchemy Function or a SqlAlchemy BinaryExpression
58
+ :return: dict
59
+ """
60
+ if isinstance (column , Function ):
61
+ data = {
62
+ 'name' : str (column ),
63
+ 'primary_key' : column .primary_key ,
64
+ 'table' : '<Function {}>' .format (column .name ),
65
+ 'type' : column .type .__class__ .__name__ ,
66
+ }
67
+ elif isinstance (column , BinaryExpression ):
68
+ data = {
69
+ 'name' : str (column ),
70
+ 'primary_key' : column .primary_key ,
71
+ 'table' : '<BinaryExpression {}>' .format (column ),
72
+ 'type' : column .type .__class__ .__name__ ,
73
+ }
57
74
return data
58
75
59
- def column_to_dict (column : Column ):
76
+ def _column_to_dict (column : Column ):
60
77
"""
61
78
Converts a SqlAlchemy Column object to a dict so we can return JSON.
62
79
@@ -80,6 +97,16 @@ def column_to_dict(column: Column):
80
97
81
98
return data
82
99
100
+ def column_to_dict (column ):
101
+ """
102
+ Converts a SqlAlchemy Column, or column-like object to a dict so we can return JSON.
103
+
104
+ :param column: a column
105
+ :return: dict
106
+ """
107
+ if isinstance (column , Column ):
108
+ return _column_to_dict (column )
109
+ return computed_column_to_dict (column )
83
110
84
111
def table_to_dict (table : Table ):
85
112
"""
@@ -184,9 +211,7 @@ def apply_group_by(query, table: Table, join_table: Table, group_by: list):
184
211
:return: A SQLAlchemy select object modified to with sorts.
185
212
"""
186
213
for group in group_by :
187
- column = table .columns .get (group , None )
188
- if join_table is not None and not column :
189
- column = join_table .columns .get (group , None )
214
+ column = get_column (group , [table , join_table ])
190
215
191
216
if column is not None :
192
217
query = query .group_by (column )
@@ -305,34 +330,25 @@ def query_table(self, table_name: str, quargs: QueryArguments): # pylint: disab
305
330
log .debug ("Query %s" , query )
306
331
result = conn .execute (query )
307
332
308
- for row in result :
309
- count_of_map = {}
310
- if quargs .format_as_list :
311
- data = []
312
- for column in columns :
313
- if isinstance (column , Function ):
314
- counter = count_of_map .get (column .name , 0 ) + 1
315
- count_of_map [column .name ] = counter
316
- column_label = column .name + '_' + str (counter )
317
- else :
318
- column_label = column .table .name + '_' + column .name
319
- data .append (row [column_label ])
320
- else :
321
- data = {}
322
- for column in columns :
323
- if isinstance (column , Function ):
324
- counter = count_of_map .get (column .name , 0 ) + 1
325
- count_of_map [column .name ] = counter
326
- full_column_name = column .name + '_' + str (counter )
327
- column_label = column .name + '_' + str (counter )
328
- else :
329
- full_column_name = column .table .name + '.' + column .name
330
- column_label = column .table .name + '_' + column .name
331
- data [full_column_name ] = row [column_label ]
332
-
333
- rows .append (data )
334
-
335
- column_data = [column_to_dict (column ) if isinstance (column , Column ) else function_to_dict (column ) for column in columns ]
333
+ if quargs .format_as_list :
334
+ # SQLalchemy is giving us the data in the correct format
335
+ rows = result
336
+ else :
337
+ column_name_map = {}
338
+ first_row = True
339
+ for row in result :
340
+ # Make friendlier names if possible
341
+ if first_row :
342
+ for column , column_label in zip (columns , row .keys ()):
343
+ if isinstance (column , Column ):
344
+ full_column_name = column .table .name + '.' + column .name
345
+ column_name_map [column_label ] = full_column_name
346
+ first_row = False
347
+
348
+ data = {column_name_map .get (key , key ): val for key , val in row .items ()}
349
+ rows .append (data )
350
+
351
+ column_data = [column_to_dict (column ) for column in columns ]
336
352
337
353
return rows , column_data
338
354
0 commit comments