diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.mproj b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.mproj index b3d1c57..72416e4 100644 --- a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.mproj +++ b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.mproj @@ -2,8 +2,7 @@ Debug 2.0 - - + {2bd3d055-377f-4ad2-8362-acb114b459dd} Exe MyRootNamespace MyAssemblyName @@ -67,12 +66,6 @@ Code - - Content - - - Content - Code diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq index df25b9f..0ab6591 100644 --- a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq +++ b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq @@ -1,15 +1,22 @@ -// This file contains your Data Connector logic -section PQGoogleSpreadsheet; +section PQGoogleSpreadsheet; // Constants -client_id = Text.FromBinary(Extension.Contents("client_id.txt")); -client_secret = Text.FromBinary(Extension.Contents("client_secret.txt")); +appKey = "YOUR_APP_KEY"; +appSecret = "YOUR_APP_SECRET"; redirect_uri = "https://preview.powerbi.com/views/oauthredirect.html"; +authorize_uri = "https://accounts.google.com/o/oauth2/v2/auth"; +token_uri = "https://www.googleapis.com/oauth2/v4/token"; +logout_uri = "https://accounts.google.com/o/oauth2/revoke?token="; + +scope_prefix = "https://www.googleapis.com/auth/"; +scopes = { +"drive.readonly" +}; + windowWidth = 1200; windowHeight = 1000; - [DataSource.Kind="PQGoogleSpreadsheet", Publish="PQGoogleSpreadsheet.UI"] shared PQGoogleSpreadsheet.Contents = Value.ReplaceType(PQGoogleSpreadsheetCore.Contents, type function (#"Google Spreadsheet url" as Uri.Type) as any); @@ -17,16 +24,39 @@ shared PQGoogleSpreadsheet.Contents = Value.ReplaceType(PQGoogleSpreadsheetCore. shared PQGoogleSpreadsheetCore.Contents = (url as text) => let finalUrl = Text.BeforeDelimiter(url, "/", {0, RelativePosition.FromEnd}) & "/export?format=xlsx", - content = Web.Contents(finalUrl), - excel = Excel.Workbook(content, null, true) + excel = Web.ContentsCustomRetry(finalUrl) in - excel; - - + Excel.Workbook(excel, null, true); + +// https://gist.github.com/CurtHagenlocher/68ac18caa0a17667c805 +Web.ContentsCustomRetry = (url as text, optional options as record) => Value.WaitFor( + (i) => + let + options2 = if options = null then [] else options, + options3 = if i=0 then options2 else options2 & [IsRetry=true], + result = Web.Contents(url, options3 & [ManualStatusHandling={429}]), + buffered = Binary.Buffer(result), /* avoid risk of double request */ + status = if buffered = null then 0 else Value.Metadata(result)[Response.Status], + actualResult = if status = 429 then null else buffered + in + actualResult, + (i) => #duration(0, 0, 0, i*0.1)); + +// https://docs.microsoft.com/en-us/power-query/helperfunctions#valuewaitfor +// This function is useful when making an asynchronous HTTP request and you need to poll the server until the request is complete. +Value.WaitFor = (producer as function, interval as function, optional count as number) as any => + let + list = List.Generate( + () => {0, null}, + (state) => state{0} <> null and (count = null or state{0} < count), + (state) => if state{1} <> null then {null, state{1}} else {1 + state{0}, Function.InvokeAfter(() => producer(state{0}), interval(state{0}))}, + (state) => state{1}) + in + List.Last(list); // Data Source Kind description PQGoogleSpreadsheet = [ - Authentication = [ + Authentication = [ OAuth = [ StartLogin = StartLogin, FinishLogin = FinishLogin, @@ -34,21 +64,32 @@ PQGoogleSpreadsheet = [ Refresh = Refresh, Label = "Google Spreadsheet Auth" ] - ], - Label = "Google Spreadsheet Connector" + ] + // TestConnection is required to enable the connector through the Gateway + ,TestConnection = (dataSourcePath) => { "PQGoogleSpreadsheet.Contents" , dataSourcePath } + ,Label = "Google Spreadsheet Connector" ]; +Value.IfNull = (a, b) => if a <> null then a else b; + +GetScopeString = (scopes as list, optional scopePrefix as text) as text => + let + prefix = Value.IfNull(scopePrefix, ""), + addPrefix = List.Transform(scopes, each prefix & _), + asText = Text.Combine(addPrefix, " ") + in + asText; StartLogin = (resourceUrl, state, display) => let - AuthorizeUrl = "https://accounts.google.com/o/oauth2/v2/auth?" & Uri.BuildQueryString([ - scope = "https://www.googleapis.com/auth/drive.readonly", - access_type = "offline", - include_granted_scopes = "true", - client_id = client_id, - state = state, - redirect_uri = redirect_uri, - response_type = "code"]) + AuthorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([ + client_id = appKey, + redirect_uri = redirect_uri, + state= state, + scope = GetScopeString(scopes, scope_prefix), + response_type = "code", + access_type = "offline", + prompt = "consent"]) in [ LoginUri = AuthorizeUrl, @@ -58,53 +99,52 @@ StartLogin = (resourceUrl, state, display) => Context = null ]; - FinishLogin = (context, callbackUri, state) => let - Parts = Uri.Parts(callbackUri)[Query] + parts = Uri.Parts(callbackUri)[Query], + result = if (Record.HasFields(parts, {"error", "error_description"})) then + error Error.Record(parts[error], parts[error_description], parts) + else + TokenMethod("authorization_code", parts[code]) in - TokenMethod(Parts[code]); - + result; -TokenMethod = (code) => +TokenMethod = (grantType, code) => let - Response = Web.Contents("https://www.googleapis.com/oauth2/v4/token", [ - Content = Text.ToBinary(Uri.BuildQueryString([ - client_id = client_id, - client_secret = client_secret, - code = code, - grant_type = "authorization_code", - access_type = "offline", - redirect_uri = redirect_uri])), - Headers=[#"Content-type" = "application/x-www-form-urlencoded", #"Accept" = "application/json"], - ManualStatusHandling = {400} - ]), - Body = Json.Document(Response), - Result = if Record.HasFields(Body, {"error", "error_description"}) then - error Error.Record(Body[error], Body[error_description], Body) - else - Body + query = [ + grant_type = grantType, + client_id = appKey, + client_secret = appSecret, + redirect_uri = redirect_uri + ], + + // {CODE From Matt Masson} added for Google API - field is "code" on initial auth, and "refresh_token" for refresh + queryWithCode = if (grantType = "refresh_token") then [ refresh_token = code ] else [code = code], + + response = Web.Contents(token_uri, [ + Content = Text.ToBinary(Uri.BuildQueryString(query & queryWithCode)), + Headers=[#"Content-type" = "application/x-www-form-urlencoded",#"Accept" = "application/json"], ManualStatusHandling = {400}]), + body = Json.Document(response), + result = if (Record.HasFields(body, {"error", "error_description"})) then + error Error.Record(body[error], body[error_description], body) + else + body in - Result; + result; +Refresh = (resourceUrl, refresh_token) => TokenMethod("refresh_token", refresh_token); -Refresh = (resourceUrl, refresh_token) => TokenMethod("refresh_token", "refresh_token", refresh_token); - -Logout = (token) => "https://accounts.google.com/o/oauth2/revoke?token=" & token; - +Logout = (token) => logout_uri & token; // Data Source UI publishing description PQGoogleSpreadsheet.UI = [ Beta = true, - Category = "Other", ButtonText = { "Google Spreadsheet Connector", "Google Spreadsheet Connector Help" }, - LearnMoreUrl = "http://skolenipowerbi.cz/", + Category = "File", SourceImage = PQGoogleSpreadsheet.Icons, SourceTypeImage = PQGoogleSpreadsheet.Icons ]; - - PQGoogleSpreadsheet.Icons = [ Icon16 = { Extension.Contents("PQGoogleSpreadsheet16.png"), Extension.Contents("PQGoogleSpreadsheet20.png"), Extension.Contents("PQGoogleSpreadsheet24.png"), Extension.Contents("PQGoogleSpreadsheet32.png") }, Icon32 = { Extension.Contents("PQGoogleSpreadsheet32.png"), Extension.Contents("PQGoogleSpreadsheet40.png"), Extension.Contents("PQGoogleSpreadsheet48.png"), Extension.Contents("PQGoogleSpreadsheet64.png") } diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png index 5f7dbe9..8e12756 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet20.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet20.png index c64499e..ec25cfc 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet20.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet20.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet24.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet24.png index d425fd0..1351b3e 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet24.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet24.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet32.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet32.png index b1216a6..c0413fe 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet32.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet32.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet40.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet40.png index cca25c9..93dd124 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet40.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet40.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet48.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet48.png index faef9cc..6a7e52c 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet48.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet48.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet64.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet64.png index ab80de1..20dd17f 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet64.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet64.png differ diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png index 9427d2a..fcaa1c0 100644 Binary files a/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png and b/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png differ diff --git a/PQGoogleSpreadsheet/README.md b/PQGoogleSpreadsheet/README.md index 58ad6db..0e99813 100644 --- a/PQGoogleSpreadsheet/README.md +++ b/PQGoogleSpreadsheet/README.md @@ -1,7 +1,8 @@ # Google Spreadsheets Connector for Microsoft Power BI -To make it run add client_id.txt and client_secret.txt containing your Google API client credentials generated in: -https://console.developers.google.com/ - +To make it work with please replace in the code: +appKey = "YOUR_APP_KEY"; +appSecret = "YOUR_APP_SECRET"; +with the credentials you've created at https://console.developers.google.com/apis diff --git a/PQGoogleSpreadsheet/logo-drive.png b/PQGoogleSpreadsheet/logo-drive.png deleted file mode 100644 index 19fff5f..0000000 Binary files a/PQGoogleSpreadsheet/logo-drive.png and /dev/null differ diff --git a/README.md b/README.md index 02fae93..5884575 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,6 @@ # Google Spreadsheets Connector for Microsoft Power BI - -To make this connector work fully securely use this tutorial: -http://www.skolenipowerbi.cz/l/google-drive-connector-kompletni-pruvodce/ - - -If you need just an a quick way to connect to Google Drive, use this tutorial: -http://www.skolenipowerbi.cz/l/google-drive-connector-compact-tutorial/ - +How to set up: +https://fpvmorais.com/post/google-spreadsheets-custom-connector/ Hope you enjoy the Google Spreadsheets Connector. - -