2
2
import logging
3
3
from typing import Optional , Tuple
4
4
5
+ import yaml
6
+
5
7
from redash .query_runner import (
6
8
TYPE_BOOLEAN ,
7
9
TYPE_DATETIME ,
40
42
41
43
42
44
class PowerBIDAX (BaseHTTPQueryRunner ):
43
- noop_query = """
44
- EVALUATE
45
- DATATABLE(
46
- "Name", STRING, "Region", STRING,
47
- {
48
- {"User1", "East"},
49
- {"User2", "East"},
50
- {"User3", "West"},
51
- {"User4", "West"},
52
- {"User4", "East"}
53
- }
54
- )
45
+ noop_query = """# yaml
46
+ group_id:
47
+ dataset_id:
48
+ query: |
49
+ EVALUATE
50
+ DATATABLE(
51
+ "Name", STRING, "Region", STRING,
52
+ {
53
+ {"User1", "East"},
54
+ {"User2", "East"},
55
+ {"User3", "West"},
56
+ {"User4", "West"},
57
+ {"User4", "East"}
58
+ }
59
+ )
55
60
"""
61
+ should_annotate_query = False
56
62
response_error = "Power BI returned unexpected status code"
57
63
client_id_title = "Client ID"
58
64
authority_url_title = "Authority URL"
59
- scope_title = "Scope"
60
- # client_id = "Enter_the_Application_Id_here"
61
- # authority_url = 'https://login.microsoftonline.com/yourdomain.com'
62
- scope = ["https://analysis.windows.net/powerbi/api/.default" ]
65
+ scopes_title = "Scopes"
63
66
64
67
requires_authentication = True
65
68
requires_url = False
66
69
url_title = "Power BI URL"
67
- url = "https://api.powerbi.com/v1.0/myorg"
68
- # should_annotate_query = False
69
70
username_title = "Username"
70
71
password_title = "Password"
72
+ default_url = "https://api.powerbi.com/v1.0/myorg"
73
+ default_scopes = '["https://analysis.windows.net/powerbi/api/.default"]'
71
74
72
75
@classmethod
73
76
def configuration_schema (cls ):
74
77
schema = super ().configuration_schema ()
75
78
properties : dict = schema ["properties" ]
79
+ properties ["url" ].update ({"default" : cls .default_url })
76
80
properties .update (
77
81
{
78
82
"client_id" : {"type" : "string" , "title" : cls .client_id_title },
79
83
"authority_url" : {
80
84
"type" : "string" ,
81
85
"title" : cls .authority_url_title ,
82
- "default" : "https://login.microsoftonline.com/" ,
86
+ "default" : "https://login.microsoftonline.com/<tenant name/yourdomain.com> " ,
83
87
},
84
- "scope " : {
85
- "type" : "list " ,
86
- "title" : cls .scope_title ,
87
- # "default": ["https://analysis.windows.net/powerbi/api/.default"] ,
88
+ "scopes " : {
89
+ "type" : "string " ,
90
+ "title" : cls .scopes_title ,
91
+ "default" : cls . default_scopes ,
88
92
},
89
93
}
90
94
)
91
95
schema ["required" ] = schema .get ("required" , []) + [
92
96
"client_id" ,
93
97
"authority_url" ,
94
- "scope" ,
95
98
]
96
99
return schema
97
100
@@ -106,61 +109,67 @@ def enabled(cls):
106
109
def __init__ (self , * args , ** kwargs ):
107
110
super ().__init__ (* args , ** kwargs )
108
111
self .syntax = "yaml"
112
+ self .configuration ["url" ] = self .configuration .get ("url" , self .default_url )
113
+ scopes = self .configuration .get ("scopes" , self .default_scopes )
114
+ self .configuration ["scopes" ] = scopes
115
+ self .configuration ["scopes_array" ] = json_loads (scopes )
109
116
110
117
def test_connection (self ):
111
118
_ , error = self .get_response ("/availableFeatures" )
112
119
if error is not None :
113
120
raise Exception (error )
114
121
115
122
def get_auth (self ):
123
+ return None
124
+
125
+ def get_authorization (self ):
116
126
client_id = self .configuration ["client_id" ]
117
127
authority_url = self .configuration ["authority_url" ]
118
- scope = self .configuration ["scope" ]
119
- # username = self.configuration["username"]
120
- # password = self.configuration["password"]
128
+ scopes = self .configuration ["scopes_array" ]
121
129
username , password = super ().get_auth ()
122
130
app = msal .PublicClientApplication (client_id = client_id , authority = authority_url )
123
131
result = app .acquire_token_by_username_password (
124
132
username = username ,
125
133
password = password ,
126
- scopes = scope ,
134
+ scopes = scopes ,
127
135
)
128
136
access_token = result ["access_token" ]
129
137
return f"Bearer { access_token } "
130
138
131
- def get_response (self , url , auth = None , http_method = "get" , ** kwargs ):
139
+ def get_response (self , url : str , auth = None , http_method = "get" , ** kwargs ):
132
140
url = "{}{}" .format (self .configuration ["url" ], url )
133
141
headers = kwargs .pop ("headers" , {})
134
142
headers ["Accept" ] = "application/json"
135
143
headers ["Content-Type" ] = "application/json"
136
- # access_token = self._get_access_token()
137
- # headers["Authorization"] = f"Bearer {access_token}"
144
+ headers ["Authorization" ] = self .get_authorization ()
138
145
return super ().get_response (url , auth , http_method , headers = headers , ** kwargs )
139
146
140
147
def _build_query (self , query : str ) -> Tuple [dict , str , Optional [list ]]:
141
- query : dict = json_loads (query )
142
- group_id = query .pop ("group_id" , "" )
143
- dataset_id = query .pop ("dataset_id" , "" )
144
- query = (
145
- {
146
- "queries" : [{"query" : query .pop ("query" , "" )}],
147
- "serializerSettings" : {"includeNulls" : True },
148
- # "impersonatedUserName": email,
149
- },
150
- )
151
- url = "/groups/{groupId}/datasets/{datasetId}/executeQueries" .format_map (
152
- {
153
- "groupId" : group_id ,
154
- "datasetId" : dataset_id ,
155
- }
156
- )
157
- return url , query
148
+ query_dict : dict = yaml .safe_load (query )
149
+ group_id = query_dict .get ("group_id" )
150
+ dataset_id = query_dict .get ("dataset_id" )
151
+ json_body = {
152
+ "queries" : [{"query" : query_dict .get ("query" , "" )}],
153
+ "serializerSettings" : {"includeNulls" : True },
154
+ # "impersonatedUserName": email,
155
+ }
156
+
157
+ if dataset_id is None :
158
+ raise ValueError ("dataset_id can't be empty" )
159
+ url = (
160
+ "" if group_id is None else f"/groups/{ group_id } "
161
+ ) + f"/datasets/{ dataset_id } " "/executeQueries"
162
+ return url , json_body
158
163
159
164
@classmethod
160
165
def _parse_results (cls , query_results : dict ):
161
166
try :
162
- rows = query_results .get ("results" , {}).get ("tables" , [{}]).get ("rows" , [])
163
- df = pandas .from_records (data = rows )
167
+ rows = (
168
+ query_results .get ("results" , [{}])[0 ]
169
+ .get ("tables" , [{}])[0 ]
170
+ .get ("rows" , [])
171
+ )
172
+ df = pandas .DataFrame .from_records (data = rows )
164
173
data = {"columns" : [], "rows" : []}
165
174
conversions = CONVERSIONS
166
175
labels = []
@@ -196,12 +205,13 @@ def _parse_results(cls, query_results: dict):
196
205
return json_data , error
197
206
198
207
def run_query (self , query , user ):
199
- url , query = self ._build_query (query )
208
+ url , json_body = self ._build_query (query )
200
209
response , error = self .get_response (
201
210
http_method = "post" ,
202
211
url = url ,
203
- json = query ,
212
+ json = json_body ,
204
213
)
214
+ response .raise_for_status ()
205
215
query_results = response .json ()
206
216
json_data , error = self ._parse_results (query_results )
207
217
return json_data , error
0 commit comments