From dc805d0c1e758c47b8f476210fcc877f67839c56 Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:02:56 +0000 Subject: [PATCH 1/7] Update README.md Added new parameter for Git folder --- accelerators/CICD/Branch-out-to-new-workspace/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/README.md b/accelerators/CICD/Branch-out-to-new-workspace/README.md index f3d44ae..552b4e7 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/README.md +++ b/accelerators/CICD/Branch-out-to-new-workspace/README.md @@ -167,8 +167,9 @@ a. Source workspace: Name of the dev workspace

f. Swap connections in pipelines: Specify connections to be replaced using format (from 1 ,to1),(from2,to2),...(fromN,toN) using either connection ID or name.

g. Enter Developer Email: Add the email address of the developer to be granted admin role on the new workspace

h. Enter Capacity ID: Enter the capacity ID of the new workspace if different from the default GUID. -

i. Enter the branch name: Enter the source branch name which the new branch will be created from. -

j. Click Run and monitor the release pipeline in Azure Devops and also the progress of the post activity notebook in the Fabric monitoring hub. +

i. Enter the source branch name: Enter the source branch name which the new branch will be created from. +

j. Enter the Git folder where Fabric content is stored. Leave as / if content is stored in root. +

k. Click Run and monitor the release pipeline in Azure Devops and also the progress of the post activity notebook in the Fabric monitoring hub.

 

 

 

12. To debug and monitor the running YAML pipeline click on the “BranchOut” job From feb32877913dd49e13f23aa0bb5dd439a5c8962b Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:03:56 +0000 Subject: [PATCH 2/7] Added git folder parameter --- accelerators/media/pipeline_params.png | Bin 140101 -> 128830 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/accelerators/media/pipeline_params.png b/accelerators/media/pipeline_params.png index 1d52d4130c2ef4a06c94a2d2af54e7879e8f6c7e..c111b87cee74c2f371fb94f32010b201abc511e7 100644 GIT binary patch literal 128830 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 o*=w%(9=i#u6ldgpmbFOm0GdE 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

MXRjNf;ZAu;30J7|)Fe_Ou8S-@&5^VwQ0#7E(DQ z%1g23AKnxh@KIT5q|k(r^6F2-7KnK_AP9fr6J{Qr?z<*ZM($LH1a)jsy(X11GGYDP z`+`!&{g*!($QhmM}5Gn%E=0CSeY>kFkS1v|^{ zgziI?@uHK4gqJm}bV|DnGBu0svk#gW@<>rWgHG7wxCrLON7{Q8(S6!^K5c$;KBR}0 zEnxegvxgoN3+3_3*!Im*KlHd#rvG>eW2t&UT#HA>z~q|C8r|>O-jWQzxcdwCx`}4E zZBF%$^$8c3OAk^Rm?vLn*`!X+E4|x(mEsYfcyguBYodc<+4eU^(qoAKD7Lt?-%eh5 zqro~9SHSI7B!ZWIp_p>=h(=m7oczPaKDxG0`{PAg{bSDr+61F3n}@`w^ex!5lgpBF zrrY|`YgeV09g$mu16|6qg;g7e3MS7&0*VKJt*>MYE7zgz7Tzt|I97O?8claJ@7bF0 zRM)ITQCZ(7*I>ApGU{WSis(Gmkk}Kp9oz3PsJ>boa5cBkzeX`=(qf3#z=^`zG2qq= zQXYJ?t9TNLnh1L>w#;xN_gA3I1;Ssm0tW9aL6`mbspLnl}km)gy zw~yLbH)wbOwF977=m5oHW#<;LNiCHT zBU5`0&#*(u~wil3kxq1zevhHhyqPPw3MedT-{P{u=mav?Y1T zoSBe@+Z&&7KJlyZ+Y*CYuV{13pE90JSD7;Y?JE5*&(;3(80w#&DBkb8g>iYN2oQt+ zG4^BH+GM_~&Z1z}50KnJ$X)6+_lDW?408eT;D1jCU;H)<4UuVrdZ(ebbirmO_8t8u zQ1G2wLIi!m3v~K;38cF}{$~IQjTG~|6EZ$r1P98u!+$zfh`V&IfYYfNXy`ZtDLIb? z&*so_=bYHHR=`B}ZJsbr)}^zEwGBG9ngjV?XwrDe6-lRd77|CUI5GUoYl=Gt4Z3f= z(<%NnkF*0~gIoWu2HS^di~q*INK)N80t|*1u$k)Mh8=82&MxMIe*qgHP})c>2X(&$?|_38Ehxz90e}kM+fY~3 zIv7^1LbKdCgL~Ux*y{<^7T015i#!3tpw56en_~XQcY&hv3LsbGDe5;z(FpYlm;i@C zBXl%x0$hxZ>7J7X5Va9*!Aic>tB=5ia35zu@J3kP*>%6Z1x>lDuA0{Z(8C$BMnWB{ zV}@X~YBA{sCNA!0VRfO)k!RlBc0Y&{`KY;d1hslh0a&9E8mVKqh$4pBU79Ky!En+z z#N2;x6jtVRy5lxKIu;7_m6hbyIBQ6-3rE3e-unTIUX7y#u(QqpAn2s}mL~KXCz+p7 zIZS0xpHCwY$vR@)Lh{de3-ER;JxiSX|B=jmuE<)XiV@txSc%@*%?hE$NTdCB;PP#r zpf_}PpZKaU1vbH z`>b~RHsm>g+DV`$qff#t?|Q(wQ*g2y;IWpRW2DBGJ%MF*9YnTQ;9&B(?>Y_zzf1ig ze}Ni@n`+}PsI+0dm6*oK7Y3;lJfTwP29ReJESrHQ`0du<2?Sle29RL(fQEbx3|cbT zw{fdgjlk_%1m>ta4Iuqo*u33)3vw9v?kF|Os_VNa>ghj6d&HuXy=TDjl*yk_4~4w~ zZI9clwg08vhm)Wpz!%UAk1}f z5p~6bcCk&D{d)Q$=;ilqX4>)rbE=!-MZDAi2;z@z0f|`ns?*uMJAd|;5#G91)X>Rf z;traK1=lXmZd(kJ1YKAJYp&lwb)%NNZ zSSu4rPP`0XD2x~jji}i^?^iS&)Ix$(o}}j? z(8cL3YH|mo-AK)QhxIpP?3^;*1S8<)+n^HKc!pf>&X`mfI9vIBDZutQ%EYDF8Bz@> zVB2Wm;Z?_gz5%Y<>i!}{SDxEA=x7}W8lpx59OO(Gw+Hp1-<9bMY;uZ1xw)`!Iut?o zBx0@qL?n@THcPGID4Mtfp*4Q)^3po zMmm|V)Rorc_l%2ySfU;#W$0K^bMLJ|AlDeNF&CmRys0%{!@K+TO<`zcO3GxkJ?krLinZ*~u&x`$loVDxVndTCFzzC}6 zLiqNJ~d{6+n#3#(P~e?P}JoMA)DYKoJ;4lr@7-knKDbX{l9z9`TD z32tCy(fK^cuFS6^ycO5(t4}_3)^7C*9iZnzuqjVtTu*x0NA$W09=tJJOo*I_u|P(Q zLSi77@V>v%=!rBDEyBRm?XINU8~}vs)Xd+XExMhW|2&|j&Yq9QivRm~&;KV{cHIibPCmR zfpNNK9rUErtSd}ofP5wUFWt7mlHzgZsRpgp?$DqQy5Igy2))1Dtt$r^rq@5mYhlhz z9bjED;-O`}8Fk&9e6+W6zq61bP8O5D9&tJL*(4G1F)?sxr9i+n&Du()d0 zDWn5ZXN>|ic}Yy`U))f+Ao!9z;1?TD5C$gaSA9#Oog$4D&(`%jh8LNPnnVg{oFKV7 zZ|I@g{~Zkaf7rnMR~e)KBfiy&+;7mT=y(7v?O>Wc9^92m{sTgXr$E47`s3P)=C##L zKsAZ(c^W?ci#q`$bn#>F?4ch=nU2&`)6Gy*;yM65otFA zDUQO)w7HMkey-L^S`{#qksu*k%?zMA*6M&{z5(ztwYU2G=D>_=4e~~NLI!KEQ~Uvq zA{PZaP!6~Jg%0ezn9aTCHW%K6l^k}TLB{Swchx=A8|2?cF*dyBIFnWTe^zdE5%h|+@d!ifZ3~uH{2caVnRHbD7G1w=GMh}=9gKAgbh*2 zqy>ja@a&pctLzh*f%+V+SK4__jsfPf3>o!}o^Mcy)Mep*cp--I0QxY+%*V;R1fn2! z$UM?$ak8BZE2elEzO@5Zz?F5d&9qzj$r1~Jp4m6T++O0=YBwPC(uh+O$z9>DpkW7? zr=vj?8V?A$YdCWmLCZh%j$Xq;)*Vv%FvIDXt1>S?LtCtsz44KPG zV0X6;wwji;z;9l=(7}D;S_`>FdcVUZwa-6t8+oKw({H1AyDFdT7;3DXoUIqDf^qtS z8)6wM?Sp{oJ@5{^r!)b&M9=*1!nu_yXsrV6-g2BYR;I0te2Rx76R?!@T*7A;7t|;yGgC zmFzAr>nfy})KuE4ebZL=&H3n@75wv~y~|z8R14qTkHKbT9ekAGuGxY~&p*k8$k*pl z&=h9oA+W)hfTldnr-Pi8r~LDhqbv}|dXU51oR0%PSZZOTKNPfVer2J0Cd%M z&-@6utt{2LUiThv+9)8oGEWVXf&3UXZ$&i1UchN-r6-@|IqxP%xR;Rjdw9NpcdiK9_`<=*f%RNa z9dL&t@`3dO#+Nc*%8WJ85E( z86z_qURvH;2d(Z3N`}clc-WZj7N3E0tY~F@TRt`>Pg|P@)&Lr6>&?QKdNmi3qyEZf z?bzN~jjycc@wFzXv8b{WB*H*w`+nzN89UM3P*L7z-@=B{1SoWu>Pb6ZXyhCLyx77E zd@`(Flt!&vsBF4X1wT2omuTbw6Q#jk8%rI2s^W}JnJtGF>h~_(#evBxJ)b{mHe}#~ zD>d+6BIwJKN^HzMEcc%%NOK9?H&roPW6;w=d7u@N+SZ;*5-tyL)$mR^NKUk}l_!jD z!$!?+ku06O)<-jXV>nPB$tQO~g25TK+;|)-I!a(VpNOt2b1bqroD{^!p8-P<@l*$~z_JcS-y0=BI)shl;D9WAzGH5IlZi!12i4SzMG>G-w7 z*W@|T9pIJ9_(QRpMQKS)oR?+f*}T=op4X48YGjOrv{f||w8VO7HA;?-fc$Ap>(XJI zgdrT2d0c8yMqdUs_fB!O73MZjRWY2nSY9)Uxnt7fd0X3P6jGnL>{Wc}=2At7&CEW6 zzZ7Y_c3W|UqA?p?+59qWaqKi9MR+7G^V!R_Y>rXpar4ZVRN~ojFL!UT7F%Ur?}{{7 z96Dd4T*Jz}LGuHg?JBw{F6ynzqL-4|aTVTm7r}WZ7Rg=oBcO0e&?@VPKl;VY_Tru6 zp=9I|BBn498{5FxQTdj}N#WwM-?;q`H|IdPkJPZ-@8bp&6B@T6Ur*p=^Tf=+M(xoG z>-3dpH{p^)l`$$7?q(>#)JXJv$o}9P1_FZ9-3O}5Pd0YcrP-N>!$-UhtKoKib6$H7 zS`3(B{9ltx649qH$yX~T=t+HhP?=n(kAAfEg!?xRycn=3-L88mn-Ih@Bg5_l+c0)p zL%{O*5ZHgc4@zA89i7mxecn=ip+c^n)V+N)@3E{h>JsjjfZpO=6y?Umy<9(q7$;cD z?%^gbzP*X5j4kq|B1ntCeC58XbPp2R+!?h6n5qls(|Ap3qwzWeQX1>z%LU|%4 z3Kqax=l_%I0cXFZlB!ePTo^^~-*WpN`%!Wa9oubOKso&g8FAkwDFO$YHO#O5fHU=4 zPfvR?b`VZ9dJ}x)8FR*b*2EGJO6rYu?Lie2Oe|(-uXFzUrQZM-)bcbO`k3?m1Q|UL zk-E)&Ku-ppXXtyatqUlYPfv5-KZk%oyonj}?{R*=FVBR73R|X4{E4QEkM02^zbKJg ze;%nhMQ%t`TUo9)(a9y8LbN{)1cQ@FY_>w?VHzVODNQG0f6_M&bZ7sq3CwbWzBLqv90F$Q#k^7RYoj%;0===z#pYTYjxV zS8`&qMJZTpOSs4UJIN8U2B+IEL9M>8`K96 zuEhrpbc7$$gk+7SDKU9|zxltIk-vNA9L{d!PG=UUDL>XZb+wVASjIG7i@wU0B(W(Xn)- z)B0H4n1ElF1eT*G zPkE6|uXTQ!&zvfPl%vh!{MgWSJO%Q{=sIbPaR3iLiTXYhtvlJ$1^%SxlmxQxccxXs zV$0`mOL?;hlGEN%&w6riPr9cetgQ%cfc@r4IuVdH7`9%(L8>04Z&v+fwy;<=WB-v$ zz}!hC0a1S2%t7L?Wp3J}kEvLplBiC3p69MvdlcHhamvJuq2Cx;${tymH>oL?|odOdo%OVIRt~Sn2$<7*%GSM%R-jL*)P^E<)TuL(yy& zqPRR&$>C2qtFJNg^kd3(O3&W1@8h4S%#vqTU+r0j8-BZ&JBR@Bbe6hNqkH26^knm) z0X+uDbJ|!2RT7HR12eaJuCZbGnX|e+w5BdczSr~k@z^(>ALdSSeoU=Ag0WjMC!fCp zamqF7EnBz5))8)`nlA~yaXw*OXTGMiQ;oBW*>h+KD}O2#WUIb6xC9++!F}}nZZJKd zT~6)IjoIS^4~hsvk5b2~bevFLK$itJ=tVT`%5q8sn)w$Pupb1XbQ%>vUc@HVGA@Tl3W~_ zt!ui1=o`EE7iooTnax668PSLDuZ-sZj&(fJxJ}zQFO}UppwWZP#BEZD8&5w9T(enq zb3vdtC{#c{Jx->XS490?KJ#sssG9el3q^Ru!=`JwzHAJ=;eK5=!4A5tcEHhzULE_+ zyEVfvze`sbFr}G3E;;V#(*ti>jK9e4sTm?DJ#E zcXuX!L!j>|ZAhlNs=CX<;t|khoMIZ1#q5-rXf_c+1v@WZzH6V+`%H{M75k-uK9h(e z(_Mav#oKWrHik@`&|FMNIVX)&4nD$q@0<k3Ja_Q&xkad z6V&|4$@V2(!8XKmB$b>)HREJB@L^IjkgZ_-mM|K7joVGrV=Ro+ID{5U#?3cQM$|TQIQi>TSb!k{oMz)B zF83;#E~7}_elpx7zw~}QeugXj`qvZZ_9rxb&(-l^E z678N_Nc@AZFB68f13IgqE_GOE8rqB9jYRo~1S$Rv#2CP4Katv2bY01wPdSQ+BQa>6 zYct#}wwEvJG!h>NF0Yl8*3GTS24(kG>cJ;VxXDiz5pBI+wCUe~-<|#Pw2R>Uo|O9V z@qI-dQnW|RTN+HOVxpOeEb5kD;c9LgNrb1k6|23=5}g2)TwxEXX_0vN3smoZ*u1)B zybyodSnJ7X@-W&_s3MjMV1#pVy^DUC^>E)vH$)i>*knxPyZ2xoe*$}rpT5T(Hjk2^ zyhH;u4fhP>qXUoMJo7Bw*jCw6O4V!PWXwx^loDaf@T8*eG<}11cvlKPj8A-!ZmMf) zsj_hF>Zro)|Eq3+=V51@EcW?f%$lzCqlt{b@(Q=Yt`&s02bXXTI?@eUSh;l@_KjNV zM@3A-Znv017OXRrtz%UCl#csZT!HH0hhDG+l>HnOCH9e=&-{Bo$xgue8{^CF66&O) zlFHP^l3G48-POHUTEu3OXj zFyc3-RZ$Xj7gsNb6sic`u6RBuS%*VxI}D#7cute&Xz!1-^geUF;y2NA3sYA0*Ii-~ zKao$ZBKUEuvDVUtR+1;$C;tIc4Tb@YzU7OgeiOJ} zGHNT|kUXC)c?D~og}{#Uq=)vl36^3k3gvf0zn%2hjeHQAsz*?6)Cm)_0FZ`xLc}PE z-Z9tG7yqV5@2gv%5C2DudJomX;a^=+0fgQq6W?#KJHTy!2?;c61o`It-Rot6iLW<( z_ui*`y2c>$u!$+L0F@cZfxl-ejNZI`qETI+{2&;Cic>^_lJs|-^8tZ+;ivePQ!j>h z^C>b*d-n6jiYK%*q2iD7gvUb&{GbLN(oSCMufCO6Tdd;&gXll=@QtDE-Ov2#{t?=E zlvhL&n~BXkGxWGMy{vPMrU{EEGBf+x^)`VY)ZF8R_Ojy!X<~$4&U^+MvC|fT!*;nm z$9%8|M0rRCzr=`VLGiT7S<>{5jDZjxKTuKd_ES-S+MDU(7T z&hwj8z&fv@i=5e#foC z7o?YoyEJ(GWNO2ty_sq>I$zm1QkqL)Q3yt@FDKER$`O_=mzB=(OA4Vz39r!#W@^ph z@CNV6d@9c&o!9X0WZSAtR!?0yq&kpbo#rV>wUt&z`Fr>T_u`9&S>(u!gGP9)|J*bs z;zg3YB34IJQ@3f`(+!fGOCM~;KCq}RC{t3aYpiW+wJej|tNQ+?Z3G`d=kY1hdoh0f zx$Cz4o#Ld?qmT%=hj5NbQa$FeZxk6p{2MDBA(B@)2Aj;wR5wUB5lWVluwD_or>-ch zFr6xY{j#JEYlZ?5C0$`L#xm=Yd1TWgT=8`GE2QIE$?wdQk~E;oAw|AnEDM(1>O~a= z1ULw^2f?rmQ)E{boIv}K%}9~HWESUPk|%4;^p9>TIHEg~vKZ5fP#Llm)43V&C?yw| zp7a<`)Cme3KYkTmQP&04I&UcwuX0L7UQ1I;sQA*qR~K~m)?VbJjo{x0{P9k_Oe$z; zR|a<#Y!P|b{*c`f9bLP2Ar&^`qm9mL)Hhy`%#fDyUPjNyHPB@y?NT+Yo>Q<*RGl*P z+1OAIhZi~_0(5*uidx)P%m z^Qj6--YHJBRL2Fq9Rt{OP|@m*3FsY1TMy=S@mJi9{u{-1y54fFh@W@<3+3m}cVo0C z4{b$#jfw*Igz>*V8(Y+V7;BOCjaXykHVYAL2-|wj67}}e;8CnN(i^FzCym%>P#7Kv z+aFBsJjUpDncAWT!sGG0PMxtwuVI9G+-qt|l~NYm7gSCROnP>M=9WNX+tavFwszea zj$;!IJ#9FOUy6Dq_t)QR@I>n6D1!stdo2q)QdW9NY?q&1SiHP?=z(^gjPRrteW+CR zM!Zk;$wXJ@N4^vQ?=k-R5WM)OAQ;AL`Ow1v?Fia~xe(o+&ucQx(*(YjVdhmz5A#h>^JxO1Q z3R`ux-(RRC3U5>M$4PdzJH_g}|MDm~;EOy0sG4|26A4yCm1|KnS9EiID_C7jV zLAswK(OXI|hM|nqcyUZ&PlwaDa3O!yH1jT9S*0#XEbe1}^v>;&ECq)gIXSiTU0uW@ z>(Ofw(eDt7p@HS`xTJT3fynb7qT zC3+nvsRQ_lx?~&C=gX39S`dzBGM=I znM^D1&D6M*T!>~ULyA`9mwj9{L6}o4l%r0P_%{lU&j7E%J(I(nydk+R*$RL!Z}0E2NQ z&xD;g?Y5sK#XPH7CxYK)UXXjfjNdU}i7nLFB2#7@eriVqANEvrN3PHdn%|Py`u*JT zTxM(mvS{{N9T|zr`@wD{?RRd)Cq<7bo06Q}tLe*vogz^umi_v6b3})_*#NNoQcS9S zAnm%FoP__i3J0eHLBf4f2+`r3kliY7QZK?*o*NuDaLZ4IBMtVLpLlrYgz8u(^O9<$ z4->35kP?Sv`|Pb1r>#H8Kj?Ji&A%|R4x4>Kot`{fA!q7h?p;`?&~mfxg3aFV6%AyO za>tTtVyi@&E4kQFYJWn%(?1laJy4wn1#~ALW(T>~m3-zght*ctOrVXkRwXQs8%50z zYmEW}r&?)4xTa`b)F^ zxqn%s=5&+r`ld^ODhGDjD)GVo3vUH>>Dv3ftI zD)h@%)CSGUaQDI9H~KPN*aCoMCp$oDsyBJO^P@P&F!p1`@JET!lR?P*?FZw=A}E2x z8Lg%2%@&AEDL^{TP4eGyLJHIx%GYN>elX{9b{v-sp|5g&9a7}$bA@CpTSm~-_eu9nSBCOQdVp(A8uLg+2$~!|P)=hLGd0K>F$ZtiDj)U7c&{&o zWk-F5R4XRS%=!d=wP-IYZ<2~dbE8mtH*h&@T#nj2pRuDxe@xm>udJk99%s=gw!X+O zyxJpO`PyH6K0}{ALXtsLT7Td!W7$#MN`06fA?$9Gc6UyMfi#0ClFlFA0?Vao5zrX- zGgKscd^{`4cW=DkF*qSxR7vQmrVgF2)ZA0%BB9APi7zL-*tlMTeb%=I6-pXbjR8rI z**|VZB-X&PxW?(9B30+7NFJsplrucF{T7ilSJYLVyrIVmuFrYYbuJS`dhS=`*tp0)`q@$o7n7#Kd2&ma+VdXWG48KK zTj(43z2t-sA81fsqpr^s|NizO`g-7y{yrg%K$Lal&23sgJ$?Upo@(o7X<&q*HW7kH zIw3F_y_b?`U*Wedl$t!vSz*aLWW-S!kcA5txoM-{-)bZy8GKhWIy?9M33(>hoda%Ipi@z$u~?&Xs?+~j24&0SvqoR%CVmHSAe z=R?d#OWM$>+nvkoS&>@AsZQ|(SIT=hFwfW?EneGknDCS-zhH>b?faW_!{)~7gSlu@ zo7nHq&x!iJvk~vV_kG`8j3(G^o&E;LIDJTTPI1Hv`RCGw#x#R>cCNVm1XObvjTohy zNZVBwSBD8pbt=1q;I9pIgt9*s@xzKWgyo<`0E6UmwYp7QqkM;8_=nSzoNDvNT)+Jb zbhpdLP={)s1QcBK0~nRwro z?Dt;fi<1G1g!h^B)QSE}3~-?ydX}b$>}{2tpb|>+Ba%QdaoR!Q`id{;iWr3DPyHO{ z2;ZG^OFg@zs^2&KyOoL+cPQXDjdTM~U}XjqY1%6meoiuUI;TGObC5+Rcb9h$sxZVy z6udoXs-s+Z4do-3_WO-sMfV&`{z_jaR`;=-^~PbXQvrM!g{_euVUG0m521N}8P2nN zjP^(Vrq8vG-XChIjvW6^Ep^p?z-orF?pSA4f{<&E`a5qgXz7CCv#IfqkM#rsdb*I! z-{{=>&1nN8ihOwDp3w)IYELg5Og?N*oR}3}kErsCYR26`$=EPP5rCV=?c1X=_4uKm zc9SU)biPQNJHcXCs0(LC9&nnPL^Jtf$%9n9!80LbgWi`br$DAa^cj0aX=n(-ZhGif z7ya7ur(WJ+MQ0*nH$5Yb*uxgwGPBe3rmHQZqSfI6odc1YTOHS^Pxt*7uWdb~#4IrS zj%p`q^-CZGg;Irt(DXZ|PeEYfc#@=Z&bPN9?yc24Tc*BNPD;!2%H&`@KO^Rm(0S^< zg~=waZ3c^(r)ez1OzWK@rF|RKjc!V}un7+$D}uEn$-6EDe2wWcV?MfIF=m&G5iRNs zM_z^Xw>aEJw;cBY=XH`dX%D2q5vUYe8UrNaQq;*Q!Zem7Loxa8{s!Z1O zmd$9gd}W(Ta);6dKZMEX^hvK1rnQRiEIM)ISEhs}&z8=G8!BwpTlPL6DewtVYEJq_ zpw7c8_T9!-lH_Vg`3_0RvTQq$lmqj-_#-$Hs5Zy6TR9#cSA}FmZ!rFY{pu;ou$>hm%Tcbf2Bde#dN)pMQ2< zGd56uqF)Vr9!K^QZ_u;#9JhoW-_$%$su|DdadoS(^tgv$D7$E9ZDEDSxLM2tSa3<^)fd7`J(OGIsD%YS zyU02#(?>o+!)SJ^KlA=8&8amP1tW6 zSp0V6KLG`EJHkSm7%QdH7Pj9$POaKlJBy~bW%EXwIoC?dgIKIT^dyz`btjD46mtiX zapgQY9y+KAI(fwEcPSCCakOiO_(HSn_(QTQLA0Pq{7KWC{d%94Dnqt}bD%dUlOyY- z*fKnQa`S1#R`c{=Q{kh+-yv=B~#=L7mA) zSt*O0!vsf{(E@#H0e2%dwh|@N%A~Jy7&qSGJlajZaN_XUQ1I60lIki)nyCVsmZ8A$ z$c-qPz1N=tFsFM-1dGF^$klv-iMyx>MuZpR)92 zv7)!PJm8yVcg#6TNP=1TzgAtv$*L*dP@rvNlr)X|Gqi#6Uytv3%xg|TeHU5qx;3Xb z*g>%>cj1Ec!hVbl%FAX#Ce)EdEL{B~sp+04ciWx3(SqZ~mYdxKTK(4;8uiYHy}nB( z(Lk8U!r-nZ9C9$E{GCn8ax^C7T3%MJMkssG>=8bXiJTc-;>CTZ{6z+QtlmLq%NMPu zkdpzLf$bR%ZhWjWZ<$6EE$nWXv0U`~`lA=~YazH+qj>fG%fsw_NtVOdf$DRLH0n!hnU&rmR;#E%RPt%0-_4N}s z>f;6+`MG7YfQ=69b zR!Nam$tf~#i7OH>kg38#NiLn`1(loXvS1dADX_HnNRFv7nBKo&RQt#%1N0tR@{PQt z*nbme0!smn@Zeitmmr3l8vZgio&Q!@wJ1wdHW7cXZn9h2@8k2IV$UX`jBuFl?2IIi zHSTKrA6yf2x@g|eZyiY&rbwM&Db6cnAG9Evau?)))Au*ug`3MQU(umHc&EiiRD0e_ z?)Oc`{ZSL+-PRO;xBWrQBPHfLA1xrVvYmj{g7to%6IX8v&HQ&_F^Tbx^TgrfprBQ? zs6o-pHwMJ$KyvUOPW8XjrUt;KJs@jz3G7B$B!BxK7Q2x+K;kv}0z@+Wg=2*T2#4H3 zyLEGx4EDYRTE9R464yb|l!Y^+hVC%LzJgXq=|3gj{y-^qM%n8Hzkm53F_!C-ZXn(w zP3Q}B8N##?QK_$l%%LJaXTUw5HBwa&^A?u5GcigI~ZwGAM$A5kX+*fsT(Ky!znFR{!16lmFKg3j}uoQuIuV2~nJ0|M=zp(HH&`?BFx! z4m{pk2a$-skoL0W7E3r!IyqFFc+iN(mrelzV8i8DnH(s)@;^sH_{?1Zj0VLhR?djL zctAAn4$XmDI6X0C1LwCOw9W&w}Ceui~IVH*gfh9qsn(Fgz7JNuc=!U9n!=!-J|p_MwW zQ#M=&x;x0{it-1#!42sNn|NnP=;CYbXwn86bruM=9=wAzo0=Rhh{HcK0t#D^`++o( zZ6@S^^=fg45-qhrst0#01L2-E1t1Z1YSj8qj6Q7ZXksA1bCrpZ24JO288gsNUr*50 z`U|;-*Kz36fLh2!2PG~TvJfWVh(w|cQ43r|)s9th{lX^V+kn0uDh1&i1253kXiy8q zEx_Pk)Z{X42}m<|;C?;NOHV>ML$I|~1yCuUT|Y;~gMFl8Qnl(Az;Qg))U?Q%GmPrg~VPy>V}!_zGR zo$GXfc?kj`USLXO-vGTS{+9rZ!>zMApEIYNftk^y1r%C>8b%vI2N!diV-$hOD#XF) zKHPK$sf8IpB~4c5f}<9;3<5JKoD&l zf06CHs=dz2hQ5irOljP0ZIvE?GtQpLrFnR?-?)YWR zo+fxF8p%UIGZd01H`|DVw7OFkb3_Y)JE`c^UwfrqQtuleXBQe2%T4hTuUeu4x^AAm z^F_`?zq(Ch#3p>#a6LZsxe}FUv@*!RmBCmyhgA%LaqsBC5K&))32u2v<cuQ!$;lMa_$E{(=LH_1aCB9!e8ovavcU_+%2p0F zPwD?9UE(lMbn|>PeHn5AC~BM6iMg>#Q2wf>0SYF~0Jq(kM7|@?5)y6oYVFsoLo!xl zfEPRWjr+U|LRmm*Kw(|S%+)u{=HsOsdPcvT%U3J=;EKAAj_r`6l5bk;=1>G>~JZwWj4LC)J414z?!a|i>&IrzBCq4kptXU&u&O-+xgC-gLg@miSkz_ zj}IRUZ~(++jY5sn;|Z=_=D_Ypk~iO_NbQMXx0owMm>PX(iEz{iWEvYIL|4VtgbD(x z^+$=Ksbx`+7xGBR7+D-%9_G0nh)#S{vV-CJ=gk=($y^rdU~3Sm$1VH!>^USeAF*$c zO|5vm34$9ECjTrayvK_C=1pUsLA|ZLh)D^Z95_iyW)u(wpRGU>_Vb-9e&~NS>z;6f z2bbzU8H)B*$PN6js6@ZQf9Wrf&;w-++=v~Za~BF|+->E~28l~zNWBOYz{j^n&%Ru~ zjNRF-{smBN)X50`Y^kL50qA{g3$_Mi4Lw1?P5VEEMZAHyQ}MY8sYw0r59`46%K5OY zvkNS*o!}=;&G_wn|DSTH8bb?liV6uPQHihq-NbzS|5Gmee?;&D?8U(2;eW@To9=q! z$J03ZF?kU9g4ButW3QM$>D>ZR5)s!=<;&%Z7Ndgjd)xf%W zzg>O}@}cSn-?RfN&qALN&Wt|+3zRN|^?OV?RcY=l{l^bW^lqatACzA)8o-zz{Y>^0 zef3Sfff;1|&WebgqlDqh&}L-L;OiU9Ch)*dRk|FTsbwR3)--3f;bQ>#yr~tE$nX4c4fk58jI(V>i2M9x#IT@s! zeSi+q^K?h?bl870gc}@ogmAk;S7JO6Fn43wkohH zpe>UqBPqIGQ2d+|PI-Oxm&Q^iVC`o315Jvgu`8?qTWAXlJh z!JHZeNYb4D#G|L|OK~Q&hSu#yDE)<7a|xURiCMEXIU}umhajUf(G1d+>%V}Dwif>t zY$I0VDQ%4ofV6d)8vM6B({;6OeQ5-14f8a(_tqU3E7e90pbSjXxul`=3wQQF(#COL z9#jv1ihW2o2h!zLWIiLr7wHVKJqY!ruY_4ZUCfoea{LB!J zW|9&fPlaxoe=GD!Gp+m9De%w3DQ)L|LEEz`GkmH`l5iKVP+7lIUccWQ5Xzcu)&t?h zn!4OI9Gx1+knFcF$>x(zxmr-dTqt#gHqAe^?KHhiJ{=Mc=Rjc_ldpRILMy$by}E0R zXRpcgEAurglN}-u^L-Gh?U!@bd%VvCvAi?jHUF^=B|LDPLB3!EUEAOXx~nl6AoK#q zSAq0{+LVIoo#501xd{Q`mm*c>;Eu^|RTZptLRy5OiaZc4^1KXU~N z>UhGI8blPYaLg%7?~4z)#@H+3CT9A~_bd5hyW0ZbZ=SWp1s*Syn7aTa=O<0aBJ!;w z&h#CSrmVG{Pn-kM(YsYexDyc7JPYe46)dTjc52y5wvWDYTHyj0$RRbN37Fs=+h;BW z9aRcU?F`Y+oPB;wx(C8o59MM*@~?a{2T^HhH$h)dkhQLnr41|domu(RV@>8F^6uM} z8)QXjkWLfcQ5∾mWx519BQCeNRk;gvE`F`i9w4fv5OX+AFT|BUgOYui$F8Zv|eg zjH-MOf%XhSkVxGNB#EVz|4wqLp0$;-ks%qg^{vxnE=iEzpMU77&7^BY&bOvdtE(C> z%i-EesCk1Sf~5KUv`xgsE_lE?=fvpzRt10psvF8~I74>VMflQq&B!9SUPcU`*B5+v z%p>jg$EYQmDu*k_`N^QDTq9W08XH0QXzGwe=KX!_mN;t8UiDVna-*wuyDob%NdJI+ z5)1`NPM;nKF@#q_86@XgaOc9KuYq503hkV8@J@yL`P#Cahg9Si6TyKUaZa+1l-1z> z#od3$Q~k&P|9D15Hbuyu8Ce;TEt0*nM-F9VWMv!~g^0>ZHkIvg?0L-M5Gu!UNJfe5 zk&(mkdpx~g-}mqH-|zSPyNNNkj=uGgG5so^;_6i&>`UEyRK$ZjzWI#45PCKJkJjboT|sV`JdEp8 zM+rubwCKeae+~s~@lmHI)5{2uJV|jJAP!VYM{#dajK3 zkuUl>P7j=s=fzrv+o{Vv7)joH3W4u*|<=pL)A06m$uz z?HW3IG=rtnd;J-rv^Z@aXf9x6A^$Cqk^WW1z}*S@zor(9ake#Wt@I6~{L@}QW-zk~ z7DTy1ziK4oyaVfg&eF{i1Jd%2@i>L0#Hp&|4-ShHvxYT*kj_K7}3c7`_+hs-(<>@Js+*e#LQ4$d&=nC)T3>*fz8O?^;{%?L@KkATvq;Y&*B zf}6+zh@r53Jf3>@FKeM&)|Ki}Ib?loWkgk{XzJ~o*ge;qFfx!ueCiiioeeAGU~T`L!?#q8~({2CnNvJ#3yUw z-J>HXLGBV^NePy6yEBnTC6OLk8)pyG{HfF?6r-W8>l)$|_r`Z1Je`93{@N!0Ti3&* zyLnBjsas{0^C0dr{BDoXYuHsvA&q`W!d`FX`FJ_bDfNN4l;XvqV(tL37j5~K4*7%% z_8ZT=q{T0%___zC4Br0b~M{&CJK-zz;@_aUj6PP@u1u^{Y8RBH{gvsq> zSTt;sa~m9<8*v*wR1)E)Ej*%kE;71KX=japspwn7 zqS@q?-!)t63pWqAn%T~qc-cnjS0Mo-1V?`L5=;a%S46-&z%Xca;9d<>%`(+al}k9l z{)w7ZUh6c&ze%vNy&EoZwnaZgLFi-92xHxIYPwSXZIPeLl!q!*d=4;C*y5VR-ZNs} zVp_f7gYS=w@SS1w%8d;4U%sionZAb%`PCKnvP1**I+K1FgtW&jhQ$w5NaRC+TPFd?w$icTh-1e_^I_38LUJ zoMS*_Or_i6^tH8dZRq#OQ@1Qr6>=}=opSc@UhJ^$R&m3wIA+sO+&Q(hnr4mP-6>&2 zkltUn?l*2fQg!6LZ4lyh;1sL^i>$l^&dy*%H$NyQyAW93M04}f_MHHb`AGdXqHN!H zYDwV|m8b)3)627_9bs}rZ>8lOIa(3dPr;AZFPVO=_IGfMF=}d%nMfmFn9M|bSTX*6 z8Tfccr##tXCePBf9-9{}ad|GaCyjsCcgW_+(`(P4eA5q=CBGht^htfW&J0|IDfojxoy>|bN#B+VCd zcDz2211<$z&*=#_@Aw^8@yd_}pSp|#Wr5j92~peW**o;M$^dT9NJ@#raai8ph2 zZ%d=ur1o4er`)Q<_7E?=WBuG-xv=+2p&t@y!a^i6c@{_O6Z0*1EzBaya_wyEU(H3Akt+Qc;mC)VX>e?U6QO=cKXlVDzjykDE zA14R57u!#_c&+{54B?yfpew5)LAFY#tq9uc-e-zyv`+KBu>qwuuqImhsBp(C|6L55 z8lR@4xV2asw+6UUfp?yxqNlufl*g9#>_}VI>HBsesr}8XyB^UD|Ff5KhfXp#U;PQR zb})1#cV6hk{=1)jY6*zF5}IpSWOqX567*ZeYGXK*`X4C zKjGzGS8AoyD@z&&`b1;{rvtATr2>L(+AZ7Lh8a21lpFo#?}#bheO?AOe?8+EmuMdk z+bpZ1mRuo{Af9Nu4{O31;TbTbE|IHIo=pDx$D#6KQNwbALX8~btEl#p^eve|;li67 zokQ*&73u4fW3q?ACwtcxFeG+A7Db9@SPHXMi`TSl6 zA!+F^(xXoPm12Dc^{sy6WZP-fm8k2cpej!|47ygTQc^42ITT+R8AykO=YO}uKOo_o z!GXDGvVD~1ufy;|S}aHj*kt3l6@fr+wr&4m+hc`#s-WIXbIdooQKk9eNr63xoNh{*NMM z8w#$OfsTC1I$RL}hugf{K!p^5b&QM%2DKoG!|YEWj?4#Cda(_1StDQ4*C8toEbP}S zoo8I5B3Htkqr-DmZ+>w3uylzkjNm0%Il3Uy+wP+wl{FPo$gM-}p$3I87 z;GH!~Be8!*Y~OfZ8*b54q*FciW&eKsB1p%_Y5=p4Y(Pc7xPKG4`e$`>C|{;&))NmYbwEY&q!qGB95x8~ z{d^(+KT0Q%?NSbfxkw*}SQ;C~U?G7;>`p-LCWxfign9rQ3yBgBa~B|6(kG~Hz^Zs+*lvB>h zoD979CQwu4vm9D*F}(++e-v(17QaXDvBMw3pwP4UwMX3C?+QZaP9X6suX;c`xLtzhHNQZK05K!tXQ^Z%S-_HUNI;Q06y z)J``o9V7NOynVuKn?OlNqI9~{k304j52!WAhn%2d>Y_52SLhDmP2fonb_7+SDemjG zz87PTz)y?|FCg=T*FZ^pz+Bap)VB)hA07hd!Gpy-gR~U-4v;I>3MyZXw!ng9lSO*s zcX@aeverONk+Y1xh$XZ9BXj&eXcIUMYB$-ZOhmZZe!c_5emQC zxH0;8q<--&4Mh$VP!a|aQD-uZA3GUM0Z?ASwDZ<9xL8Y-)LO;5BhXpcZMrOW3~GOX z(wSdX&RPg4QK>xOb~^xxf9*iP2A`+`Cwi)tEl{9ov31LW*(g&C@SwMV{h<>aWgX5= zczoo+sZfN)`>=IS$LW!KELe~XGK)JlfBDOdTjAf)ne_@3ejJTymwambnT_5iUEQSv zb*qhA5}_ssz%tiqZ}oT<|KlJCJY=>^9Nd!s$`vbtHE)I-q|a@DGkI`!cpj)YQEk|l zlGK)SF}gVdru^+G?dk`gynwH|GvJ;3aaIk3evXYZ-W^!1>J1*^NnR&hjQrao=X*`{ z1k{|A+zC?Hcp`9Up__gJN$8GWL@=e6~^2CEcvW~O7B0~U` z%Z)LZt?=CI|3v*utI})4mz@j`)iRbLgGm4=ypPX@J5Zd1J}gWsX9^4Ox%C}+O0<2a z3sB`;e>%ZsYufo!5_srk_n74p>nV-gWI@7j2>(tPHK9=H8xQW>%@w4`gv=x$x6zh9 z4GijIK<(~p2S`b}zsKDL^#R?C*3W85ZJJ^(IWU#YhAh0ki`f zfa*nMVjXlbK4+#tVOVFSp|75mwc0~nG3Qvu{vAPmsPjSQwB2Up718_IUKp~T89A;T?r?Bfx?Tr$v)_CnFvN$o1Ze&mSSChJ zklUKPS`*JN^EyR>$it+I2y|I{EDe`DMZr{Y_3CWKqx4{4we#RDbF7BSA~1CPMHlZh zHR4xVHwPYefD#sgecNV_B>zBi$;^bh;DhP zySo9&KZbsvfZpmcJ1R!qdhXTZafgbkMjNI_=eb_lA8*B}en-|7BWH_WKIPTkBOzQ( zeK;5xAM}b+AQ{$pVNU^6Ky5w-fKsqG>L=eyEu<=0moei}x--XEAm~L!71i2vLG=2p z*xy_zPh~Gy?_)*^r1g{Q#>F|7F@bX}vmm{6--=DD{q#jkn+NYR1gh7`r%ZaHeGkIq z-ahX{CGBLnOa_0-RV~lQwgmpzI_QNBwHncU&NSCqY zY*-pSfbBqq*``&+{B4?v6QZ|==A727(Q{y;z$a$K2fmvvnB$6Zjdy*Q_C=Ob>{Q+t zk7~Wzv&?j&%X2lE#v%Y#z_q3-#e9SYZFeFepM#irn5m6meehQ_S zu9cGv^f6vl2{z(IE91kcbMcTQhGr+0HcH_3fuDEat9gO)hRija1S?f2gEUBjFaSc# zKgW;Ga~gZQCfJaDecYG6^Ji^sUZ~zX!OGiGMM>*KO?iCSW4+Xxh@>qID)Z6L4!^+czG`DUL5fxGYkH!0V$2sPu~~MQ!qYz46tRRj6*7v zoK)>Q3DIlU;&tJp^5yvzLOj3cmZV=C)e;I)+N!H)!>bl5cd(=vRos=f3seh#f0~L> z{nLi7F!-0)ddQg{tW0-{mF}+$%1UKvT)|}TQXK#xPW`JV_dsE+H63wO$FJ!~9bY(= znqQu{o20X!XmzEQ?H$*aiLG&^SVF_v2UPYO-0PlC_L`Nf@+N^komVIBhxy@*_Nqdv z*N&gf0Q}TzPU+)}htR>;zDr`rF&HX&sQ!J`}jx0ai-9gS^w{}Y!QNwuLQg_JQrPQM4%BTiX@g=SK2=hic%2sLRRV+}$F68+B^dG5e+rU7ZB0?$NovMzkX(aGJ@ zB$0?(m|bx}zOOv4q+__Y`^gRY_1LN{GbGReUiPpziC|t@LeQmPDj;spu_DN+YuAI7Egi1l#R<#1ott`-N!C286FS1)q*>4VMZ)Rg=-q_3WEGOK^uj4}P z_(&P#|GpQl9&SvBMbhXbjqguo*3^q^zs0tLfT}9$xn5m1kIiO6r9^XbbEV2r1f^ML z!G_m6W!&9=lXbB-`y}8iH~qeL-{%daQo6RX#u32 zg{7XiXXR0)QQ41XXdx|Cw{?;oqq-(}jw(;ugE(fEy*(mslELoInsnnZbD`!iYe~)Qg_w>*S4~(BJCEg7mmi1|wu365HUdhMO@%jh8T!sehi+yJ9i%Hh)=1+Ae}e&ai#rU2@10 zb_|tvtDRfA57gL^KTlROLa^kA)NMPrwm-~%nhT3PKTYrJ^3dRBf1wHZP19{r&eP6h z$%kK=!(Sm*4~~l7e2E)PM2zg^&i|rE4>E(;y9C{E6Wbk=eFUY*_KM1}oxZu-`a6jS zk=s>`(uWVFH8oY4{q65%mib+6>UY11x#<(eE0 z;v(?TwlyI=s7}nhf~(;VrK|P#p5*UKYR8u_im!ZVUwfCVC5C^_S1hRIJO~}GfK6EB z_Db4GTpq;*tk9KXhZ?Eo!>hiO%)~?iTIvH|X64CAijwucC`HJ;h?CFlcBl})Q}2Jm z!~KYj*rrC1R!@wDIOWOu(TWL$T;GFrrZsX(7`Hj~H8Oj3LtALhWguQXklW!I-B)_I zY9UYOEhn=@DJ3CG(mrIJ;^Xryh3~}3h~&WXt8kGB`rNDxsl-aBIRbGX+Ok0hW9kS) z9EQNEz_&II$*RACF<9fV6>4VsL)`AachJ5gTa?x`avwY zb!V|KWKs1PQ{en0@P77`E#TR?`&P#gIK37Feb@2B zZ_Pw-fTh?i%P;)h!XK;W_Em@GUgS}@I4I!oFHg#;8T$rRhQia;;Z_7!Kk8IUVx&=b zdp;Z)IZ`vVg^#wtjh--cb|TsXefHD&B_FT26AVOL1FDh}t^)Ug!Q4%|vdAQ~zQ(Z| z)Ao~dRfEhZOV@PA42%xnPY24lYKPUuPOQyea?Oe3AxZ$31>hO~zQ9gA&eVTsJd30P_%wf>wCwKnRKO%F(uHzFGk;R)8 zB}X^nX>hw!2fD6T+f>YnM>a)v%fCzCv1I?tu(;s9rzle!E3qeo5OA28K5=^9*;8+i7C``0x1=ZCpI%?U<%q zL?r#G%dqk7a>Qx^<=Sd^kkrPG-hR11=3N6yL-sUlU_xyGTI_dwG>LPc=Ox{f}(vg#V*? zHe@aFOGQdGpsc51JigVK%tu4wbZTKu%Ee%V_h@j(HPezveKI#3S^Hj=VU`F;Y<~vZ zsW-}Eajca`tT)zOB?GDzF`c@wO}bJ62TZY`bgYhdA$R+VZg8ixVLMyep^>U_sSMH2 zP+&LLSV0&Z(ILCl)LiLXdzBHfN$MZDH`r(ctCEnXOhIVe0Sk=hT!LbZWkQICx#euv zN@rgx>>&;-66x5!!{=QPVPIO%r|J|8|Kx}%k#N8mI2vc7-S{|ewgPZ#p$6fSyePENKQX;slD6>}c)`q!I?|7jzeUI`nSr<>Ai zOH}UF6$M}0qkO{Mp|*%aif!&^30w_y9gXyt(9Xo-dYebRL`lugR*P2}h7X)t`ZsfG;a$$c3N&7ddf>Pxu=PUkV*-vI@sZKHUn0r2 zx0yO<++pR$cM=*E_v|PB+2ggvo=6$4GEb2hsb`U6lo8wLO#ydahs^M^uMEh3l4>}A zj+*yyFnPkUY)2`#Dw0BjR`0G>KTs5PMb{7&?^`h-qUR(<|8NqwcwSBMWT2C#(HfSV z18>WIW+Bg3==-$ynmy(Dk2&_bn2C@z3+ba2NV3twpX|3O*cSO560RJ$6^BywY?XS_ zr^VXR5SvM*hb;CsijU*95?Ff-9v|l$P;W#xD_&NhE+R80@pAk{+h3uskv-1X8u1D# zDnJV^Hd5z=5^dbhrcd)M`gH5*ueD!SmT3Dul^awZmeu!FB$>@R0(<;%x??FHS9wSL zu`T{$@381!*_h_{eKtL5x#ZayAywp!Y5jreo^Nv5&a=l`c&QCEH~CK#e#ShdOTUfq zCRy_^c7L>Ydsq?6d$aXm=(m5O@k#rc7 zPFlgT5Zp1Mr!>JXav`V|$xonZI^V@_VEeqgSKg@Wo$dFr&XeJagyR|&a5#q-qb%~Z z9NYKtbHE31xhwOydSOC1-fEv`;po802fp~DxUUtz6TBzt{P_pk?WqoRP13c-HfEE8 zn}VNvcS;ef7N~6T?ehHc7>k_wyV2-I&&2uO#4+;zJKH7sk1;7=rQfd0UwjdgD+b-& zmWzm&9Q2c)d+eW))jJwAsc~Ta*EoSt_cTDA5EPubEJ%*)^{ZIg8 z7nZE&y9+~Pml<4pPppaE#cMIEe)YAXj4mhL*gYCAv?HSUtzr9x`&vm|0u{!1UaI~5 z2pnh6rxRCEinRF{Lt&-jA96LN3l^-@(O&B^IK@R-rwdbLAu)H9KZ|*yN8s4e)*=aq zbhF0^5?Q)|<`Q}^0dwYC?INPtY~vKqJ5_7=xEha@zspI^Op`HNw(sc0q$)@3H{vvf ziY>G3z@T$O`>I3)u5liYAup$n*j@>(mbh2W5ofJc6SX5Os>Dba-qsj9PWlb?k2i`x zTl86BBMmjzr^k8ZY8xJm+L-mr08EsdwFfdUb@3Hh|}OO)fVC zlbQ5$;WcG{=~}HKWA;YH>>e;`{izU)IyTJ#3i??H#&{&|K%&;Fpb|mz*>fz%~l#NW>)Q2l3de5WUh9 zS@kKz5k@9KByn@rceF2=xJ0wd{5hVEIVjUxS4Pd-!O)I3`Eh@Po!JfQ z3;VTfm-U)`6&q9kNZn1(O-~uc%qiGrS;6?5(`xgcWl-{a9AkKga>*hZCDp<_Ci=9p zsr&bg+oK(YmmUFk^?ABia7g2oLM-Rbr;PT_Hg@PZo*L~s z$06y@7d!NuF;PWW%>HEAP%_&_6JYX@?dveu!iSd=7 z4zV9BBG)!6GQxNK-94~_ytu`#@&1SOJln=SAmLWq<06czj?bdf(+&D8{XTX6v^nwR zXq1@OEd9gYm#V9_XB)&X*`FGkDO0;2@Z4&76fAD>pD$%j+P2Jq1j_TXi*ZxgCnAQ( z>~y|^Of6rIec!E1K_=Q7MwK;3zk?g<4bAI}(r6C`LE@fo zT~0O{iYj$-+|1*5pu8E=-X*fC`<+r{q#NpjGsHbZe$I~F`2VeG& z%Z_+aAWBpuWw(m4xhX{bGHU^*U+xL~w{@kf(aFn{!2%J_Q*kbCT7z@?#($N1zTS`y zklns=e%r&PQWflAC-ZYx10OF~cBq;Uui~=`Hg9fgpxlhYCeq)N=IA#LKKGo+AF5jC zn|lAlsY|f4;zfK1+hM#6VJWH}c!OiBIk))b8W@O_E+R5!`eO@2FhY~ylh}V%&cCDe znYS7v`AS9QQKsCU%7bkH!?12*JA7$QtfxbI9IlkBiVp{Uyd6FKIK0Nerxd}o@R7l< zI^4{$)T{E%z&`rdRJD-y!L4#Xs~j-%Bh}q!F&)q367Lnd-gMwz(GOeFa~k(#!_2-j z4i7N8_rUuWBYQ*K<>KQJDdxl6Rjcm+NHVZZ0lOQp{(B@Ph1l<$5j8+!5~NuAJf(+Q zoZ|8t#krV@s0xBV%}*Sg+8at@>3d{UQCcrynI;Rm+-|n&>Mxm&DP!kJEZGLn4SXyn z&3MAs#yd1;L!7PuUiyL}TCj#mmMrMBTFiqh%H{*Ea$2qY8ld(uPt0&syy}msi3RS& z)5Khklze#+&C`49F69XNp0DI>*5#u``hjqiga5Sx(HLCv_j#XnZ>K)=5RcOUzc zcwupDBQ|B7D}CEEBNR~)WMLG;$n$P}C55OtW1rWfp?izFI<4+UlLxWIy4@_+Aw!R{ zmw@buWdC*8oHQ)iMJYDrqwv^Rh>nK!FCH(IAO)M#>BEd+?ehnCDi6uJyJi!#*Wcx{ z-Hvdarjz+~K3_b$m2-L*wpb>1tT6e(fvA6EZ|=Q{oZYkZ_nAa)?n{jCijU;EYQ9%E zOd%Vy4`&pJX!p3E|NS<<`tL%tNfN*1m5o`Jmhj6z=oqZz}o5M~A(`6oEfH`MwH6e?K)te0FS zjI!9R_4S&5NRM8dgfV2! zd);Y=81Xg2~|D_TVb+xV$luPgZ) z!`i^21cp9B%Ao~Y?|BI9<`9~*X6x@?UrjhgT>`n(P2t5tSr?2mqGBW`2|`hDP%P73 z)Bj+ha0m+f|0<{^{{JD5Aii^JiT}j@fRJB7bvA-x#1Wl-`EN-qpMyJVB<;7^hbllx zJ~b;l`oA!5?R{voW*CPH8Oekm#^l4`x{0kafIGeY@ z{>MZOOZ*4L_VoS#3EKnB{%KG!{WlEt{}~hN{|F5u*oSZT&p^{B5E9gzvZM@=y}|z$ zGlM}ylOYu0Cl@A0-5Ci5xj~U`8F%pF5AM@-Rw_fR=KLq{Km8pKu&PbKo1R4gfv(?d z0Mx6Wo}U%U@zppB0&6=Z;Nu*#7?Mn%QOf)TQ7Mlg$`1ieNK6;Zx;L&r216o&0Atx~ z1G6>n@u0ah1rJ`WYLXM$z(@ZaH#72f+1Y5@+{g=ANSJ@`b{?hxrocs)B(hNgW9A z0WfLhwG9B(;{5KP(T?pp(Qc!e0wPJ^dNGRfIU5O{7iRcG~}bq zTgOT$Ukv1@U~&ji6EXxuknHb0{83lmAm?Jl`G@`!-l4p!c1>~!#Oy< zX;RGpftV&}I=u~w3+qi`cGAtf{c`*DpB*2KZXPPDp%H55>?lP9G=oCM?lp$ICtbUb z+Cwbe%<7w$eRcq0`_m}|We~CH$az2j3KSam5Ms&Qsw45P;sqoz;t{ zn&CRQ@Qt?_So?eC6E)SFSFi!zvwL`v>`<&M%@IVB96()QCBWrQ|3h0Yy|DR+K>l5Y z_P&e7eXGKb?EsrN%k&9b1)&duf|+2FDs{^1iq~peMA*U58rw<$Nbm~;j@wgQ-wp^L z{IQ}?pMP2*qtw29OJb!HFcFQk%dH#VSP&o?te;pZWv5o9`bKs5PhWAlpp;HktFUB^ zf9b`?KZ48kf#JQ^-!eCMx+(6_acB7Zl+#t2-c}$X$8h!%N;x$nR}KU|37;ZD=9UjE zmVkM2ql)^Zwbk&{K?MQ7kwm^`yRKMyPf83F^A57ExfP(-a>}L@KoBBn#)WN<#IFCi zF#5{$Q$UA$xtMc+`MQ*c&;&*FM`REUG9yeLL?3QI?8X5E_P{dB72V%{j6g6o zGzc{<{qGE*H81a81Q{6j!yTVaf3!b@#=s~Z3us0!a8CwVyv z$n*TO5u>&bk@=B?e^i`X+q->CMqUSL>KU+OzyW?;DIbB5uKfCNpSilGQTS)@hJ|$p zrk{Ld_X&SpQ%e3?A;I(NEJRjk1O_-_Z-6AiY@gCZf7|lNUiKej9lr!5{xnXyBnm3d ztb*V~i(n`es3TT$yC?}QeGRVw4yPl%-R#G6U}#u-IujjSGR3&SIkH3tMfrHr1%2U! z!fz_L;-kQquzA`w)u0tJUq58ZL+)-Bigw;p-GH?B8_*w45uDb0v>Q3nLK^m-Ahyg> zrI#k3ckiEsUgcwlCP=*2e><_LNVgx*6XU^KpzfInp;!&s`{qU(Idlhpj$t8~bdK+< zP_`p#H*2>l{62O{nmEu@Wsx_p;Cpy~2HgQ$w0j-pMHx#TTKgins z2Kp`!tbYsi#s;1MLa`}%mR6_ndvr zl|Sa=xWFUYeb<)Jeq|jFgaLM8x1TDM?YhQ6pkmM|UBt5Hj7UZ0*G7-#>Rsxw6~xWz z{#pu2y+i4rjX{%&U7Tmhkx*(^mDt+MC^2giCbvfBNy+!f!E_dffx}t_RbHx}Lur>%Oud>Ib&pDCIe)+go1LhGMkseA1?s&f}#o6@71ZZG?U%6-r zc(L{C+QOi<&?llb_Vp^geR!GIeAv`&{ERsB{oIe&dKq8x&9NiqGFufY5y0{2DvpJ9Ss z(3Y~Cs~(|ZHd~D`0)xfbw;0`6Ok-B6m)~B$52%&;Kv%_w>UZ;jjUn5QQdbLYEMMjlHD_5>ROplclW;~x~Ds&AN zWB=&{Lrz*ge1Ygg^@c$RJ+*W}!?;LsadpNjGzr@@{PxI0J|?60301VNE&4ls2`TY* z?bkb^4aKA_L|_^9fYneiVE>v~W4ci^MOv9i_N4R(0E2nX-V2U4Y4ep{l>D>o-jg;D z91Hgp>@RI1eWhX1cj zLB!;!)<8tOg~qG=ZK0y+KAn!CO;XC9WQkRF=^Yimt@uT-7Ih|Y<7^k~we9UO3A4Mm z;yh#W8R2U~nQ@2Lq;$N{7c(f999Jq(j9&%AM;5Kng0_g$8%B<%a{I;-+iGHb_Y)pQ z87kEwJZr1q;-1$s{?Ve+9xs-7RY)A7#BUvE&OQiR?-J5qsE;9Vx7oPlc6{%(WQfqL zbdKCs%lHe;?c&>i+%FKXPdHo&J<5X@eLuPDGp2%ePeem-XyaxM z7iQ^LFX^YzPr(~tem$VpJl=|xKPH6;%e^Ui`7WcyjhMLM0qG&JZ|K=iUG7c-H|N6_ z4EB4}ZpEuTaes5B?GVlg383c#2LwZWvU-9^gw^kXx$B0pm$b-g-JArIKhg*`xPLD2 zJoku8qx;-GpmZDTb@uPtJ3z^6Q6bI4x9tWOChw?`wy;P&7z^Hj=JLr@^p0a`7|0K# z23mu}Pig-oVi{GcZEgPZZAyJDrVZwwaDKn1pBGaxdXH;aq@`^@9o$ z>CEt#6uV}TrIq{5?G%HC>&(sano?^ z;rF!bL_7YaLSZMW-HGqru+x6@U9|hVW{@e0mp-anp|@E8?UH~dvA8}>&NQ-VwG7{8TsWifa}%EXTjcTC{4 z3jFrF6XE5gl*)zuI@-_O`TK{U0AZ6LNaH0zODA;4DUwc8s>^m{mhpxY8#5enggnUM%AmjBVAM zgpA4BXmhKwQq08dNtJ*(icciGB!f3@!TlmyON^~lZ{S7r{aW|TE{G+c?)=Fa`?A|4 zDB6J3d9;2TaD%eh$BRUJ%kirWV6J;y2!2}h!k^aEltKaB*rnKVU@X@aX_f=F* zm6H1&3CGCE%r7UtN4FzoUd`y(GO9W-eEgue0zpRC)qBZ3zgn{D;9wk~7X=&|Bp7Dn z-W)-xlUhZ(tbV0=mhzJzXjrJb)P7GikoWnaa|0@DInh$EQl&Ig@kpP{{FLosm2f#K zOKq8H?cJ4(v3_=IQ&x9YM?!TK#r<{I>iJVHOlG`%MDFai71QNKNutVi+NYijErLxe zdVNzzW}=G5*F*)h_+J@LdbGjYo~v$@8B~h>{lXmk?vRI_MlP5Jt{?u~-pj|7uUc4U z+Qajd>p)R0NUkLWQ{jM_Ds{2+WPt`5`=c?Aa%2Liqcbhuk|>@?02BrLD)a5r-?Jn+4K$jC*eUo@a{i==$b zi*gaZA284pZ*|v3wvE55C`VPSn#sEq6*%id`@gH2V&4m8PU`PV-FGfl7>+KhF~XG9Q;qxX5Pi6>68 zZ<3VIlNAmEmFMT7igAEjLoHeA%|J1#7*rr?vFQ>{EiiWKZ8J(9} z7;y+Xz+z#hd#Qw8ho}B4sMvQ2h$@LrBUd^hYG9%T>d#|6PYG7rFvB>7lqUj}{}7#8 zzb4?OFzKrdksj=ZT(!lh^r)0pX7;kW&hpbi`0CP8M(FA%loCVuxWV_SvF7@fbIj1wqzm^K4Dicz}`hkW%7>xA8s) zNrjl0pVs)mFg;?$W@x#X)UUQ?p`p==o}yFFd_eF;m~ll&IY?`wh(g~<3A3kHzt@Eo zES5&}y`dx_^0H=0)N5yq^YTEIk@x-83FJ5`ntdVq3EV(}*1Mq}IMqK>y&pT_=r>9gZKD?Yo?%Umt+WFE2yVTYJp0uxmpcNEegd9SDr!{*lV|!yk*um|_~0 zOSN3Nyw%Ma(MYqtXJUzBEKyh%KQb|r_mzduSGa9IPHC8rJY_@UwjoQJelzs z7}CLM;V9f_v7fsBgpyh54Iw+EJ z9eotf?QRjh{7Aef1%YjhQE6DhxW;Yn7({esr263u>1Xw^mHFAYqYyVaA%s6^n``@d_W74(vl%g$fP|8Kuf{!~GaUu(2)n2l>hN;m?7^dA`=;WN_roAbt z?aQW5NwR=lt-3>jq}mHYajWM}zRmY?~=L?l#Mg(R$?g{<#Uyq%_UPCG5r} zObE`>*WJHKNE77J%FKkKpNoG|zDN%Hy|n*^Q8ql5=HIK?A-1EA)yz?`q!3x_FSMkI zp3T?3G)Mxk+VMBhCRYw{{F`H+irc#4{t4VR(z42{L_)TeaI{_;!-Hc7+yxa+suZM+ z#?nxA;R}62`>Uw@p)N!6?cYQ~_+fj!eKQVw>W=Hus8TKz5uP)NtTBFZXwufWp;fU*u$nd1y%F-s zH_JS(l<=jYw~S_eo-|V@@Acsep|p!`nlXwLQVSEE8n{+xd{T68Ye}PXHY4JAmvuBm zJB;Sid=Fi`TeRhgYhC@P<|I!pm#>x=8wIY#v&-?{hmBLbBfJ)M?xMp4DsGp*ShL^{ zGAKQ~I=xM#aR(O{dh5J7hVt@&K2#ZfiVLw*PRG4Qs@xOR=YqYou1A@o z$ZH20_w$cLU5@i2Jv|S2{iy z;tRwXtPi$xL!C^IrLH=gojg0S!~Pmtc?Nelu%v_Q)U+ixn@>~JqXs=rQ@Dglq`VVT zMqShP?nT+Pm615FU9rBm=PECiLYF+v!T}xqcOBZd(uOTQ{;*?XR zaI;hT)p0`P1poOJyGVt5;|>cZ1Kdcnj;ZpO^J#;DRiW5-BGE5?ikuRuH-_tsqz5zQW zP`=ND{ubG`$BmiJVLyqo$ky86NlsNfr+Cv8`2gz3mQv%51U;hh^5iNgvvq@Rl~((; zz{$GhLQeejv&hfxy4^o+g+;GNH7wPgY8=r2X5W{XX8So`;l&PmM1K-O?9p2n8-(f` zrnl5z3Y^E=-C+N+Qi$}6{382(*t zo(UIMV43z4gexBZ?{?8GB?2U~3I;saHxbt*V15O1*j6fu_8PNJk^j>^7jz|Tt#$?o zWU;vs`?wk9gYPnF4v8;-0hVp+c|07qGU&2Tpt6*;TvjqP~XKVaVV@>0YRNtyVrk!-lLb_AKsdLW#K>vsberLWg z(8R#&w}%LrrvUKqyvrVB84|h@IBqfnxlTk50bqHWUj7E}|Ib7?WX&Ww#CM$B6A7J)Dg`-HbKD!MANZqG+R|PpV&9w< zGh&ur;YctU501mG$)v6h$3ER#SBy-H_=IVR{L%dA*^>B$lhqU#b5^&|&9W)jcwpS8 zfLSj0>7tP=kC&`g&*nQy6cLx;%|Om*Id;tp5q!>ub$((L%ZM z&mpiHzlVsNIRVemW;Gyr&!d3d?2|1YXcDQe!Vi17RLZEUt4yGW;}tC^Wzz*h#VA|r z+{iI?@`hc|c-RoiE%^?5WM@j3 zucY%2@5{F6Z+`$wgbciGpc*lC!u$*f-74F*dZ=vOao|_8wKr5+ub+^M_0o5zI5vEXz_BycgZ6wH^9j*hR%3$-&_Hh^UY1Me{I`Em%TTkVW-H|>BIbx{`O2pHTb2aGdXqRf>UjO+4; zG(ln>$aU|v{e`=3f3+?GH7~9wv*rqX(P#1M=tBJBRwg>a)&Ov8P$<0wAUD+d2`d+KMU9c zf`y4|+x$E;`s;WUD!AV1rCUy5CbcY}+s=xa6}ykM>`lgM_beCY%&h7xVL!&#mSJBs zdJ$XvxnK};!Wqng<$paJKvX<|cIRIQOu?Q&OFM3a^v|DZt-BxAn|B6`Sv{kPJ3PY3 z#s10wC%ESdvMum}>~F z$Gr=K`M|U6@8v7Z=rupWx7U@3VPWmuz^UpC@uPh`&Q(o=5^q%lwbJP2W}Z3WmN&$jA^YI^1=E6 zjNwGfis^=Zj^v0J7!D4BY9>vBY#)EDA1`YLHJTnF)nBf`Ot$g|T|hkfI^-a9@PqW2P9?tq zYf8GzXjac>Oo6$kvip^Z9uNoaoGbXKj%4}*5&$8Xm#T7P*dB~DU2@D5+(*87mlzF0_i|xN zQ2R-;X)enwV3|-kfYNa%CmYbmsJfQ2NyVV+ixg<&tfb0)g5!Cg^$zh-V*pdZe3pgO zeO16th)_45&zlMx%o=^WM^pFeMYqWO?NAiJ~k9_E12 zle67oflcO~W{?}RtlpK-6d~$YO7GqGPRAR{p%0Dx85cwrFnE5{3&C@2;s8e1ro+~f z)XZnH4?eblT(LWcIF$me7|GK|i;&5kc5ve7>F5e1m#++VOR+I4cw_#3pvli5_yrB2 z$jzmkpg=G5t-=c+Ch3pBMml^0yvWr|wHURy7o=PJ5kmgCF%jDI-6VKnhY(yqEd463 z2-O~hP$ao>;Pv^CxRa1S#OHmynEfQ%Q{lX~tb2Y=O8$ofkiH}~QKT4S_l6$V)cnAY z9C<}V5@VJ(5zO&EPlNCg-rOVZtPw)qD8EqQ(KDq1NL=C4Cn04wF(l{jQX9VKpLm)w zD2l*N^qM;b>lR|9&eSsjlwsC>zzC%yh!E *6shTCh66mWq&M2`F>fu-uTD;AJC; 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

AmEJ2XlKex2lk};4fEbW3#Rx(uX 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 From 29272be4c956cb366b6d35fab8d18a878fe0134e Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:30:10 +0000 Subject: [PATCH 3/7] Fixed shortcut and swap pipeline bugs Bug fixes and parameter clean up --- .../Fabric/Branch out to new workspace - Post Activity.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb b/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb index 9fdd1eb..938943c 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb +++ b/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb @@ -1 +1 @@ -{"cells":[{"cell_type":"markdown","source":["##### Branch out to new workspace notebook - post activity\n","\n","After cloning a workspace, this notebook will reconfigure any references to the old workspace by rebinding them to the new workspace. \n","\n","For example a pipeline referencing a warehouse or a default lakehouse of a notebook.\n","\n","This notebook runs post activity tasks can be run after [branch out to new workspace functionality](https://blog.fabric.microsoft.com/en-us/blog/introducing-new-branching-capabilities-in-fabric-git-integration) or the [custom AzDO script](https://github.com/microsoft/fabric-toolbox/blob/main/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py).\n","\n","Summary of post activities in order:\n","

    \n","
  • Default lakehouses and warehouse are updated to local lakehouse/warehouses
  • \n","
  • Either creates shortcuts in local lakehouse back to tables in the source lakehouse, or copies the data from source lakehouse. Set via parameter below.
  • \n","
  • Copy warehouse data. Set via parameter below
  • \n","
  • Changes directlake semantic model connections for semantic models to \"local\" lakehouse/warehouse
  • \n","
  • Rebinds reports to \"local\" semantic models
  • \n","
  • Changes pipeline lakehouse/warehouse references to local item
  • \n","
  • Ability to swap connections in pipelines from old to new
  • \n","
  • Commit changes to git
  • \n","
\n","\n","Requirements:\n","
    \n","
  • Requires Semantic Link Labs installed by pip install below or added to environment library.
  • \n","
  • Requires JmesPath library for data pipeline JSON manipulation i.e. connection swaps.
  • \n","
\n","\n","Limitations of current script:\n","\n","
    \n","
  • Does not recreate item shares or external shortcuts
  • \n","
  • Does not re-apply lakehouse SQL Endpoint or Warehouse object/row/column level security
  • \n","
  • Does not recreate data access roles in Lakehouse
  • \n","
  • Untested with Lakehouses where with schema support enabled
  • \n","
\n","\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a98b6d0a-7a36-4116-ab0d-aa70144eb737"},{"cell_type":"markdown","source":["##### Install semantic link labs to support advanced functionality\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/index.html\n","https://github.com/microsoft/semantic-link-labs/blob/main/README.md\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"3b887bd6-a9c9-430f-b58f-b58a93f5ce29"},{"cell_type":"code","source":["%pip -q install semantic-link-labs\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":true},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"1b03316d-c088-4a0e-a2f0-44d45d112121"},{"cell_type":"markdown","source":["##### Install Jmespath to make data pipeline changes such as updating linked notebooks, warehouses and lakehouses "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"8a74ed11-dd64-43bb-a735-906a947c8666"},{"cell_type":"code","source":["%pip install jmespath"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"c68be6ba-7648-457f-af82-f1987d12d7f7"},{"cell_type":"markdown","source":["##### Set parameters\n","Before running this notebook ensure these parameters are set correctly. If necessary these can be passed in via a data factory pipeline"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"dee81614-b92b-4242-890a-b11f97b1a640"},{"cell_type":"code","source":["source_ws = ''\n","target_ws = ''\n","\n","# Either copy lakehouse data or create shortcuts, set at most one of these to True \n","copy_lakehouse_data = True\n","create_lakehouse_shortcuts = False\n","\n","# Option to copy warehouse data if required\n","copy_warehouse_data = True\n","\n","# If false then shortcuts will be created. If you wish to create shortcuts based on a pattern match please set the param below\n","# enter pattern match for creating shortcuts - see https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py \n","PATTERN_MATCH = [\"*\"]\n","_inlineInstallationEnabled = True\n","\n","# Set connections to be replaced from previous name or ID to new name or ID.\n","p_connections_from_to = ()#('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726'),('4498340c-27cf-4c6e-a025-00e5de6b0726','https://api.fabric.microsoft.com/v1/workspaces/ admin'),('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726')"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"tags":["parameters"]},"id":"90efaa4f-846d-4924-900e-258837a3467d"},{"cell_type":"markdown","source":["##### Library imports and fabric rest client setup\n","\n","https://learn.microsoft.com/en-us/python/api/semantic-link-sempy/sempy.fabric.fabricrestclient"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4fb01e1d-ec4e-4c69-b544-66f6d8c5a475"},{"cell_type":"code","source":["import pandas as pd\n","import datetime, time\n","import re,json, fnmatch,os\n","import requests, base64\n","import sempy\n","import sempy.fabric as fabric\n","from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException\n","from pyspark.sql import DataFrame\n","from pyspark.sql.functions import col,current_timestamp,lit\n","import sempy_labs as labs\n","from sempy_labs import migration, directlake\n","from sempy_labs import lakehouse as lake\n","from sempy_labs import report as rep\n","from sempy_labs.tom import connect_semantic_model\n","\n","# instantiate the Fabric rest client\n","client = fabric.FabricRestClient()\n","\n","# get the current workspace ID based on the context of where this notebook is run from\n","thisWsId = notebookutils.runtime.context['currentWorkspaceId']\n","thisWsName = notebookutils.runtime.context['currentWorkspaceName']\n","\n","source_ws_id = fabric.resolve_workspace_id(source_ws)\n","target_ws_id = fabric.resolve_workspace_id(target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"391624c1-b299-452d-9ebf-f32626d49970"},{"cell_type":"markdown","source":["##### Update default and attached lakehouses/warehouses for notebooks\n","\n","Update notebook dependencies based on but now supports T-SQL notebooks:\n","https://github.com/PowerBiDevCamp/FabConWorkshopSweden/blob/main/DemoFiles/GitUpdateWorkspace/updateWorkspaceDependencies_v1.ipynb\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"aaae8a08-588d-4dd8-9d2c-2200b7a88d30"},{"cell_type":"code","source":["for notebook in notebookutils.notebook.list(workspaceId=target_ws_id):\n"," updates = False\n"," if notebook.displayName == 'ETL':#True: #notebook.displayName == 'T-SQL_Notebook': #notebook.displayName != 'Create Feature Branch':\n","\n"," # Get the current notebook definition\n"," json_payload = json.loads(notebookutils.notebook.getDefinition(notebook.displayName,workspaceId=source_ws_id))\n"," #print(json.dumps(json_payload, indent=4))\n"," # Check for any attached lakehouses\n"," if 'dependencies' in json_payload['metadata'] \\\n"," and 'lakehouse' in json_payload['metadata']['dependencies'] \\\n"," and json_payload['metadata'][\"dependencies\"][\"lakehouse\"] is not None:\n"," # Extract attached and default lakehouses\n"," current_lakehouse = json_payload['metadata']['dependencies']['lakehouse']\n"," # if default lakehouse setting exists\n"," if 'default_lakehouse_name' in current_lakehouse:\n"," print(f\"Updating notebook {notebook.displayName} with new default lakehouse: {current_lakehouse['default_lakehouse_name']} in workspace {target_ws}\")\n"," source_lh_name = fabric.resolve_item_name(item_id = current_lakehouse['default_lakehouse'],type='Lakehouse',workspace=source_ws_id)\n"," current_lakehouse['default_lakehouse'] = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," current_lakehouse['default_lakehouse_workspace_id'] = target_ws_id\n"," updates = True\n"," # loop through all attached lakehouess\n"," for lakehouse in json_payload['metadata']['dependencies']['lakehouse']['known_lakehouses']:\n"," source_lh_id = lakehouse['id']\n"," # find source lakehouse name\n"," source_lh_name = fabric.resolve_item_name(item_id = lakehouse['id'],type='Lakehouse',workspace=source_ws_id)\n"," # find target lakehouse id based on name\n"," target_lh_id = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," lakehouse['id'] = target_lh_id\n"," print(f'Updating attached lakehouse {source_lh_name} from {source_lh_id} to target ID {target_lh_id}')\n"," updates = True\n","\n"," if 'dependencies' in json_payload['metadata'] and 'warehouse' in json_payload['metadata']['dependencies']:\n"," # Fetch existing details\n"," current_warehouse = json_payload['metadata']['dependencies']['warehouse']\n"," current_warehouse_id = current_warehouse['default_warehouse']\n"," source_wh_name = fabric.resolve_item_name(item_id = current_warehouse_id,workspace=source_ws_id)\n"," #print('Source warehouse name is ' + source_wh_name)\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n","\n"," if 'default_warehouse' in current_warehouse:\n"," #json_payload['metadata']['dependencies']['warehouse'] = {}\n"," print(f\"Attempting to update notebook {notebook.displayName} with new default warehouse: {target_wh_id} in {target_ws}\")\n"," \n"," json_payload['metadata']['dependencies']['warehouse']['default_warehouse'] = target_wh_id\n"," for warehouse in json_payload['metadata']['dependencies']['warehouse']['known_warehouses']:\n"," if warehouse['id'] == current_warehouse_id:\n"," warehouse['id'] = target_wh_id\n"," updates = True\n","\n"," if updates:\n"," notebookutils.notebook.updateDefinition(\n"," name = notebook.displayName,\n"," content = json.dumps(json_payload),\n"," workspaceId = target_ws_id\n"," )\n"," \n"," print(f\"Updated notebook {notebook.displayName} in {target_ws}\")\n","\n"," else:\n"," print(f'No default lakehouse set for notebook {notebook.displayName}, ignoring.')"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"5c60b5d2-f83c-46f8-9870-9fd609166b67"},{"cell_type":"markdown","source":["##### Run the below cell - contains utility functions to support lakehouse and warehouse initialisation\n","\n","Shortcut creator:\n","https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a47a56df-219d-4d6c-b950-491909638deb"},{"cell_type":"code","source":["##### \n","### Shortcut utility function \n","####\n","\n","# Extract workspace_id, item_id and path from a onelake URI\n","def extract_onelake_https_uri_components(uri):\n"," # Define a regular expression to match any string between slashes and capture the final path element(s) without the leading slash\n"," pattern = re.compile(r\"abfss://([^@]+)@[^/]+/([^/]+)/(.*)\")\n"," match = pattern.search(uri)\n"," if match:\n"," workspace_id, item_id, path = match.groups()\n"," return workspace_id, item_id, path\n"," else:\n"," return None, None, None\n","\n","\n","def is_valid_onelake_uri(uri: str) -> bool:\n"," workspace_id, item_id, path = extract_onelake_https_uri_components(uri)\n"," if \"abfss://\" not in uri or workspace_id is None or item_id is None or path is None:\n"," return False\n","\n"," return True\n","\n","\n","def get_last_path_segment(uri: str):\n"," path = uri.split(\"/\") # Split the entire URI by '/'\n"," return path[-1] if path else None\n","\n","\n","def is_delta_table(uri: str):\n"," delta_log_path = os.path.join(uri, \"_delta_log\")\n"," return mssparkutils.fs.exists(delta_log_path)\n","\n","\n","def get_onelake_shorcut(workspace_id: str, item_id: str, path: str, name: str):\n"," shortcut_uri = (\n"," f\"v1/workspaces/{workspace_id}/items/{item_id}/shortcuts/{path}/{name}\"\n"," )\n"," result = client.get(shortcut_uri).json()\n"," return result\n","\n","\n","def is_folder_matching_pattern(path: str, folder_name: str, patterns: []):\n"," if folder_name in patterns:\n"," return True\n"," else:\n"," for pattern in patterns:\n"," if fnmatch.fnmatch(folder_name, pattern):\n"," return is_delta_table(path)\n","\n"," return False\n","\n","\n","def get_matching_delta_tables_uris(uri: str, patterns: []) -> []:\n"," # Use a set to avoid duplicates\n"," matched_uris = set()\n"," files = mssparkutils.fs.ls(uri)\n"," folders = [item for item in files if item.isDir]\n","\n"," # Filter folders to only those that matches the pattern and is a delta table\n"," matched_uris.update(\n"," folder.path\n"," for folder in folders\n"," if is_folder_matching_pattern(folder.path, folder.name, patterns)\n"," )\n","\n"," return matched_uris\n","\n","\n","def create_onelake_shorcut(source_uri: str, dest_uri: str):\n"," src_workspace_id, src_item_id, src_path = extract_onelake_https_uri_components(\n"," source_uri\n"," )\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri\n"," )\n","\n"," name = get_last_path_segment(source_uri)\n"," dest_uri_joined = os.path.join(dest_uri, name)\n","\n"," # If the destination path already exists, return without creating shortcut\n"," if mssparkutils.fs.exists(dest_uri_joined):\n"," print(f\"Destination already exists: {dest_uri_joined}\")\n"," return None\n","\n"," request_body = {\n"," \"name\": name,\n"," \"path\": dest_path,\n"," \"target\": {\n"," \"oneLake\": {\n"," \"itemId\": src_item_id,\n"," \"path\": src_path,\n"," \"workspaceId\": src_workspace_id,\n"," }\n"," },\n"," }\n","\n"," shortcut_uri = f\"v1/workspaces/{dest_workspace_id}/items/{dest_item_id}/shortcuts\"\n"," print(f\"Creating shortcut: {shortcut_uri}/{name}..\")\n"," try:\n"," client.post(shortcut_uri, json=request_body)\n"," except FabricHTTPException as e:\n"," print(e)\n"," return None\n","\n"," return get_onelake_shorcut(dest_workspace_id, dest_item_id, dest_path, name)\n"," \n","\n","####\n","## Copy lakehouse and warehouse utility functions\n","####\n","\n","def get_lh_object_list(base_path,data_types = ['Tables', 'Files'])->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a lakehouse\n"," adapted from https://fabric.guru/getting-a-list-of-folders-and-delta-tables-in-the-fabric-lakehouse\n"," This function will return a pandas dataframe containing names and abfss paths of each folder for Files and Tables\n"," '''\n"," #data_types = ['Tables', 'Files'] #for if you want a list of files and tables\n"," #data_types = ['Tables'] #for if you want a list of tables\n","\n"," df = pd.concat([\n"," pd.DataFrame({\n"," 'name': [item.name for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," 'type': data_type[:-1].lower() , \n"," 'src_path': [item.path for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," }) for data_type in data_types], ignore_index=True)\n","\n"," return df\n","\n","def get_wh_object_list(schema_list,base_path)->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a warehouse by schema\n"," '''\n"," data_type = 'Tables'\n"," dfs = []\n","\n"," for schema_prefix in schema_list:\n"," if notebookutils.fs.exists(f'{base_path}/{data_type}/{schema_prefix}/'):\n"," items = notebookutils.fs.ls(f'{base_path}/{data_type}/{schema_prefix}/')\n"," if items: # Check if the list is not empty\n"," df = pd.DataFrame({\n"," 'schema': schema_prefix,\n"," 'name': [item.name for item in items],\n"," 'type': data_type[:-1].lower(),\n"," 'src_path': [item.path for item in items],\n"," })\n"," dfs.append(df)\n","\n"," if dfs: # Check if the list of dataframes is not empty\n"," df = pd.concat(dfs, ignore_index=True)\n"," else:\n"," df = pd.DataFrame() # Return an empty dataframe if no dataframes were created\n","\n"," return df\n","\n","def copy_lh_objects(table_list,workspace_src,workspace_tgt,lakehouse_src,lakehouse_tgt,fastcopy=True,usingIDs=False)->pd.DataFrame:\n"," # declare an array to keep the instrumentation\n"," cpresult = []\n"," # loop through all the tables to extract the source path \n"," for table in table_list.src_path:\n"," source = table\n"," destination = source.replace(f'abfss://{workspace_src}', f'abfss://{workspace_tgt}')\n"," if usingIDs:\n"," destination = destination.replace(f'{lakehouse_src}', f'{lakehouse_tgt}')\n"," else:\n"," destination = destination.replace(f'{lakehouse_src}.Lakehouse', f'{lakehouse_tgt}.Lakehouse')\n"," start_time = datetime.datetime.now()\n"," if notebookutils.fs.exists(destination):\n"," notebookutils.fs.rm(destination, True)\n"," if fastcopy:\n"," # use fastcopy util which is a python wrapper to azcopy\n"," notebookutils.fs.fastcp(source+'/*', destination+'/', True)\n"," else:\n"," notebookutils.fs.cp(source, destination, True)\n","\n"," # recording the timing and add it to the results list\n"," end_time = datetime.datetime.now()\n"," copyreslist = [source, destination, start_time.strftime(\"%Y-%m-%d %H:%M:%S\"), end_time.strftime(\"%Y-%m-%d %H:%M:%S\"), str((end_time - start_time).total_seconds())]\n"," cpresult.append(copyreslist)\n"," return pd.DataFrame(cpresult,columns =['source--------------------------------------','target--------------------------------------','start------------','end_time------------','elapsed seconds----'])\n","\n","def createDWrecoverypl(ws_id,pl_name = 'Recover_Warehouse_Data_From_DR'):\n"," client = fabric.FabricRestClient()\n","\n"," dfurl= \"v1/workspaces/\"+ ws_id + \"/items\"\n"," payload = { \n"," \"displayName\": pl_name, \n"," \"type\": \"DataPipeline\", \n"," \"definition\": { \n"," \"parts\": [ \n"," { \n"," \"path\": \"pipeline-content.json\", \n"," \"payload\": \"ewogICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgImFjdGl2aXRpZXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICJuYW1lIjogIkl0ZXJhdGVTY2hlbWFUYWJsZXMiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiRm9yRWFjaCIsCiAgICAgICAgICAgICAgICAiZGVwZW5kc09uIjogW10sCiAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy50YWJsZXNUb0NvcHkiLAogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgImJhdGNoQ291bnQiOiAyMCwKICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdGllcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiQ29weVdhcmVob3VzZVRhYmxlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJDb3B5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlcGVuZGVuY3lDb25kaXRpb25zIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlN1Y2NlZWRlZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicG9saWN5IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0aW1lb3V0IjogIjAuMTI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZXRyeSI6IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJldHJ5SW50ZXJ2YWxJblNlY29uZHMiOiAzMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZVNvdXJjZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJxdWVyeVRpbWVvdXQiOiAiMDI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicGFydGl0aW9uT3B0aW9uIjogIk5vbmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwN2EwMzAwNl9kMWI2XzRhMzlfYmViMV8wYmJhMmFhZjVmZjciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLmxha2Vob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy5sYWtlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAY29uY2F0KGNvbmNhdChpdGVtKCkuc2NoZW1hLCdfJyksaXRlbSgpLm5hbWUpIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRXhwcmVzc2lvbiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzaW5rIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlU2luayIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhbGxvd0NvcHlDb21tYW5kIjogdHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRhYmxlT3B0aW9uIjogImF1dG9DcmVhdGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwYzAzMTIzYV9kMzEyXzQ2YzRfYThlN181YjRjYWQ4ZjEyZDciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLndhcmVob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53YXJlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAaXRlbSgpLm5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVuYWJsZVN0YWdpbmciOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0cmFuc2xhdG9yIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJUYWJ1bGFyVHJhbnNsYXRvciIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvbiI6IHRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvblNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFsbG93RGF0YVRydW5jYXRpb24iOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRyZWF0Qm9vbGVhbkFzTnVtYmVyIjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHNjaGVtYSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRlbmN5Q29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJTdWNjZWVkZWQiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInBvbGljeSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhcmlhYmxlTmFtZSI6ICJUYWJsZW5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogIkBpdGVtKCkubmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkV4cHJlc3Npb24iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJTZXQgc2NoZW1hIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb2xpY3kiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZU91dHB1dCI6IGZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzZWN1cmVJbnB1dCI6IGZhbHNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YXJpYWJsZU5hbWUiOiAiU2NoZW1hbmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQGl0ZW0oKS5zY2hlbWEiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgInBhcmFtZXRlcnMiOiB7CiAgICAgICAgICAgICJsYWtlaG91c2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjBmMGY2YjdjLTE3NjEtNDFlNi04OTZlLTMwMDE0ZjE2ZmY2ZCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInRhYmxlc1RvQ29weSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogImFycmF5IiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiBbCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkRhdGUiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiR2VvZ3JhcGh5IgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkhhY2tuZXlMaWNlbnNlIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIk1lZGFsbGlvbiIKICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6ICJkYm8iLAogICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJUaW1lIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIlRyaXAiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiV2VhdGhlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ3b3Jrc3BhY2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjE1MDExNDNjLTI3MmYtNGEyZi05NzZhLTdlNTU5NzFlNGMyYiIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIndhcmVob3VzZUlkIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNGQxYmQ5NTEtOTlkZS00YmQ3LWI3YmMtNzFjOGY1NmRiNDExIgogICAgICAgICAgICB9LAogICAgICAgICAgICAid2FyZWhvdXNlQ29ublN0ciI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjcyd3diaXZpMnViZWpicnRtdGFobzMyYjR5LWhxa2FjZmpwZTR4dXZmM2twemt6b2hzbWZtLmRhdGF3YXJlaG91c2UuZmFicmljLm1pY3Jvc29mdC5jb20iCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYWtlaG91c2VDb25uU3RyIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNzJ3d2JpdmkydWJlamJydG10YWhvMzJiNHktaHFrYWNmanBlNHh1dmYza3B6a3pvaHNtZm0uZGF0YXdhcmVob3VzZS5mYWJyaWMubWljcm9zb2Z0LmNvbSIKICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInZhcmlhYmxlcyI6IHsKICAgICAgICAgICAgIlRhYmxlbmFtZSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogIlN0cmluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIlNjaGVtYW5hbWUiOiB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJTdHJpbmciCiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJsYXN0TW9kaWZpZWRCeU9iamVjdElkIjogIjRhYTIwYWY3LTk0YmQtNDM0OC1iZWY4LWY4Y2JjZDg0MGQ1MSIsCiAgICAgICAgImxhc3RQdWJsaXNoVGltZSI6ICIyMDI0LTExLTEzVDE1OjUyOjUyWiIKICAgIH0KfQ==\", \n"," \"payloadType\": \"InlineBase64\" \n"," } \n"," ] \n"," } \n","} \n"," \n"," response = json.loads(client.post(dfurl,json= payload).content)\n"," return response['id']\n","\n","def getItemId(wks_id,itm_name,itm_type):\n"," df = fabric.list_items(type=None,workspace=wks_id)\n"," #print(df)\n"," if df.empty:\n"," return 'NotExists'\n"," else:\n"," #display(df)\n"," #print(df.query('\"Display Name\"=\"'+itm_name+'\"'))\n"," if itm_type != '':\n"," newdf= df.loc[(df['Display Name'] == itm_name) & (df['Type'] == itm_type)]['Id']\n"," else:\n"," newdf= df.loc[(df['Display Name'] == itm_name)]['Id'] \n"," if newdf.empty:\n"," return 'NotExists'\n"," else:\n"," return newdf.iloc[0]\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":true,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"e46210e9-58c9-483a-84ae-bbdc2ad1c37f"},{"cell_type":"markdown","source":["##### Either create shortcuts from source to target lakehouse(s) or copy data\n","\n","Loops through lakehouse(s) in the target workspace and either populates them with shortcuts or data\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a15065f3-670d-4bc9-b337-51709f6cdb1f"},{"cell_type":"code","source":["df_lhs = labs.list_lakehouses(source_ws)\n","for index, row in df_lhs.iterrows():\n","\n","\n"," if copy_lakehouse_data:\n"," df_lakehouses = (labs.list_lakehouses(source_ws))\n"," lh_name= row['Lakehouse Name']\n"," if lh_name.find('temp')==-1:\n"," # Gathers the list of recovers tables and source paths to be copied into the lakehouse associated with this notebook \n"," src_path = f'abfss://{source_ws}@onelake.dfs.fabric.microsoft.com/{lh_name}.Lakehouse'\n","\n"," table_list = get_lh_object_list(src_path)\n"," print(f'Attempting to copy table data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n"," display(table_list)\n","\n"," #print('Copy Lakehouse Delta tables...')\n"," res = copy_lh_objects(table_list[table_list['type']=='table'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," # Copy files\n"," print(f'Attempting to copy file data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n","\n"," #print('Copy Lakehouse files...')\n"," res = copy_lh_objects(table_list[table_list['type']=='file'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," print('Done.')\n","\n"," else:\n"," # fetch ID of source lakehouse based on name and workspace\n"," source_lh_id = fabric.resolve_item_id(\n"," item_name=row['Lakehouse Name'], type=\"Lakehouse\", workspace=source_ws\n"," )\n"," #target_lh_id = notebookutils.lakehouse.getWithProperties(name=current_lakehouse['default_lakehouse_name'], workspaceId=new_workspace_id)['id']\n","\n"," SOURCE_URI = f\"abfss://{source_ws_id}@onelake.dfs.fabric.microsoft.com/{source_lh_id}/Tables\"\n"," DEST_URI = f\"abfss://{target_ws_id}@onelake.dfs.fabric.microsoft.com/{row['Lakehouse ID']}/Tables\"\n","\n"," if PATTERN_MATCH is None or len(PATTERN_MATCH) == 0:\n"," raise TypeError(\"Argument 'PATTERN_MATCH' should be a valid list of patterns or [\"*\"] to match everything\")\n","\n"," # Collect created shortcuts\n"," result = []\n","\n"," # If either URI's are invalid, just return\n"," if not is_valid_onelake_uri(SOURCE_URI) or not is_valid_onelake_uri(DEST_URI):\n"," print(\n"," \"invalid URI's provided. URI's should be in the form: abfss://@onelake.dfs.fabric.microsoft.com//\"\n"," )\n"," else:\n"," # Remove any trailing '/' from uri's\n"," source_uri_addr = SOURCE_URI.rstrip(\"/\")\n"," dest_uri_addr = DEST_URI.rstrip(\"/\")\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri_addr\n"," )\n","\n"," # If we are not shortcutting to a managed table folder or\n"," # the source uri is a delta table, just shortcut it 1-1.\n"," if not dest_path.startswith(\"Tables\") or is_delta_table(source_uri_addr):\n"," shortcut = create_onelake_shorcut(source_uri_addr, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," else:\n"," # If source is not a delta table, and destination is managed table folder:\n"," # Iterate over source folders and create table shortcuts @ destination\n"," for delta_table_uri in get_matching_delta_tables_uris(\n"," source_uri_addr, PATTERN_MATCH\n"," ):\n"," shortcut = create_onelake_shorcut(delta_table_uri, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," print(result)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"8bec3fbc-4a75-4ba3-86d5-0620ec504a8f"},{"cell_type":"markdown","source":["##### Copy warehouse data via parameterised pipeline\n","\n","Loop through all warehouses and copy the data"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"f198b816-77e9-4f04-9139-d78237bedc72"},{"cell_type":"code","source":["p_logging_verbose = True\n","df_warehouses = (labs.list_warehouses(target_ws))\n","display(df_warehouses)\n","for index, row in df_warehouses.iterrows():\n"," source_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],source_ws_id)\n"," target_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],target_ws_id)\n"," \n"," src_path = f'abfss://'+source_ws_id+'@onelake.dfs.fabric.microsoft.com/'+source_wh_id\n"," tgt_path = f'abfss://'+target_ws_id+'@onelake.dfs.fabric.microsoft.com/'+target_wh_id\n","\n"," # extract the list of schemas per data \n"," schema_list = get_lh_object_list(src_path,['Tables'])\n"," # extract a list of warehouse objects per schema and store in a list\n"," table_list = get_wh_object_list(schema_list['name'],src_path)\n"," \n"," # create a temporary staging lakehouse per warehouse to create shortcuts into, \n"," # which point back to original warehouse data currently in the DR storage account\n"," lhname = 'temp_rlh_' + source_ws+'_'+row['Warehouse Name']\n"," # check if it exists before attempting create\n"," if p_logging_verbose:\n"," print('Checking whether the temporary lakehouse \"'+ lhname +'\" exists in workspace '+target_ws+'...')\n"," temp_lh_id = getItemId(target_ws_id,lhname,'Lakehouse')\n"," if temp_lh_id == 'NotExists':\n"," lhname = lhname[:256] # lakehouse name should not exceed 256 characters\n"," payload = payload = '{\"displayName\": \"' + lhname + '\",' \\\n"," + '\"description\": \"Interim staging lakehouse for primary warehouse recovery: ' \\\n"," + source_ws+'_'+row['Warehouse Name'] + 'into workspace '+ target_ws + '(' + target_ws +')\"}'\n"," try:\n"," lhurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses\"\n"," lhresponse = client.post(lhurl,json= json.loads(payload))\n"," temp_lh_id = lhresponse.json()['id']\n"," if p_logging_verbose:\n"," print('Temporary lakehouse \"'+ lhname +'\" created with Id ' + temp_lh_id + ': ' + str(lhresponse.status_code) + ' ' + str(lhresponse.text))\n"," except Exception as error:\n"," print(error.errorCode)\n"," else:\n"," if p_logging_verbose:\n"," print('Temporary lakehouse '+lhname+' (' + temp_lh_id + ') already exists.')\n"," \n"," time.sleep(60) # waiting for temporary lakehouse to provision completely \n","\n"," # Create shortcuts for every table in the format of schema_table under the tables folder\n"," for index,itable in table_list.iterrows():\n"," shortcutExists=False\n"," # Check if shortcut exists\n"," try:\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts/Tables/\"+itable['schema']+'_'+itable['name']\n"," tlhresponse = client.get(url)\n"," shortcutExists = True\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] +' already exists')\n"," except Exception as error:\n"," shortcutExists = False \n","\n"," if not shortcutExists: \n"," # Create shortcuts - one per table per schema\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts\"\n"," scpayload = '{' \\\n"," '\"path\": \"Tables/\",' \\\n"," '\"name\": \"'+itable['schema']+'_'+itable['name']+'\",' \\\n"," '\"target\": {' \\\n"," '\"oneLake\": {' \\\n"," '\"workspaceId\": \"' + source_ws_id + '\",' \\\n"," '\"itemId\": \"'+ source_wh_id +'\",' \\\n"," '\"path\": \"/Tables/' + itable['schema']+'/'+itable['name'] + '\"' \\\n"," '}}}' \n"," try:\n"," #print(scpayload) \n"," shctresponse = client.post(url,json= json.loads(scpayload))\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] + ' created.' )\n","\n"," except Exception as error:\n"," print('Error creating shortcut '+itable['schema']+'_'+itable['name']+' due to '+str(error) + ':' + shctresponse.text)\n"," \n"," recovery_pipeline_prefix= 'plRecover_WH' \n"," # recovery pipeline name should not exceed 256 characters\n"," recovery_pipeline = recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'][:256]\n"," if p_logging_verbose:\n"," print('Attempting to deploy a copy pipeline in the target workspace to load the target warehouse tables from the shortcuts created above... ')\n"," # Create the pipeline in the target workspace that loads the target warehouse from shortcuts created above \n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," #print(plid)\n"," if plid == 'NotExists':\n"," plid = createDWrecoverypl(target_ws_id,recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'])\n"," if p_logging_verbose:\n"," print('Recovery pipeline ' + recovery_pipeline + ' created with Id '+plid)\n"," else:\n"," if p_logging_verbose:\n"," print('Datawarehouse recovery pipeline \"' + recovery_pipeline + '\" ('+plid+') already exist in workspace \"'+target_ws + '\" ('+target_ws_id+')') \n"," print('\\n')\n","\n"," tablesToCopyParam = table_list[['schema','name']].to_json( orient='records')\n"," # ensure the temporary lakehouse exists\n","\n"," # obtain the connection string for the lakehouse to pass to the copy pipeline\n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses/\" + temp_lh_id\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['sqlEndpointProperties']['connectionString']\n","\n"," # get the SQLEndpoint ID of the lakehouse to pass to the copy pipeline\n"," items = fabric.list_items(workspace=target_ws_id)\n"," print(items)\n"," temp_lh_sqle_id = items[(items['Type'] == 'SQLEndpoint') & (items['Display Name']==lhname)]['Id'].values[0]\n","\n","\n"," # obtain the connection string for the warehouse to pass to the copy pipeline \n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/warehouses/\" + target_wh_id\n"," whresponse = client.get(whurl)\n"," whconnStr = whresponse.json()['properties']['connectionInfo']\n","\n"," # obtain the pipeline id created to recover this warehouse\n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," if plid == 'NotExists':\n"," print('Error: Could not execute pipeline '+recovery_pipeline+ ' as the ID could not be obtained ')\n"," else:\n"," # pipeline url including pipeline Id unique to each warehouse\n"," plurl = 'v1/workspaces/'+target_ws_id+'/items/'+plid+'/jobs/instances?jobType=Pipeline'\n"," #print(plurl)\n","\n"," payload_data = '{' \\\n"," '\"executionData\": {' \\\n"," '\"parameters\": {' \\\n"," '\"lakehouseId\": \"' + temp_lh_sqle_id + '\",' \\\n"," '\"tablesToCopy\": ' + tablesToCopyParam + ',' \\\n"," '\"workspaceId\": \"' + target_ws_id +'\",' \\\n"," '\"warehouseId\": \"' + target_wh_id + '\",' \\\n"," '\"lakehouseConnStr\": \"' + lhconnStr + '\",' \\\n"," '\"warehouseConnStr\": \"' + whconnStr + '\"' \\\n"," '}}}'\n"," #print(payload_data)\n"," plresponse = client.post(plurl, json=json.loads(payload_data))\n"," if p_logging_verbose:\n"," print(str(plresponse.status_code)) \n","print('Done')\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"57dafef7-17a2-475f-9e62-eecc6660440c"},{"cell_type":"markdown","source":["##### Update directlake model lakehouse/warehouse connection\n","\n","https://semantic-link-labs.readthedocs.io/en/stable/sempy_labs.directlake.html#sempy_labs.directlake.update_direct_lake_model_connection "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"cc97be77-116e-4cde-bdc6-2971ab98a083"},{"cell_type":"code","source":["\n","df_datasets = fabric.list_datasets(target_ws)\n","\n","# Iterate over each dataset in the dataframe\n","for index, row in df_datasets.iterrows():\n"," # Check if the dataset is not the default semantic model\n"," if not labs.is_default_semantic_model(row['Dataset Name'], fabric.resolve_workspace_id(target_ws)):\n"," print('Updating semantic model connection ' + row['Dataset Name'] + ' in workspace '+ target_ws)\n"," labs.directlake.update_direct_lake_model_connection(dataset=row['Dataset Name'], \n"," workspace= target_ws,\n"," source=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[1], \n"," source_type=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[0], \n"," source_workspace=target_ws)\n"," labs.refresh_semantic_model(dataset=row['Dataset Name'], workspace= target_ws)\n","\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9deccda6-5c3d-4b88-8ed8-68855ca0949a"},{"cell_type":"markdown","source":["##### Rebind reports to local datasets\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/sempy_labs.report.html#sempy_labs.report.report_rebind"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"36783f3b-4904-4d74-842d-dbd026a3184a"},{"cell_type":"code","source":["df_reports = fabric.list_reports(workspace=target_ws)\n","for index, row in df_reports.iterrows():\n"," #print(row['Name'] + '-' + row['Dataset Id'])\n"," df_datasets = fabric.list_datasets(workspace=target_ws)\n"," dataset_name = df_datasets[df_datasets['Dataset ID'] == row['Dataset Id']]['Dataset Name'].values[0]\n"," print(f'Rebinding report to {dataset_name} in {target_ws}')\n"," labs.report.report_rebind(report=row['Name'],dataset=dataset_name, report_workspace=target_ws, dataset_workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"06268ede-b795-493e-9a8d-772654ce7e20"},{"cell_type":"markdown","source":["##### Update data pipeline source & sink connections\n","\n","Support changes lakehouses, warehouses, notebooks and connections from source to target.
\n","Connections changes should be expressed as an array of tuples [{from_1:to_1},{from_N:to_N}]"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4ae65012-350c-40c0-a68a-4069c567a85f"},{"cell_type":"code","source":["from typing import Optional\n","from sempy_labs._helper_functions import (\n"," resolve_workspace_name_and_id,\n"," lro,\n"," _decode_b64,\n",")\n","import sempy_labs._icons as icons\n","\n","import base64\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","\n","def update_data_pipeline_definition(\n"," name: str, pipeline_content: dict, workspace: Optional[str] = None\n","):\n"," \"\"\"\n"," Updates an existing data pipeline with a new definition.\n","\n"," Parameters\n"," ----------\n"," name : str\n"," The name of the data pipeline.\n"," pipeline_content : dict\n"," The data pipeline content (not in Base64 format).\n"," workspace : str, default=None\n"," The name of the workspace.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," \"\"\"\n","\n"," (workspace, workspace_id) = resolve_workspace_name_and_id(workspace)\n"," client = fabric.FabricRestClient()\n"," pipeline_payload = base64.b64encode(json.dumps(pipeline_content).encode('utf-8')).decode('utf-8')\n"," pipeline_id = fabric.resolve_item_id(\n"," item_name=name, type=\"DataPipeline\", workspace=workspace\n"," )\n","\n"," request_body = {\n"," \"definition\": {\n"," \"parts\": [\n"," {\n"," \"path\": \"pipeline-content.json\",\n"," \"payload\": pipeline_payload,\n"," \"payloadType\": \"InlineBase64\"\n"," }\n"," ]\n"," }\n"," }\n","\n","\n"," response = client.post(\n"," f\"v1/workspaces/{workspace_id}/items/{pipeline_id}/updateDefinition\",\n"," json=request_body,\n"," )\n","\n"," lro(client, response, return_status_code=True)\n","\n"," print(\n"," f\"{icons.green_dot} The '{name}' pipeline was updated within the '{workspace}' workspace.\"\n"," )\n","\n","def _is_valid_uuid(\n"," guid: str,\n","):\n"," \"\"\"\n"," Validates if a string is a valid GUID in version 4\n","\n"," Parameters\n"," ----------\n"," guid : str\n"," GUID to be validated.\n","\n"," Returns\n"," -------\n"," bool\n"," Boolean that indicates if the string is a GUID or not.\n"," \"\"\"\n","\n"," try:\n"," UUID(str(guid), version=4)\n"," return True\n"," except ValueError:\n"," return False"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"bdd46d4a-ef58-4f9a-b2e8-a428361a17c1"},{"cell_type":"code","source":["import json\n","from jsonpath_ng import jsonpath, parse\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","source_ws = ''\n","target_ws = ''\n","\n","\n","# Swaps the connection properties of an activity belonging to the specified item type(s)\n","def swap_pipeline_connection(pl_json: dict, p_source_ws: str,p_target_ws: str, \n"," p_item_type: List =['DataWarehouse','Lakehouse','Notebook'], \n"," p_conn_id_from_to: Optional[List[Tuple[str,str]]]=[]):\n"," \n"," source_ws_id = fabric.resolve_workspace_id(source_ws)\n","\n"," target_ws_id = fabric.resolve_workspace_id(target_ws)\n","\n"," if 'Warehouse' in p_item_type or 'Lakehouse' in p_item_type:\n"," ls_expr = parse('$..linkedService')\n"," for endpoint_match in ls_expr.find(pl_json):\n"," if endpoint_match.value['properties']['type'] == 'DataWarehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Warehouse' in p_item_type:\n"," # only update the warehouse if it was located in the source workspace i.e. we will update the properties to the target workspace if the warehouse resided in the same workspace as the pipeline\n"," #print(endpoint_match.value)\n"," warehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," #print(warehouse_id)\n"," warehouse_endpoint = endpoint_match.value['properties']['typeProperties']['endpoint']\n"," #print(warehouse_endpoint)\n"," \n"," source_wh_name = fabric.resolve_item_name(item_id = warehouse_id,workspace=source_ws_id)\n"," #print(remote_wh_name)\n"," # find the warehouse id of the warehouse with the same name in the target workspace\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n"," # look up the connection string for the warehouse in the target workspace\n"," whurl = f\"v1/workspaces/{target_ws_id}/warehouses/{target_wh_id}\"\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['connectionString']\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_wh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," endpoint_match.value['properties']['typeProperties']['endpoint'] = lhconnStr\n"," #print(endpoint_match.value)\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," if endpoint_match.value['properties']['type'] == 'Lakehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Lakehouse' in p_item_type:\n"," #print(endpoint_match.value)\n"," lakehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," remote_lh_name = fabric.resolve_item_name(item_id = lakehouse_id,workspace=source_ws_id)\n"," # find the lakehouse id of the lakehouse with the same name in the target workspace\n"," target_lh_id = fabric.resolve_item_id(item_name = remote_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_lh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," # print(endpoint_match.value)\n","\n","\n"," if 'Notebook' in p_item_type: \n"," ls_expr = parse('$..activities')\n","\n"," for endpoint_match in ls_expr.find(pl_json):\n"," for activity in endpoint_match.value:\n"," #print(activity['type'])\n"," if activity['type']=='TridentNotebook' and 'Notebook' in p_item_type: #only update if the notebook was in the same workspace as the pipeline\n"," print('change from '+activity['typeProperties']['workspaceId'])\n"," source_nb_id = activity['typeProperties']['notebookId']\n"," source_nb_name = fabric.resolve_item_name(item_id = source_nb_id,workspace=source_ws_id)\n"," target_nb_id = fabric.resolve_item_id(item_name = source_nb_name,type='Notebook',workspace=target_ws_id)\n"," activity['typeProperties']['notebookId']=target_nb_id\n"," activity['typeProperties']['workspaceId']=target_ws_id\n"," print('to notebook '+ target_nb_id)\n"," #ls_expr.update(endpoint_match,endpoint_match.value)\n","\n"," if p_conn_from_to:\n"," for ti_conn_from_to in p_conn_from_to:\n"," if not _is_valid_uuid(ti_conn_from_to[0]):\n"," print('Connection from is string '+ str(ti_conn_from_to[0]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[0]] \n"," connId_from = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_from = ti_conn_from_to[0]\n","\n"," if not _is_valid_uuid(ti_conn_from_to[1]):\n"," print('Connection from is string '+ str(ti_conn_from_to[1]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[1]] \n"," connId_to = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_to = ti_conn_from_to[1]\n","\n"," ls_expr = parse('$..externalReferences')\n"," for externalRef in ls_expr.find(pl_json):\n"," if externalRef.value['connection']==connId_from:\n"," print('Changing connection from '+str(connId_from))\n"," externalRef.value['connection']=connId_to\n"," ls_expr.update(externalRef,externalRef.value)\n"," print('to '+str(connId_to))\n","\n"," return pl_json\n","\n","\n","\n","# loading a dataframe of connections to perform an ID lookup if required \n","df_conns = labs.list_connections()\n","\n","df_pipeline = labs.list_data_pipelines(target_ws)\n","for index, row in df_pipeline.iterrows():\n"," #print(labs.get_data_pipeline_definition(row['Data Pipeline Name'],target_ws))\n"," if row['Data Pipeline Name']=='plRecover_WH6_Prod2_Warehouse2_fixed':\n"," pipeline_json = json.loads(labs.get_data_pipeline_definition(row['Data Pipeline Name'],source_ws))\n","\n"," p_new_json = swap_pipeline_connection(pipeline_json, source_ws,target_ws,\n"," ['DataWarehouse','Lakehouse','Notebook'],\n"," [p_connections_from_to]) \n"," #print(json.dumps(pipeline_json, indent=4))\n"," \n"," update_data_pipeline_definition(name=row['Data Pipeline Name'],pipeline_content=pipeline_json, workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"079958e8-2880-484a-a994-41caf47e747e"},{"cell_type":"markdown","source":["##### Commit changes made above to Git"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"44174276-b983-4e80-9451-0afb9589cf1f"},{"cell_type":"code","source":["labs.commit_to_git(comment='Initial', workspace=target_ws)"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9a5c3d84-f71d-4348-b419-c4953ac9e1d0"}],"metadata":{"kernel_info":{"name":"synapse_pyspark"},"kernelspec":{"name":"synapse_pyspark","language":"Python","display_name":"Synapse PySpark"},"language_info":{"name":"python"},"microsoft":{"language":"python","language_group":"synapse_pyspark","ms_spell_check":{"ms_spell_check_language":"en"}},"widgets":{},"nteract":{"version":"nteract-front-end@1.0.0"},"synapse_widget":{"version":"0.1","state":{}},"spark_compute":{"compute_id":"/trident/default","session_options":{"conf":{"spark.synapse.nbs.session.timeout":"1200000"}}},"dependencies":{"lakehouse":{}}},"nbformat":4,"nbformat_minor":5} \ No newline at end of file +{"cells":[{"cell_type":"markdown","source":["##### Branch out to new workspace notebook - post activity\n","\n","After cloning a workspace, this notebook will reconfigure any references to the old workspace by rebinding them to the new workspace. \n","\n","For example a pipeline referencing a warehouse or a default lakehouse of a notebook.\n","\n","This notebook runs post activity tasks can be run after [branch out to new workspace functionality](https://blog.fabric.microsoft.com/en-us/blog/introducing-new-branching-capabilities-in-fabric-git-integration) or the [custom AzDO script](https://github.com/microsoft/fabric-toolbox/blob/main/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py).\n","\n","Summary of post activities in order:\n","
    \n","
  • Default lakehouses and warehouse are updated to local lakehouse/warehouses
  • \n","
  • Either creates shortcuts in local lakehouse back to tables in the source lakehouse, or copies the data from source lakehouse. Set via parameter below.
  • \n","
  • Copy warehouse data. Set via parameter below
  • \n","
  • Changes directlake semantic model connections for semantic models to \"local\" lakehouse/warehouse
  • \n","
  • Rebinds reports to \"local\" semantic models
  • \n","
  • Changes pipeline lakehouse/warehouse references to local item
  • \n","
  • Ability to swap connections in pipelines from old to new
  • \n","
  • Commit changes to git
  • \n","
\n","\n","Requirements:\n","
    \n","
  • Requires Semantic Link Labs installed by pip install below or added to environment library.
  • \n","
  • Requires JmesPath library for data pipeline JSON manipulation i.e. connection swaps.
  • \n","
\n","\n","Limitations of current script:\n","\n","
    \n","
  • Does not recreate item shares or external shortcuts
  • \n","
  • Does not re-apply lakehouse SQL Endpoint or Warehouse object/row/column level security
  • \n","
  • Does not recreate data access roles in Lakehouse
  • \n","
  • Untested with Lakehouses where with schema support enabled
  • \n","
\n","\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a98b6d0a-7a36-4116-ab0d-aa70144eb737"},{"cell_type":"markdown","source":["##### Install semantic link labs to support advanced functionality\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/index.html\n","https://github.com/microsoft/semantic-link-labs/blob/main/README.md\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"3b887bd6-a9c9-430f-b58f-b58a93f5ce29"},{"cell_type":"code","source":["%pip -q install semantic-link-labs\n"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":3,"statement_ids":[3],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:19:28.0006255Z","session_start_time":"2025-02-14T16:19:28.002459Z","execution_start_time":"2025-02-14T16:19:44.8101222Z","execution_finish_time":"2025-02-14T16:19:59.5859051Z","parent_msg_id":"1be3b208-385f-4a2e-a567-f3f29f01fab0"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 3, Finished, Available, Finished)"},"metadata":{}},{"output_type":"stream","name":"stdout","text":["Note: you may need to restart the kernel to use updated packages.\n"]}],"execution_count":1,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":true},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"1b03316d-c088-4a0e-a2f0-44d45d112121"},{"cell_type":"markdown","source":["##### Install Jmespath to make data pipeline changes such as updating linked notebooks, warehouses and lakehouses "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"8a74ed11-dd64-43bb-a735-906a947c8666"},{"cell_type":"code","source":["%pip install jmespath"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":8,"statement_ids":[4,5,6,7,8],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:19:59.5881976Z","session_start_time":null,"execution_start_time":"2025-02-14T16:19:59.7550889Z","execution_finish_time":"2025-02-14T16:20:12.6668949Z","parent_msg_id":"cdb7a488-6f67-45ab-97d6-b49e3b450225"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 8, Finished, Available, Finished)"},"metadata":{}},{"output_type":"stream","name":"stdout","text":["Collecting jmespath\n Downloading jmespath-1.0.1-py3-none-any.whl.metadata (7.6 kB)\nDownloading jmespath-1.0.1-py3-none-any.whl (20 kB)\nInstalling collected packages: jmespath\nSuccessfully installed jmespath-1.0.1\n\n\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.0.1\u001b[0m\n\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpython -m pip install --upgrade pip\u001b[0m\nNote: you may need to restart the kernel to use updated packages.\nWarning: PySpark kernel has been restarted to use updated packages.\n\n"]}],"execution_count":2,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"c68be6ba-7648-457f-af82-f1987d12d7f7"},{"cell_type":"markdown","source":["##### Set parameters\n","Before running this notebook ensure these parameters are set correctly. If necessary these can be passed in via a data factory pipeline"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"dee81614-b92b-4242-890a-b11f97b1a640"},{"cell_type":"code","source":["source_ws = ''\n","target_ws = ''\n","\n","# Either copy lakehouse data or create shortcuts, set at most one of these to True \n","copy_lakehouse_data = False\n","create_lakehouse_shortcuts = True\n","\n","# Option to copy warehouse data if required\n","copy_warehouse_data = True\n","\n","# If false then shortcuts will be created. If you wish to create shortcuts based on a pattern match please set the param below\n","# enter pattern match for creating shortcuts - see https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py \n","PATTERN_MATCH = [\"*\"]\n","_inlineInstallationEnabled = True\n","\n","# Set connections to be replaced from previous name or ID to new name or ID.\n","p_connections_from_to = ()#('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726'),('4498340c-27cf-4c6e-a025-00e5de6b0726','https://api.fabric.microsoft.com/v1/workspaces/ admin'),('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726')"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":10,"statement_ids":[10],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:20:16.4107096Z","session_start_time":null,"execution_start_time":"2025-02-14T16:20:23.1088616Z","execution_finish_time":"2025-02-14T16:20:23.3970563Z","parent_msg_id":"497ea86f-6f85-4631-a26c-8d36b57cc7be"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 10, Finished, Available, Finished)"},"metadata":{}}],"execution_count":3,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"tags":["parameters"]},"id":"90efaa4f-846d-4924-900e-258837a3467d"},{"cell_type":"markdown","source":["##### Library imports and fabric rest client setup\n","\n","https://learn.microsoft.com/en-us/python/api/semantic-link-sempy/sempy.fabric.fabricrestclient"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4fb01e1d-ec4e-4c69-b544-66f6d8c5a475"},{"cell_type":"code","source":["import pandas as pd\n","import datetime, time\n","import re,json, fnmatch,os\n","import requests, base64\n","import sempy\n","import sempy.fabric as fabric\n","from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException\n","from pyspark.sql import DataFrame\n","from pyspark.sql.functions import col,current_timestamp,lit\n","import sempy_labs as labs\n","from sempy_labs import migration, directlake\n","from sempy_labs import lakehouse as lake\n","from sempy_labs import report as rep\n","from sempy_labs.tom import connect_semantic_model\n","\n","# instantiate the Fabric rest client\n","client = fabric.FabricRestClient()\n","\n","# get the current workspace ID based on the context of where this notebook is run from\n","thisWsId = notebookutils.runtime.context['currentWorkspaceId']\n","thisWsName = notebookutils.runtime.context['currentWorkspaceName']\n","\n","source_ws_id = fabric.resolve_workspace_id(source_ws)\n","target_ws_id = fabric.resolve_workspace_id(target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"391624c1-b299-452d-9ebf-f32626d49970"},{"cell_type":"markdown","source":["##### Update default and attached lakehouses/warehouses for notebooks\n","\n","Update notebook dependencies based on but now supports T-SQL notebooks:\n","https://github.com/PowerBiDevCamp/FabConWorkshopSweden/blob/main/DemoFiles/GitUpdateWorkspace/updateWorkspaceDependencies_v1.ipynb\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"aaae8a08-588d-4dd8-9d2c-2200b7a88d30"},{"cell_type":"code","source":["for notebook in notebookutils.notebook.list(workspaceId=target_ws_id):\n"," updates = False\n"," if notebook.displayName == 'ETL':#True: #notebook.displayName == 'T-SQL_Notebook': #notebook.displayName != 'Create Feature Branch':\n","\n"," # Get the current notebook definition\n"," json_payload = json.loads(notebookutils.notebook.getDefinition(notebook.displayName,workspaceId=source_ws_id))\n"," #print(json.dumps(json_payload, indent=4))\n"," # Check for any attached lakehouses\n"," if 'dependencies' in json_payload['metadata'] \\\n"," and 'lakehouse' in json_payload['metadata']['dependencies'] \\\n"," and json_payload['metadata'][\"dependencies\"][\"lakehouse\"] is not None:\n"," # Extract attached and default lakehouses\n"," current_lakehouse = json_payload['metadata']['dependencies']['lakehouse']\n"," # if default lakehouse setting exists\n"," if 'default_lakehouse_name' in current_lakehouse:\n"," print(f\"Updating notebook {notebook.displayName} with new default lakehouse: {current_lakehouse['default_lakehouse_name']} in workspace {target_ws}\")\n"," source_lh_name = fabric.resolve_item_name(item_id = current_lakehouse['default_lakehouse'],type='Lakehouse',workspace=source_ws_id)\n"," current_lakehouse['default_lakehouse'] = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," current_lakehouse['default_lakehouse_workspace_id'] = target_ws_id\n"," updates = True\n"," # loop through all attached lakehouess\n"," for lakehouse in json_payload['metadata']['dependencies']['lakehouse']['known_lakehouses']:\n"," source_lh_id = lakehouse['id']\n"," # find source lakehouse name\n"," source_lh_name = fabric.resolve_item_name(item_id = lakehouse['id'],type='Lakehouse',workspace=source_ws_id)\n"," # find target lakehouse id based on name\n"," target_lh_id = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," lakehouse['id'] = target_lh_id\n"," print(f'Updating attached lakehouse {source_lh_name} from {source_lh_id} to target ID {target_lh_id}')\n"," updates = True\n","\n"," if 'dependencies' in json_payload['metadata'] and 'warehouse' in json_payload['metadata']['dependencies']:\n"," # Fetch existing details\n"," current_warehouse = json_payload['metadata']['dependencies']['warehouse']\n"," current_warehouse_id = current_warehouse['default_warehouse']\n"," source_wh_name = fabric.resolve_item_name(item_id = current_warehouse_id,workspace=source_ws_id)\n"," #print('Source warehouse name is ' + source_wh_name)\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n","\n"," if 'default_warehouse' in current_warehouse:\n"," #json_payload['metadata']['dependencies']['warehouse'] = {}\n"," print(f\"Attempting to update notebook {notebook.displayName} with new default warehouse: {target_wh_id} in {target_ws}\")\n"," \n"," json_payload['metadata']['dependencies']['warehouse']['default_warehouse'] = target_wh_id\n"," for warehouse in json_payload['metadata']['dependencies']['warehouse']['known_warehouses']:\n"," if warehouse['id'] == current_warehouse_id:\n"," warehouse['id'] = target_wh_id\n"," updates = True\n","\n"," if updates:\n"," notebookutils.notebook.updateDefinition(\n"," name = notebook.displayName,\n"," content = json.dumps(json_payload),\n"," workspaceId = target_ws_id\n"," )\n"," \n"," print(f\"Updated notebook {notebook.displayName} in {target_ws}\")\n","\n"," else:\n"," print(f'No default lakehouse set for notebook {notebook.displayName}, ignoring.')"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"5c60b5d2-f83c-46f8-9870-9fd609166b67"},{"cell_type":"markdown","source":["##### Run the below cell - contains utility functions to support lakehouse and warehouse initialisation\n","\n","Shortcut creator:\n","https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a47a56df-219d-4d6c-b950-491909638deb"},{"cell_type":"code","source":["##### \n","### Shortcut utility function \n","####\n","\n","# Extract workspace_id, item_id and path from a onelake URI\n","def extract_onelake_https_uri_components(uri):\n"," # Define a regular expression to match any string between slashes and capture the final path element(s) without the leading slash\n"," pattern = re.compile(r\"abfss://([^@]+)@[^/]+/([^/]+)/(.*)\")\n"," match = pattern.search(uri)\n"," if match:\n"," workspace_id, item_id, path = match.groups()\n"," return workspace_id, item_id, path\n"," else:\n"," return None, None, None\n","\n","\n","def is_valid_onelake_uri(uri: str) -> bool:\n"," workspace_id, item_id, path = extract_onelake_https_uri_components(uri)\n"," if \"abfss://\" not in uri or workspace_id is None or item_id is None or path is None:\n"," return False\n","\n"," return True\n","\n","\n","def get_last_path_segment(uri: str):\n"," path = uri.split(\"/\") # Split the entire URI by '/'\n"," return path[-1] if path else None\n","\n","\n","def is_delta_table(uri: str):\n"," delta_log_path = os.path.join(uri, \"_delta_log\")\n"," return mssparkutils.fs.exists(delta_log_path)\n","\n","\n","def get_onelake_shorcut(workspace_id: str, item_id: str, path: str, name: str):\n"," shortcut_uri = (\n"," f\"v1/workspaces/{workspace_id}/items/{item_id}/shortcuts/{path}/{name}\"\n"," )\n"," result = client.get(shortcut_uri).json()\n"," return result\n","\n","\n","def is_folder_matching_pattern(path: str, folder_name: str, patterns: []):\n"," if folder_name in patterns:\n"," return True\n"," else:\n"," for pattern in patterns:\n"," if fnmatch.fnmatch(folder_name, pattern):\n"," return is_delta_table(path)\n","\n"," return False\n","\n","\n","def get_matching_delta_tables_uris(uri: str, patterns: []) -> []:\n"," # Use a set to avoid duplicates\n"," matched_uris = set()\n"," files = mssparkutils.fs.ls(uri)\n"," folders = [item for item in files if item.isDir]\n","\n"," # Filter folders to only those that matches the pattern and is a delta table\n"," matched_uris.update(\n"," folder.path\n"," for folder in folders\n"," if is_folder_matching_pattern(folder.path, folder.name, patterns)\n"," )\n","\n"," return matched_uris\n","\n","\n","def create_onelake_shorcut(source_uri: str, dest_uri: str):\n"," src_workspace_id, src_item_id, src_path = extract_onelake_https_uri_components(\n"," source_uri\n"," )\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri\n"," )\n","\n"," name = get_last_path_segment(source_uri)\n"," dest_uri_joined = os.path.join(dest_uri, name)\n","\n"," # If the destination path already exists, return without creating shortcut\n"," if mssparkutils.fs.exists(dest_uri_joined):\n"," print(f\"Destination already exists: {dest_uri_joined}\")\n"," return None\n","\n"," request_body = {\n"," \"name\": name,\n"," \"path\": dest_path,\n"," \"target\": {\n"," \"oneLake\": {\n"," \"itemId\": src_item_id,\n"," \"path\": src_path,\n"," \"workspaceId\": src_workspace_id,\n"," }\n"," },\n"," }\n","\n"," shortcut_uri = f\"v1/workspaces/{dest_workspace_id}/items/{dest_item_id}/shortcuts\"\n"," print(f\"Creating shortcut: {shortcut_uri}/{name}..\")\n"," try:\n"," client.post(shortcut_uri, json=request_body)\n"," except FabricHTTPException as e:\n"," print(e)\n"," return None\n","\n"," return get_onelake_shorcut(dest_workspace_id, dest_item_id, dest_path, name)\n"," \n","\n","####\n","## Copy lakehouse and warehouse utility functions\n","####\n","\n","def get_lh_object_list(base_path,data_types = ['Tables', 'Files'])->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a lakehouse\n"," adapted from https://fabric.guru/getting-a-list-of-folders-and-delta-tables-in-the-fabric-lakehouse\n"," This function will return a pandas dataframe containing names and abfss paths of each folder for Files and Tables\n"," '''\n"," #data_types = ['Tables', 'Files'] #for if you want a list of files and tables\n"," #data_types = ['Tables'] #for if you want a list of tables\n","\n"," df = pd.concat([\n"," pd.DataFrame({\n"," 'name': [item.name for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," 'type': data_type[:-1].lower() , \n"," 'src_path': [item.path for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," }) for data_type in data_types], ignore_index=True)\n","\n"," return df\n","\n","def get_wh_object_list(schema_list,base_path)->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a warehouse by schema\n"," '''\n"," data_type = 'Tables'\n"," dfs = []\n","\n"," for schema_prefix in schema_list:\n"," if notebookutils.fs.exists(f'{base_path}/{data_type}/{schema_prefix}/'):\n"," items = notebookutils.fs.ls(f'{base_path}/{data_type}/{schema_prefix}/')\n"," if items: # Check if the list is not empty\n"," df = pd.DataFrame({\n"," 'schema': schema_prefix,\n"," 'name': [item.name for item in items],\n"," 'type': data_type[:-1].lower(),\n"," 'src_path': [item.path for item in items],\n"," })\n"," dfs.append(df)\n","\n"," if dfs: # Check if the list of dataframes is not empty\n"," df = pd.concat(dfs, ignore_index=True)\n"," else:\n"," df = pd.DataFrame() # Return an empty dataframe if no dataframes were created\n","\n"," return df\n","\n","def copy_lh_objects(table_list,workspace_src,workspace_tgt,lakehouse_src,lakehouse_tgt,fastcopy=True,usingIDs=False)->pd.DataFrame:\n"," # declare an array to keep the instrumentation\n"," cpresult = []\n"," # loop through all the tables to extract the source path \n"," for table in table_list.src_path:\n"," source = table\n"," destination = source.replace(f'abfss://{workspace_src}', f'abfss://{workspace_tgt}')\n"," if usingIDs:\n"," destination = destination.replace(f'{lakehouse_src}', f'{lakehouse_tgt}')\n"," else:\n"," destination = destination.replace(f'{lakehouse_src}.Lakehouse', f'{lakehouse_tgt}.Lakehouse')\n"," start_time = datetime.datetime.now()\n"," if notebookutils.fs.exists(destination):\n"," notebookutils.fs.rm(destination, True)\n"," if fastcopy:\n"," # use fastcopy util which is a python wrapper to azcopy\n"," notebookutils.fs.fastcp(source+'/*', destination+'/', True)\n"," else:\n"," notebookutils.fs.cp(source, destination, True)\n","\n"," # recording the timing and add it to the results list\n"," end_time = datetime.datetime.now()\n"," copyreslist = [source, destination, start_time.strftime(\"%Y-%m-%d %H:%M:%S\"), end_time.strftime(\"%Y-%m-%d %H:%M:%S\"), str((end_time - start_time).total_seconds())]\n"," cpresult.append(copyreslist)\n"," return pd.DataFrame(cpresult,columns =['source--------------------------------------','target--------------------------------------','start------------','end_time------------','elapsed seconds----'])\n","\n","def createDWrecoverypl(ws_id,pl_name = 'Recover_Warehouse_Data_From_DR'):\n"," client = fabric.FabricRestClient()\n","\n"," dfurl= \"v1/workspaces/\"+ ws_id + \"/items\"\n"," payload = { \n"," \"displayName\": pl_name, \n"," \"type\": \"DataPipeline\", \n"," \"definition\": { \n"," \"parts\": [ \n"," { \n"," \"path\": \"pipeline-content.json\", \n"," \"payload\": \"ewogICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgImFjdGl2aXRpZXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICJuYW1lIjogIkl0ZXJhdGVTY2hlbWFUYWJsZXMiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiRm9yRWFjaCIsCiAgICAgICAgICAgICAgICAiZGVwZW5kc09uIjogW10sCiAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy50YWJsZXNUb0NvcHkiLAogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgImJhdGNoQ291bnQiOiAyMCwKICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdGllcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiQ29weVdhcmVob3VzZVRhYmxlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJDb3B5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlcGVuZGVuY3lDb25kaXRpb25zIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlN1Y2NlZWRlZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicG9saWN5IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0aW1lb3V0IjogIjAuMTI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZXRyeSI6IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJldHJ5SW50ZXJ2YWxJblNlY29uZHMiOiAzMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZVNvdXJjZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJxdWVyeVRpbWVvdXQiOiAiMDI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicGFydGl0aW9uT3B0aW9uIjogIk5vbmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwN2EwMzAwNl9kMWI2XzRhMzlfYmViMV8wYmJhMmFhZjVmZjciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLmxha2Vob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy5sYWtlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAY29uY2F0KGNvbmNhdChpdGVtKCkuc2NoZW1hLCdfJyksaXRlbSgpLm5hbWUpIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRXhwcmVzc2lvbiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzaW5rIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlU2luayIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhbGxvd0NvcHlDb21tYW5kIjogdHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRhYmxlT3B0aW9uIjogImF1dG9DcmVhdGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwYzAzMTIzYV9kMzEyXzQ2YzRfYThlN181YjRjYWQ4ZjEyZDciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLndhcmVob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53YXJlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAaXRlbSgpLm5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVuYWJsZVN0YWdpbmciOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0cmFuc2xhdG9yIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJUYWJ1bGFyVHJhbnNsYXRvciIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvbiI6IHRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvblNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFsbG93RGF0YVRydW5jYXRpb24iOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRyZWF0Qm9vbGVhbkFzTnVtYmVyIjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHNjaGVtYSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRlbmN5Q29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJTdWNjZWVkZWQiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInBvbGljeSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhcmlhYmxlTmFtZSI6ICJUYWJsZW5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogIkBpdGVtKCkubmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkV4cHJlc3Npb24iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJTZXQgc2NoZW1hIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb2xpY3kiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZU91dHB1dCI6IGZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzZWN1cmVJbnB1dCI6IGZhbHNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YXJpYWJsZU5hbWUiOiAiU2NoZW1hbmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQGl0ZW0oKS5zY2hlbWEiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgInBhcmFtZXRlcnMiOiB7CiAgICAgICAgICAgICJsYWtlaG91c2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjBmMGY2YjdjLTE3NjEtNDFlNi04OTZlLTMwMDE0ZjE2ZmY2ZCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInRhYmxlc1RvQ29weSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogImFycmF5IiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiBbCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkRhdGUiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiR2VvZ3JhcGh5IgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkhhY2tuZXlMaWNlbnNlIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIk1lZGFsbGlvbiIKICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6ICJkYm8iLAogICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJUaW1lIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIlRyaXAiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiV2VhdGhlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ3b3Jrc3BhY2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjE1MDExNDNjLTI3MmYtNGEyZi05NzZhLTdlNTU5NzFlNGMyYiIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIndhcmVob3VzZUlkIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNGQxYmQ5NTEtOTlkZS00YmQ3LWI3YmMtNzFjOGY1NmRiNDExIgogICAgICAgICAgICB9LAogICAgICAgICAgICAid2FyZWhvdXNlQ29ublN0ciI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjcyd3diaXZpMnViZWpicnRtdGFobzMyYjR5LWhxa2FjZmpwZTR4dXZmM2twemt6b2hzbWZtLmRhdGF3YXJlaG91c2UuZmFicmljLm1pY3Jvc29mdC5jb20iCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYWtlaG91c2VDb25uU3RyIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNzJ3d2JpdmkydWJlamJydG10YWhvMzJiNHktaHFrYWNmanBlNHh1dmYza3B6a3pvaHNtZm0uZGF0YXdhcmVob3VzZS5mYWJyaWMubWljcm9zb2Z0LmNvbSIKICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInZhcmlhYmxlcyI6IHsKICAgICAgICAgICAgIlRhYmxlbmFtZSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogIlN0cmluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIlNjaGVtYW5hbWUiOiB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJTdHJpbmciCiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJsYXN0TW9kaWZpZWRCeU9iamVjdElkIjogIjRhYTIwYWY3LTk0YmQtNDM0OC1iZWY4LWY4Y2JjZDg0MGQ1MSIsCiAgICAgICAgImxhc3RQdWJsaXNoVGltZSI6ICIyMDI0LTExLTEzVDE1OjUyOjUyWiIKICAgIH0KfQ==\", \n"," \"payloadType\": \"InlineBase64\" \n"," } \n"," ] \n"," } \n","} \n"," \n"," response = json.loads(client.post(dfurl,json= payload).content)\n"," return response['id']\n","\n","def getItemId(wks_id,itm_name,itm_type):\n"," df = fabric.list_items(type=None,workspace=wks_id)\n"," #print(df)\n"," if df.empty:\n"," return 'NotExists'\n"," else:\n"," #display(df)\n"," #print(df.query('\"Display Name\"=\"'+itm_name+'\"'))\n"," if itm_type != '':\n"," newdf= df.loc[(df['Display Name'] == itm_name) & (df['Type'] == itm_type)]['Id']\n"," else:\n"," newdf= df.loc[(df['Display Name'] == itm_name)]['Id'] \n"," if newdf.empty:\n"," return 'NotExists'\n"," else:\n"," return newdf.iloc[0]\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":true,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"e46210e9-58c9-483a-84ae-bbdc2ad1c37f"},{"cell_type":"markdown","source":["##### Either create shortcuts from source to target lakehouse(s) or copy data\n","\n","Loops through lakehouse(s) in the target workspace and either populates them with shortcuts or data\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a15065f3-670d-4bc9-b337-51709f6cdb1f"},{"cell_type":"code","source":["df_lhs = labs.list_lakehouses(source_ws)\n","for index, row in df_lhs.iterrows():\n","\n","\n"," if copy_lakehouse_data:\n"," lh_name= row['Lakehouse Name']\n"," if lh_name.find('temp')==-1:\n"," # Gathers the list of tables and source paths to be copied into the target lakehouse \n"," src_path = f'abfss://{source_ws}@onelake.dfs.fabric.microsoft.com/{lh_name}.Lakehouse'\n","\n"," table_list = get_lh_object_list(src_path)\n"," print(f'Attempting to copy table data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n"," display(table_list)\n","\n"," #print('Copy Lakehouse Delta tables...')\n"," res = copy_lh_objects(table_list[table_list['type']=='table'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," # Copy files\n"," print(f'Attempting to copy file data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n","\n"," #print('Copy Lakehouse files...')\n"," res = copy_lh_objects(table_list[table_list['type']=='file'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," print('Done.')\n","\n"," else:\n"," # fetch ID of source lakehouse based on name and workspace\n"," source_lh_id = row['Lakehouse ID']\n"," target_lh_id = fabric.resolve_item_id(\n"," item_name=row['Lakehouse Name'], type=\"Lakehouse\", workspace=target_ws\n"," )\n","\n"," SOURCE_URI = f\"abfss://{source_ws_id}@onelake.dfs.fabric.microsoft.com/{source_lh_id}/Tables\"\n"," DEST_URI = f\"abfss://{target_ws_id}@onelake.dfs.fabric.microsoft.com/{target_lh_id}/Tables\"\n","\n"," if PATTERN_MATCH is None or len(PATTERN_MATCH) == 0:\n"," raise TypeError(\"Argument 'PATTERN_MATCH' should be a valid list of patterns or [\"*\"] to match everything\")\n","\n"," # Collect created shortcuts\n"," result = []\n","\n"," # If either URI's are invalid, just return\n"," if not is_valid_onelake_uri(SOURCE_URI) or not is_valid_onelake_uri(DEST_URI):\n"," print(\n"," \"invalid URI's provided. URI's should be in the form: abfss://@onelake.dfs.fabric.microsoft.com//\"\n"," )\n"," else:\n"," # Remove any trailing '/' from uri's\n"," source_uri_addr = SOURCE_URI.rstrip(\"/\")\n"," dest_uri_addr = DEST_URI.rstrip(\"/\")\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri_addr\n"," )\n","\n"," # If we are not shortcutting to a managed table folder or\n"," # the source uri is a delta table, just shortcut it 1-1.\n"," if not dest_path.startswith(\"Tables\") or is_delta_table(source_uri_addr):\n"," shortcut = create_onelake_shorcut(source_uri_addr, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," else:\n"," # If source is not a delta table, and destination is managed table folder:\n"," # Iterate over source folders and create table shortcuts @ destination\n"," for delta_table_uri in get_matching_delta_tables_uris(\n"," source_uri_addr, PATTERN_MATCH\n"," ):\n"," shortcut = create_onelake_shorcut(delta_table_uri, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," print(result)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"8bec3fbc-4a75-4ba3-86d5-0620ec504a8f"},{"cell_type":"markdown","source":["##### Copy warehouse data via parameterised pipeline\n","\n","Loop through all warehouses and copy the data"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"f198b816-77e9-4f04-9139-d78237bedc72"},{"cell_type":"code","source":["p_logging_verbose = True\n","df_warehouses = (labs.list_warehouses(target_ws))\n","display(df_warehouses)\n","for index, row in df_warehouses.iterrows():\n"," source_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],source_ws_id)\n"," target_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],target_ws_id)\n"," \n"," src_path = f'abfss://'+source_ws_id+'@onelake.dfs.fabric.microsoft.com/'+source_wh_id\n"," tgt_path = f'abfss://'+target_ws_id+'@onelake.dfs.fabric.microsoft.com/'+target_wh_id\n","\n"," # extract the list of schemas per data \n"," schema_list = get_lh_object_list(src_path,['Tables'])\n"," # extract a list of warehouse objects per schema and store in a list\n"," table_list = get_wh_object_list(schema_list['name'],src_path)\n"," \n"," # create a temporary staging lakehouse per warehouse to create shortcuts into, \n"," # which point back to original warehouse data currently in the DR storage account\n"," lhname = 'temp_rlh_' + source_ws+'_'+row['Warehouse Name']\n"," # check if it exists before attempting create\n"," if p_logging_verbose:\n"," print('Checking whether the temporary lakehouse \"'+ lhname +'\" exists in workspace '+target_ws+'...')\n"," temp_lh_id = getItemId(target_ws_id,lhname,'Lakehouse')\n"," if temp_lh_id == 'NotExists':\n"," lhname = lhname[:256] # lakehouse name should not exceed 256 characters\n"," payload = payload = '{\"displayName\": \"' + lhname + '\",' \\\n"," + '\"description\": \"Interim staging lakehouse for primary warehouse recovery: ' \\\n"," + source_ws+'_'+row['Warehouse Name'] + 'into workspace '+ target_ws + '(' + target_ws +')\"}'\n"," try:\n"," lhurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses\"\n"," lhresponse = client.post(lhurl,json= json.loads(payload))\n"," temp_lh_id = lhresponse.json()['id']\n"," if p_logging_verbose:\n"," print('Temporary lakehouse \"'+ lhname +'\" created with Id ' + temp_lh_id + ': ' + str(lhresponse.status_code) + ' ' + str(lhresponse.text))\n"," except Exception as error:\n"," print(error.errorCode)\n"," else:\n"," if p_logging_verbose:\n"," print('Temporary lakehouse '+lhname+' (' + temp_lh_id + ') already exists.')\n"," \n"," time.sleep(60) # waiting for temporary lakehouse to provision completely \n","\n"," # Create shortcuts for every table in the format of schema_table under the tables folder\n"," for index,itable in table_list.iterrows():\n"," shortcutExists=False\n"," # Check if shortcut exists\n"," try:\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts/Tables/\"+itable['schema']+'_'+itable['name']\n"," tlhresponse = client.get(url)\n"," shortcutExists = True\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] +' already exists')\n"," except Exception as error:\n"," shortcutExists = False \n","\n"," if not shortcutExists: \n"," # Create shortcuts - one per table per schema\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts\"\n"," scpayload = '{' \\\n"," '\"path\": \"Tables/\",' \\\n"," '\"name\": \"'+itable['schema']+'_'+itable['name']+'\",' \\\n"," '\"target\": {' \\\n"," '\"oneLake\": {' \\\n"," '\"workspaceId\": \"' + source_ws_id + '\",' \\\n"," '\"itemId\": \"'+ source_wh_id +'\",' \\\n"," '\"path\": \"/Tables/' + itable['schema']+'/'+itable['name'] + '\"' \\\n"," '}}}' \n"," try:\n"," #print(scpayload) \n"," shctresponse = client.post(url,json= json.loads(scpayload))\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] + ' created.' )\n","\n"," except Exception as error:\n"," print('Error creating shortcut '+itable['schema']+'_'+itable['name']+' due to '+str(error) + ':' + shctresponse.text)\n"," \n"," recovery_pipeline_prefix= 'plRecover_WH' \n"," # recovery pipeline name should not exceed 256 characters\n"," recovery_pipeline = recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'][:256]\n"," if p_logging_verbose:\n"," print('Attempting to deploy a copy pipeline in the target workspace to load the target warehouse tables from the shortcuts created above... ')\n"," # Create the pipeline in the target workspace that loads the target warehouse from shortcuts created above \n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," #print(plid)\n"," if plid == 'NotExists':\n"," plid = createDWrecoverypl(target_ws_id,recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'])\n"," if p_logging_verbose:\n"," print('Recovery pipeline ' + recovery_pipeline + ' created with Id '+plid)\n"," else:\n"," if p_logging_verbose:\n"," print('Datawarehouse recovery pipeline \"' + recovery_pipeline + '\" ('+plid+') already exist in workspace \"'+target_ws + '\" ('+target_ws_id+')') \n"," print('\\n')\n","\n"," tablesToCopyParam = table_list[['schema','name']].to_json( orient='records')\n"," # ensure the temporary lakehouse exists\n","\n"," # obtain the connection string for the lakehouse to pass to the copy pipeline\n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses/\" + temp_lh_id\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['sqlEndpointProperties']['connectionString']\n","\n"," # get the SQLEndpoint ID of the lakehouse to pass to the copy pipeline\n"," items = fabric.list_items(workspace=target_ws_id)\n"," print(items)\n"," temp_lh_sqle_id = items[(items['Type'] == 'SQLEndpoint') & (items['Display Name']==lhname)]['Id'].values[0]\n","\n","\n"," # obtain the connection string for the warehouse to pass to the copy pipeline \n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/warehouses/\" + target_wh_id\n"," whresponse = client.get(whurl)\n"," whconnStr = whresponse.json()['properties']['connectionInfo']\n","\n"," # obtain the pipeline id created to recover this warehouse\n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," if plid == 'NotExists':\n"," print('Error: Could not execute pipeline '+recovery_pipeline+ ' as the ID could not be obtained ')\n"," else:\n"," # pipeline url including pipeline Id unique to each warehouse\n"," plurl = 'v1/workspaces/'+target_ws_id+'/items/'+plid+'/jobs/instances?jobType=Pipeline'\n"," #print(plurl)\n","\n"," payload_data = '{' \\\n"," '\"executionData\": {' \\\n"," '\"parameters\": {' \\\n"," '\"lakehouseId\": \"' + temp_lh_sqle_id + '\",' \\\n"," '\"tablesToCopy\": ' + tablesToCopyParam + ',' \\\n"," '\"workspaceId\": \"' + target_ws_id +'\",' \\\n"," '\"warehouseId\": \"' + target_wh_id + '\",' \\\n"," '\"lakehouseConnStr\": \"' + lhconnStr + '\",' \\\n"," '\"warehouseConnStr\": \"' + whconnStr + '\"' \\\n"," '}}}'\n"," #print(payload_data)\n"," plresponse = client.post(plurl, json=json.loads(payload_data))\n"," if p_logging_verbose:\n"," print(str(plresponse.status_code)) \n","print('Done')\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"57dafef7-17a2-475f-9e62-eecc6660440c"},{"cell_type":"markdown","source":["##### Update directlake model lakehouse/warehouse connection\n","\n","https://semantic-link-labs.readthedocs.io/en/stable/sempy_labs.directlake.html#sempy_labs.directlake.update_direct_lake_model_connection "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"cc97be77-116e-4cde-bdc6-2971ab98a083"},{"cell_type":"code","source":["\n","df_datasets = fabric.list_datasets(target_ws)\n","\n","# Iterate over each dataset in the dataframe\n","for index, row in df_datasets.iterrows():\n"," # Check if the dataset is not the default semantic model\n"," if not labs.is_default_semantic_model(row['Dataset Name'], fabric.resolve_workspace_id(target_ws)):\n"," print('Updating semantic model connection ' + row['Dataset Name'] + ' in workspace '+ target_ws)\n"," labs.directlake.update_direct_lake_model_connection(dataset=row['Dataset Name'], \n"," workspace= target_ws,\n"," source=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[1], \n"," source_type=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[0], \n"," source_workspace=target_ws)\n"," labs.refresh_semantic_model(dataset=row['Dataset Name'], workspace= target_ws)\n","\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9deccda6-5c3d-4b88-8ed8-68855ca0949a"},{"cell_type":"markdown","source":["##### Rebind reports to local datasets\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/sempy_labs.report.html#sempy_labs.report.report_rebind"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"36783f3b-4904-4d74-842d-dbd026a3184a"},{"cell_type":"code","source":["df_reports = fabric.list_reports(workspace=target_ws)\n","for index, row in df_reports.iterrows():\n"," #print(row['Name'] + '-' + row['Dataset Id'])\n"," df_datasets = fabric.list_datasets(workspace=target_ws)\n"," dataset_name = df_datasets[df_datasets['Dataset ID'] == row['Dataset Id']]['Dataset Name'].values[0]\n"," print(f'Rebinding report to {dataset_name} in {target_ws}')\n"," labs.report.report_rebind(report=row['Name'],dataset=dataset_name, report_workspace=target_ws, dataset_workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"06268ede-b795-493e-9a8d-772654ce7e20"},{"cell_type":"markdown","source":["##### Update data pipeline source & sink connections\n","\n","Support changes lakehouses, warehouses, notebooks and connections from source to target.
\n","Connections changes should be expressed as an array of tuples [{from_1:to_1},{from_N:to_N}]"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4ae65012-350c-40c0-a68a-4069c567a85f"},{"cell_type":"code","source":["from typing import Optional\n","from sempy_labs._helper_functions import (\n"," resolve_workspace_name_and_id,\n"," lro,\n"," _decode_b64,\n",")\n","import sempy_labs._icons as icons\n","\n","import base64\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","\n","def update_data_pipeline_definition(\n"," name: str, pipeline_content: dict, workspace: Optional[str] = None\n","):\n"," \"\"\"\n"," Updates an existing data pipeline with a new definition.\n","\n"," Parameters\n"," ----------\n"," name : str\n"," The name of the data pipeline.\n"," pipeline_content : dict\n"," The data pipeline content (not in Base64 format).\n"," workspace : str, default=None\n"," The name of the workspace.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," \"\"\"\n","\n"," (workspace, workspace_id) = resolve_workspace_name_and_id(workspace)\n"," client = fabric.FabricRestClient()\n"," pipeline_payload = base64.b64encode(json.dumps(pipeline_content).encode('utf-8')).decode('utf-8')\n"," pipeline_id = fabric.resolve_item_id(\n"," item_name=name, type=\"DataPipeline\", workspace=workspace\n"," )\n","\n"," request_body = {\n"," \"definition\": {\n"," \"parts\": [\n"," {\n"," \"path\": \"pipeline-content.json\",\n"," \"payload\": pipeline_payload,\n"," \"payloadType\": \"InlineBase64\"\n"," }\n"," ]\n"," }\n"," }\n","\n","\n"," response = client.post(\n"," f\"v1/workspaces/{workspace_id}/items/{pipeline_id}/updateDefinition\",\n"," json=request_body,\n"," )\n","\n"," lro(client, response, return_status_code=True)\n","\n"," print(\n"," f\"{icons.green_dot} The '{name}' pipeline was updated within the '{workspace}' workspace.\"\n"," )\n","\n","def _is_valid_uuid(\n"," guid: str,\n","):\n"," \"\"\"\n"," Validates if a string is a valid GUID in version 4\n","\n"," Parameters\n"," ----------\n"," guid : str\n"," GUID to be validated.\n","\n"," Returns\n"," -------\n"," bool\n"," Boolean that indicates if the string is a GUID or not.\n"," \"\"\"\n","\n"," try:\n"," UUID(str(guid), version=4)\n"," return True\n"," except ValueError:\n"," return False"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"bdd46d4a-ef58-4f9a-b2e8-a428361a17c1"},{"cell_type":"code","source":["import json\n","from jsonpath_ng import jsonpath, parse\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","# Swaps the connection properties of an activity belonging to the specified item type(s)\n","def swap_pipeline_connection(pl_json: dict, p_source_ws: str,p_target_ws: str, \n"," p_item_type: List =['DataWarehouse','Lakehouse','Notebook'], \n"," p_conn_from_to: Optional[List[Tuple[str,str]]]=[]):\n"," \n"," source_ws_id = fabric.resolve_workspace_id(source_ws)\n","\n"," target_ws_id = fabric.resolve_workspace_id(target_ws)\n","\n"," if 'Warehouse' in p_item_type or 'Lakehouse' in p_item_type:\n"," ls_expr = parse('$..linkedService')\n"," for endpoint_match in ls_expr.find(pl_json):\n"," if endpoint_match.value['properties']['type'] == 'DataWarehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Warehouse' in p_item_type:\n"," # only update the warehouse if it was located in the source workspace i.e. we will update the properties to the target workspace if the warehouse resided in the same workspace as the pipeline\n"," #print(endpoint_match.value)\n"," warehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," #print(warehouse_id)\n"," warehouse_endpoint = endpoint_match.value['properties']['typeProperties']['endpoint']\n"," #print(warehouse_endpoint)\n"," \n"," source_wh_name = fabric.resolve_item_name(item_id = warehouse_id,workspace=source_ws_id)\n"," #print(remote_wh_name)\n"," # find the warehouse id of the warehouse with the same name in the target workspace\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n"," # look up the connection string for the warehouse in the target workspace\n"," whurl = f\"v1/workspaces/{target_ws_id}/warehouses/{target_wh_id}\"\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['connectionString']\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_wh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," endpoint_match.value['properties']['typeProperties']['endpoint'] = lhconnStr\n"," #print(endpoint_match.value)\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," if endpoint_match.value['properties']['type'] == 'Lakehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Lakehouse' in p_item_type:\n"," #print(endpoint_match.value)\n"," lakehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," remote_lh_name = fabric.resolve_item_name(item_id = lakehouse_id,workspace=source_ws_id)\n"," # find the lakehouse id of the lakehouse with the same name in the target workspace\n"," target_lh_id = fabric.resolve_item_id(item_name = remote_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_lh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," # print(endpoint_match.value)\n","\n","\n"," if 'Notebook' in p_item_type: \n"," ls_expr = parse('$..activities')\n","\n"," for endpoint_match in ls_expr.find(pl_json):\n"," for activity in endpoint_match.value:\n"," #print(activity['type'])\n"," if activity['type']=='TridentNotebook' and 'Notebook' in p_item_type: #only update if the notebook was in the same workspace as the pipeline\n"," print('change from '+activity['typeProperties']['workspaceId'])\n"," source_nb_id = activity['typeProperties']['notebookId']\n"," source_nb_name = fabric.resolve_item_name(item_id = source_nb_id,workspace=source_ws_id)\n"," target_nb_id = fabric.resolve_item_id(item_name = source_nb_name,type='Notebook',workspace=target_ws_id)\n"," activity['typeProperties']['notebookId']=target_nb_id\n"," activity['typeProperties']['workspaceId']=target_ws_id\n"," print('to notebook '+ target_nb_id)\n"," #ls_expr.update(endpoint_match,endpoint_match.value)\n","\n"," if p_conn_from_to:\n"," for ti_conn_from_to in p_conn_from_to:\n"," if not _is_valid_uuid(ti_conn_from_to[0]):\n"," print('Connection from is string '+ str(ti_conn_from_to[0]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[0]] \n"," connId_from = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_from = ti_conn_from_to[0]\n","\n"," if not _is_valid_uuid(ti_conn_from_to[1]):\n"," print('Connection from is string '+ str(ti_conn_from_to[1]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[1]] \n"," connId_to = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_to = ti_conn_from_to[1]\n","\n"," ls_expr = parse('$..externalReferences')\n"," for externalRef in ls_expr.find(pl_json):\n"," if externalRef.value['connection']==connId_from:\n"," print('Changing connection from '+str(connId_from))\n"," externalRef.value['connection']=connId_to\n"," ls_expr.update(externalRef,externalRef.value)\n"," print('to '+str(connId_to))\n","\n"," return pl_json\n","\n","\n","\n","# loading a dataframe of connections to perform an ID lookup if required \n","df_conns = labs.list_connections()\n","\n","df_pipeline = labs.list_data_pipelines(target_ws)\n","for index, row in df_pipeline.iterrows():\n"," pipeline_json = json.loads(labs.get_data_pipeline_definition(row['Data Pipeline Name'],source_ws))\n","\n"," p_new_json = swap_pipeline_connection(pipeline_json, source_ws,target_ws,\n"," ['DataWarehouse','Lakehouse','Notebook'],\n"," [p_connections_from_to]) \n"," #print(json.dumps(pipeline_json, indent=4))\n"," \n"," update_data_pipeline_definition(name=row['Data Pipeline Name'],pipeline_content=pipeline_json, workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"079958e8-2880-484a-a994-41caf47e747e"},{"cell_type":"markdown","source":["##### Commit changes made above to Git"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"44174276-b983-4e80-9451-0afb9589cf1f"},{"cell_type":"code","source":["labs.commit_to_git(comment='Initial', workspace=target_ws)"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9a5c3d84-f71d-4348-b419-c4953ac9e1d0"}],"metadata":{"kernel_info":{"name":"synapse_pyspark"},"kernelspec":{"name":"synapse_pyspark","language":"Python","display_name":"Synapse PySpark"},"language_info":{"name":"python"},"microsoft":{"language":"python","language_group":"synapse_pyspark","ms_spell_check":{"ms_spell_check_language":"en"}},"widgets":{},"nteract":{"version":"nteract-front-end@1.0.0"},"synapse_widget":{"version":"0.1","state":{}},"spark_compute":{"compute_id":"/trident/default","session_options":{"conf":{"spark.synapse.nbs.session.timeout":"1200000"}}},"dependencies":{"lakehouse":{}}},"nbformat":4,"nbformat_minor":5} \ No newline at end of file From 2aba87224fa8b277d9ef8be2f6ccb5fca6beda6b Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:31:03 +0000 Subject: [PATCH 4/7] Added Git folder support --- .../AzDO/Branch_out_workspace.yml | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/Branch_out_workspace.yml b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/Branch_out_workspace.yml index ea6e885..1aeb9a2 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/Branch_out_workspace.yml +++ b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/Branch_out_workspace.yml @@ -9,37 +9,37 @@ parameters: - name: source_workspace displayName: Enter source workspace type: string - default: 'Dev_WS_CICDSample_3' + default: '' - name: target_workspace displayName: Enter target workspace name type: string - default: 'Dev_WS_CICDSample_Clone5' + default: '' - name: copy_lakehouse_data displayName: Copy Lakehouse Data (enter True or False) type: string - default: 'True' + default: 'False' - name: copy_warehouse_data - displayName: Copy Lakehouse Data (enter True or False) + displayName: Copy Warehouse Data (enter True or False) type: string default: 'False' - name: create_lakehouse_shortcuts displayName: Create lakehouse shortcuts (only if copy lakehouse data set to False) type: string - default: 'False' + default: 'True' - name: developer_email displayName: Enter developer email type: string - default: 'reportbuilder1@MngEnvMCAP553100.onmicrosoft.com' + default: '' - name: capacity_id displayName: Enter capacity ID of the new workspace type: string - default: 'B34D9528-0FF8-4E40-865D-8BA769F574BB' + default: '' - name: ado_branch @@ -47,10 +47,15 @@ parameters: type: string default: 'main' +- name: ado_git_folder + displayName: Folder in the repo where the Fabric content is stored. Leave as / if content is stored in root. + type: string + default: "/" + - name: connections_from_to - displayName: Swap connections in pipelines using names or IDs in the format (from,to) format + displayName: Swap connections in pipelines using names or IDs in the format (from,to) format. Leave as () if no connections to swap. type: string - default: "('4498340c-27cf-4c6e-a025-00e5de6b0726','4498340c-27cf-4c6e-a025-00e5de6b0726')" + default: "()" variables: - group: Fabric_Deployment_Group_S @@ -77,7 +82,7 @@ stages: inputs: scriptSource: 'filePath' scriptPath: 'scripts/BranchOut-Feature-Workspace-Automation.py' - arguments: '--ADO_ORG_NAME $(ADO_ORG_NAME) --ADO_REPO_NAME $(ADO_REPO_NAME) --ADO_PROJECT_NAME $(ADO_PROJECT_NAME) --ADO_NEW_BRANCH ${{ parameters.target_workspace}} --DEVELOPER ${{ parameters.developer_email }} --WORKSPACE_NAME ${{ parameters.target_workspace }} --CAPACITY_ID ${{ parameters.capacity_id }} --ADO_API_URL $(ADO_API_URL) --ADO_MAIN_BRANCH ${{ parameters.ado_branch }} --TENANT_ID $(TENANT_ID) --FABRIC_TOKEN $(fabrictoken) --ADO_PAT_TOKEN $(azdopat) --CLIENT_ID $(azclientid) --USER_NAME $(username) --PASSWORD $(password)' + arguments: '--ADO_ORG_NAME $(ADO_ORG_NAME) --ADO_REPO_NAME $(ADO_REPO_NAME) --ADO_PROJECT_NAME $(ADO_PROJECT_NAME) --ADO_NEW_BRANCH ${{ parameters.target_workspace}} --DEVELOPER ${{ parameters.developer_email }} --WORKSPACE_NAME ${{ parameters.target_workspace }} --CAPACITY_ID ${{ parameters.capacity_id }} --ADO_API_URL $(ADO_API_URL) --ADO_MAIN_BRANCH ${{ parameters.ado_branch }} --ADO_GIT_FOLDER ${{ parameters.ado_git_folder }} --TENANT_ID $(TENANT_ID) --FABRIC_TOKEN $(fabrictoken) --ADO_PAT_TOKEN $(azdopat) --CLIENT_ID $(azclientid) --USER_NAME $(username) --PASSWORD $(password)' #failOnStderr: true displayName: 'Run Branch-Out-To-New-Workspace Script' From 8155b4a06cd24d1e91e60b241fce7d73e8d40019 Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:31:47 +0000 Subject: [PATCH 5/7] Added Git folder support --- .../BranchOut-Feature-Workspace-Automation.py | 17 ++-- .../AzDO/scripts/Run_post_activity.py | 81 +++++++++++-------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py index 405bda3..fba84d3 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py +++ b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py @@ -18,6 +18,7 @@ DEVELOPER = "" ADO_MAIN_BRANCH = "" ADO_NEW_BRANCH = "" +ADO_GIT_FOLDER = "" ADO_PROJECT_NAME = "" ADO_REPO_NAME = "" ADO_ORG_NAME = "" @@ -187,9 +188,9 @@ def get_branch_object_id(project_name, repo_name, branch_name, token, token_type return None # Function to connect Azure DevOps branch to Fabric workspace -def connect_branch_to_workspace(workspace_id, project_name, org_name, repo_name, branch_name, token): +def connect_branch_to_workspace(workspace_id, project_name, org_name, repo_name, branch_name, git_folder, token): try: - logging.info(f"Conecting workspace {workspace_id} to feature branch {branch_name} is in progess..") + logging.info(f"Conecting workspace {workspace_id} to feature branch {branch_name} at folder {git_folder}..") headers = {"Authorization": f"Bearer {token}"} data = { "gitProviderDetails": { @@ -198,7 +199,7 @@ def connect_branch_to_workspace(workspace_id, project_name, org_name, repo_name, "gitProviderType": "AzureDevOps", "repositoryName": repo_name, "branchName": branch_name, - "directoryName": "" + "directoryName": git_folder.rstrip("/") } } response = requests.post(f"{FABRIC_API_URL}/workspaces/{workspace_id}/git/connect", headers=headers, json=data) @@ -233,7 +234,7 @@ def long_running_operation_polling(uri,retry_after,headers): def initialize_workspace_from_git(workspace_id,token): try: - logging.info(f"Connecting f{WORKSPACE_NAME} to feature branch {ADO_NEW_BRANCH} is in propress... ") + logging.info(f"Initializing {WORKSPACE_NAME} to feature branch {ADO_NEW_BRANCH} is in propress... ") headers = {"Authorization": f"Bearer {token}"} # Initialize the connection to the GIT repository gitinitializeurl = f"{FABRIC_API_URL}/workspaces/{workspace_id}/git/initializeConnection" @@ -290,6 +291,7 @@ def set_main_parameters(): global DEVELOPER global ADO_MAIN_BRANCH global ADO_NEW_BRANCH + global ADO_GIT_FOLDER global ADO_PROJECT_NAME global ADO_REPO_NAME global ADO_ORG_NAME @@ -311,6 +313,7 @@ def set_main_parameters(): parser.add_argument('--WORKSPACE_NAME',type=str, help= 'Name of the feature workspace to be created') parser.add_argument('--DEVELOPER',type=str, help= 'Developr UPN to be added to workspace as admin') parser.add_argument('--ADO_MAIN_BRANCH',type=str, help= 'Main development branch') + parser.add_argument('--ADO_GIT_FOLDER',type=str, help= 'Folder where Fabric content is stored') parser.add_argument('--ADO_NEW_BRANCH',type=str, help= 'New branch to be created') parser.add_argument('--ADO_PROJECT_NAME',type=str, help= 'ADO project name') parser.add_argument('--ADO_REPO_NAME',type=str, help= 'ADO repository name') @@ -324,6 +327,7 @@ def set_main_parameters(): logging.error(f'Error: {e}') raise ValueError("Could not extract parameters: {e}") + logging.info('Binding parameters...') #Bind parameters to script variables TENANT_ID = args.TENANT_ID USERNAME = args.USER_NAME @@ -332,6 +336,7 @@ def set_main_parameters(): DEVELOPER = args.DEVELOPER ADO_MAIN_BRANCH = args.ADO_MAIN_BRANCH ADO_NEW_BRANCH = args.ADO_NEW_BRANCH + ADO_GIT_FOLDER = args.ADO_GIT_FOLDER ADO_PROJECT_NAME = args.ADO_PROJECT_NAME ADO_REPO_NAME = args.ADO_REPO_NAME ADO_ORG_NAME = args.ADO_ORG_NAME @@ -364,10 +369,10 @@ def main(): logging.info(f'Workspace {WORKSPACE_NAME} ({workspace_id}) successfully created and assigned to capacity {CAPACITY_ID}') logging.info(f'Adding workspace admins {DEVELOPER}...') add_workspace_admins(workspace_id, DEVELOPER, token) - logging.info(f'Creating ado branch from main {ADO_MAIN_BRANCH}...') + logging.info(f'Creating ado branch {ADO_NEW_BRANCH} from {ADO_MAIN_BRANCH}...') create_azure_devops_branch(ADO_PROJECT_NAME, ADO_REPO_NAME, ADO_MAIN_BRANCH, ADO_NEW_BRANCH) logging.info(f'Connecting workspace to branch {ADO_NEW_BRANCH}...') - connect_branch_to_workspace(workspace_id, ADO_PROJECT_NAME, ADO_ORG_NAME,ADO_REPO_NAME, ADO_NEW_BRANCH, token) + connect_branch_to_workspace(workspace_id, ADO_PROJECT_NAME, ADO_ORG_NAME,ADO_REPO_NAME, ADO_NEW_BRANCH, ADO_GIT_FOLDER, token) logging.info('Initialize workspace...') initialize_workspace_from_git(workspace_id, token) else: diff --git a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/Run_post_activity.py b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/Run_post_activity.py index a191070..9875d59 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/Run_post_activity.py +++ b/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/Run_post_activity.py @@ -69,41 +69,54 @@ def acquire_token_user_id_password(tenant_id, client_id,user_name,password): logging.error('Error: Token could not be obtained: '+str(result)) return access_token -logging.info('Checking for supplied credentials...') -if FABRIC_TOKEN!="": - logging.info('Fabric token found...') - token = FABRIC_TOKEN -else: - logging.info('User creds found, generating token...') - token = acquire_token_user_id_password(TENANT_ID,CLIENT_ID,user_name,password) - -if token: - if NOTEBOOK_ID == '': - raise ValueError('Error: Could not execute notebook as no Notebook ID has been specified.') +def main(): + logging.info('Checking for supplied credentials...') + if FABRIC_TOKEN!="": + logging.info('Fabric token found...') + token = FABRIC_TOKEN + else: + logging.info('User creds found, generating token...') + token = acquire_token_user_id_password(TENANT_ID,CLIENT_ID,user_name,password) + + if token: + if NOTEBOOK_ID == '': + raise ValueError('Error: Could not execute notebook as no Notebook ID has been specified.') + + plurl = 'https://api.fabric.microsoft.com/v1/workspaces/'+WS_ID +'/items/'+NOTEBOOK_ID+'/jobs/instances?jobType=RunNotebook' - plurl = 'https://api.fabric.microsoft.com/v1/workspaces/'+WS_ID +'/items/'+NOTEBOOK_ID+'/jobs/instances?jobType=RunNotebook' + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" # Set the content type based on your request + } + logging.info('Setting notebook parameters...') + payload_data = '{' \ + '"executionData": {' \ + '"parameters": {' \ + '"_inlineInstallationEnabled": {"value": "True", "type": "bool"},' \ + '"source_ws": {"value": "' + SOURCE_WS + '", "type": "string"},' \ + '"copy_lakehouse_data": {"value": "' + COPY_LH + '", "type": "bool"},' \ + '"create_lakehouse_shortcuts": {"value": "' + CREATE_SC + '", "type": "bool"},' \ + '"copy_warehouse_data": {"value": "' + COPY_WH + '", "type": "bool"},' \ + '"target_ws": {"value": "' + TARGET_WS + '", "type": "string"},' \ + '"p_connections_from_to": {"value": "' + CONNECTIONS_FROM_TO + '", "type": "string"}' \ + '}}}' + logging.info('Invoking Fabric notebook job...') + plresponse = requests.post(plurl, json=json.loads(payload_data), headers=headers) + #logging.info(str(plresponse.status_code) + ' - ' + plresponse.text) + if plresponse.status_code==202: + logging.info('Job invoked. Please check the Fabric monitoring hub to review the status of the job.') + else: + logging.error('An error occurred when trying to invoke job: ' + str(plresponse.status_code) + ' - ' + plresponse.text) + raise ValueError("Error invoking Fabric notebook. Please review the debug logs.") - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json" # Set the content type based on your request - } - logging.info('Setting notebook parameters...') - payload_data = '{' \ - '"executionData": {' \ - '"parameters": {' \ - '"_inlineInstallationEnabled": {"value": "True", "type": "bool"},' \ - '"source_ws": {"value": "' + SOURCE_WS + '", "type": "string"},' \ - '"copy_lakehouse_data": {"value": "' + COPY_LH + '", "type": "bool"},' \ - '"create_lakehouse_shortcuts": {"value": "' + CREATE_SC + '", "type": "bool"},' \ - '"copy_warehouse_data": {"value": "' + COPY_WH + '", "type": "bool"},' \ - '"target_ws": {"value": "' + TARGET_WS + '", "type": "string"},' \ - '"p_connections_from_to": {"value": "' + CONNECTIONS_FROM_TO + '", "type": "string"}' \ - '}}}' - logging.info('Invoking Fabric notebook job...') - plresponse = requests.post(plurl, json=json.loads(payload_data), headers=headers) - logging.info(str(plresponse.status_code) + ' - ' + plresponse.text) -else: - logging.error("Could not aquire token") - raise ValueError("Could not generate authentication token. Please review the debug logs.") + else: + logging.error("Could not aquire token") + raise ValueError("Could not generate authentication token. Please review the debug logs.") + + +if __name__ == "__main__": + logging.info('Starting Run_post_activity script...') + main() + From 3c7048644664163ac32c207da71a5668a1940668 Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Fri, 14 Feb 2025 16:33:25 +0000 Subject: [PATCH 6/7] Added documentation for git folder support --- accelerators/CICD/Branch-out-to-new-workspace/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/README.md b/accelerators/CICD/Branch-out-to-new-workspace/README.md index 552b4e7..064824e 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/README.md +++ b/accelerators/CICD/Branch-out-to-new-workspace/README.md @@ -157,7 +157,7 @@ authentication method has been chosen: 11. This will display run-time parameters which can be modified as necessary: -
+

 

 

a. Source workspace: Name of the dev workspace

b. Target workspace: Name of the new workspace to be created which will also serve as the new branch name From dc7626d53947c5c39e7bf0a65b197013095c24b3 Mon Sep 17 00:00:00 2001 From: Nick Hurt Date: Thu, 20 Feb 2025 23:12:20 +0000 Subject: [PATCH 7/7] Various enhancements and bug fixes Added Git folder support Issue #42 Fixed shortcut and pipeline swap bugs Issue #41 Added standalone mode if not run from ADO --- .../Fabric/Branch out to new workspace - Post Activity.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb b/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb index 938943c..26ea005 100644 --- a/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb +++ b/accelerators/CICD/Branch-out-to-new-workspace/Fabric/Branch out to new workspace - Post Activity.ipynb @@ -1 +1 @@ -{"cells":[{"cell_type":"markdown","source":["##### Branch out to new workspace notebook - post activity\n","\n","After cloning a workspace, this notebook will reconfigure any references to the old workspace by rebinding them to the new workspace. \n","\n","For example a pipeline referencing a warehouse or a default lakehouse of a notebook.\n","\n","This notebook runs post activity tasks can be run after [branch out to new workspace functionality](https://blog.fabric.microsoft.com/en-us/blog/introducing-new-branching-capabilities-in-fabric-git-integration) or the [custom AzDO script](https://github.com/microsoft/fabric-toolbox/blob/main/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py).\n","\n","Summary of post activities in order:\n","

    \n","
  • Default lakehouses and warehouse are updated to local lakehouse/warehouses
  • \n","
  • Either creates shortcuts in local lakehouse back to tables in the source lakehouse, or copies the data from source lakehouse. Set via parameter below.
  • \n","
  • Copy warehouse data. Set via parameter below
  • \n","
  • Changes directlake semantic model connections for semantic models to \"local\" lakehouse/warehouse
  • \n","
  • Rebinds reports to \"local\" semantic models
  • \n","
  • Changes pipeline lakehouse/warehouse references to local item
  • \n","
  • Ability to swap connections in pipelines from old to new
  • \n","
  • Commit changes to git
  • \n","
\n","\n","Requirements:\n","
    \n","
  • Requires Semantic Link Labs installed by pip install below or added to environment library.
  • \n","
  • Requires JmesPath library for data pipeline JSON manipulation i.e. connection swaps.
  • \n","
\n","\n","Limitations of current script:\n","\n","
    \n","
  • Does not recreate item shares or external shortcuts
  • \n","
  • Does not re-apply lakehouse SQL Endpoint or Warehouse object/row/column level security
  • \n","
  • Does not recreate data access roles in Lakehouse
  • \n","
  • Untested with Lakehouses where with schema support enabled
  • \n","
\n","\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a98b6d0a-7a36-4116-ab0d-aa70144eb737"},{"cell_type":"markdown","source":["##### Install semantic link labs to support advanced functionality\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/index.html\n","https://github.com/microsoft/semantic-link-labs/blob/main/README.md\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"3b887bd6-a9c9-430f-b58f-b58a93f5ce29"},{"cell_type":"code","source":["%pip -q install semantic-link-labs\n"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":3,"statement_ids":[3],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:19:28.0006255Z","session_start_time":"2025-02-14T16:19:28.002459Z","execution_start_time":"2025-02-14T16:19:44.8101222Z","execution_finish_time":"2025-02-14T16:19:59.5859051Z","parent_msg_id":"1be3b208-385f-4a2e-a567-f3f29f01fab0"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 3, Finished, Available, Finished)"},"metadata":{}},{"output_type":"stream","name":"stdout","text":["Note: you may need to restart the kernel to use updated packages.\n"]}],"execution_count":1,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":true},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"1b03316d-c088-4a0e-a2f0-44d45d112121"},{"cell_type":"markdown","source":["##### Install Jmespath to make data pipeline changes such as updating linked notebooks, warehouses and lakehouses "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"8a74ed11-dd64-43bb-a735-906a947c8666"},{"cell_type":"code","source":["%pip install jmespath"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":8,"statement_ids":[4,5,6,7,8],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:19:59.5881976Z","session_start_time":null,"execution_start_time":"2025-02-14T16:19:59.7550889Z","execution_finish_time":"2025-02-14T16:20:12.6668949Z","parent_msg_id":"cdb7a488-6f67-45ab-97d6-b49e3b450225"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 8, Finished, Available, Finished)"},"metadata":{}},{"output_type":"stream","name":"stdout","text":["Collecting jmespath\n Downloading jmespath-1.0.1-py3-none-any.whl.metadata (7.6 kB)\nDownloading jmespath-1.0.1-py3-none-any.whl (20 kB)\nInstalling collected packages: jmespath\nSuccessfully installed jmespath-1.0.1\n\n\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.0.1\u001b[0m\n\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpython -m pip install --upgrade pip\u001b[0m\nNote: you may need to restart the kernel to use updated packages.\nWarning: PySpark kernel has been restarted to use updated packages.\n\n"]}],"execution_count":2,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"c68be6ba-7648-457f-af82-f1987d12d7f7"},{"cell_type":"markdown","source":["##### Set parameters\n","Before running this notebook ensure these parameters are set correctly. If necessary these can be passed in via a data factory pipeline"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"dee81614-b92b-4242-890a-b11f97b1a640"},{"cell_type":"code","source":["source_ws = ''\n","target_ws = ''\n","\n","# Either copy lakehouse data or create shortcuts, set at most one of these to True \n","copy_lakehouse_data = False\n","create_lakehouse_shortcuts = True\n","\n","# Option to copy warehouse data if required\n","copy_warehouse_data = True\n","\n","# If false then shortcuts will be created. If you wish to create shortcuts based on a pattern match please set the param below\n","# enter pattern match for creating shortcuts - see https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py \n","PATTERN_MATCH = [\"*\"]\n","_inlineInstallationEnabled = True\n","\n","# Set connections to be replaced from previous name or ID to new name or ID.\n","p_connections_from_to = ()#('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726'),('4498340c-27cf-4c6e-a025-00e5de6b0726','https://api.fabric.microsoft.com/v1/workspaces/ admin'),('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726')"],"outputs":[{"output_type":"display_data","data":{"application/vnd.livy.statement-meta+json":{"spark_pool":null,"statement_id":10,"statement_ids":[10],"state":"finished","livy_statement_state":"available","session_id":"bc84ee10-77eb-4aa5-b08b-c8f4a35751f7","normalized_state":"finished","queued_time":"2025-02-14T16:20:16.4107096Z","session_start_time":null,"execution_start_time":"2025-02-14T16:20:23.1088616Z","execution_finish_time":"2025-02-14T16:20:23.3970563Z","parent_msg_id":"497ea86f-6f85-4631-a26c-8d36b57cc7be"},"text/plain":"StatementMeta(, bc84ee10-77eb-4aa5-b08b-c8f4a35751f7, 10, Finished, Available, Finished)"},"metadata":{}}],"execution_count":3,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"tags":["parameters"]},"id":"90efaa4f-846d-4924-900e-258837a3467d"},{"cell_type":"markdown","source":["##### Library imports and fabric rest client setup\n","\n","https://learn.microsoft.com/en-us/python/api/semantic-link-sempy/sempy.fabric.fabricrestclient"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4fb01e1d-ec4e-4c69-b544-66f6d8c5a475"},{"cell_type":"code","source":["import pandas as pd\n","import datetime, time\n","import re,json, fnmatch,os\n","import requests, base64\n","import sempy\n","import sempy.fabric as fabric\n","from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException\n","from pyspark.sql import DataFrame\n","from pyspark.sql.functions import col,current_timestamp,lit\n","import sempy_labs as labs\n","from sempy_labs import migration, directlake\n","from sempy_labs import lakehouse as lake\n","from sempy_labs import report as rep\n","from sempy_labs.tom import connect_semantic_model\n","\n","# instantiate the Fabric rest client\n","client = fabric.FabricRestClient()\n","\n","# get the current workspace ID based on the context of where this notebook is run from\n","thisWsId = notebookutils.runtime.context['currentWorkspaceId']\n","thisWsName = notebookutils.runtime.context['currentWorkspaceName']\n","\n","source_ws_id = fabric.resolve_workspace_id(source_ws)\n","target_ws_id = fabric.resolve_workspace_id(target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"391624c1-b299-452d-9ebf-f32626d49970"},{"cell_type":"markdown","source":["##### Update default and attached lakehouses/warehouses for notebooks\n","\n","Update notebook dependencies based on but now supports T-SQL notebooks:\n","https://github.com/PowerBiDevCamp/FabConWorkshopSweden/blob/main/DemoFiles/GitUpdateWorkspace/updateWorkspaceDependencies_v1.ipynb\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"aaae8a08-588d-4dd8-9d2c-2200b7a88d30"},{"cell_type":"code","source":["for notebook in notebookutils.notebook.list(workspaceId=target_ws_id):\n"," updates = False\n"," if notebook.displayName == 'ETL':#True: #notebook.displayName == 'T-SQL_Notebook': #notebook.displayName != 'Create Feature Branch':\n","\n"," # Get the current notebook definition\n"," json_payload = json.loads(notebookutils.notebook.getDefinition(notebook.displayName,workspaceId=source_ws_id))\n"," #print(json.dumps(json_payload, indent=4))\n"," # Check for any attached lakehouses\n"," if 'dependencies' in json_payload['metadata'] \\\n"," and 'lakehouse' in json_payload['metadata']['dependencies'] \\\n"," and json_payload['metadata'][\"dependencies\"][\"lakehouse\"] is not None:\n"," # Extract attached and default lakehouses\n"," current_lakehouse = json_payload['metadata']['dependencies']['lakehouse']\n"," # if default lakehouse setting exists\n"," if 'default_lakehouse_name' in current_lakehouse:\n"," print(f\"Updating notebook {notebook.displayName} with new default lakehouse: {current_lakehouse['default_lakehouse_name']} in workspace {target_ws}\")\n"," source_lh_name = fabric.resolve_item_name(item_id = current_lakehouse['default_lakehouse'],type='Lakehouse',workspace=source_ws_id)\n"," current_lakehouse['default_lakehouse'] = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," current_lakehouse['default_lakehouse_workspace_id'] = target_ws_id\n"," updates = True\n"," # loop through all attached lakehouess\n"," for lakehouse in json_payload['metadata']['dependencies']['lakehouse']['known_lakehouses']:\n"," source_lh_id = lakehouse['id']\n"," # find source lakehouse name\n"," source_lh_name = fabric.resolve_item_name(item_id = lakehouse['id'],type='Lakehouse',workspace=source_ws_id)\n"," # find target lakehouse id based on name\n"," target_lh_id = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," lakehouse['id'] = target_lh_id\n"," print(f'Updating attached lakehouse {source_lh_name} from {source_lh_id} to target ID {target_lh_id}')\n"," updates = True\n","\n"," if 'dependencies' in json_payload['metadata'] and 'warehouse' in json_payload['metadata']['dependencies']:\n"," # Fetch existing details\n"," current_warehouse = json_payload['metadata']['dependencies']['warehouse']\n"," current_warehouse_id = current_warehouse['default_warehouse']\n"," source_wh_name = fabric.resolve_item_name(item_id = current_warehouse_id,workspace=source_ws_id)\n"," #print('Source warehouse name is ' + source_wh_name)\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n","\n"," if 'default_warehouse' in current_warehouse:\n"," #json_payload['metadata']['dependencies']['warehouse'] = {}\n"," print(f\"Attempting to update notebook {notebook.displayName} with new default warehouse: {target_wh_id} in {target_ws}\")\n"," \n"," json_payload['metadata']['dependencies']['warehouse']['default_warehouse'] = target_wh_id\n"," for warehouse in json_payload['metadata']['dependencies']['warehouse']['known_warehouses']:\n"," if warehouse['id'] == current_warehouse_id:\n"," warehouse['id'] = target_wh_id\n"," updates = True\n","\n"," if updates:\n"," notebookutils.notebook.updateDefinition(\n"," name = notebook.displayName,\n"," content = json.dumps(json_payload),\n"," workspaceId = target_ws_id\n"," )\n"," \n"," print(f\"Updated notebook {notebook.displayName} in {target_ws}\")\n","\n"," else:\n"," print(f'No default lakehouse set for notebook {notebook.displayName}, ignoring.')"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"5c60b5d2-f83c-46f8-9870-9fd609166b67"},{"cell_type":"markdown","source":["##### Run the below cell - contains utility functions to support lakehouse and warehouse initialisation\n","\n","Shortcut creator:\n","https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a47a56df-219d-4d6c-b950-491909638deb"},{"cell_type":"code","source":["##### \n","### Shortcut utility function \n","####\n","\n","# Extract workspace_id, item_id and path from a onelake URI\n","def extract_onelake_https_uri_components(uri):\n"," # Define a regular expression to match any string between slashes and capture the final path element(s) without the leading slash\n"," pattern = re.compile(r\"abfss://([^@]+)@[^/]+/([^/]+)/(.*)\")\n"," match = pattern.search(uri)\n"," if match:\n"," workspace_id, item_id, path = match.groups()\n"," return workspace_id, item_id, path\n"," else:\n"," return None, None, None\n","\n","\n","def is_valid_onelake_uri(uri: str) -> bool:\n"," workspace_id, item_id, path = extract_onelake_https_uri_components(uri)\n"," if \"abfss://\" not in uri or workspace_id is None or item_id is None or path is None:\n"," return False\n","\n"," return True\n","\n","\n","def get_last_path_segment(uri: str):\n"," path = uri.split(\"/\") # Split the entire URI by '/'\n"," return path[-1] if path else None\n","\n","\n","def is_delta_table(uri: str):\n"," delta_log_path = os.path.join(uri, \"_delta_log\")\n"," return mssparkutils.fs.exists(delta_log_path)\n","\n","\n","def get_onelake_shorcut(workspace_id: str, item_id: str, path: str, name: str):\n"," shortcut_uri = (\n"," f\"v1/workspaces/{workspace_id}/items/{item_id}/shortcuts/{path}/{name}\"\n"," )\n"," result = client.get(shortcut_uri).json()\n"," return result\n","\n","\n","def is_folder_matching_pattern(path: str, folder_name: str, patterns: []):\n"," if folder_name in patterns:\n"," return True\n"," else:\n"," for pattern in patterns:\n"," if fnmatch.fnmatch(folder_name, pattern):\n"," return is_delta_table(path)\n","\n"," return False\n","\n","\n","def get_matching_delta_tables_uris(uri: str, patterns: []) -> []:\n"," # Use a set to avoid duplicates\n"," matched_uris = set()\n"," files = mssparkutils.fs.ls(uri)\n"," folders = [item for item in files if item.isDir]\n","\n"," # Filter folders to only those that matches the pattern and is a delta table\n"," matched_uris.update(\n"," folder.path\n"," for folder in folders\n"," if is_folder_matching_pattern(folder.path, folder.name, patterns)\n"," )\n","\n"," return matched_uris\n","\n","\n","def create_onelake_shorcut(source_uri: str, dest_uri: str):\n"," src_workspace_id, src_item_id, src_path = extract_onelake_https_uri_components(\n"," source_uri\n"," )\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri\n"," )\n","\n"," name = get_last_path_segment(source_uri)\n"," dest_uri_joined = os.path.join(dest_uri, name)\n","\n"," # If the destination path already exists, return without creating shortcut\n"," if mssparkutils.fs.exists(dest_uri_joined):\n"," print(f\"Destination already exists: {dest_uri_joined}\")\n"," return None\n","\n"," request_body = {\n"," \"name\": name,\n"," \"path\": dest_path,\n"," \"target\": {\n"," \"oneLake\": {\n"," \"itemId\": src_item_id,\n"," \"path\": src_path,\n"," \"workspaceId\": src_workspace_id,\n"," }\n"," },\n"," }\n","\n"," shortcut_uri = f\"v1/workspaces/{dest_workspace_id}/items/{dest_item_id}/shortcuts\"\n"," print(f\"Creating shortcut: {shortcut_uri}/{name}..\")\n"," try:\n"," client.post(shortcut_uri, json=request_body)\n"," except FabricHTTPException as e:\n"," print(e)\n"," return None\n","\n"," return get_onelake_shorcut(dest_workspace_id, dest_item_id, dest_path, name)\n"," \n","\n","####\n","## Copy lakehouse and warehouse utility functions\n","####\n","\n","def get_lh_object_list(base_path,data_types = ['Tables', 'Files'])->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a lakehouse\n"," adapted from https://fabric.guru/getting-a-list-of-folders-and-delta-tables-in-the-fabric-lakehouse\n"," This function will return a pandas dataframe containing names and abfss paths of each folder for Files and Tables\n"," '''\n"," #data_types = ['Tables', 'Files'] #for if you want a list of files and tables\n"," #data_types = ['Tables'] #for if you want a list of tables\n","\n"," df = pd.concat([\n"," pd.DataFrame({\n"," 'name': [item.name for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," 'type': data_type[:-1].lower() , \n"," 'src_path': [item.path for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," }) for data_type in data_types], ignore_index=True)\n","\n"," return df\n","\n","def get_wh_object_list(schema_list,base_path)->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a warehouse by schema\n"," '''\n"," data_type = 'Tables'\n"," dfs = []\n","\n"," for schema_prefix in schema_list:\n"," if notebookutils.fs.exists(f'{base_path}/{data_type}/{schema_prefix}/'):\n"," items = notebookutils.fs.ls(f'{base_path}/{data_type}/{schema_prefix}/')\n"," if items: # Check if the list is not empty\n"," df = pd.DataFrame({\n"," 'schema': schema_prefix,\n"," 'name': [item.name for item in items],\n"," 'type': data_type[:-1].lower(),\n"," 'src_path': [item.path for item in items],\n"," })\n"," dfs.append(df)\n","\n"," if dfs: # Check if the list of dataframes is not empty\n"," df = pd.concat(dfs, ignore_index=True)\n"," else:\n"," df = pd.DataFrame() # Return an empty dataframe if no dataframes were created\n","\n"," return df\n","\n","def copy_lh_objects(table_list,workspace_src,workspace_tgt,lakehouse_src,lakehouse_tgt,fastcopy=True,usingIDs=False)->pd.DataFrame:\n"," # declare an array to keep the instrumentation\n"," cpresult = []\n"," # loop through all the tables to extract the source path \n"," for table in table_list.src_path:\n"," source = table\n"," destination = source.replace(f'abfss://{workspace_src}', f'abfss://{workspace_tgt}')\n"," if usingIDs:\n"," destination = destination.replace(f'{lakehouse_src}', f'{lakehouse_tgt}')\n"," else:\n"," destination = destination.replace(f'{lakehouse_src}.Lakehouse', f'{lakehouse_tgt}.Lakehouse')\n"," start_time = datetime.datetime.now()\n"," if notebookutils.fs.exists(destination):\n"," notebookutils.fs.rm(destination, True)\n"," if fastcopy:\n"," # use fastcopy util which is a python wrapper to azcopy\n"," notebookutils.fs.fastcp(source+'/*', destination+'/', True)\n"," else:\n"," notebookutils.fs.cp(source, destination, True)\n","\n"," # recording the timing and add it to the results list\n"," end_time = datetime.datetime.now()\n"," copyreslist = [source, destination, start_time.strftime(\"%Y-%m-%d %H:%M:%S\"), end_time.strftime(\"%Y-%m-%d %H:%M:%S\"), str((end_time - start_time).total_seconds())]\n"," cpresult.append(copyreslist)\n"," return pd.DataFrame(cpresult,columns =['source--------------------------------------','target--------------------------------------','start------------','end_time------------','elapsed seconds----'])\n","\n","def createDWrecoverypl(ws_id,pl_name = 'Recover_Warehouse_Data_From_DR'):\n"," client = fabric.FabricRestClient()\n","\n"," dfurl= \"v1/workspaces/\"+ ws_id + \"/items\"\n"," payload = { \n"," \"displayName\": pl_name, \n"," \"type\": \"DataPipeline\", \n"," \"definition\": { \n"," \"parts\": [ \n"," { \n"," \"path\": \"pipeline-content.json\", \n"," \"payload\": \"ewogICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgImFjdGl2aXRpZXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICJuYW1lIjogIkl0ZXJhdGVTY2hlbWFUYWJsZXMiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiRm9yRWFjaCIsCiAgICAgICAgICAgICAgICAiZGVwZW5kc09uIjogW10sCiAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy50YWJsZXNUb0NvcHkiLAogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgImJhdGNoQ291bnQiOiAyMCwKICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdGllcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiQ29weVdhcmVob3VzZVRhYmxlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJDb3B5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlcGVuZGVuY3lDb25kaXRpb25zIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlN1Y2NlZWRlZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicG9saWN5IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0aW1lb3V0IjogIjAuMTI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZXRyeSI6IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJldHJ5SW50ZXJ2YWxJblNlY29uZHMiOiAzMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZVNvdXJjZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJxdWVyeVRpbWVvdXQiOiAiMDI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicGFydGl0aW9uT3B0aW9uIjogIk5vbmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwN2EwMzAwNl9kMWI2XzRhMzlfYmViMV8wYmJhMmFhZjVmZjciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLmxha2Vob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy5sYWtlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAY29uY2F0KGNvbmNhdChpdGVtKCkuc2NoZW1hLCdfJyksaXRlbSgpLm5hbWUpIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRXhwcmVzc2lvbiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzaW5rIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlU2luayIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhbGxvd0NvcHlDb21tYW5kIjogdHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRhYmxlT3B0aW9uIjogImF1dG9DcmVhdGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwYzAzMTIzYV9kMzEyXzQ2YzRfYThlN181YjRjYWQ4ZjEyZDciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLndhcmVob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53YXJlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAaXRlbSgpLm5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVuYWJsZVN0YWdpbmciOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0cmFuc2xhdG9yIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJUYWJ1bGFyVHJhbnNsYXRvciIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvbiI6IHRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvblNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFsbG93RGF0YVRydW5jYXRpb24iOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRyZWF0Qm9vbGVhbkFzTnVtYmVyIjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHNjaGVtYSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRlbmN5Q29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJTdWNjZWVkZWQiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInBvbGljeSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhcmlhYmxlTmFtZSI6ICJUYWJsZW5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogIkBpdGVtKCkubmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkV4cHJlc3Npb24iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJTZXQgc2NoZW1hIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb2xpY3kiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZU91dHB1dCI6IGZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzZWN1cmVJbnB1dCI6IGZhbHNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YXJpYWJsZU5hbWUiOiAiU2NoZW1hbmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQGl0ZW0oKS5zY2hlbWEiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgInBhcmFtZXRlcnMiOiB7CiAgICAgICAgICAgICJsYWtlaG91c2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjBmMGY2YjdjLTE3NjEtNDFlNi04OTZlLTMwMDE0ZjE2ZmY2ZCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInRhYmxlc1RvQ29weSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogImFycmF5IiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiBbCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkRhdGUiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiR2VvZ3JhcGh5IgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkhhY2tuZXlMaWNlbnNlIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIk1lZGFsbGlvbiIKICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6ICJkYm8iLAogICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJUaW1lIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIlRyaXAiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiV2VhdGhlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ3b3Jrc3BhY2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjE1MDExNDNjLTI3MmYtNGEyZi05NzZhLTdlNTU5NzFlNGMyYiIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIndhcmVob3VzZUlkIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNGQxYmQ5NTEtOTlkZS00YmQ3LWI3YmMtNzFjOGY1NmRiNDExIgogICAgICAgICAgICB9LAogICAgICAgICAgICAid2FyZWhvdXNlQ29ublN0ciI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjcyd3diaXZpMnViZWpicnRtdGFobzMyYjR5LWhxa2FjZmpwZTR4dXZmM2twemt6b2hzbWZtLmRhdGF3YXJlaG91c2UuZmFicmljLm1pY3Jvc29mdC5jb20iCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYWtlaG91c2VDb25uU3RyIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNzJ3d2JpdmkydWJlamJydG10YWhvMzJiNHktaHFrYWNmanBlNHh1dmYza3B6a3pvaHNtZm0uZGF0YXdhcmVob3VzZS5mYWJyaWMubWljcm9zb2Z0LmNvbSIKICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInZhcmlhYmxlcyI6IHsKICAgICAgICAgICAgIlRhYmxlbmFtZSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogIlN0cmluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIlNjaGVtYW5hbWUiOiB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJTdHJpbmciCiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJsYXN0TW9kaWZpZWRCeU9iamVjdElkIjogIjRhYTIwYWY3LTk0YmQtNDM0OC1iZWY4LWY4Y2JjZDg0MGQ1MSIsCiAgICAgICAgImxhc3RQdWJsaXNoVGltZSI6ICIyMDI0LTExLTEzVDE1OjUyOjUyWiIKICAgIH0KfQ==\", \n"," \"payloadType\": \"InlineBase64\" \n"," } \n"," ] \n"," } \n","} \n"," \n"," response = json.loads(client.post(dfurl,json= payload).content)\n"," return response['id']\n","\n","def getItemId(wks_id,itm_name,itm_type):\n"," df = fabric.list_items(type=None,workspace=wks_id)\n"," #print(df)\n"," if df.empty:\n"," return 'NotExists'\n"," else:\n"," #display(df)\n"," #print(df.query('\"Display Name\"=\"'+itm_name+'\"'))\n"," if itm_type != '':\n"," newdf= df.loc[(df['Display Name'] == itm_name) & (df['Type'] == itm_type)]['Id']\n"," else:\n"," newdf= df.loc[(df['Display Name'] == itm_name)]['Id'] \n"," if newdf.empty:\n"," return 'NotExists'\n"," else:\n"," return newdf.iloc[0]\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":true,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"e46210e9-58c9-483a-84ae-bbdc2ad1c37f"},{"cell_type":"markdown","source":["##### Either create shortcuts from source to target lakehouse(s) or copy data\n","\n","Loops through lakehouse(s) in the target workspace and either populates them with shortcuts or data\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a15065f3-670d-4bc9-b337-51709f6cdb1f"},{"cell_type":"code","source":["df_lhs = labs.list_lakehouses(source_ws)\n","for index, row in df_lhs.iterrows():\n","\n","\n"," if copy_lakehouse_data:\n"," lh_name= row['Lakehouse Name']\n"," if lh_name.find('temp')==-1:\n"," # Gathers the list of tables and source paths to be copied into the target lakehouse \n"," src_path = f'abfss://{source_ws}@onelake.dfs.fabric.microsoft.com/{lh_name}.Lakehouse'\n","\n"," table_list = get_lh_object_list(src_path)\n"," print(f'Attempting to copy table data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n"," display(table_list)\n","\n"," #print('Copy Lakehouse Delta tables...')\n"," res = copy_lh_objects(table_list[table_list['type']=='table'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," # Copy files\n"," print(f'Attempting to copy file data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n","\n"," #print('Copy Lakehouse files...')\n"," res = copy_lh_objects(table_list[table_list['type']=='file'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," print('Done.')\n","\n"," else:\n"," # fetch ID of source lakehouse based on name and workspace\n"," source_lh_id = row['Lakehouse ID']\n"," target_lh_id = fabric.resolve_item_id(\n"," item_name=row['Lakehouse Name'], type=\"Lakehouse\", workspace=target_ws\n"," )\n","\n"," SOURCE_URI = f\"abfss://{source_ws_id}@onelake.dfs.fabric.microsoft.com/{source_lh_id}/Tables\"\n"," DEST_URI = f\"abfss://{target_ws_id}@onelake.dfs.fabric.microsoft.com/{target_lh_id}/Tables\"\n","\n"," if PATTERN_MATCH is None or len(PATTERN_MATCH) == 0:\n"," raise TypeError(\"Argument 'PATTERN_MATCH' should be a valid list of patterns or [\"*\"] to match everything\")\n","\n"," # Collect created shortcuts\n"," result = []\n","\n"," # If either URI's are invalid, just return\n"," if not is_valid_onelake_uri(SOURCE_URI) or not is_valid_onelake_uri(DEST_URI):\n"," print(\n"," \"invalid URI's provided. URI's should be in the form: abfss://@onelake.dfs.fabric.microsoft.com//\"\n"," )\n"," else:\n"," # Remove any trailing '/' from uri's\n"," source_uri_addr = SOURCE_URI.rstrip(\"/\")\n"," dest_uri_addr = DEST_URI.rstrip(\"/\")\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri_addr\n"," )\n","\n"," # If we are not shortcutting to a managed table folder or\n"," # the source uri is a delta table, just shortcut it 1-1.\n"," if not dest_path.startswith(\"Tables\") or is_delta_table(source_uri_addr):\n"," shortcut = create_onelake_shorcut(source_uri_addr, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," else:\n"," # If source is not a delta table, and destination is managed table folder:\n"," # Iterate over source folders and create table shortcuts @ destination\n"," for delta_table_uri in get_matching_delta_tables_uris(\n"," source_uri_addr, PATTERN_MATCH\n"," ):\n"," shortcut = create_onelake_shorcut(delta_table_uri, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," print(result)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"8bec3fbc-4a75-4ba3-86d5-0620ec504a8f"},{"cell_type":"markdown","source":["##### Copy warehouse data via parameterised pipeline\n","\n","Loop through all warehouses and copy the data"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"f198b816-77e9-4f04-9139-d78237bedc72"},{"cell_type":"code","source":["p_logging_verbose = True\n","df_warehouses = (labs.list_warehouses(target_ws))\n","display(df_warehouses)\n","for index, row in df_warehouses.iterrows():\n"," source_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],source_ws_id)\n"," target_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],target_ws_id)\n"," \n"," src_path = f'abfss://'+source_ws_id+'@onelake.dfs.fabric.microsoft.com/'+source_wh_id\n"," tgt_path = f'abfss://'+target_ws_id+'@onelake.dfs.fabric.microsoft.com/'+target_wh_id\n","\n"," # extract the list of schemas per data \n"," schema_list = get_lh_object_list(src_path,['Tables'])\n"," # extract a list of warehouse objects per schema and store in a list\n"," table_list = get_wh_object_list(schema_list['name'],src_path)\n"," \n"," # create a temporary staging lakehouse per warehouse to create shortcuts into, \n"," # which point back to original warehouse data currently in the DR storage account\n"," lhname = 'temp_rlh_' + source_ws+'_'+row['Warehouse Name']\n"," # check if it exists before attempting create\n"," if p_logging_verbose:\n"," print('Checking whether the temporary lakehouse \"'+ lhname +'\" exists in workspace '+target_ws+'...')\n"," temp_lh_id = getItemId(target_ws_id,lhname,'Lakehouse')\n"," if temp_lh_id == 'NotExists':\n"," lhname = lhname[:256] # lakehouse name should not exceed 256 characters\n"," payload = payload = '{\"displayName\": \"' + lhname + '\",' \\\n"," + '\"description\": \"Interim staging lakehouse for primary warehouse recovery: ' \\\n"," + source_ws+'_'+row['Warehouse Name'] + 'into workspace '+ target_ws + '(' + target_ws +')\"}'\n"," try:\n"," lhurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses\"\n"," lhresponse = client.post(lhurl,json= json.loads(payload))\n"," temp_lh_id = lhresponse.json()['id']\n"," if p_logging_verbose:\n"," print('Temporary lakehouse \"'+ lhname +'\" created with Id ' + temp_lh_id + ': ' + str(lhresponse.status_code) + ' ' + str(lhresponse.text))\n"," except Exception as error:\n"," print(error.errorCode)\n"," else:\n"," if p_logging_verbose:\n"," print('Temporary lakehouse '+lhname+' (' + temp_lh_id + ') already exists.')\n"," \n"," time.sleep(60) # waiting for temporary lakehouse to provision completely \n","\n"," # Create shortcuts for every table in the format of schema_table under the tables folder\n"," for index,itable in table_list.iterrows():\n"," shortcutExists=False\n"," # Check if shortcut exists\n"," try:\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts/Tables/\"+itable['schema']+'_'+itable['name']\n"," tlhresponse = client.get(url)\n"," shortcutExists = True\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] +' already exists')\n"," except Exception as error:\n"," shortcutExists = False \n","\n"," if not shortcutExists: \n"," # Create shortcuts - one per table per schema\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts\"\n"," scpayload = '{' \\\n"," '\"path\": \"Tables/\",' \\\n"," '\"name\": \"'+itable['schema']+'_'+itable['name']+'\",' \\\n"," '\"target\": {' \\\n"," '\"oneLake\": {' \\\n"," '\"workspaceId\": \"' + source_ws_id + '\",' \\\n"," '\"itemId\": \"'+ source_wh_id +'\",' \\\n"," '\"path\": \"/Tables/' + itable['schema']+'/'+itable['name'] + '\"' \\\n"," '}}}' \n"," try:\n"," #print(scpayload) \n"," shctresponse = client.post(url,json= json.loads(scpayload))\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] + ' created.' )\n","\n"," except Exception as error:\n"," print('Error creating shortcut '+itable['schema']+'_'+itable['name']+' due to '+str(error) + ':' + shctresponse.text)\n"," \n"," recovery_pipeline_prefix= 'plRecover_WH' \n"," # recovery pipeline name should not exceed 256 characters\n"," recovery_pipeline = recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'][:256]\n"," if p_logging_verbose:\n"," print('Attempting to deploy a copy pipeline in the target workspace to load the target warehouse tables from the shortcuts created above... ')\n"," # Create the pipeline in the target workspace that loads the target warehouse from shortcuts created above \n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," #print(plid)\n"," if plid == 'NotExists':\n"," plid = createDWrecoverypl(target_ws_id,recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'])\n"," if p_logging_verbose:\n"," print('Recovery pipeline ' + recovery_pipeline + ' created with Id '+plid)\n"," else:\n"," if p_logging_verbose:\n"," print('Datawarehouse recovery pipeline \"' + recovery_pipeline + '\" ('+plid+') already exist in workspace \"'+target_ws + '\" ('+target_ws_id+')') \n"," print('\\n')\n","\n"," tablesToCopyParam = table_list[['schema','name']].to_json( orient='records')\n"," # ensure the temporary lakehouse exists\n","\n"," # obtain the connection string for the lakehouse to pass to the copy pipeline\n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses/\" + temp_lh_id\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['sqlEndpointProperties']['connectionString']\n","\n"," # get the SQLEndpoint ID of the lakehouse to pass to the copy pipeline\n"," items = fabric.list_items(workspace=target_ws_id)\n"," print(items)\n"," temp_lh_sqle_id = items[(items['Type'] == 'SQLEndpoint') & (items['Display Name']==lhname)]['Id'].values[0]\n","\n","\n"," # obtain the connection string for the warehouse to pass to the copy pipeline \n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/warehouses/\" + target_wh_id\n"," whresponse = client.get(whurl)\n"," whconnStr = whresponse.json()['properties']['connectionInfo']\n","\n"," # obtain the pipeline id created to recover this warehouse\n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," if plid == 'NotExists':\n"," print('Error: Could not execute pipeline '+recovery_pipeline+ ' as the ID could not be obtained ')\n"," else:\n"," # pipeline url including pipeline Id unique to each warehouse\n"," plurl = 'v1/workspaces/'+target_ws_id+'/items/'+plid+'/jobs/instances?jobType=Pipeline'\n"," #print(plurl)\n","\n"," payload_data = '{' \\\n"," '\"executionData\": {' \\\n"," '\"parameters\": {' \\\n"," '\"lakehouseId\": \"' + temp_lh_sqle_id + '\",' \\\n"," '\"tablesToCopy\": ' + tablesToCopyParam + ',' \\\n"," '\"workspaceId\": \"' + target_ws_id +'\",' \\\n"," '\"warehouseId\": \"' + target_wh_id + '\",' \\\n"," '\"lakehouseConnStr\": \"' + lhconnStr + '\",' \\\n"," '\"warehouseConnStr\": \"' + whconnStr + '\"' \\\n"," '}}}'\n"," #print(payload_data)\n"," plresponse = client.post(plurl, json=json.loads(payload_data))\n"," if p_logging_verbose:\n"," print(str(plresponse.status_code)) \n","print('Done')\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"57dafef7-17a2-475f-9e62-eecc6660440c"},{"cell_type":"markdown","source":["##### Update directlake model lakehouse/warehouse connection\n","\n","https://semantic-link-labs.readthedocs.io/en/stable/sempy_labs.directlake.html#sempy_labs.directlake.update_direct_lake_model_connection "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"cc97be77-116e-4cde-bdc6-2971ab98a083"},{"cell_type":"code","source":["\n","df_datasets = fabric.list_datasets(target_ws)\n","\n","# Iterate over each dataset in the dataframe\n","for index, row in df_datasets.iterrows():\n"," # Check if the dataset is not the default semantic model\n"," if not labs.is_default_semantic_model(row['Dataset Name'], fabric.resolve_workspace_id(target_ws)):\n"," print('Updating semantic model connection ' + row['Dataset Name'] + ' in workspace '+ target_ws)\n"," labs.directlake.update_direct_lake_model_connection(dataset=row['Dataset Name'], \n"," workspace= target_ws,\n"," source=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[1], \n"," source_type=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[0], \n"," source_workspace=target_ws)\n"," labs.refresh_semantic_model(dataset=row['Dataset Name'], workspace= target_ws)\n","\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9deccda6-5c3d-4b88-8ed8-68855ca0949a"},{"cell_type":"markdown","source":["##### Rebind reports to local datasets\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/sempy_labs.report.html#sempy_labs.report.report_rebind"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"36783f3b-4904-4d74-842d-dbd026a3184a"},{"cell_type":"code","source":["df_reports = fabric.list_reports(workspace=target_ws)\n","for index, row in df_reports.iterrows():\n"," #print(row['Name'] + '-' + row['Dataset Id'])\n"," df_datasets = fabric.list_datasets(workspace=target_ws)\n"," dataset_name = df_datasets[df_datasets['Dataset ID'] == row['Dataset Id']]['Dataset Name'].values[0]\n"," print(f'Rebinding report to {dataset_name} in {target_ws}')\n"," labs.report.report_rebind(report=row['Name'],dataset=dataset_name, report_workspace=target_ws, dataset_workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"06268ede-b795-493e-9a8d-772654ce7e20"},{"cell_type":"markdown","source":["##### Update data pipeline source & sink connections\n","\n","Support changes lakehouses, warehouses, notebooks and connections from source to target.
\n","Connections changes should be expressed as an array of tuples [{from_1:to_1},{from_N:to_N}]"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4ae65012-350c-40c0-a68a-4069c567a85f"},{"cell_type":"code","source":["from typing import Optional\n","from sempy_labs._helper_functions import (\n"," resolve_workspace_name_and_id,\n"," lro,\n"," _decode_b64,\n",")\n","import sempy_labs._icons as icons\n","\n","import base64\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","\n","def update_data_pipeline_definition(\n"," name: str, pipeline_content: dict, workspace: Optional[str] = None\n","):\n"," \"\"\"\n"," Updates an existing data pipeline with a new definition.\n","\n"," Parameters\n"," ----------\n"," name : str\n"," The name of the data pipeline.\n"," pipeline_content : dict\n"," The data pipeline content (not in Base64 format).\n"," workspace : str, default=None\n"," The name of the workspace.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," \"\"\"\n","\n"," (workspace, workspace_id) = resolve_workspace_name_and_id(workspace)\n"," client = fabric.FabricRestClient()\n"," pipeline_payload = base64.b64encode(json.dumps(pipeline_content).encode('utf-8')).decode('utf-8')\n"," pipeline_id = fabric.resolve_item_id(\n"," item_name=name, type=\"DataPipeline\", workspace=workspace\n"," )\n","\n"," request_body = {\n"," \"definition\": {\n"," \"parts\": [\n"," {\n"," \"path\": \"pipeline-content.json\",\n"," \"payload\": pipeline_payload,\n"," \"payloadType\": \"InlineBase64\"\n"," }\n"," ]\n"," }\n"," }\n","\n","\n"," response = client.post(\n"," f\"v1/workspaces/{workspace_id}/items/{pipeline_id}/updateDefinition\",\n"," json=request_body,\n"," )\n","\n"," lro(client, response, return_status_code=True)\n","\n"," print(\n"," f\"{icons.green_dot} The '{name}' pipeline was updated within the '{workspace}' workspace.\"\n"," )\n","\n","def _is_valid_uuid(\n"," guid: str,\n","):\n"," \"\"\"\n"," Validates if a string is a valid GUID in version 4\n","\n"," Parameters\n"," ----------\n"," guid : str\n"," GUID to be validated.\n","\n"," Returns\n"," -------\n"," bool\n"," Boolean that indicates if the string is a GUID or not.\n"," \"\"\"\n","\n"," try:\n"," UUID(str(guid), version=4)\n"," return True\n"," except ValueError:\n"," return False"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"bdd46d4a-ef58-4f9a-b2e8-a428361a17c1"},{"cell_type":"code","source":["import json\n","from jsonpath_ng import jsonpath, parse\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","\n","# Swaps the connection properties of an activity belonging to the specified item type(s)\n","def swap_pipeline_connection(pl_json: dict, p_source_ws: str,p_target_ws: str, \n"," p_item_type: List =['DataWarehouse','Lakehouse','Notebook'], \n"," p_conn_from_to: Optional[List[Tuple[str,str]]]=[]):\n"," \n"," source_ws_id = fabric.resolve_workspace_id(source_ws)\n","\n"," target_ws_id = fabric.resolve_workspace_id(target_ws)\n","\n"," if 'Warehouse' in p_item_type or 'Lakehouse' in p_item_type:\n"," ls_expr = parse('$..linkedService')\n"," for endpoint_match in ls_expr.find(pl_json):\n"," if endpoint_match.value['properties']['type'] == 'DataWarehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Warehouse' in p_item_type:\n"," # only update the warehouse if it was located in the source workspace i.e. we will update the properties to the target workspace if the warehouse resided in the same workspace as the pipeline\n"," #print(endpoint_match.value)\n"," warehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," #print(warehouse_id)\n"," warehouse_endpoint = endpoint_match.value['properties']['typeProperties']['endpoint']\n"," #print(warehouse_endpoint)\n"," \n"," source_wh_name = fabric.resolve_item_name(item_id = warehouse_id,workspace=source_ws_id)\n"," #print(remote_wh_name)\n"," # find the warehouse id of the warehouse with the same name in the target workspace\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n"," # look up the connection string for the warehouse in the target workspace\n"," whurl = f\"v1/workspaces/{target_ws_id}/warehouses/{target_wh_id}\"\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['connectionString']\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_wh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," endpoint_match.value['properties']['typeProperties']['endpoint'] = lhconnStr\n"," #print(endpoint_match.value)\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," if endpoint_match.value['properties']['type'] == 'Lakehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Lakehouse' in p_item_type:\n"," #print(endpoint_match.value)\n"," lakehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," remote_lh_name = fabric.resolve_item_name(item_id = lakehouse_id,workspace=source_ws_id)\n"," # find the lakehouse id of the lakehouse with the same name in the target workspace\n"," target_lh_id = fabric.resolve_item_id(item_name = remote_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_lh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," # print(endpoint_match.value)\n","\n","\n"," if 'Notebook' in p_item_type: \n"," ls_expr = parse('$..activities')\n","\n"," for endpoint_match in ls_expr.find(pl_json):\n"," for activity in endpoint_match.value:\n"," #print(activity['type'])\n"," if activity['type']=='TridentNotebook' and 'Notebook' in p_item_type: #only update if the notebook was in the same workspace as the pipeline\n"," print('change from '+activity['typeProperties']['workspaceId'])\n"," source_nb_id = activity['typeProperties']['notebookId']\n"," source_nb_name = fabric.resolve_item_name(item_id = source_nb_id,workspace=source_ws_id)\n"," target_nb_id = fabric.resolve_item_id(item_name = source_nb_name,type='Notebook',workspace=target_ws_id)\n"," activity['typeProperties']['notebookId']=target_nb_id\n"," activity['typeProperties']['workspaceId']=target_ws_id\n"," print('to notebook '+ target_nb_id)\n"," #ls_expr.update(endpoint_match,endpoint_match.value)\n","\n"," if p_conn_from_to:\n"," for ti_conn_from_to in p_conn_from_to:\n"," if not _is_valid_uuid(ti_conn_from_to[0]):\n"," print('Connection from is string '+ str(ti_conn_from_to[0]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[0]] \n"," connId_from = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_from = ti_conn_from_to[0]\n","\n"," if not _is_valid_uuid(ti_conn_from_to[1]):\n"," print('Connection from is string '+ str(ti_conn_from_to[1]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[1]] \n"," connId_to = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_to = ti_conn_from_to[1]\n","\n"," ls_expr = parse('$..externalReferences')\n"," for externalRef in ls_expr.find(pl_json):\n"," if externalRef.value['connection']==connId_from:\n"," print('Changing connection from '+str(connId_from))\n"," externalRef.value['connection']=connId_to\n"," ls_expr.update(externalRef,externalRef.value)\n"," print('to '+str(connId_to))\n","\n"," return pl_json\n","\n","\n","\n","# loading a dataframe of connections to perform an ID lookup if required \n","df_conns = labs.list_connections()\n","\n","df_pipeline = labs.list_data_pipelines(target_ws)\n","for index, row in df_pipeline.iterrows():\n"," pipeline_json = json.loads(labs.get_data_pipeline_definition(row['Data Pipeline Name'],source_ws))\n","\n"," p_new_json = swap_pipeline_connection(pipeline_json, source_ws,target_ws,\n"," ['DataWarehouse','Lakehouse','Notebook'],\n"," [p_connections_from_to]) \n"," #print(json.dumps(pipeline_json, indent=4))\n"," \n"," update_data_pipeline_definition(name=row['Data Pipeline Name'],pipeline_content=pipeline_json, workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"079958e8-2880-484a-a994-41caf47e747e"},{"cell_type":"markdown","source":["##### Commit changes made above to Git"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"44174276-b983-4e80-9451-0afb9589cf1f"},{"cell_type":"code","source":["labs.commit_to_git(comment='Initial', workspace=target_ws)"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9a5c3d84-f71d-4348-b419-c4953ac9e1d0"}],"metadata":{"kernel_info":{"name":"synapse_pyspark"},"kernelspec":{"name":"synapse_pyspark","language":"Python","display_name":"Synapse PySpark"},"language_info":{"name":"python"},"microsoft":{"language":"python","language_group":"synapse_pyspark","ms_spell_check":{"ms_spell_check_language":"en"}},"widgets":{},"nteract":{"version":"nteract-front-end@1.0.0"},"synapse_widget":{"version":"0.1","state":{}},"spark_compute":{"compute_id":"/trident/default","session_options":{"conf":{"spark.synapse.nbs.session.timeout":"1200000"}}},"dependencies":{"lakehouse":{}}},"nbformat":4,"nbformat_minor":5} \ No newline at end of file +{"cells":[{"cell_type":"markdown","source":["##### Branch out to new workspace notebook - post activity\n","\n","After cloning a workspace, this notebook will reconfigure any references to the old workspace by rebinding them to the new workspace. \n","\n","For example a pipeline referencing a warehouse or a default lakehouse of a notebook.\n","\n","This notebook runs post activity tasks can be run after [branch out to new workspace functionality](https://blog.fabric.microsoft.com/en-us/blog/introducing-new-branching-capabilities-in-fabric-git-integration) or the [custom AzDO script](https://github.com/microsoft/fabric-toolbox/blob/main/accelerators/CICD/Branch-out-to-new-workspace/AzDO/scripts/BranchOut-Feature-Workspace-Automation.py).\n","\n","Summary of post activities in order:\n","
    \n","
  • Default lakehouses and warehouse are updated to local lakehouse/warehouses
  • \n","
  • Either creates shortcuts in local lakehouse back to tables in the source lakehouse, or copies the data from source lakehouse. Set via parameter below.
  • \n","
  • Copy warehouse data. Set via parameter below
  • \n","
  • Changes directlake semantic model connections for semantic models to \"local\" lakehouse/warehouse
  • \n","
  • Rebinds reports to \"local\" semantic models
  • \n","
  • Changes pipeline lakehouse/warehouse references to local item
  • \n","
  • Ability to swap connections in pipelines from old to new
  • \n","
  • Commit changes to git
  • \n","
\n","\n","Requirements:\n","
    \n","
  • Requires Semantic Link Labs installed by pip install below or added to environment library.
  • \n","
  • Requires JmesPath library for data pipeline JSON manipulation i.e. connection swaps.
  • \n","
\n","\n","Limitations of current script:\n","\n","
    \n","
  • Does not recreate item shares or external shortcuts
  • \n","
  • Does not re-apply lakehouse SQL Endpoint or Warehouse object/row/column level security
  • \n","
  • Does not recreate data access roles in Lakehouse
  • \n","
  • Untested with Lakehouses where with schema support enabled
  • \n","
\n","\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a98b6d0a-7a36-4116-ab0d-aa70144eb737"},{"cell_type":"markdown","source":["##### Install semantic link labs to support advanced functionality\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/index.html\n","https://github.com/microsoft/semantic-link-labs/blob/main/README.md\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"3b887bd6-a9c9-430f-b58f-b58a93f5ce29"},{"cell_type":"code","source":["%pip -q install semantic-link-labs\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":true},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"1b03316d-c088-4a0e-a2f0-44d45d112121"},{"cell_type":"markdown","source":["##### Install Jmespath to make data pipeline changes such as updating linked notebooks, warehouses and lakehouses "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"8a74ed11-dd64-43bb-a735-906a947c8666"},{"cell_type":"code","source":["%pip install jmespath"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"c68be6ba-7648-457f-af82-f1987d12d7f7"},{"cell_type":"markdown","source":["##### Set these parameters if running as a standaone noteook\n","Before running this notebook ensure these parameters are set correctly. If necessary these can be passed in via a data factory pipeline.\n","\n","If running this notebook from Azure DevOps ignore these parameters as these will be passed from the release pipeline script"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"dee81614-b92b-4242-890a-b11f97b1a640"},{"cell_type":"code","source":["source_ws = ''\n","target_ws = ''\n","\n","# Either copy lakehouse data or create shortcuts, set at most one of these to True \n","copy_lakehouse_data = False\n","create_lakehouse_shortcuts = True\n","\n","# Option to copy warehouse data if required\n","copy_warehouse_data = False\n","\n","# If false then shortcuts will be created. If you wish to create shortcuts based on a pattern match please set the param below\n","# enter pattern match for creating shortcuts - see https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py \n","PATTERN_MATCH = [\"*\"]\n","\n","# Set connections to be replaced from previous name or ID to new name or ID.\n","connections_from_to = () #('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726'),('4498340c-27cf-4c6e-a025-00e5de6b0726','https://api.fabric.microsoft.com/v1/workspaces/ admin'),('https://api.fabric.microsoft.com/v1/workspaces/ admin','4498340c-27cf-4c6e-a025-00e5de6b0726')\n","\n","# Determines whether lakehoues tables/shortcuts should be created before git sync due to warehouse dependencies \n","has_wh_views_on_lh = True\n","\n","# Set the Azure DevOps related parameters \n","project_name=''\n","repo_name=''\n","main_branch = ''\n","branch_name = ''\n","org_name = ''\n","ado_api_url = 'https://dev.azure.com/'\n","\n","# Azure Key Vault and Secret Name which stores the Azure DevOps PAT\n","key_vault_name = ''\n","secret_name = ''\n","target_capacity = '' # name or ID. leave empty '' to use the same capacity as source workspace\n","\n","\n","### Do not change these parameters ####\n","# internal parameter to allow the installation of Python libraries when being run programatically. See https://learn.microsoft.com/en-us/fabric/data-engineering/library-management#python-inline-installation\n","_inlineInstallationEnabled = True\n","\n","# indicates whether this script is called from Azure Devops\n","_runStandalone = True\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"tags":["parameters"]},"id":"90efaa4f-846d-4924-900e-258837a3467d"},{"cell_type":"markdown","source":["##### Library imports and fabric rest client setup\n","\n","https://learn.microsoft.com/en-us/python/api/semantic-link-sempy/sempy.fabric.fabricrestclient"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4fb01e1d-ec4e-4c69-b544-66f6d8c5a475"},{"cell_type":"code","source":["import pandas as pd\n","import datetime, time\n","import re,json, fnmatch,os\n","import requests, base64\n","import sempy\n","import sempy.fabric as fabric\n","from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException\n","from pyspark.sql import DataFrame\n","from pyspark.sql.functions import col,current_timestamp,lit\n","import sempy_labs as labs\n","from sempy_labs import migration, directlake\n","from sempy_labs import lakehouse as lake\n","from sempy_labs import report as rep\n","from sempy_labs.tom import connect_semantic_model\n","from typing import Optional\n","from sempy_labs._helper_functions import (\n"," resolve_workspace_name_and_id,\n"," lro,\n"," _decode_b64,\n",")\n","import sempy_labs._icons as icons\n","\n","import base64\n","from typing import Optional, Tuple, List\n","from uuid import UUID\n","import ast\n","from jsonpath_ng import jsonpath, parse\n","\n","# instantiate the Fabric rest client\n","client = fabric.FabricRestClient()\n","\n","# get the current workspace ID based on the context of where this notebook is run from\n","thisWsId = notebookutils.runtime.context['currentWorkspaceId']\n","thisWsName = notebookutils.runtime.context['currentWorkspaceName']\n","\n","source_ws_id = fabric.resolve_workspace_id(source_ws)\n","target_ws_id = '' # will be set later"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"391624c1-b299-452d-9ebf-f32626d49970"},{"cell_type":"markdown","source":["##### Always run this cell\n","\n","Contains utility functions to support lakehouse and warehouse initialisation\n","\n","and\n","\n","Shortcut creator:\n","https://github.com/arasdk/fabric-code-samples/blob/main/shortcuts/fabric_shortcut_creator.py "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4e7d0414-515c-4627-a183-553ce4ccf8e5"},{"cell_type":"code","source":["#### \n","### Setup and AzDO functions \n","####\n","\n","\n","def _is_valid_uuid(\n"," guid: str,\n","):\n"," \"\"\"\n"," Validates if a string is a valid GUID in version 4\n","\n"," Parameters\n"," ----------\n"," guid : str\n"," GUID to be validated.\n","\n"," Returns\n"," -------\n"," bool\n"," Boolean that indicates if the string is a GUID or not.\n"," \"\"\"\n","\n"," try:\n"," UUID(str(guid), version=4)\n"," return True\n"," except ValueError:\n"," return False\n","\n","def get_capacity_status(p_target_cap):\n"," dfC = fabric.list_capacities()\n"," dfC_filt = dfC[dfC[\"Id\"] == p_target_cap]\n"," return dfC_filt['State'].iloc[0]\n","\n","\n","#TODO create token via SPN\n","def get_branch_object_id(p_ado_url,p_branch_name, p_token):\n"," try:\n"," print(f\"Retriving ID of main branch {branch_name} to be cloned \")\n"," headers = {'Authorization': f'Basic {p_token}',\n"," 'Content-Type': 'application/json'\n"," }\n","\n"," response = requests.get(f\"{p_ado_url}/heads/{p_branch_name}?api-version=7.1\", headers=headers)\n"," return response.json()[\"value\"][0][\"objectId\"]\n"," except requests.exceptions.RequestException as e:\n"," print(f\"Error getting branch object ID: {e}\")\n"," return None\n","\n","def encode_pat(pat):\n"," # Encode the PAT in base64\n"," encoded_pat = base64.b64encode(pat.encode('utf-8')).decode('utf-8')\n"," return encoded_pat\n","\n","def create_azdo_branch(p_key_vault_name,p_secret_name, p_branch_name, p_main_branch,p_repo_name,p_ado_url):\n"," access_token =notebookutils.credentials.getToken('keyvault')\n"," url = f'https://{p_key_vault_name}.vault.azure.net/secrets/{p_secret_name}?api-version=7.3'\n"," headers = {\n"," 'Authorization': f'Bearer {access_token}',\n"," 'Content-Type': 'application/json'\n"," }\n","\n"," response = requests.get(url, headers=headers)\n"," if response.status_code == 200:\n"," #print(response.json()['value'])\n"," pat_token = encode_pat(':'+response.json()['value'])\n"," else:\n"," raise ValueError(f\"Failed to get secret: {response.status_code} - {response.text}\")\n","\n"," try:\n"," print(f\"Creating feature branch {p_branch_name} based on {p_main_branch} in progress\")\n"," headers = {\"Authorization\": f\"Basic {pat_token}\", \"Content-Type\": \"application/json\"}\n"," data = [\n"," {\n"," \"name\":f\"refs/heads/{p_branch_name}\",\n"," \"oldObjectId\": \"0000000000000000000000000000000000000000\",\n"," \"newObjectId\": get_branch_object_id(p_ado_url,p_main_branch, pat_token)\n"," }\n"," ]\n"," response = requests.post(f\"{p_ado_url}?api-version=7.1\", headers=headers, json=data)\n"," response.raise_for_status()\n"," if (response.json()['value'][0]['success']):\n"," return True\n"," else:\n"," return False\n"," except requests.exceptions.RequestException as e:\n"," raise ValueError(f\"Error creating Azure DevOps branch: {e}\")\n","\n","\n","\n","##### \n","### Shortcut utility function \n","####\n","\n","# Extract workspace_id, item_id and path from a onelake URI\n","def extract_onelake_https_uri_components(uri):\n"," # Define a regular expression to match any string between slashes and capture the final path element(s) without the leading slash\n"," pattern = re.compile(r\"abfss://([^@]+)@[^/]+/([^/]+)/(.*)\")\n"," match = pattern.search(uri)\n"," if match:\n"," workspace_id, item_id, path = match.groups()\n"," return workspace_id, item_id, path\n"," else:\n"," return None, None, None\n","\n","\n","def is_valid_onelake_uri(uri: str) -> bool:\n"," workspace_id, item_id, path = extract_onelake_https_uri_components(uri)\n"," if \"abfss://\" not in uri or workspace_id is None or item_id is None or path is None:\n"," return False\n","\n"," return True\n","\n","\n","def get_last_path_segment(uri: str):\n"," path = uri.split(\"/\") # Split the entire URI by '/'\n"," return path[-1] if path else None\n","\n","\n","def is_delta_table(uri: str):\n"," delta_log_path = os.path.join(uri, \"_delta_log\")\n"," return mssparkutils.fs.exists(delta_log_path)\n","\n","\n","def get_onelake_shorcut(workspace_id: str, item_id: str, path: str, name: str):\n"," shortcut_uri = (\n"," f\"v1/workspaces/{workspace_id}/items/{item_id}/shortcuts/{path}/{name}\"\n"," )\n"," result = client.get(shortcut_uri).json()\n"," return result\n","\n","\n","def is_folder_matching_pattern(path: str, folder_name: str, patterns: []):\n"," if folder_name in patterns:\n"," return True\n"," else:\n"," for pattern in patterns:\n"," if fnmatch.fnmatch(folder_name, pattern):\n"," return is_delta_table(path)\n","\n"," return False\n","\n","\n","def get_matching_delta_tables_uris(uri: str, patterns: []) -> []:\n"," # Use a set to avoid duplicates\n"," matched_uris = set()\n"," files = mssparkutils.fs.ls(uri)\n"," folders = [item for item in files if item.isDir]\n","\n"," # Filter folders to only those that matches the pattern and is a delta table\n"," matched_uris.update(\n"," folder.path\n"," for folder in folders\n"," if is_folder_matching_pattern(folder.path, folder.name, patterns)\n"," )\n","\n"," return matched_uris\n","\n","\n","def create_onelake_shorcut(source_uri: str, dest_uri: str):\n"," src_workspace_id, src_item_id, src_path = extract_onelake_https_uri_components(\n"," source_uri\n"," )\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri\n"," )\n","\n"," name = get_last_path_segment(source_uri)\n"," dest_uri_joined = os.path.join(dest_uri, name)\n","\n"," # If the destination path already exists, return without creating shortcut\n"," if mssparkutils.fs.exists(dest_uri_joined):\n"," print(f\"Destination already exists: {dest_uri_joined}\")\n"," return None\n","\n"," request_body = {\n"," \"name\": name,\n"," \"path\": dest_path,\n"," \"target\": {\n"," \"oneLake\": {\n"," \"itemId\": src_item_id,\n"," \"path\": src_path,\n"," \"workspaceId\": src_workspace_id,\n"," }\n"," },\n"," }\n","\n"," shortcut_uri = f\"v1/workspaces/{dest_workspace_id}/items/{dest_item_id}/shortcuts\"\n"," print(f\"Creating shortcut: {shortcut_uri}/{name}..\")\n"," try:\n"," client.post(shortcut_uri, json=request_body)\n"," except FabricHTTPException as e:\n"," print(e)\n"," return None\n","\n"," return get_onelake_shorcut(dest_workspace_id, dest_item_id, dest_path, name)\n"," \n","\n","####\n","## Copy lakehouse and warehouse utility functions\n","####\n","\n","def get_lh_object_list(base_path,data_types = ['Tables', 'Files'])->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a lakehouse\n"," adapted from https://fabric.guru/getting-a-list-of-folders-and-delta-tables-in-the-fabric-lakehouse\n"," This function will return a pandas dataframe containing names and abfss paths of each folder for Files and Tables\n"," '''\n"," #data_types = ['Tables', 'Files'] #for if you want a list of files and tables\n"," #data_types = ['Tables'] #for if you want a list of tables\n","\n"," df = pd.concat([\n"," pd.DataFrame({\n"," 'name': [item.name for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," 'type': data_type[:-1].lower() , \n"," 'src_path': [item.path for item in notebookutils.fs.ls(f'{base_path}/{data_type}/')],\n"," }) for data_type in data_types], ignore_index=True)\n","\n"," return df\n","\n","def get_wh_object_list(schema_list,base_path)->pd.DataFrame:\n","\n"," '''\n"," Function to get a list of tables for a warehouse by schema\n"," '''\n"," data_type = 'Tables'\n"," dfs = []\n","\n"," for schema_prefix in schema_list:\n"," if notebookutils.fs.exists(f'{base_path}/{data_type}/{schema_prefix}/'):\n"," items = notebookutils.fs.ls(f'{base_path}/{data_type}/{schema_prefix}/')\n"," if items: # Check if the list is not empty\n"," df = pd.DataFrame({\n"," 'schema': schema_prefix,\n"," 'name': [item.name for item in items],\n"," 'type': data_type[:-1].lower(),\n"," 'src_path': [item.path for item in items],\n"," })\n"," dfs.append(df)\n","\n"," if dfs: # Check if the list of dataframes is not empty\n"," df = pd.concat(dfs, ignore_index=True)\n"," else:\n"," df = pd.DataFrame() # Return an empty dataframe if no dataframes were created\n","\n"," return df\n","\n","def copy_lh_objects(table_list,workspace_src,workspace_tgt,lakehouse_src,lakehouse_tgt,fastcopy=True,usingIDs=False)->pd.DataFrame:\n"," # declare an array to keep the instrumentation\n"," cpresult = []\n"," # loop through all the tables to extract the source path \n"," for table in table_list.src_path:\n"," source = table\n"," destination = source.replace(f'abfss://{workspace_src}', f'abfss://{workspace_tgt}')\n"," if usingIDs:\n"," destination = destination.replace(f'{lakehouse_src}', f'{lakehouse_tgt}')\n"," else:\n"," destination = destination.replace(f'{lakehouse_src}.Lakehouse', f'{lakehouse_tgt}.Lakehouse')\n"," start_time = datetime.datetime.now()\n"," if notebookutils.fs.exists(destination):\n"," notebookutils.fs.rm(destination, True)\n"," if fastcopy:\n"," # use fastcopy util which is a python wrapper to azcopy\n"," notebookutils.fs.fastcp(source+'/*', destination+'/', True)\n"," else:\n"," notebookutils.fs.cp(source, destination, True)\n","\n"," # recording the timing and add it to the results list\n"," end_time = datetime.datetime.now()\n"," copyreslist = [source, destination, start_time.strftime(\"%Y-%m-%d %H:%M:%S\"), end_time.strftime(\"%Y-%m-%d %H:%M:%S\"), str((end_time - start_time).total_seconds())]\n"," cpresult.append(copyreslist)\n"," return pd.DataFrame(cpresult,columns =['source--------------------------------------','target--------------------------------------','start------------','end_time------------','elapsed seconds----'])\n","\n","def createDWrecoverypl(ws_id,pl_name = 'Recover_Warehouse_Data_From_DR'):\n"," client = fabric.FabricRestClient()\n","\n"," dfurl= \"v1/workspaces/\"+ ws_id + \"/items\"\n"," payload = { \n"," \"displayName\": pl_name, \n"," \"type\": \"DataPipeline\", \n"," \"definition\": { \n"," \"parts\": [ \n"," { \n"," \"path\": \"pipeline-content.json\", \n"," \"payload\": \"ewogICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgImFjdGl2aXRpZXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICJuYW1lIjogIkl0ZXJhdGVTY2hlbWFUYWJsZXMiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiRm9yRWFjaCIsCiAgICAgICAgICAgICAgICAiZGVwZW5kc09uIjogW10sCiAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy50YWJsZXNUb0NvcHkiLAogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgImJhdGNoQ291bnQiOiAyMCwKICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdGllcyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiQ29weVdhcmVob3VzZVRhYmxlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJDb3B5IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlcGVuZGVuY3lDb25kaXRpb25zIjogWwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlN1Y2NlZWRlZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgXQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicG9saWN5IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0aW1lb3V0IjogIjAuMTI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZXRyeSI6IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInJldHJ5SW50ZXJ2YWxJblNlY29uZHMiOiAzMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZVNvdXJjZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJxdWVyeVRpbWVvdXQiOiAiMDI6MDA6MDAiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicGFydGl0aW9uT3B0aW9uIjogIk5vbmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwN2EwMzAwNl9kMWI2XzRhMzlfYmViMV8wYmJhMmFhZjVmZjciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLmxha2Vob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy5sYWtlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAY29uY2F0KGNvbmNhdChpdGVtKCkuc2NoZW1hLCdfJyksaXRlbSgpLm5hbWUpIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRXhwcmVzc2lvbiIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzaW5rIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlU2luayIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhbGxvd0NvcHlDb21tYW5kIjogdHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRhYmxlT3B0aW9uIjogImF1dG9DcmVhdGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YXNldFNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFubm90YXRpb25zIjogW10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGlua2VkU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICIwYzAzMTIzYV9kMzEyXzQ2YzRfYThlN181YjRjYWQ4ZjEyZDciLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiRGF0YVdhcmVob3VzZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJlbmRwb2ludCI6ICJAcGlwZWxpbmUoKS5wYXJhbWV0ZXJzLndhcmVob3VzZUNvbm5TdHIiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFydGlmYWN0SWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53YXJlaG91c2VJZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAid29ya3NwYWNlSWQiOiAiQHBpcGVsaW5lKCkucGFyYW1ldGVycy53b3Jrc3BhY2VJZCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhV2FyZWhvdXNlVGFibGUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6IFtdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGFibGUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6ICJAaXRlbSgpLm5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImVuYWJsZVN0YWdpbmciOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0cmFuc2xhdG9yIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJUYWJ1bGFyVHJhbnNsYXRvciIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvbiI6IHRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlQ29udmVyc2lvblNldHRpbmdzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImFsbG93RGF0YVRydW5jYXRpb24iOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRyZWF0Qm9vbGVhbkFzTnVtYmVyIjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiU2V0IHRhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYWN0aXZpdHkiOiAiU2V0IHNjaGVtYSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRlbmN5Q29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJTdWNjZWVkZWQiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInBvbGljeSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAic2VjdXJlT3V0cHV0IjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZUlucHV0IjogZmFsc2UKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZVByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhcmlhYmxlTmFtZSI6ICJUYWJsZW5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogIkBpdGVtKCkubmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkV4cHJlc3Npb24iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJTZXQgc2NoZW1hIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIjogIlNldFZhcmlhYmxlIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkZXBlbmRzT24iOiBbXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb2xpY3kiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInNlY3VyZU91dHB1dCI6IGZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzZWN1cmVJbnB1dCI6IGZhbHNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGVQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YXJpYWJsZU5hbWUiOiAiU2NoZW1hbmFtZSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiOiAiQGl0ZW0oKS5zY2hlbWEiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJFeHByZXNzaW9uIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIF0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgInBhcmFtZXRlcnMiOiB7CiAgICAgICAgICAgICJsYWtlaG91c2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjBmMGY2YjdjLTE3NjEtNDFlNi04OTZlLTMwMDE0ZjE2ZmY2ZCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInRhYmxlc1RvQ29weSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogImFycmF5IiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiBbCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkRhdGUiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiR2VvZ3JhcGh5IgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIkhhY2tuZXlMaWNlbnNlIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIk1lZGFsbGlvbiIKICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAgICAgInNjaGVtYSI6ICJkYm8iLAogICAgICAgICAgICAgICAgICAgICAgICAibmFtZSI6ICJUaW1lIgogICAgICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICAgICAic2NoZW1hIjogImRibyIsCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIjogIlRyaXAiCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgICJzY2hlbWEiOiAiZGJvIiwKICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiOiAiV2VhdGhlciIKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBdCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ3b3Jrc3BhY2VJZCI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjE1MDExNDNjLTI3MmYtNGEyZi05NzZhLTdlNTU5NzFlNGMyYiIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIndhcmVob3VzZUlkIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNGQxYmQ5NTEtOTlkZS00YmQ3LWI3YmMtNzFjOGY1NmRiNDExIgogICAgICAgICAgICB9LAogICAgICAgICAgICAid2FyZWhvdXNlQ29ublN0ciI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgICAiZGVmYXVsdFZhbHVlIjogIjcyd3diaXZpMnViZWpicnRtdGFobzMyYjR5LWhxa2FjZmpwZTR4dXZmM2twemt6b2hzbWZtLmRhdGF3YXJlaG91c2UuZmFicmljLm1pY3Jvc29mdC5jb20iCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYWtlaG91c2VDb25uU3RyIjogewogICAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAgICJkZWZhdWx0VmFsdWUiOiAiNzJ3d2JpdmkydWJlamJydG10YWhvMzJiNHktaHFrYWNmanBlNHh1dmYza3B6a3pvaHNtZm0uZGF0YXdhcmVob3VzZS5mYWJyaWMubWljcm9zb2Z0LmNvbSIKICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInZhcmlhYmxlcyI6IHsKICAgICAgICAgICAgIlRhYmxlbmFtZSI6IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogIlN0cmluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgIlNjaGVtYW5hbWUiOiB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJTdHJpbmciCiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJsYXN0TW9kaWZpZWRCeU9iamVjdElkIjogIjRhYTIwYWY3LTk0YmQtNDM0OC1iZWY4LWY4Y2JjZDg0MGQ1MSIsCiAgICAgICAgImxhc3RQdWJsaXNoVGltZSI6ICIyMDI0LTExLTEzVDE1OjUyOjUyWiIKICAgIH0KfQ==\", \n"," \"payloadType\": \"InlineBase64\" \n"," } \n"," ] \n"," } \n","} \n"," \n"," response = json.loads(client.post(dfurl,json= payload).content)\n"," return response['id']\n","\n","def getItemId(wks_id,itm_name,itm_type):\n"," df = fabric.list_items(type=None,workspace=wks_id)\n"," #print(df)\n"," if df.empty:\n"," return 'NotExists'\n"," else:\n"," #display(df)\n"," #print(df.query('\"Display Name\"=\"'+itm_name+'\"'))\n"," if itm_type != '':\n"," newdf= df.loc[(df['Display Name'] == itm_name) & (df['Type'] == itm_type)]['Id']\n"," else:\n"," newdf= df.loc[(df['Display Name'] == itm_name)]['Id'] \n"," if newdf.empty:\n"," return 'NotExists'\n"," else:\n"," return newdf.iloc[0]\n","\n","\n","# modified semantic link labs function - pendin PR\n","import sempy_labs._icons as icons\n","from typing import Optional, List\n","from sempy_labs._helper_functions import (\n"," resolve_workspace_name_and_id,\n"," _base_api,\n",")\n","from uuid import UUID\n","\n","def initialize_git_connection(workspace: Optional[str | UUID] = None, initialization_strategy: Optional[str] = 'None') -> str:\n"," \"\"\"\n"," Initializes a connection for a workspace that is connected to Git.\n","\n"," This is a wrapper function for the following API: `Git - Initialize Connection `_.\n","\n"," Parameters\n"," ----------\n"," workspace : str | uuid.UUID, default=None\n"," The Fabric workspace name or ID.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," initialization_strategy : str\n"," Defines the strategy required for an initialization process when content \n"," exists on both the remote side and the workspace side. \n"," Additional strategies may be added over time.\n"," Supported options: PreferWorkspace, PreferRemote or None (default)\n","\n"," Returns\n"," -------\n"," str\n"," Remote full SHA commit hash.\n"," \"\"\"\n","\n"," (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)\n","\n","\n"," payload = {\n"," \"initializationStrategy\": initialization_strategy\n"," }\n"," response_json = _base_api(\n"," request=f\"/v1/workspaces/{workspace_id}/git/initializeConnection\",\n"," payload=payload,\n"," method=\"post\",\n"," lro_return_json=True,\n"," status_codes=None,\n"," )\n"," \n"," print(\n"," f\"{icons.green_dot} The '{workspace_name}' workspace git connection has been initialized.\"\n"," )\n"," return response_json.get(\"remoteCommitHash\")\n","\n","\n","\n","def initialize_git_connection_int(workspace: Optional[str | UUID] = None, initialization_strategy: Optional[str] = 'None') -> str:\n"," \"\"\"\n"," Initializes a connection for a workspace that is connected to Git.\n","\n"," This is a wrapper function for the following API: `Git - Initialize Connection `_.\n","\n"," Parameters\n"," ----------\n"," workspace : str | uuid.UUID, default=None\n"," The Fabric workspace name or ID.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," initialization_strategy : str\n"," Defines the strategy required for an initialization process when content \n"," exists on both the remote side and the workspace side. \n"," Additional strategies may be added over time.\n"," Supported options: PreferWorkspace, PreferRemote or None (default)\n","\n"," Returns\n"," -------\n"," str\n"," Remote full SHA commit hash.\n"," \"\"\"\n","\n"," (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)\n","\n","\n"," #payload = {\n"," # \"initializationStrategy\": initialization_strategy\n"," #}\n"," url = f\"https://wabi-west-us3-a-primary-redirect.analysis.windows.net/metadata/git/workspaces/{target_ws_id}/connection/initialize\"\n"," #response_json = _base_api(\n"," # request=f\"/v1/workspaces/{workspace_id}/git/initializeConnection\",\n"," # payload=payload,\n"," # method=\"post\",\n"," # lro_return_json=True,\n"," # status_codes=None,\n"," #)\n"," from notebookutils import credentials\n","\n"," data = {\"mergePolicy\":2}\n"," # Get the Fabric token\n"," token = credentials.getToken(\"pbi\")\n","\n"," # Print the token\n"," #print(token)\n"," access_token = token\n"," headers = {'content-type': 'application/json', 'authorization': f'Bearer {access_token}'}\n"," response = requests.post(url, headers=headers, json=data)\n","\n"," print(\n"," f\"{icons.green_dot} The '{workspace_name}' workspace git connection has been initialized.\"\n"," )\n"," return response.json().get(\"partialSyncBaseCommit\")\n"," #return response_json.get(\"remoteCommitHash\")\n","\n","\n","def create_shortcuts_or_copy_data(p_copy_lakehouse_data,p_create_lakehouse_shortcuts):\n"," df_lhs = labs.list_lakehouses(source_ws)\n"," for index, row in df_lhs.iterrows():\n","\n","\n"," if copy_lakehouse_data:\n"," lh_name= row['Lakehouse Name']\n"," if lh_name.find('temp')==-1:\n"," # Gathers the list of tables and source paths to be copied into the target lakehouse \n"," src_path = f'abfss://{source_ws}@onelake.dfs.fabric.microsoft.com/{lh_name}.Lakehouse'\n","\n"," table_list = get_lh_object_list(src_path)\n"," print(f'Attempting to copy table data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n"," display(table_list)\n","\n"," #print('Copy Lakehouse Delta tables...')\n"," res = copy_lh_objects(table_list[table_list['type']=='table'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," # Copy files\n"," print(f'Attempting to copy file data for lakehouse {lh_name} from workspace {source_ws} to {target_ws}...')\n","\n"," #print('Copy Lakehouse files...')\n"," res = copy_lh_objects(table_list[table_list['type']=='file'],source_ws,target_ws,\n"," lh_name,lh_name,False,False)\n"," display(res)\n"," print('Done.')\n","\n"," else:\n"," # fetch ID of source lakehouse based on name and workspace\n"," source_lh_id = row['Lakehouse ID']\n"," target_lh_id = fabric.resolve_item_id(\n"," item_name=row['Lakehouse Name'], type=\"Lakehouse\", workspace=target_ws\n"," )\n","\n"," SOURCE_URI = f\"abfss://{source_ws_id}@onelake.dfs.fabric.microsoft.com/{source_lh_id}/Tables\"\n"," DEST_URI = f\"abfss://{target_ws_id}@onelake.dfs.fabric.microsoft.com/{target_lh_id}/Tables\"\n","\n"," if PATTERN_MATCH is None or len(PATTERN_MATCH) == 0:\n"," raise TypeError(\"Argument 'PATTERN_MATCH' should be a valid list of patterns or [\"*\"] to match everything\")\n","\n"," # Collect created shortcuts\n"," result = []\n","\n"," # If either URI's are invalid, just return\n"," if not is_valid_onelake_uri(SOURCE_URI) or not is_valid_onelake_uri(DEST_URI):\n"," print(\n"," \"invalid URI's provided. URI's should be in the form: abfss://@onelake.dfs.fabric.microsoft.com//\"\n"," )\n"," else:\n"," # Remove any trailing '/' from uri's\n"," source_uri_addr = SOURCE_URI.rstrip(\"/\")\n"," dest_uri_addr = DEST_URI.rstrip(\"/\")\n","\n"," dest_workspace_id, dest_item_id, dest_path = extract_onelake_https_uri_components(\n"," dest_uri_addr\n"," )\n","\n"," # If we are not shortcutting to a managed table folder or\n"," # the source uri is a delta table, just shortcut it 1-1.\n"," if not dest_path.startswith(\"Tables\") or is_delta_table(source_uri_addr):\n"," shortcut = create_onelake_shorcut(source_uri_addr, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," else:\n"," # If source is not a delta table, and destination is managed table folder:\n"," # Iterate over source folders and create table shortcuts @ destination\n"," for delta_table_uri in get_matching_delta_tables_uris(\n"," source_uri_addr, PATTERN_MATCH\n"," ):\n"," shortcut = create_onelake_shorcut(delta_table_uri, dest_uri_addr)\n"," if shortcut is not None:\n"," result.append(shortcut)\n"," print(result)\n","\n","\n","########\n","### Pipeline utilities\n","########\n","\n","def update_data_pipeline_definition(\n"," name: str, pipeline_content: dict, workspace: Optional[str] = None\n","):\n"," \"\"\"\n"," Updates an existing data pipeline with a new definition.\n","\n"," Parameters\n"," ----------\n"," name : str\n"," The name of the data pipeline.\n"," pipeline_content : dict\n"," The data pipeline content (not in Base64 format).\n"," workspace : str, default=None\n"," The name of the workspace.\n"," Defaults to None which resolves to the workspace of the attached lakehouse\n"," or if no lakehouse attached, resolves to the workspace of the notebook.\n"," \"\"\"\n","\n"," (workspace, workspace_id) = resolve_workspace_name_and_id(workspace)\n"," client = fabric.FabricRestClient()\n"," pipeline_payload = base64.b64encode(json.dumps(pipeline_content).encode('utf-8')).decode('utf-8')\n"," pipeline_id = fabric.resolve_item_id(\n"," item_name=name, type=\"DataPipeline\", workspace=workspace\n"," )\n","\n"," request_body = {\n"," \"definition\": {\n"," \"parts\": [\n"," {\n"," \"path\": \"pipeline-content.json\",\n"," \"payload\": pipeline_payload,\n"," \"payloadType\": \"InlineBase64\"\n"," }\n"," ]\n"," }\n"," }\n","\n","\n"," response = client.post(\n"," f\"v1/workspaces/{workspace_id}/items/{pipeline_id}/updateDefinition\",\n"," json=request_body,\n"," )\n","\n"," lro(client, response, return_status_code=True)\n","\n"," print(\n"," f\"{icons.green_dot} The '{name}' pipeline was updated within the '{workspace}' workspace.\"\n"," )\n","\n","# Swaps the connection properties of an activity belonging to the specified item type(s)\n","def swap_pipeline_connection(pl_json: dict, p_source_ws: str,p_target_ws: str, \n"," p_item_type: List =['DataWarehouse','Lakehouse','Notebook'], \n"," p_conn_from_to: Optional[List[Tuple[str,str]]]=[]):\n"," \n"," source_ws_id = fabric.resolve_workspace_id(source_ws)\n","\n"," target_ws_id = fabric.resolve_workspace_id(target_ws)\n","\n"," if 'Warehouse' in p_item_type or 'Lakehouse' in p_item_type:\n"," ls_expr = parse('$..linkedService')\n"," for endpoint_match in ls_expr.find(pl_json):\n"," if endpoint_match.value['properties']['type'] == 'DataWarehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Warehouse' in p_item_type:\n"," # only update the warehouse if it was located in the source workspace i.e. we will update the properties to the target workspace if the warehouse resided in the same workspace as the pipeline\n"," warehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," warehouse_endpoint = endpoint_match.value['properties']['typeProperties']['endpoint']\n"," \n"," source_wh_name = fabric.resolve_item_name(item_id = warehouse_id,workspace=source_ws_id)\n"," # find the warehouse id of the warehouse with the same name in the target workspace\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n"," # look up the connection string for the warehouse in the target workspace\n"," whurl = f\"v1/workspaces/{target_ws_id}/warehouses/{target_wh_id}\"\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['connectionString']\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_wh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," endpoint_match.value['properties']['typeProperties']['endpoint'] = lhconnStr\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," if endpoint_match.value['properties']['type'] == 'Lakehouse' \\\n"," and endpoint_match.value['properties']['typeProperties']['workspaceId'] == source_ws_id \\\n"," and 'Lakehouse' in p_item_type:\n"," #print(endpoint_match.value)\n"," lakehouse_id = endpoint_match.value['properties']['typeProperties']['artifactId']\n"," remote_lh_name = fabric.resolve_item_name(item_id = lakehouse_id,workspace=source_ws_id)\n"," # find the lakehouse id of the lakehouse with the same name in the target workspace\n"," target_lh_id = fabric.resolve_item_id(item_name = remote_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," endpoint_match.value['properties']['typeProperties']['artifactId'] = target_lh_id\n"," endpoint_match.value['properties']['typeProperties']['workspaceId'] = target_ws_id\n"," ls_expr.update(endpoint_match,endpoint_match.value)\n"," # print(endpoint_match.value)\n","\n","\n"," if 'Notebook' in p_item_type: \n"," ls_expr = parse('$..activities')\n","\n"," for endpoint_match in ls_expr.find(pl_json):\n"," for activity in endpoint_match.value:\n"," #print(activity['type'])\n"," if activity['type']=='TridentNotebook' and 'Notebook' in p_item_type: #only update if the notebook was in the same workspace as the pipeline\n"," print('change from '+activity['typeProperties']['workspaceId'])\n"," source_nb_id = activity['typeProperties']['notebookId']\n"," source_nb_name = fabric.resolve_item_name(item_id = source_nb_id,workspace=source_ws_id)\n"," target_nb_id = fabric.resolve_item_id(item_name = source_nb_name,type='Notebook',workspace=target_ws_id)\n"," activity['typeProperties']['notebookId']=target_nb_id\n"," activity['typeProperties']['workspaceId']=target_ws_id\n"," print('to notebook '+ target_nb_id)\n"," #ls_expr.update(endpoint_match,endpoint_match.value)\n","\n"," if p_conn_from_to:\n"," for ti_conn_from_to in p_conn_from_to:\n"," if not _is_valid_uuid(ti_conn_from_to[0]):\n"," print('Connection from is string '+ str(ti_conn_from_to[0]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[0]] \n"," connId_from = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_from = ti_conn_from_to[0]\n","\n"," if not _is_valid_uuid(ti_conn_from_to[1]):\n"," print('Connection from is string '+ str(ti_conn_from_to[1]))\n"," dfC_filt = df_conns[df_conns[\"Connection Name\"] == ti_conn_from_to[1]] \n"," connId_to = dfC_filt['Connection Id'].iloc[0] \n"," else:\n"," connId_to = ti_conn_from_to[1]\n","\n"," ls_expr = parse('$..externalReferences')\n"," for externalRef in ls_expr.find(pl_json):\n"," if externalRef.value['connection']==connId_from:\n"," print('Changing connection from '+str(connId_from))\n"," externalRef.value['connection']=connId_to\n"," ls_expr.update(externalRef,externalRef.value)\n"," print('to '+str(connId_to))\n","\n"," return pl_json\n","\n"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"},"jupyter":{"source_hidden":true}},"id":"1ec311ff-c557-4e50-84d2-c873b789a5da"},{"cell_type":"markdown","source":["##### Standalone mode: Create AzDO branch and workspace\n","\n","This cell runs only if not invoked from AzDO pipeline"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"0e1ff516-120f-4bc3-803c-5d558e336787"},{"cell_type":"code","source":["if _runStandalone:\n"," # get capacity details\n"," if target_capacity != '':\n"," if _is_valid_uuid(target_capacity):\n"," target_capacity_id = target_capacity.lower()\n"," else: \n"," target_capacity_id = labs.resolve_capacity_id(target_capacity)\n"," else: # if not set then fetch capacity of source workspace\n"," target_capacity_id = labs.resolve_workspace_capacity(source_ws)[0]\n"," target_capacity = labs.resolve_capacity_name(target_capacity_id)\n"," # check capacity status\n"," cap_status = get_capacity_status(target_capacity_id)\n"," if cap_status == 'Inactive':\n"," raise ValueError(f\"Status of capacity {target_capacity} is {cap_status}. Please resume the capacity and retry\")\n"," else:\n"," print(f\"Status of capacity {target_capacity} is {cap_status}\")\n","\n","\n","\n"," # Create Azure DevOps Branch\n"," ado_api_url = f\"{ado_api_url}/{org_name}/{project_name}/_apis/git/repositories/{repo_name}/refs\"\n"," result = create_azdo_branch(key_vault_name,secret_name, branch_name, main_branch,repo_name, ado_api_url)\n"," if result:\n"," print(f\"Feature branch {branch_name} created\")\n"," else:\n"," raise ValueError(f\"Could not create branch {branch_name}, perhaps it already exists?\")\n","\n"," # Create new feature workspace\n"," try:\n"," print(f\"Creating workspace: \" + target_ws + \" in capacity \"+ target_capacity_id +\"...\",end=\"\")\n"," target_ws_id = fabric.create_workspace(target_ws,target_capacity_id) \n"," print(f\"done. New workspace ID = {target_ws_id = }\")\n"," except Exception as error:\n"," errmsg = f\"Failed to create workspace {target_ws} with capacity ID ({target_capacity_id}) due to: {str(error)}\"\n"," raise ValueError(errmsg)\n"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"6d334824-7c65-4a6f-9fb0-61267403aa36"},{"cell_type":"markdown","source":["##### If required, create Lakehouses in new feature workspace\n","\n","This cell will only run if there are warehouse views which depend on lakehouse tables. Determined by the parameter has_wh_views_on_lh set to True above.\n","
In this case the lakehouses will be pre-created. Shortcuts created or data copied done in subsequent cell"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"2d515be2-cc9f-4a09-9c10-f2ad12485c23"},{"cell_type":"code","source":["if has_wh_views_on_lh:\n"," lhs = fabric.list_items(type='Lakehouse',workspace=source_ws)\n"," for index,row in fabric.list_items(type='Lakehouse',workspace=source_ws).iterrows():\n"," #try:\n"," print(f\"Creating {row['Display Name']} lakehouse...\",end=\"\")\n"," fabric.create_lakehouse(display_name = row['Display Name'],description='Created programatically via branch out script',max_attempts=3, workspace=target_ws)\n"," print('done')"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"53dbce3a-a318-400c-acca-e3a8e9fd19ae"},{"cell_type":"markdown","source":["##### Either create shortcuts from source to target lakehouse(s) or copy data\n","\n","Loops through lakehouse(s) in the target workspace and either populates them with shortcuts or data\n","\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"a15065f3-670d-4bc9-b337-51709f6cdb1f"},{"cell_type":"code","source":["# ensure target workspace ID is set based on name\n","if target_ws_id == '':\n"," target_ws_id = fabric.resolve_workspace_id(target_ws)\n","\n","# populate the lakehouses with shortcuts or data\n","create_shortcuts_or_copy_data(copy_lakehouse_data,create_lakehouse_shortcuts)\n","time.sleep(120) # wait for sql endpoint to update\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"8bec3fbc-4a75-4ba3-86d5-0620ec504a8f"},{"cell_type":"markdown","source":["##### If not done in ADO, connect workspace to Git, initialize and update\n","This cell will only run if there are warehouse views which depend on lakehouse tables based on parameter has_wh_views_on_lh"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"c7dd8a46-f6bd-4a84-90f0-e176357ced17"},{"cell_type":"code","source":["if has_wh_views_on_lh:\n","\n"," # branch name should be the same name as the target workspace, if not modify parameters to pass in the branch name from ADO\n"," branch_name = target_ws \n"," # fetch existing Git connection details from source workspace\n"," gitconnx = labs.get_git_connection(source_ws)\n"," # connect target workspace to the same git provider and new branch\n"," if gitconnx.loc[0, 'Git Provider Type'] == 'AzureDevOps':\n"," result = labs.connect_workspace_to_azure_dev_ops(\n"," gitconnx.loc[0, 'Organization Name'],\n"," gitconnx.loc[0, 'Project Name'],\n"," gitconnx.loc[0, 'Repository Name'],\n"," branch_name,\n"," gitconnx.loc[0, 'Directory Name'],\n"," target_ws)\n"," elif gitconnx.loc[0, 'Git Provider Type'] == 'GitHub': #### TODO ####\n"," #TODO connect workspace with Github details\n"," labs.connect_workspace_to_github(\n"," '', #owner_name: str,\n"," gitconnx.loc[0, 'Repository Name'], #repository_name: str,\n"," branch_name,\n"," gitconnx.loc[0, 'Directory Name'], #directory_name: str,\n"," '', #connection_id: UUID,\n"," '', #source: str = \"ConfiguredConnection\",\n"," target_ws)\n"," else:\n"," raise ValueError(f\"Unsupported Git provider type {gitconnx.loc[0, 'Git Provider Type']}.\")"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"93fe3d56-d870-433f-bdea-1e50ca58440b"},{"cell_type":"code","source":["if has_wh_views_on_lh:\n"," # inialize the git connection\n"," commit_hash = initialize_git_connection(target_ws,'PreferRemote')\n","\n","\n"," # update from git \n"," labs.update_from_git(remote_commit_hash=commit_hash,\n"," conflict_resolution_policy='PreferRemote', \n"," allow_override=True,\n"," workspace=target_ws)\n","\n"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"b0061d77-6213-4eeb-a55f-5a870077628c"},{"cell_type":"code","source":["labs.commit_to_git(comment='Initial', workspace=target_ws)"],"outputs":[],"execution_count":null,"metadata":{"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"89d2fa99-1073-44a9-94cc-74e74abf68b4"},{"cell_type":"markdown","source":["##### Update default and attached lakehouses/warehouses for notebooks\n","\n","Update notebook dependencies based on but now supports T-SQL notebooks:\n","https://github.com/PowerBiDevCamp/FabConWorkshopSweden/blob/main/DemoFiles/GitUpdateWorkspace/updateWorkspaceDependencies_v1.ipynb\n"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"aaae8a08-588d-4dd8-9d2c-2200b7a88d30"},{"cell_type":"code","source":["for notebook in notebookutils.notebook.list(workspaceId=target_ws_id):\n"," updates = False\n"," if notebook.displayName == 'ETL':#True: #notebook.displayName == 'T-SQL_Notebook': #notebook.displayName != 'Create Feature Branch':\n","\n"," # Get the current notebook definition\n"," json_payload = json.loads(notebookutils.notebook.getDefinition(notebook.displayName,workspaceId=source_ws_id))\n"," #print(json.dumps(json_payload, indent=4))\n"," # Check for any attached lakehouses\n"," if 'dependencies' in json_payload['metadata'] \\\n"," and 'lakehouse' in json_payload['metadata']['dependencies'] \\\n"," and json_payload['metadata'][\"dependencies\"][\"lakehouse\"] is not None:\n"," # Extract attached and default lakehouses\n"," current_lakehouse = json_payload['metadata']['dependencies']['lakehouse']\n"," # if default lakehouse setting exists\n"," if 'default_lakehouse_name' in current_lakehouse:\n"," print(f\"Updating notebook {notebook.displayName} with new default lakehouse: {current_lakehouse['default_lakehouse_name']} in workspace {target_ws}\")\n"," source_lh_name = fabric.resolve_item_name(item_id = current_lakehouse['default_lakehouse'],type='Lakehouse',workspace=source_ws_id)\n"," current_lakehouse['default_lakehouse'] = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," current_lakehouse['default_lakehouse_workspace_id'] = target_ws_id\n"," updates = True\n"," # loop through all attached lakehouess\n"," for lakehouse in json_payload['metadata']['dependencies']['lakehouse']['known_lakehouses']:\n"," source_lh_id = lakehouse['id']\n"," # find source lakehouse name\n"," source_lh_name = fabric.resolve_item_name(item_id = lakehouse['id'],type='Lakehouse',workspace=source_ws_id)\n"," # find target lakehouse id based on name\n"," target_lh_id = fabric.resolve_item_id(item_name = source_lh_name,type='Lakehouse',workspace=target_ws_id)\n"," lakehouse['id'] = target_lh_id\n"," print(f'Updating attached lakehouse {source_lh_name} from {source_lh_id} to target ID {target_lh_id}')\n"," updates = True\n","\n"," if 'dependencies' in json_payload['metadata'] and 'warehouse' in json_payload['metadata']['dependencies']:\n"," # Fetch existing details\n"," current_warehouse = json_payload['metadata']['dependencies']['warehouse']\n"," current_warehouse_id = current_warehouse['default_warehouse']\n"," source_wh_name = fabric.resolve_item_name(item_id = current_warehouse_id,workspace=source_ws_id)\n"," #print('Source warehouse name is ' + source_wh_name)\n"," target_wh_id = fabric.resolve_item_id(item_name = source_wh_name,type='Warehouse',workspace=target_ws_id)\n","\n"," if 'default_warehouse' in current_warehouse:\n"," #json_payload['metadata']['dependencies']['warehouse'] = {}\n"," print(f\"Attempting to update notebook {notebook.displayName} with new default warehouse: {target_wh_id} in {target_ws}\")\n"," \n"," json_payload['metadata']['dependencies']['warehouse']['default_warehouse'] = target_wh_id\n"," for warehouse in json_payload['metadata']['dependencies']['warehouse']['known_warehouses']:\n"," if warehouse['id'] == current_warehouse_id:\n"," warehouse['id'] = target_wh_id\n"," updates = True\n","\n"," if updates:\n"," notebookutils.notebook.updateDefinition(\n"," name = notebook.displayName,\n"," content = json.dumps(json_payload),\n"," workspaceId = target_ws_id\n"," )\n"," \n"," print(f\"Updated notebook {notebook.displayName} in {target_ws}\")\n","\n"," else:\n"," print(f'No default lakehouse set for notebook {notebook.displayName}, ignoring.')"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"5c60b5d2-f83c-46f8-9870-9fd609166b67"},{"cell_type":"markdown","source":["##### Copy warehouse data via parameterised pipeline\n","\n","Loop through all warehouses and copy the data"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"f198b816-77e9-4f04-9139-d78237bedc72"},{"cell_type":"code","source":["if copy_warehouse_data:\n"," p_logging_verbose = False\n"," df_warehouses = (labs.list_warehouses(target_ws))\n"," #display(df_warehouses)\n"," for index, row in df_warehouses.iterrows():\n"," source_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],source_ws_id)\n"," target_wh_id = labs.resolve_warehouse_id(row['Warehouse Name'],target_ws_id)\n"," \n"," src_path = f'abfss://'+source_ws_id+'@onelake.dfs.fabric.microsoft.com/'+source_wh_id\n"," tgt_path = f'abfss://'+target_ws_id+'@onelake.dfs.fabric.microsoft.com/'+target_wh_id\n","\n"," # extract the list of schemas per data \n"," schema_list = get_lh_object_list(src_path,['Tables'])\n"," # extract a list of warehouse objects per schema and store in a list\n"," table_list = get_wh_object_list(schema_list['name'],src_path)\n"," \n"," # create a temporary staging lakehouse per warehouse to create shortcuts into, \n"," # which point back to original warehouse data currently in the DR storage account\n"," lhname = 'temp_rlh_' + source_ws+'_'+row['Warehouse Name']\n"," # check if it exists before attempting create\n"," if p_logging_verbose:\n"," print('Checking whether the temporary lakehouse \"'+ lhname +'\" exists in workspace '+target_ws+'...')\n"," temp_lh_id = getItemId(target_ws_id,lhname,'Lakehouse')\n"," if temp_lh_id == 'NotExists':\n"," lhname = lhname[:256] # lakehouse name should not exceed 256 characters\n"," payload = payload = '{\"displayName\": \"' + lhname + '\",' \\\n"," + '\"description\": \"Interim staging lakehouse for primary warehouse recovery: ' \\\n"," + source_ws+'_'+row['Warehouse Name'] + 'into workspace '+ target_ws + '(' + target_ws +')\"}'\n"," try:\n"," lhurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses\"\n"," lhresponse = client.post(lhurl,json= json.loads(payload))\n"," temp_lh_id = lhresponse.json()['id']\n"," if p_logging_verbose:\n"," print('Temporary lakehouse \"'+ lhname +'\" created with Id ' + temp_lh_id + ': ' + str(lhresponse.status_code) + ' ' + str(lhresponse.text))\n"," except Exception as error:\n"," print(error.errorCode)\n"," else:\n"," if p_logging_verbose:\n"," print('Temporary lakehouse '+lhname+' (' + temp_lh_id + ') already exists.')\n"," \n"," time.sleep(60) # waiting for temporary lakehouse to provision completely \n","\n"," # Create shortcuts for every table in the format of schema_table under the tables folder\n"," for index,itable in table_list.iterrows():\n"," shortcutExists=False\n"," # Check if shortcut exists\n"," try:\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts/Tables/\"+itable['schema']+'_'+itable['name']\n"," tlhresponse = client.get(url)\n"," shortcutExists = True\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] +' already exists')\n"," except Exception as error:\n"," shortcutExists = False \n","\n"," if not shortcutExists: \n"," # Create shortcuts - one per table per schema\n"," url = \"v1/workspaces/\" + target_ws_id + \"/items/\" + temp_lh_id + \"/shortcuts\"\n"," scpayload = '{' \\\n"," '\"path\": \"Tables/\",' \\\n"," '\"name\": \"'+itable['schema']+'_'+itable['name']+'\",' \\\n"," '\"target\": {' \\\n"," '\"oneLake\": {' \\\n"," '\"workspaceId\": \"' + source_ws_id + '\",' \\\n"," '\"itemId\": \"'+ source_wh_id +'\",' \\\n"," '\"path\": \"/Tables/' + itable['schema']+'/'+itable['name'] + '\"' \\\n"," '}}}' \n"," try:\n"," #print(scpayload) \n"," shctresponse = client.post(url,json= json.loads(scpayload))\n"," if p_logging_verbose:\n"," print('Shortcut '+itable['schema']+'_'+itable['name'] + ' created.' )\n","\n"," except Exception as error:\n"," print('Error creating shortcut '+itable['schema']+'_'+itable['name']+' due to '+str(error) + ':' + shctresponse.text)\n"," \n"," recovery_pipeline_prefix= 'plRecover_WH' \n"," # recovery pipeline name should not exceed 256 characters\n"," recovery_pipeline = recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'][:256]\n"," if p_logging_verbose:\n"," print('Attempting to deploy a copy pipeline in the target workspace to load the target warehouse tables from the shortcuts created above... ')\n"," # Create the pipeline in the target workspace that loads the target warehouse from shortcuts created above \n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," #print(plid)\n"," if plid == 'NotExists':\n"," plid = createDWrecoverypl(target_ws_id,recovery_pipeline_prefix+'_'+source_ws + '_'+row['Warehouse Name'])\n"," if p_logging_verbose:\n"," print('Recovery pipeline ' + recovery_pipeline + ' created with Id '+plid)\n"," else:\n"," if p_logging_verbose:\n"," print('Datawarehouse recovery pipeline \"' + recovery_pipeline + '\" ('+plid+') already exist in workspace \"'+target_ws + '\" ('+target_ws_id+')') \n"," print('\\n')\n","\n"," tablesToCopyParam = table_list[['schema','name']].to_json( orient='records')\n"," # ensure the temporary lakehouse exists\n","\n"," # obtain the connection string for the lakehouse to pass to the copy pipeline\n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/lakehouses/\" + temp_lh_id\n"," whresponse = client.get(whurl)\n"," lhconnStr = whresponse.json()['properties']['sqlEndpointProperties']['connectionString']\n","\n"," # get the SQLEndpoint ID of the lakehouse to pass to the copy pipeline\n"," items = fabric.list_items(workspace=target_ws_id)\n"," #print(items)\n"," temp_lh_sqle_id = items[(items['Type'] == 'SQLEndpoint') & (items['Display Name']==lhname)]['Id'].values[0]\n","\n","\n"," # obtain the connection string for the warehouse to pass to the copy pipeline \n"," whurl = \"v1/workspaces/\" + target_ws_id + \"/warehouses/\" + target_wh_id\n"," whresponse = client.get(whurl)\n"," whconnStr = whresponse.json()['properties']['connectionInfo']\n","\n"," # obtain the pipeline id created to recover this warehouse\n"," plid = getItemId( target_ws_id,recovery_pipeline,'DataPipeline')\n"," if plid == 'NotExists':\n"," print('Error: Could not execute pipeline '+recovery_pipeline+ ' as the ID could not be obtained ')\n"," else:\n"," # pipeline url including pipeline Id unique to each warehouse\n"," plurl = 'v1/workspaces/'+target_ws_id+'/items/'+plid+'/jobs/instances?jobType=Pipeline'\n"," #print(plurl)\n","\n"," payload_data = '{' \\\n"," '\"executionData\": {' \\\n"," '\"parameters\": {' \\\n"," '\"lakehouseId\": \"' + temp_lh_sqle_id + '\",' \\\n"," '\"tablesToCopy\": ' + tablesToCopyParam + ',' \\\n"," '\"workspaceId\": \"' + target_ws_id +'\",' \\\n"," '\"warehouseId\": \"' + target_wh_id + '\",' \\\n"," '\"lakehouseConnStr\": \"' + lhconnStr + '\",' \\\n"," '\"warehouseConnStr\": \"' + whconnStr + '\"' \\\n"," '}}}'\n"," #print(payload_data)\n"," plresponse = client.post(plurl, json=json.loads(payload_data))\n"," if p_logging_verbose:\n"," print(str(plresponse.status_code)) \n"," print('Done')\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":true,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"57dafef7-17a2-475f-9e62-eecc6660440c"},{"cell_type":"markdown","source":["##### Update directlake model lakehouse/warehouse connection\n","\n","https://semantic-link-labs.readthedocs.io/en/stable/sempy_labs.directlake.html#sempy_labs.directlake.update_direct_lake_model_connection "],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"cc97be77-116e-4cde-bdc6-2971ab98a083"},{"cell_type":"code","source":["\n","df_datasets = fabric.list_datasets(target_ws)\n","\n","# Iterate over each dataset in the dataframe\n","for index, row in df_datasets.iterrows():\n"," try:\n"," # Check if the dataset is not the default semantic model\n"," if not labs.is_default_semantic_model(row['Dataset Name'], fabric.resolve_workspace_id(target_ws)):\n"," #print('Updating semantic model connection ' + row['Dataset Name'] + ' in workspace '+ target_ws)\n"," labs.directlake.update_direct_lake_model_connection(dataset=row['Dataset Name'], \n"," workspace= target_ws,\n"," source=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[1], \n"," source_type=labs.directlake.get_direct_lake_source(row['Dataset Name'], workspace= target_ws)[0], \n"," source_workspace=target_ws)\n"," labs.refresh_semantic_model(dataset=row['Dataset Name'], workspace= target_ws)\n"," except Exception as error:\n"," errmsg = f\"Failed to update and refresh semantic model {row['Dataset Name']} due to: {str(error)}\"\n"," print(errmsg)\n"," #raise ValueError(errmsg)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9deccda6-5c3d-4b88-8ed8-68855ca0949a"},{"cell_type":"markdown","source":["##### Rebind reports to local datasets\n","\n","https://semantic-link-labs.readthedocs.io/en/latest/sempy_labs.report.html#sempy_labs.report.report_rebind"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"36783f3b-4904-4d74-842d-dbd026a3184a"},{"cell_type":"code","source":["df_reports = fabric.list_reports(workspace=target_ws)\n","for index, row in df_reports.iterrows():\n"," #print(row['Name'] + '-' + row['Dataset Id'])\n"," df_datasets = fabric.list_datasets(workspace=target_ws)\n"," dataset_name = df_datasets[df_datasets['Dataset ID'] == row['Dataset Id']]['Dataset Name'].values[0]\n"," print(f'Rebinding report to {dataset_name} in {target_ws}')\n"," labs.report.report_rebind(report=row['Name'],dataset=dataset_name, report_workspace=target_ws, dataset_workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"},"collapsed":false},"id":"06268ede-b795-493e-9a8d-772654ce7e20"},{"cell_type":"markdown","source":["##### Update data pipeline source & sink connections\n","\n","Support changes lakehouses, warehouses, notebooks and connections from source to target.
\n","Connections changes should be expressed as an array of tuples [{from_1:to_1},{from_N:to_N}]"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"4ae65012-350c-40c0-a68a-4069c567a85f"},{"cell_type":"code","source":["if len(connections_from_to)>0: \n"," # convert from a string to a proper type i.e. list of tuples \n"," #connections_from_to = ast.literal_eval(connections_from_to)\n"," # loading a dataframe of connections to perform an ID lookup if required \n"," df_conns = labs.list_connections()\n","\n"," df_pipeline = labs.list_data_pipelines(source_ws)\n"," for index, row in df_pipeline.iterrows():\n"," pipeline_json = json.loads(labs.get_data_pipeline_definition(row['Data Pipeline Name'],source_ws))\n","\n"," p_new_json = swap_pipeline_connection(pipeline_json, source_ws,target_ws,\n"," ['DataWarehouse','Lakehouse','Notebook'],\n"," [connections_from_to]) \n"," #print(json.dumps(pipeline_json, indent=4))\n"," \n"," update_data_pipeline_definition(name=row['Data Pipeline Name'],pipeline_content=pipeline_json, workspace=target_ws)\n"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"079958e8-2880-484a-a994-41caf47e747e"},{"cell_type":"markdown","source":["##### Commit changes made above to Git"],"metadata":{"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"44174276-b983-4e80-9451-0afb9589cf1f"},{"cell_type":"code","source":["labs.commit_to_git(comment='Initial', workspace=target_ws)"],"outputs":[],"execution_count":null,"metadata":{"jupyter":{"source_hidden":false,"outputs_hidden":false},"nteract":{"transient":{"deleting":false}},"microsoft":{"language":"python","language_group":"synapse_pyspark"}},"id":"9a5c3d84-f71d-4348-b419-c4953ac9e1d0"}],"metadata":{"kernel_info":{"name":"synapse_pyspark"},"kernelspec":{"name":"synapse_pyspark","language":"Python","display_name":"Synapse PySpark"},"language_info":{"name":"python"},"microsoft":{"language":"python","language_group":"synapse_pyspark","ms_spell_check":{"ms_spell_check_language":"en"}},"widgets":{"application/vnd.jupyter.widget-state+json":{"version_major":2,"version_minor":0,"state":{"0bf5b65883d845b8a785f718f257f0fe":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}},"34636ca1188542389c57e315b907be79":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}},"729255d539d44c0da98b3f6e1c79f301":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"Initial text","layout":"IPY_MODEL_34b4c41de9ad476088e9e30f193fd2d3","style":"IPY_MODEL_57e49b7587e9459f9f53fc6e48e1ed70"}},"c70d5bf879c243a984ade0d098ab4ff5":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"4cd8bbf291024e7bb03f1fa73d110710":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}},"41f8d04c976245918a4be0806750173c":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}},"2529422048414fcabaaa68abefbd8d68":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"bbd0570346bf4891a6740d5c472f557d":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"⌛ Loading","layout":"IPY_MODEL_41f8d04c976245918a4be0806750173c","style":"IPY_MODEL_d464a36f82f14b8d92cd20b8b77b2d4c"}},"1130fc45eb4f47b1bf8a05201004a829":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"Initial text","layout":"IPY_MODEL_0bf5b65883d845b8a785f718f257f0fe","style":"IPY_MODEL_477e0a21d0ee429d847afc4f29265fa7"}},"1a9e3ec3c78245a58d7da90f00663d0f":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"Initial text","layout":"IPY_MODEL_34636ca1188542389c57e315b907be79","style":"IPY_MODEL_d13c5bb179b04d789caf440915ad4ab4"}},"638b47eed64847fba74e81e558f85aef":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"⌛ Loading","layout":"IPY_MODEL_4cd8bbf291024e7bb03f1fa73d110710","style":"IPY_MODEL_c70d5bf879c243a984ade0d098ab4ff5"}},"748fa550c8c34bf68caf6fb5480c2bcc":{"model_name":"LabelModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"value":"⌛ Loading","layout":"IPY_MODEL_31877256e23b46fb9fb9619833d4a58a","style":"IPY_MODEL_2529422048414fcabaaa68abefbd8d68"}},"d13c5bb179b04d789caf440915ad4ab4":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"57e49b7587e9459f9f53fc6e48e1ed70":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"34b4c41de9ad476088e9e30f193fd2d3":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}},"477e0a21d0ee429d847afc4f29265fa7":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"d464a36f82f14b8d92cd20b8b77b2d4c":{"model_name":"LabelStyleModel","model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","state":{"description_width":"","font_size":null,"text_color":null,"font_family":null,"font_style":null,"font_variant":null,"font_weight":null,"text_decoration":null}},"31877256e23b46fb9fb9619833d4a58a":{"model_name":"LayoutModel","model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","state":{}}}}},"nteract":{"version":"nteract-front-end@1.0.0"},"synapse_widget":{"version":"0.1","state":{}},"spark_compute":{"compute_id":"/trident/default","session_options":{"conf":{"spark.synapse.nbs.session.timeout":"1200000"}}},"dependencies":{"lakehouse":{}}},"nbformat":4,"nbformat_minor":5} \ No newline at end of file