From 5ff08e99b0a1947a5e6d141c466a7b7539480993 Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Wed, 26 Feb 2020 11:58:52 +0000 Subject: [PATCH 1/6] Removed client_id.txt and client_secret.txt and updated README Following the instructions on the blog post there's no way to store those credentials besides hard coding them. https://www.thebiccountant.com/2017/09/24/custom-connector-import-google-sheets-oauth2-powerbi/ --- PQGoogleSpreadsheet/PQGoogleSpreadsheet.mproj | 9 +-------- PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq | 4 ++-- PQGoogleSpreadsheet/README.md | 7 ++++--- 3 files changed, 7 insertions(+), 13 deletions(-) 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..022b1ca 100644 --- a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq +++ b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq @@ -3,8 +3,8 @@ 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"; windowWidth = 1200; windowHeight = 1000; diff --git a/PQGoogleSpreadsheet/README.md b/PQGoogleSpreadsheet/README.md index 58ad6db..ac02423 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"; +By the credentials you've created at https://console.developers.google.com/apis From 6f5d213259e25a9086c334406c1d6d2d231db3fe Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Wed, 26 Feb 2020 12:05:01 +0000 Subject: [PATCH 2/6] Updated code to make it work with Power BI Enterprise Gateway --- PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq | 105 ++++++++++++--------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq index 022b1ca..e33799f 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 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); @@ -22,11 +29,9 @@ shared PQGoogleSpreadsheetCore.Contents = (url as text) => in excel; - - // Data Source Kind description PQGoogleSpreadsheet = [ - Authentication = [ + Authentication = [ OAuth = [ StartLogin = StartLogin, FinishLogin = FinishLogin, @@ -34,21 +39,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 +74,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_token); - -Logout = (token) => "https://accounts.google.com/o/oauth2/revoke?token=" & token; +Refresh = (resourceUrl, refresh_token) => TokenMethod("refresh_token", refresh_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/", 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") } From 866ba93e673aa759d6f5ca38b43e455c2761a665 Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Wed, 26 Feb 2020 12:18:36 +0000 Subject: [PATCH 3/6] Fixed grammar --- PQGoogleSpreadsheet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PQGoogleSpreadsheet/README.md b/PQGoogleSpreadsheet/README.md index ac02423..0e99813 100644 --- a/PQGoogleSpreadsheet/README.md +++ b/PQGoogleSpreadsheet/README.md @@ -5,4 +5,4 @@ To make it work with please replace in the code: appKey = "YOUR_APP_KEY"; appSecret = "YOUR_APP_SECRET"; -By the credentials you've created at https://console.developers.google.com/apis +with the credentials you've created at https://console.developers.google.com/apis From e50eaae1cdea53ad57ff0362762eb4459feb4958 Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Fri, 28 Feb 2020 15:29:44 +0000 Subject: [PATCH 4/6] Exchanged Google Drive icon for Google Spreadsheet --- PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png | Bin 728 -> 341 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet20.png | Bin 920 -> 484 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet24.png | Bin 1089 -> 392 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet32.png | Bin 1451 -> 495 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet40.png | Bin 1812 -> 700 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet48.png | Bin 2337 -> 447 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet64.png | Bin 3360 -> 800 bytes PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png | Bin 4606 -> 883 bytes PQGoogleSpreadsheet/logo-drive.png | Bin 11492 -> 0 bytes 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 PQGoogleSpreadsheet/logo-drive.png diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet16.png index 5f7dbe9eaa8b32fdf86727cab17c510ad314a9f9..8e127569a7d26b9994b114a25d538efefeccc067 100644 GIT binary patch delta 314 zcmV-A0mc5<1=RwOB!32COGiWi{{a60|De66lK=n#3Q0skR5*=eV4x5%!Who;Jhd2@ z{vBgrVBp85FxDXIXo5k!4Z1iJjPalGzcwz-3=9km8e$sO^Dizqj?(~QfTpOX1-c<{ zFF4Kj2xVmazY3RT{=a+-pFV$LU|?WiVCG<|*qO1tlYxPOfq#Wq+dnXVWcc{)Bg60S zzZp1qImKb(M0N2bZe?^JG%heMF*7s+TY3Nh0!T?j zK~y+TrIFoBQ(+u{j}_S;kXtTiWuFveT^iLkkNZ-$HbWCy`+qQy|uWguD5 z&7}5urX;wVK^Lu%DsZs#A z*8sGxh3O*kH}d=6=w^G%`yV@&Q1bXHO0;0jSko$EO-u07y`GHyoqbd12X=+JVg6Yw zK-`2qhBX4P27f?Fb3Yj?P2EhNWanh}w;f?8=%?(EWyJ*sVF9qsX~bG2Wc*KdPoDAA zgu8%ud#DQnPudEn;~3inC~owS@!zSPK6hXn)0xA9=)s`| z83CxW8m4P*3v(KZt@b&3vWxm2F&2#J0I^L^NttI4bAPr)9_j$zYN9;gNVM|LH>X&a zi|Ug3SdY~TqJ+^b#0sB#LshWj?dG5Zs*037u+hQ*20ho&{K;YrF%7U7TTX3d zI$f0k8_0t8Uj}Ru(iT83G!Zd|s2baVmo&PlN}n|;^;=;RUcxK$IY2QoKWiBKK4BR9 zQP6q+)qm{Wa3nX*%ka+R?lF(zl+xTlD%tMQ5c7xbJgp; zRiihtM4PV;jZ34XO(|VExJ1&q5ZbB#LdR^9C7XXkw$jZlbuNe$lR^^`sB0}k1C}b} znh)O&!sRNt;x+kg@A>gQJe+gjo}=7Det&Z#px6~ry4ON3oqxTG>(TeceDUvXeb7B< zXdgx7%{YVPgCv=Una1ANz0A(*o%U$24e2WJG;?QZW+C|`nQ0WOrP;ASKUs<|&ONnO zevU$eUJG9N(LAPlQbUOV%=MLg0bWiGJh#uexVGv3>41Q4hSGO31TbR;zyLauseo>1 zuVc$Zz!wV}SbwHHFiF8u;VcZLk5FD0ds0KyaUdeWDYUkP9N4{~F0>%PMJlmGw#07*qoM6N<$fN2bZe?^JG%heMF*7s+TY3Nh0|-e( zK~y+Tz0pZb6JZ#}@k!9YMbSjSf)=GEm`Ds~F_gAIO95E~Nq=a;vRH$F!6+)A)CQFG z!a+nMOlhe^LK;taz(fw}0ig_FFdh_)3yTQE)}>(Q_4_&>WmsGe#wYoumv{c@wA6oF za!+yQSLXObb~J-ClHSLT979t3*s;t^a`tyq_UgAEuoOaz?=; zi4QxZe1FZ4apKUcCpEf{A}nV@13R?NK$8}*l`tk~%Sm+~@wPk=leRtvwpd_?Vx8>s zbTK6~=ilGmaRM*_Pav)BFX?5yJ(@Xsdcwy!sOn#`DP2o8g==XDi{sc5tyDmJc29sY z2QW|AsD;SNK9{O~aXF#Uv8utbtU}@&>@H=!oqxp?b(}aPrh2}I@@vfz`O_L_fH50I zu^b3%s0C@+I4El7A->+>rX&<~oH(qO6N|{N*}qFGXGHGb#jjn{E>Y*zSq&LvliO~Lf3IlR=;f6#HTS(F*a?5@J} z_kREbBO@~*&2GxBM=NYfj^I$4}t0+Ik98y|~~l48?Ik$P%hV zGCoW2#(}1&27{)UCSJp@zq-zF{q+q7W)>y}0cimYdAvT*6w_qT7KK=#Es9{nfVQY6 ziUXKfFiRJz1ZELFFlxcTTYxv2UVn8R&c6EkI)?n!*Vj?hkZ!^28w}T9-@t1CAwd5) zU}RvpLxFj>(FHJq=npf4C-WbMNJg}BogiRj{PK^1aXAH!BM$%oTe(Y8I4Xi;00000 LNkvXXu0mjfq4T5- delta 1068 zcmV+{1k?M71HlN8B!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm000tn z000tn0p4aGcmMzZ8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*7s+TY3Nh1F=a& zK~zXf#gbo4Q&#}SuS?wA!}j7Vf1Hy^nfhR&Vq#bzg_f2g%zuF>v?BB(&>=43AOT~<*|#aGfor^5_B4n=>umC|N^~|4V>* zEBbc~E$gd9B9Y6uDzVm4hR0W<q88Wb}rvfbl zrZ71hAad^p8PRXrOf|~c{PlsC$&G(YAaV&d#GVto_!~h_?oB1RJmAT_jq{cTR2Q9< z$Es~hUFBdfvHaXyjwNf8yODwILB$CB;zMWQQ61;3^SCYeHfao>^;oF)l7X?9hA)&YYVtfw| z327vpcn5!XdDr$=QnZQ-di zD$|!U%ar7BXUd4af+(}%8dsw3Mzh;fua8exQnO0m9i0EX58vO6YcX%AtfWPKLG zz<>C35_*bO_;WX>{=)c@%3ktIXoOJ;^???o*ZQDq{yj<>E}oGSA(LnBBl5lDKPOwS z%$NU5)mO#&Dk5gY+IghZokM#4EohrYpx^sYS;~lzOK5~Y@?<&7q9esWEb86{iIr`g z(A4^o(Rc^i=27TAx<>>Pic%(Ka-DSh)_+GAsCG6B51ZD#W-kuL9kwF1=0|AiZzIDt zLI&{MNF(ZDKmjHoBH-=VoEoR_CgwUZmCi1Wm&oq&JNs7E%%{o zeE|Kwi6|)(%X~74jE~10R2Q9?{Z*=zYPUJH=B7Ed_Lezy&#z`OL>M(|ngb@Sa3!Dc mAPP+wDRluA)%_RYxPJh8W5%fp)t31H0000sm85 zLs8%DJ%;yw^X7d6{C5d<$@o#++tE+}5C!0%DYy|{zuXKbV}GRQMZ2V?VFy5rwmAR< zXM>BmoBYMM03cwFwy_oB;5Jto9e{yCo{&NU!3}^Ef-3+Cgv7exX|;&g_M0`WBu};q z`L+F3?f|8SGM*lv(bKx7{J3*0YImOb6G#&hX1vo79Au@r>;Me}w8lSrf#RXR>N{j(7sHPln6s9vbU%Io*(b@<3jY9BsS0RR6%) zeG%7n6##I&a%hTmMf@_-Zt`hS0i=}_pbU>k5#0pt&QWXC`pu28u7E3m#KIDBP3|uE zN>mfD-i+p^OaS(-vZf_8#=0VY`C~VD03_y@p%^S?jZOjBT9XNY+E71@b+ulaP#aD5 z0LGtN2bZe?^JG%heMF*7s+TY3Nh1sh33 zK~z{r?UZXwTV)u>>!!vmnk;eLuPO!cmgzExtI!*?3=jkXxqpp9p}=$tXfy6zxda&M z2Qw2V0aj=W)o#fQOUyDqpt3o(18qUZqAnXaHW;vBxH*xY$M>}FfkSCK=Jv(JfAULn znm+IE|Gquvu>LavfX851`Q)ZRzr|yZE`0rm<+q6U7RjGeFv*MLP!i@{Od=v-ab+mI z9FIMkM4VN627h<%UpRO&3t%V_lK?RI4ug0^9O7*tP4V;ONU;YVfB5YDx=0<^SqN-Z zK3w`S$%))r08EIXq;@?1a1orY2np;eFtfV~AW)MJaaswO*U6a$kQf&rO|dI*C2tpA z=HErBRBn0os{=m;bmjw9fQ>!_+GU_SlY+&xoKb+tbT1%F9bx*hr2?pF&KW9GD4~VCn$!W;J)HFP{K3u)>X>@aqd#iJRf?32C@78Q79$U>y zUu5q6wfFWXqG=HUrvivl;y>9JUKowwOTjs^)a&6Ypi4a-y z;%?b-2Gq3;7r(32Ts>E~Y@xRRBeNlIdH9E-?^*7x8BJ6H8rvci-Yo^dlM^7WpN6QW zdBlQK#77oDbYu}kjf)V8iJ)k(BE)d3W|~-jD#>=&uW_CC^ zS-%zAt#LoLFWN&d+D<8KOP)wi#w?SAl4WfoQhf=1PDHqhuEq`-L#$Luc z`+2rCt=avHLf{;@2x0PHfQxp|t3UEhbT z#qt}TwM@ZMwKTH5x2p+)aG77N$K6Y1{o}V!`Eu^gYp-i#ti;Y9y%2;P01%mo=aj5g zd0X*(Kc=7o;gmluDMq_Xw^o3sG{xSCzS zwV4G=%*+%~aR16Zt~%U;#pwce+Y|thO`f*J%#2IUopS@XU?H7%74wU)E#CV19YC*h zg>>E$Gc$SYB$jhaTy-KHGj8DSpi@V9yVMVELA9<~UXn~QV~Lp=mk9Us$t}3Lp&pKu zERacNtbZ{RQ$z`0a5=XGz1usXh?KC!EJYXt+zp;r(tU~sZCYdP7HGT0T%&h&G-wla z-@g5U3#xU^cAgHB|9;F21^vSQcT4CrS-GGQV%TKmn1UycPP_x~Y7jzy@pA6V`S+}3 z-!FpMhqFf}q~nV)gc$)C3DU{{IuMM{Kib>X+8yoM?;!6z#LOQnwZqk@3w$R40000< KMNUMnLSTa0`8_oN delta 1796 zcmV+f2mAQE1(XhuB!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm000tn z000tn0p4aGcmMzZ8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*7s+TY3Nh282mO zK~z{r?bmrw6IUF-@$~SYs%@u50RgpKPSx6aFjXq#;zmTNR)2*6iG)i<5dlTS2#R=g zTBmke4uNpW(NU+PZT+KK?V*Kmig?x{2vkH-NEMarukR%s3jqY$y%U+*VzM;yoGzuI`BEhjR0{n|YL1pyxv_OO- zKsB-<;CBFcg={-|v+zig$e|RSoVMZ!u+I;Nij&I`fPaqD0%@l=2wbF&Y6=S^+m6mG zKDKxKjKga>>`M@emP27UIOR$}S)-3*-d-JG!_u43E_;J)|NJwrGH$0W0@>*S?bW9! zh4iB<5yS|N0rW-r09;tLgKYnNp(aY-Oc963RZY+P1KE;F*>9!Xw^0MvWMQotDks()7~ zg(-)^;LIufX=aDhoTASF)hwM)F86GMA1l+#)+(kF+^`j)hiFg`QN)|OirN&o}t8G_+#3M7 z86Hh7>15S%H zg|*5JHu*k%dfsCvTO8E7qY&~!U@%#(8T@SpJ#ag`p&E+9# zoPLI|7Wp9qGqaMoF-6^PYNA+m{b#A*VU5qzN4371@V^K1*2_zsvhK4oj(JW{0S3}D zgvDXtw9iw)Gv!LZZ$?-DqJ6#net-M>)GZ=E=`Gqfl<$7!BcJ`fDx5bhuYJ0CYTMhZ za!c)!b1V)8OqrM`dT1^W)fb^KynP~@s7h41T#4F&DcE=zHM@NkW=eAo3+<%nYLyxct zUoRgwF;~9mVAhoA`)~0&hZ#U+d zA_)fSrvZv!c2Y5LH`W0!@frw{T0xlH4(@5)CQ)o)bes_7y16Zcu!d%CR>YVIhk_a= z7@_#q*x84H&wssJtbb@C_pJ^11piJTTJMLA>WWapY;W7Va zr2qrzp$`yz9Crd-W9xyNcpcr|RuG`m6Kv}M;r4&PEw!78p+`W&!L&Hxu3n0}xs69S zhG#-{;P+1;J*98uzx6%FcN=z_c6jS zJ{CDE-Z#q)dV+xyvqNFQx?jL$VdXF>;!%a26lm9 zM>h!4?x7RtLARQT(PjvxjhA}Ad;Z3zcgW@0jZ@qS#|6<<&gx<=kS;m9;NrAh)0C*K mWkk6qv^kK=zMN4W4(C6sz$OpA-Bob_00009#{q=(QrO=jC&>Av%1XWq_C zLLd-O5lI34r~dZzYkmU^fPGtu2c4tWv(D+E_MC>eWyc2+G z5?%_x@)BF?C}t7oj~C?ATng(-0nR!Qr!h%0QJ>>v7KV7W1@zbTYQbC$?kBL&oL1L;J`{{GLy(KZXU)vS8K`_*j&B=F+601-Ft%M P00000NkvXXu0mjf&2_{) delta 2326 zcmV+x3F-E~1ECU-B!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm000tn z000tn0p4aGcmMzZ8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*7s+TY3Nh2$4xd zK~!i%?U;K|ROcCoNz!VYq-|mmBuGH;lGo%D4>7ez+PB6n~r@tNyy9l z@mm$^>ZX>ifT_jHVe;X4(4Xdaz_tRQC%dqu3ILv?NY}7Zu=kA*dQa{XD(= z_Yd>;^k}!@IG9utCoL`n-{)Qs3x4_GcxR!~Ch`Pn`N0zB<09zgzhCB^&VKccvcw)D zZxy5f*@eR~Pk4A9R6;3fPA5-Be?AS#UDf!>uH?pO?29EZ|xg1Md5ymiK?9T4|KEZsmsx z1mbhjy#Kv3}oxHF>00*mISOXCJ0v$DSW*yrQ=Yk?U;sTbK zcqFQCMzQJR09&K-Fw_L}+bTauV`LN^WjX$*)Z_5c;KszF=)nSFq{Im>#nIrZiGSWf zN15bNwQBdML%nldx5)pAXdd zciJisI)B6uA5WI$&{5K1wCc%t3w-2^!US;%F+zFrv+IGV5IV|i2G00}4Hq9toK{=bn*+7)zG$dnF0y*1fq&I2jOYkH0^|}fh_``lT$pR;qzV5AkryIWgol@(P!tZH zJIZ<&=Crje`eRSa;yrg;7Vqt|%$~lMMIu7~+|?bouk9frmZNouPlt#R5KZ_C{`;QB z6L!v;u-`ap!VQi`jJsfMnMt3df;_1j{M460m3<2WKfDKlxqZ@nXn%@IL_p3E{z9^c z@Drfiap$3T+Bd)Aw$kxn*db#99@8Q`TmgKKXUYkXC)a@@y#)pC8pX`W@`$Cy;?~Oh z?(PF7tLp$KPcm-*VrNH9*uCK;%$J##vlEy#UxPfg34GEof&ca%NnswF#R*hgC{M%* zitV?0XC+@5NA-0iJk>%b?i859|9pv=4ms+@b0qC&*d z)`|<|3ErF9H&X4PIDQoNl7lAvDqlj#V1dFgcvJl;c(1L&)jyA`|1BssUBv?J;JdBI zYH5NcaZB<(TYFol2RDwP+QZ{;AS7gbL6}Jao{9SsLAI(2ynmC=fjq4Rd@?SB0#{$L zxg8W)9eClplCnG|(gZ3jB`%g%?z+dy(yuM0x)Qi&L1*UDp4n zul@d@DfWj$zcUsHfvG#P!HcZ^`bO|hyMzU*@ptE#uV+&&>+@S*37ohQpRG5$rmMSNrdqbe zDR)(f=SrQ{b9J3|&YC9eoaAP$SIR}LEcFu7s+Fa+X_<{zv`ofTEwhQxp=B~VwDPP@ w4lHCAxtJ}cFos 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 diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png b/PQGoogleSpreadsheet/PQGoogleSpreadsheet80.png index 9427d2a65515991b49b966a96fcf3fd8dfab2c7c..fcaa1c0eeac8948e8deaf33be357dc2adf388404 100644 GIT binary patch delta 860 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 Date: Fri, 5 Jun 2020 18:31:49 +0100 Subject: [PATCH 5/6] Added way to deal with 429 errors When refreshing Power BI launches all queries in parallel what sometimes causes HTTP Error 429 (Too Many Requests) To solve this problem when loading, the code is sleeping/looping until it manages to get the file. # Conflicts: # PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq --- PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq | 33 +++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq index e33799f..0ab6591 100644 --- a/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq +++ b/PQGoogleSpreadsheet/PQGoogleSpreadsheet.pq @@ -24,10 +24,35 @@ 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 = [ @@ -114,8 +139,8 @@ Logout = (token) => logout_uri & token; // Data Source UI publishing description PQGoogleSpreadsheet.UI = [ Beta = true, - Category = "Other", ButtonText = { "Google Spreadsheet Connector", "Google Spreadsheet Connector Help" }, + Category = "File", SourceImage = PQGoogleSpreadsheet.Icons, SourceTypeImage = PQGoogleSpreadsheet.Icons ]; From e33c78486c0c7a1df216d3ef9579b6126215f7ff Mon Sep 17 00:00:00 2001 From: Pedro Morais Date: Fri, 12 Nov 2021 09:39:38 +0000 Subject: [PATCH 6/6] Removed dead links to http://www.skolenipowerbi.cz --- README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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. - -