h8zn>4=f#M(Xey3P=p8uZ;_kW@Oe<#KNWu`9_
zb~*g-thg+H1dJVEk>~%H{_S>&|98!O{Kt9j0rWrO6|?%E_5Z(c{=W$EU-~ynOSTyo
z^#IZ?z`Hi34=GsJyFYJe@xp4TaWc4a>}pjEagb?;%{{~zrPr4;ytXTx|9WgKr^K-TlcWJD{!CgK}VhRYf
zayWa%v30Q`AyDZ5!`_?6L)pIn!%ApTR6=EGMT@PCtZgbqNK*Ea5R&ZMOvqkFDNB|q
zijYzEb%rdH#MosW#xfX8mNCX`zw^?4f7@E5
zzu(6>d7g8(OTGLT=H4k=pCu;vUTyVO1%+8iYM;{4ZVX$xbS=mH#(xO)VNpW7Hta8G
zIx5xbRK|cKl0Xeu5x8RQib*f@oLFqU7ZU=;8#pF&Iue!Lk7V(1Fas>Wyk=}LU9-HF
zzVBOwIv
zJlYVi4mWE()(C3IIQP05p&Al%R6+JkeYEsrnFPh1acLc^7RovQF)+MTgx%L&uibIh0V`B#7;BuEhez>ktXA4Uw
ziE*riFsoU2$YtLuU4Xcu)MEjYU88mAZ9xaWnxi_2+FbU6qpqK%~yamvt}?
z-+TYgmH{9YxKdJl6KhyZdX$yHu=^tUBpY+PVZwvWdBXLt*#C1-e5nXJ8~f&qY=;_^
zJKsI2C6n>4T*Ew^u#5iuUZ(0C_uiT1XN)sx^)?lju8yzCYdA8#2Rc#c_5Ul%*MLG1
z#%67sFWdq+Z_^xxu_Jn`&RoEX^@_b~^Y`DYr`wpUEa%z2L9zY9;0!kANEYYcul`b%
z>HPBMs`y$9A+ey+MZFKEN$RkK#KZxiu0GYL3jy)bg{UErgEgbYw<&D`_w}>OF75`y
zmCmGYC;Wx|Qb2izz(=iL9W8Bq$8g01wZ9UK;LkX(C4e2Y)*8kEHBlTH$c{&$0&|+&
zC~at8Q8uMrRBwzTDh{95%XxPyXxH1lmCp@LlHM8aso?lKo2OOs@j%agLKUR3G-;{z
zm*sXaradFXRPR`y1MlqOD4mmM-TIZ2qa&)1F@tf|Pe)`*?%h1p$GsWEm6?t^w*kVc
zyAKtTNvWMz6kqAy09DHE$2aCp7qefH`8bx={X@i&`vOW1?Xb~sL7DftDc-~{nxp1E
zdUMxS9y_AwGN#+=Gc(8|ekA{3lSRdb?Yy9*+kGymbn9#vhMkov$g@=V%^d!@L4j)>
z92U+~&c`vP{tpqULJmNa{woRa$>wt@fC$l`Q^|;cmGJ(}ItOzzv6h^R^(srBs%Bd0
z6SdZLgc@xwU#@j9r$%nh%hJO0NqLXKi&r&zKky;d!G_;57|jF!7X-V7$9!YVEm!Yz
z;k@^FIm(woQO1T}vKDU&jwFQ3Edy|K2KHer*9?#VZL?PcS8kK6!1951o5wjPDO~jn;jaH=a?7z2go4j-%o$E_3-5y7h>XJ>*0llY~R!luKnm9*LF6$
zjAxYlG?%(C#7h*t`3NR=-CfJ+UV1;eJrEsReBtJcNx%^b%K2nd&irtfro?6P`>S1e_89
z2Vtlb>IQK&iYUxZi1UHkKS0DE=;hz!z(j6!5oif=P%5DP@*_9?lBE7e$n?t-{lFdn
zCfUELS^mmD(3{2b<=?aSukiB62!DwgKak-s!T$%S`!^%}Pm=nNr*C@B4-N5;f&UT}
z{x6U4pP}Ke5%b5>L;L+7257N-`S+P^ZQ_GYWaQv@1uN
z?)CpNi?hwdzA{heWHn<0C@L|M3eb%E4BzAZaCW)J62xg^s_rI}zfOgN@B*@$E+iBG
zUM*1xl)+$eiB_S59tY#lP%eHf#Y$3R3mb&E?}&$0I(aQ_bREf}W7Kte8VVo85-s^ya0d;9U0Tm6lR$}dK@fa2t(_jed}
zni)y-+63cs~k=on+b^5leTQkk%*QM$pPg007T+nkyW?j!XAVxwbb0=a!A^%+95ub=6zYZM`+)p8>qL5pW>Ys0XN#}KN6U(*!?Rf`cdWFNf
zPW1j{I_K`fZ9ZS~VV`EJ_|(pK;8M0%R&?jv>1R8ZFD8c*n4*L_$)xkr;;HQ|4Um3OY!Q*4w@Esw|oCCh%7s
zyexW$KJ!lK2e+vgW2o!7FGkagvWFVZaUP)8+u)^5mpnuS-?|2>;~Lz@Jm!ho#k5o#
zW{GPvuzCoo&JP4=4~75g!pW+T@X?q2{jf0yHB&Q{Qw|n5^mNRt)t&;f(f)0GJ2JnqYPUP?sZT#aHd+rL
z=l(cU8Z-Bs@rFhE0uV%^#ufTJsy?k1yC^6dfwe4Jcwu)48_Y2#_zyVd#NqwXrGyXM
zAJ-2e)L)_;4uVq}3NMxKjYCsLV$NNa8&jS`z7t3TU|m)aJ8gS
z1UZs=1fw4PKooPMJ4-*x6TOXO(U{;(h!$SD%W;wdAXNNV)RUSCm`EeZtftbd&mz(jDP@2v&2TeLKF(bbJyG~q(eVr6bQZ`S0BGgq#(!Hk
ze3|}HZ;vp@7#8n6rrfRaygN}(wyazyYtWyo&SlHTTt$z2d#*)7Q>kujR%wQ;VcRPeRPW|fPta<;
zw&MDtUnpDY&eoUHAxCVSJ04VR6X?py6x_g3{_uPB_yx`gk2b~~e(L{?L7SVJ+`*?h
zZ}C3lo!U&F%X)tM=895l=B!X9A>T-Najv(bRIR%cHe)Qffjj!~D$c3-6uhR~%b|~K
z`|S!F+&2!VSWaXd$UYw(=Y$olQ(0d-2_GdMbJ%u$=ExioXJ=H4FNA7rEx6an=O@oq
z1fq+q+CHv$8S_0={gT1`q*yAj%5K>l;tiy|I+%GCUvmHG-JmNLS2w}{BMa%wwqDZ4
zWr77oV48B1vy2OCv$kDDUE64SkF}UeTTmr}f=@wt?csUYkph2o)h@ubt6^1ht%X3;
zRe#iJ4(jq&I@+N-&jVlPJ8&ZJuIb#%if;5r4y5PY*;=v4?!rvNtgvVqUzL)jjLa9K
znsEG*20T4l6dd=qk>@D6%vq|x5Ew`7I#F9aZ#)qDtC)jk_+P0(Aj=%1wvStt#iMiPaCaY0<
zQ$3hbl9O&>XZDT$G}
zSiW+8(?4v=6B-OVu*%f0p4d45tk~GF#wK{-310PyzHcvH+4@o$yuGtL)#~wHNUO6T
zacJu|o9GZqZKp$hz_>!5A22+k8YdeZ6!Epjf6-F@Su+UD(#8cx)-O!9skO11~Kw*a^MjVl(uXd`#SaItfszV&Y6HfTy-
zs5_dujBJTfcr5ht!zzvpUg#mO{0gw;qMK$EJZH0Ctb7nzA1(73apS>$P;Rl+y{QX!
z8qdk=5-{T`2{Fv~&SIBxq7oCL5{7RPZxM%YQETYy^~w}>b(`r{U);UY-{a(!rnB2t
zY(HacyX}>0owk6{x0deJuUcmus8uSgFtPkvk-3~vqjd=yGqgRs*BczY)A25&<_;X6
zA@PNGGK-A%6Z{9&~1mB?mgmYMjWi+bM46TcB+k1sX}PW
zahz!Nv;Q;O*eQwIziy2YvFY>Z(qXGP#{VUCyM5SnS)WaW2Wu4(+BuZt$p
zTAf+<|9l_wI?
z3PDdew0p`O?!=fc%-z`#a8hF-G&HpFW2w;md?{hJUMUlK-GGbhO}f8dHky%hm^~V=
z-;US6sF^`!cb^c+{PO
zAQ!>e=p4HkU}koi#;{`&?NI29OtNdIS!>tQX#DA_Bt>&R)x{Hr_%R`(!a|RZ2+7#7
zygkl+s5gnc-PasGmG4JA&;YR0%keUZ&zIJN0!Wqi7T#)4BNyDqB!YbxVHx2HtitEo
z$x|VqYSZ#GTbS%>ZI?tlu39tVW>rdRRL*p;m4ViU0#*j8Tg+*VuY#Yh4
zM@kp7G;tu&9q*f#>qR+i%IwVY{xB^>l#nI#v+LYmNCl3AtNTOG6|sRj1!xzPz*b`BZZo%HftJHyTrMWGJ|`(v+_xY&aK-|*rh6qJ98
zF+uVBg+Cz^)IDd<6qh)II>O6Wr82QPieO6{La0Vl6eWZBFASa#InBb~V*2|9@bI~b
z+6h!!-)yt6U^OThb}9>X9>+_FI{U=r9Skbo$EF_&>Z%JS*98l#)o+lOYqjH_wb;=}
z4c#nR(t#z844&X+KC8x^Ld&cSYmr2Xq<5YR-x+s`p)x{LD&O}cykc(_f*gDB`t12~
zm5+$ayxz|`Vpgng0E+3$ab8GR{A)R_U&l=ldF`tAjw~70&kQS_x#eL5!jdS{fM
zv;P(`@LSvGWS8SI@Hla(h}1(h!0S+w3&DFv~SilN9Vg9C>1CiF6md-9VGt7>(i{qiFNTNK>@
zK|EkP3WG8e@2OoR%lCc(?@f6*C#a-;bh-Ylu||>6EL)p`Ti2DYp{H6G;3p-ARPoHo
zr9D!pbV_&j=zEk4|KIq3w>J`}p3%qM*~_Smg<=*9p;xL#wXH
z@SVTeLZHZI#pl%M>c3gMdv1gHblEqCb9>LOk>W>W!%LgAodJ-wQF8uQ`mDHDdvmPLQ3+Epn;~^1?T}
zF^3M@iv1ZwugSeGTY1jC&m3k+gu=wfHiv0pv|4+IP-khfr1R
zFxQ~8F|0KoCLpv+I7ESiP)Wf-8VF~CSsg-ET@pMNcyDs?78aDlnG++BwAztk
z3}d99M+%YY>yghUdq1Rj_3Sj#`YM7~k2v0F-FrIVAiX_`!ZJi%(?g~1T*in$u+#Fp
zRH;FBIN;G)cBUcD(LytppW^q(?uX*&yUy34p@<0wF`ogJ7wx6Rj$tmM;==sd%C|CS
zmn`EGRl26yCsxxRdw7TJTF0J(d{!@5s3wJF_e6Rw
zrSW_0^!F>$nI&)2X(BVL#3#Im+{x&kqdLImtJai{6ZV(P3b)u9Hn(q8?Dz41Y&j=B
z)eV0{fe%#pO_ZJ+h%&D-SM6e6t-MI;aHW`*E_5{pi?cpB&yO|VF@;^a8T~6SC}i%_+zxNrzJ_TMVzFbgD>sekO|hGyrRliqBrs}I
zwN-oG6t2DPz5e!g-fCtN4k7ophQTv)_*ppeO|X!9TdqSFL3qep;scVVfbY#U#T3Ap
z7rP`|jw}a4kspE3M2z=YU?0B@6?+C_b@JF!jhuoLYW9^&a*h7r&PH$Ex%++jo%8bu
zL5*3Dm8z)I04eNvXH31>`JtM!RpX0i*fHJ($)=ITl
zapP|H*?~otd=Q*9@2dvlFzVzI8hkKb0i%aP!LFM5jyy-zCC^!g5PUG+l3m5l=O{EQ
zyiI+Kdi$2Odu9$(d%N>ZiF+J3J}fJDw(fcy1dKOLF8x0+?c8oO=}A?vGQjNPo&?qE
zud_4Hmd0Dt>J6SBwr;#qRdJ?LX<_5(wfP3Uk~7CnuHq7htLSm9rfuJN_-K#aJ+*4w
z#}AfBgQ`;+?++#JvS--)E{vzO%fpxy!`+Edi^m^(j|@}>w9_8pSfsApk=sW=6XVb5
zouk5IIUt<|u88k={_iUwK>U>jIUWHK^+D?4XF%Wjo)`bR@}mg&`;Y#s+x(Pp|KHsB
z&(Zwx2>-!H;x}V}t?EIzIh6nZ8qS{sj2{vL;(XrepFZ^G^!$0{&l~-Dr!&dk7;}sp2q+Uqg&Mt`
zynNN>soQiMuPQ0(IF54bx}zlb_ery(oNYd
z7f0^M?D!RTtf)MJV%$wBIGSk~Q(E$NHFx8bq1?BjS5MY4s+AL%S^6{ph%g0#P)Z0k&+cw-OP_Hac!
z`#Nf(1>nfZ?)=Pt7AW#!GQFW9bwcb#L=lzX0|UuvB;!f7b#3|aFCfpsHZ-!D)H|@q
zwB3#7Cb;p1bu+m__~Wp~r>Uzr4;KeHZD>q5u1`skcCK%Ku0D;&Uw=O!)LwkPrx8ONZS1y2iYoHzKje_j
zGVUy1$~fTNsO`h1;=QvcTdar?qN>QPorjicXP$kw*bzahS$FdMql5IUB>QO1n^D*OiUc{Z|n{4Vnbu_Wl+}mLQ*Y*a;fq#@)A#-_SNr9??
zxExcU$*2xi{8RbsxLp43XHV*}l)ph$tl9w~D(>@^172wT{#_ij(Rjy_r5aU%z@hs+
zIU4tL!{~E^HR0X`(mg{6>8Xhr9!@9N9LcQUnmc202F2Q$6oeRf*{kOGfeah0fU@Xe
z>(gDya!6JJXz_b1{~f`zo{6#n@JsEKgUJwt>mBOdIuqTm_Lyb-#;QTZ;x#H^9cAh3
zv9O_R^wI@lpF4j1of%WhknKt`dq=M+u3Pt-%AQ3x8ut_$G70-8bBd7L1t9Br9%+uC
z9XKv%I%oM}0>e49VSnmr$LH@HP
z{e!<>oLQB7&d$_ih!@E}*#GW&iOcNLS#QutkfT$U_giYKxTnv>>DjyhK;Fni$1!_lpbw$%kZ{Nhz7?NRb8X@9bFy`t
zpw8zQjUg$MGzt_B^JPN4_XTon^na}xGnYoB2TD7c*La2&fD#E***zKW%~JTO*?lBS>4oqj96TNnV?
zFWEKQ0)k4=vt=f01-|5N2%)+~7CWx1okqnPkxwB4~G%L?aG*abS2z_7e
zC?F*i)&|uD6KV2LX;Wod+K-CS2)EnjnE#Ag_Xdpjj}UU6po{@O0F`KvdG+e)lwpmD
zHV~Pf4RxdmauI~1d_gAlU6<}Dr(58$()-w~Z;d_`@-gTb=X$wPj?)Tga))Tfo0Gv}oNPZxOr5DALLmsYBX(qO6kchnA-(-y`Zf~7oS@RrY_F;o1GR}LDv
z=-`fdhR;k5!Fc8O=o-OD(sLx{qdUiHC_XU1@jOrp&7Q_*QSiI(ZIwXVbuXOa;Uo@i
ztDUJGt{Gv*6&78^GiW$hkyXDRPSd(L6nNDK*icPY@
z=WjA-V?uN5hi{y^18{VDAz(d(NB0I*hkjU?82Kj5Djj)smUqkM*_Cc0
z^-D1wkK2|WXNwb6f-Z*bjGL8T(7$L&!IsBFYaiXCqn^7DMbu$pnYys-lhUxOYKs<*
zeeQt0J*bvttj1{5^7r$|Oi@_?U$2Pll$(xPd=z`D*r#=m(HENTrFoz9-$4Q(Od}B~
zVl`unXy$8Q-gVsfou&b^o`}-CKexsR6xG^E+dJiw(3k41m0s(by%&A+plH5%FJ-~8
zoN~2XMbvv04^jW2wEXZTSi*Q&+MEb`JMms*(cO#DDwp$z*iw-QBNMU^@38NXkJdSc
z{cCi9#QRILnyJ>Ccn_9X-@ttchE<*@((ROEm45}kCuUr;+f=u~T{q2t80arNfcVqB
zeB6YF2Oiz(wN>Ir2ET#c5PYGd81L}S^U-BrORVZ0OF9s{$m$nXI1Sa-tt6AOF;`^~
zo`Nm2I5p-GQnPdzVD8{zoBmo20|A%xWhn`%+j#>2&@m?ldcMIkhyo^SBdx
z)bZA9-5YHLFAP8dl6{gN$R|4w*G?fg_7#MZ#Fzj9PRVwIw5aEnS;JUsTdE=)qKRsq
z1x4aBN~{3(8Jw)QoD&uccb^I@3UWg7ZE`&Snn`rNVshd4g9r8oo1EC`edlov~j9COGhtjosQ
z`}^IYP1KF(Ng>FW!(O?y90Q%oj^!#6?=IC3c~j_(w4QyRDrbR*J2?p;l^@2}uqO#i+%jt6-n>Ozy
zB1JpZLy>W1jy}Pw!wE%H)*xS^xhn09A>PJH?jFE5ZKPcSo+BeMnK+1e?JQ{n;Ps`Um?T-@!V3AISk0buD-_imrB+ho
z<%;a({qFTTYOdm7N?1&H&aU(hx#^{*Q1xlR
z?W`LHenm4aLq8lQ-=>?8$w*(anaOhvnG-wxc6exnDBZ8<0h-P-NRa^RttNGpe|0_R
z@6X|BSU{JI5dTgA~LI)!oKt^
z-9rfr@$dma)liOs(a9&l_DwTAQg*6Krq?i5dM`}wD{Z@kWTE!IkMSKM1MLI!VS^Mr
z`HDwes?=SL<&jMGj8HgFuw|N-x=~aYR_nLS9ui&+p3+Ga)Htxtokpu}JCb{^yOqBF
z9hT8H;*TbkMMxH=R2YY?xjCfm*P5mileu4S{_!dIhuj|!9(T|V1d%fvm69S5lJn50F4U`W)g&I7ZV=b+<#B4T6i&e~za5s(SoC~^j9dNe_I>d)
zNDuFw(`k#Isz|_dTgEG<0%_!`qsJLGCP~#8gu&m+!xax^mILDts^;Hc`5|C(2=o0~
zV*UL`qyJ)j{=VYOAOM=d4H7p0zVxFw{rk#)b>}~a{KsvqrFQ|AW!|cenY!
zG`>F`>W5PNznDH}f)H>y-W(WCJ%0anuN4At5r#dN
zw6vaOKDf9{ul+>)xF}r|H*KMH>A(*S6$}WuKQEnhqB|6J^l+|%R?1H^2$tK>?_3E=7Ux#*Y~kQgGuJ$G@(Q~YkQ0fIw=2v`&t?M&tQ&x-K4T`cI&^b8wQRcE
zE`t@!Cz=s`ZuPg864!QWUGVKNK`fo+O&%!?ya?R4fYAIwqwy*j$bFm0%x)ur^UU5k
zH?$kNZb9qI$=S~iN_7*F`UO&X*j3mW;#ZW*=$E(>VSKg*5-aZgjztG&pp`DM0YB4d
z>fYbnD(mRPuu_bS*teDyynzA>Ud>H_3A@>+Yuw|A*#^DwUv+CGAZX>m^Q2eE57IQ0(j9zw`YOZ5pDzP2+Zeoc6)y^fdPX%~iYd)XDg~$zn$~O_v+Moooyk0n6
z)96iRqHvaDV%%?=JLSNXZ_L?$dI`F-I1vus}V~Sj_OMsupHZ&68)-MMlqiZ%(t)wFDmnhk!
zDNscf;E-(yCdHXh9g4F265!XSzOcO$g=8r1RyqIKVgRhp_{n*`je_9b+$Z-$jIuY8mjBd_5cJ^;F^6T-ARm>;lyT@uyf2+;~e_z##`z>u)w;Lm)DcMNEd6Honp;S
z6&gSV@BPJlET{bYQm@+x+WvnRfM~~U6>orH@fRysP&{Zn7%Id_8+L*(3>Wu$Z{`M^
z0`?>`+nYlw!k>sd!V9%00qD^vcL2Zn$7;2ezj6OlfZvT8CeR#Qpx|Py?r97djrMY`
zm`%`P_z_$E27&j-XMQkvWWoqzVL_Wso3PR^myg78MM5&qw0iq?PWdak-C(LfTUTaG
zjof*Xxsk%W9t=VHxwYBd$$P3Vm2Biv)g01NXpWL3m>MZP@oS&X(Ix
z<>GD|yDPg+Tqy^yWH@Ri4uqO_=L(C^pi#gIdJ4imcAh=pl*7#Q>jV7aLUx9V!R_yd
zq}Vv&D!#LOKa0Zaz~Kst_glD((pZie*lWi<-v(O#z?05^|}0@Ui_9M1VGLc(#;yDl}%HG-W>euNIy4
z1ul|z*UL-H#B|75SJ|3xCvD^D)1`%GY6f!r)bmE-ey4gRnbkcTY)ip03dH+I^T2VM
zKdU;+-?n?699d(?X1-3
zts+1tQIDMm_tcLa9Jqn|Bk!nYEfK4_FA>xB0KdPw{0dih69p_`HJzB)y1
z%S+wyhxWf%eY|XFp(nH8eofeYSLs@Yjs{aw4F1l}v}`XwQ|RELhHLrKaj@+B)JJ!4
zyg2!r)?7CG3QpL!beZ=<6rx!?Y9%qrsNkI$mEnwY%~}Un^)5I}t}Uh<+pcg|N?}DU
zINabDf~uC3-+8PS7xm83>Dh4?}oFEP>G
zZ6Eh3&gJd(V_E!L-SCf({QvsiuNv-uB0&BKV}I+se@yF7koVudi~pzF%nRY2=?e~t
z5XpV;8aNY2dv4Sf%0#(fYc%#KyI&>Vj#T6;btj{`LGY+=KYV{_-kqYBQbUQ$QY`R6
z0xzhH!q)vLyM8u+fTkpbh?uQJ7^y^DH@UWgN
ziqgy9tz$069|+^9o9=n5SAg@DCK_?@h1Mij&~jipO(+8~EU6x63fz_Vb8P{&x(IXf
zJ-4y5Km}N+v=G0=^4IQeQxp4Lvb|>lC+NdaLlq>ynf5?(t7%UH*}{Sp@391XU{a+Tac4m?4`iiF>|_l`)sbVU@wOnp
zHK*&RWq99Fy4@{XttyT`JUura+hBd_H>1r!f+A0Ph3M4R54g|TCBPP(yx4AN+Eon9
z8zmiyI3Pky*_s2|B0YMqLF`|70nabc=wf~j7%_$s+`Y}R5x2aD(oB;Rn{RZY>s
zxS{jXfO>rckCJ(1g)&*|QV;|1Qf>7^K1A$RI5HHFN;|`W
zIaQCXiaxDgnzQBw=*!68%Ed|hc3K|(5hyXP{oZ8vc76KBW3OE|Ua9s~?14GKiE~G)
zjm02KGcjwbM;UmY+YJ^G_r`pDBpxlRr$6DV%RWTmZ4z6&>)l$*JQIOI?}|<^-@3Jp
zhI0Il(yy72xM*kkMIo}o0ie6i620e@6@b$uVPs89@_NgjitC$@#sLL|f*ZL(?j3>F
zp|Vp1I&U#(a1H*XMEx*{D9mPk(;xsB`<
z=)>M9_N+dY9w+w)<2VV?1rQbJ3`ookBGzira2Abd%4|F*_kN+vn&exy_;KCH#Dpz{
z5lgt1mT4}>e|qG#RVmo?-K;mRqzPBsUBh5HcKS!;sj$jGvDRLG=y}|HxVJpT^P3;N
zzTK4YI5N+ncj$%$a!txIkM%QO(G2YHlAm@4v!1X
zogVHv8++*ONDp(Q$I+S*C$}Hk3_sze*f^iwB^zVBPp+9lkHrqz0z~)HdgkHhpqTqa
zl!es0{K-#)Rg|2OdSpgnqNW@_LFD<0i!|R|5BF&ontLXmbwK(YI!>E)QQv%ances)
zeiN(Zj?e7z$>UQ~RG>{>iDpjJl_-`CKM5v{TA9XvjS#?9DgG(f(<;sPm
zAXNGG@0tC<4f#3G?zFs`Wpo`iO#-TVI{nN>l{jr6@`Q>@H_pk?irB5pcPD|VkA6f;
zPoQGREA5qAa#0eXhjqi8-GOhmjY*h0C;t`?tm=lc;rE7ow$1-Fk3SLCijop!ZLWS{
zbrz6aWEI9gfYT65aLb%GpmA=Vf6?X+S_Yysy%RuJMZa3NOCtq%Lf#n_Cz}b)3B6Va
z=bPG@_JB1Uk~U$TuZ1
z#Trv+#XZ#f4P_;4#w#GdP=P1K5N{U=+(4uk?-zy;k`u0AEVDcBNJyydH>7SsK3RIS
zRB&2YkQXWvG6xckj#oE=V%~>NhkKgicUkos+r~?M365J>=lNi6(eh%ZooU!$z;_WO
z-~Jew>)%AF*k?NLw@a^O&kXwrgkY9t>oaPQAQN|QS$pP&TBAWZ@}O|Y6ac^WuNV5g
zF{J&6s0S>l5U=5iS*x(Lzk}URjY=go!#SSv9vOnK
zP8|2q664iM>jMlQ1=;!TdS5>727y7OcY$)hSkoMs4hIk2|M&`C|^UMx|?0@WAfws&+yC
zi*7MfN*bQwTSA;fdkeMu4&hQfKx7DpnVI17)WRm1V#48Rq}F3l(9Q=W6Mr%uBCDJ?
zB-eO`?1U3o`x^4hL+FPZwP8v6;iN9v#nrhqE#b4j6%5?r=h`Sg)mT$
z%)cGr&xGV(ANupT{=D+%lm2<-KN#bmulVQj072y4{@FJ?m|cN;yPU?Z87-dha}7W#v(&Y
z6xjyF9gFgfTu}tZ8|M;m(7=6)hIL&edijIV9hMA1^Z%Kgs}fu7yvR8)Y&CU
zV#+y$+xqs|v>~iA#1Pt*dR)9wA~Rq7(zwUp576pY9HY*J-$)x0>u_glIuL_~=U?$Pf-1ALu9u{x_Z9Q1xC5pg;<1xqS{T(_~P1ZVJ
znX!%niwDiWQz5U%1*)nbk5%R(8saV$(t$xIxqli2G*`&L$W{$nu}vbW0;evya5T)L
zMV)dz_qKMi*V}cTGEa*f(cqU)?$3N8h>46igu@6)>b^@Zu1MUOdBmERLuk@xUB=ow
zvqKS(BhV#6L`~(g+&3(fuDni=k-RNQQXL01F6@ttS(xrK7WK|sWE}a@W?*QhM@9Hb
z1N6OgU2G35!tx{Os}5}P19Ei&!5Tfk(3oh7G<{K*zk0^8ZZkr?!*#=D{Ni1qp7$ER
zZy{TWnr{qEn$*>W%|sblSZZ2UA#ey%A1BZ3@Z9L76o@NAN$MoM>kbsTAQcDBx$iAb
zA+M_Qy!c9U6V=#ANP}`eDKIAscH6*}9KB071Z)W3{#|ePt!sh$Sd4_Q<$#(@Qu7Sw
zNJC03=}^jrfOmKa2TELOJ>d$p_n}Hk89>GURUtP$SFNGRV;QSG-$w#brKV@i%oT4fmWZSBFew6b^7!6Q
z+aE?GS<)B3WEtb%^*{ZD?AOWS<-VGgz{^FrX4yhq{XBUw2@ZS6DX;1Jl!r6DPtpCZ
zHE4u%IpzpUA(qt`^A)dtHcQ#~rP|uDYc_uWsydvp4}y^ua1wG5YOta;CwHURkO)*F
zxNNVI@qYvx)ZP3$dHp(0uxgHAkC9xOTR92nm(q?JA5L3S0lSO9F=HfNg<@Q5Bo>Ew
zORlZ3^~6Y)r06DfoSRVsOoWt3jLys8)xto?Sg2BsweLAbyM30|KtDoLT<1NpYPgzT
zMRO}}8GTy$I?nl~M_L|Q#Za&J5(%PyqHqWJ3Dy!QJJwbwnWw@*lF|t0Ya6x-(CZQM
z{RbhHC*7*vYwalDwl8PP2E)UKv`IHtr_8QlEf(YO5}$Al%a)m4qdU&m6T}W+&Lsph
zR2JyR?RWi(Km0TgO9v4H*=27
z>m@$ylN4Sy#s-ZY8VF6A$y#|am{fqDqIkeC*AgSk?+8Qp;aX;G6`#DorHSoaq&>1F
zSmU+K>g^kB;{ltL9)oRCu=NyZ8nS+iR7_fh*{gMsl^-LQ4Y#XQ_?8`ySYLA2qC5dbE%V3QZ{Dt;*Q9ojpU^SsnYc>
zYB&uollbVG;VSSn3+(=cBfqllO^LG*5?b3){!XeKR)cWD7NdE*DDM
zaTzbHW)r#SP5`nbkdYJT*KyzWmYyxVM8G*E5%96$aldm*&^k=^NeJH4cth>_G{nMr
zE?2Pq>Z~OCw<`4x(&txZ_w{TZGh?LL3}7!EbC)3$wdP2v@!heFw}CR`j$>v5g+1%8i}w~
zPh6;zB!+c)%0=RgBY{1;dM0&b<2T+Y{DFPKA^skoCMkKbk;jKCE6Dx63hW{o(D^GY
zC`D1dH*lhWVgu^>+;YL43(s}TObQmkQCUkD%6(gx_xMj*#OpYLs=2gS$0Ak<%9S>p
z)oQEkJ~`H{!&>Ev3H1)HljKAs?&b6!l2~2yzh&I@U?|Za4wQJ
z`&+O*srmcMzb~&j1IluP_1UgKgm0&7I}FvXw~9IYPZ>EOrA%sTP17E69aW=*>>IN<
znPA?};jMbN#0>;)%~=*jf~D*HM4TOW*g6Nug5g7krk*uT$+uY#eIMzG13!AHR~G;&
z0;;LUz*tG~QBgAIb{zUL(H`ZzYS@{#+C9RGDS=A*4BUD?cf%iV=N9T3!atB|JQqF~
z{1gTvXm*X<%vK2hl(UP3WV!!HhDtTx6s`4owR*6dCp>4=gAQwwT0Ce~-%E8PSR;_5
zdMF(y9-UKvUId%D;)cep+rq2mDwF&^p=15_(Y^i8LeSv1=Z}gz;F44|vNi2G-lZ3Y
z`@=#kFA`BID3eD-b!reP(;@k7LRcD9f?G%g>4zcHUXeWbiia~T0tLu_4ww>;giSgJ@WhSj<>v_%t8-@(kNTOPn7e23i_&_o9?H*H*V&h
z!e}ms77I{HK!L)GQ!;P|xG>%6n!HdqG!*Y0t0@4xz-*QPU)67#t|=C|~+~
ztBGGaY#?B*4`Ak}d!6W0rakABomTk!={0KfZcg7J!tNm}62v}-52Y8G0!GSgsBW$W
z^s>1IO3qMkHB7WqW~E+Y%T2>}+WlpZ^Z~85pMv$gfSn}CPSDr#
z=8~LK#>xPbmzg|HM8f;K^6d<_NDXt%mQfCT;=7M-)W9$`!r$)<>pzDO%jy_$RDb|o
ziTPIEGvUsgbuzF~L-O)>1zydOMK;S239K)j=XR>}UkdrKxNHw5!1Gi-#W~1*YYrB7
zn^H#IFiBGvRX1xmpljA@15(9oq~vrO%s_ZK(J;W-%Lnn<;k!T%&7nXXk}|KjH6@`3j1zO2h5W%`d}Dh+pLl&!0$Ky;tY?y
zBAA_aNAKmdg1ntTL+dfH3S|uW1c>r@`0*PKCTTI9+2rF9C#7q<5JMN?NLx9%3)@{F
zOfF`|(AB|!{__bZ_Aql6CsinGEbzi|>`eD_J3uJPeb)1jgbT7Y@(mrjAG
z*_jr4zLXJmbN|e<1t2{yZ6IwTO2c<~mEf6waU;w#ua0hUrsoT9hRuyCOxVRn5P@ZR
zzx&Z9nxYUBf}~35`|##yS$M_)fz@D&iM#vdJ_Vb6Mo9KY3m=JqhxlUEmdf=u>XcFA
zF*|}vHG3Xy;vKHyAAz))qwjR@%;doZDt52BP`}i$b0gjk^govbQ8lH*CZPi^D2HEo
zTX&+o;(=57tB(52*$bG$)5$f)tC;?=zdp+>HlYRfU`7-@!CU396L=n#XAl!`Ej!DM4dAxe#0ZAn<865~k9
z4$4{XV~*eZGacG?zrWY(`}#hAc=347_x*g{@8|tMHn+c_@oivh5E$Ce_I)QUtz+h4
zyjFjwHJMc%mP9yHHXz~m>_r9##GPjHAfODF{Cmwemq$KF)y*8jzjT_pMf|zu$Yl>m
zcl#;(%a=wo(F~t!{+p5CRYC#;e}{VG!};H7+n@~ZmXnEpw{B+YzHz2{C1zY=i8}*m
zK6`Fw>SjEuGo5E{dCt`RPXm7AA)cA}e_74Ht?>V^D<5e^PIYjkMm$|&
zUh3}__}8U1Vb#;HwUn-5l_o>3H;X#pba68#)z_$L5RJ)jx
z855xrC=`(|j?td#hf6Hr-SI6+U3;%{Dyt>)q4n#)p_o)ieA8Eu65yTa_g0MOZ=h&j
zMzVic{?mz9&k+m0%@Xn=^(3gB;$6{^UV#p|TAI%EZW&2Jh~wL9y`OsJ!-=P{`!H-j
zSv2J#I#?p0@hfQ~hko~N3HAPtr3{OcAqK=u2;p|6?xr!2)}`_uc&iIUf&G=zm^9=9
z6nG3E*Q9@;b}JZ3W=N)|x*_LX3~qv*hNAflVS25Z@xPW_k(l8
z-b`|70&Y&of6(rGG&{6-ajebYzbU9Nlf{J$n+hO%TJ7Eh_7e-uJ-cI3LZ
zO*YpBHr^e8_t9=nJ;^h3c8!diJO5*sjh_F++jIVxPdFuk(@cLM&NDOh4j;Wd)U
zX{(;{GICJCXCtq-<8U~o?4zl5X{~6QCdw^1GdZNA7kBtx~xI1zIGVcj-+^ZIOOa?wvU+9_i
zV>u@GxQTYx89{8PMpipS?`H1>Tc_U)pmJ|)q!kPzt|@4yTy6!n>bJ{~esF9AiOlFp
z?ecWZBd;$x1lfEn1b&0B!4FH_k;A={u?8Wwna{_;)c*2dwC@#c*o*UnE=T0`m%XZu
zXo3tQ4oRk?BY6EMEqKT$fpJ+(FpMgzl^ve}kX`S~_J05uW}$NvhkHp^CEQ53wG7g5ZL1Li#0N+7B{x)CW>uKr
zTPu=&@IEDcDas)|I6alFG+xb04RpI}TxVUJ95uBuLgNy|2dw#>n~wCY5jX5X?-J
z$U^<}S5tiNaNIdbh`!}#O$O?7cD9&a7@RCRHU}-Y?|L9)GRq2c*+Pq)RIKi=8M6{~
zi*DY-Kl-`Tc4fLo$i~A)b?far7*pN)VF7NpJvUhJu4Irdic7H(4IdFAC*kc{P=C(uZE%zdaz27gpw*>rKSdCgW2q_f8|bd)dl5n1c6Wgh?iD
zju`#`IlCc>oJZBTt~3j0zkoDWe`nc0Xh?tM#atpgA6RaH}b6Dx3u&3V31<}LyOZ1CD=#0Dj5Yt+r12GLJE^VSqHWip??ajHKm7&=`l<^)
zyb_dZRjNYT^-9iaxZg$Apsc^HGO=dVd*sMNV$FxCz#$X*)RJie>XIUwE^;tGgg80M
zPPW0KXzwd?3RE)79-Yqg8K0VZJ(b!wNHY_ajKAJ>iWZND2VFXR;SSq}3huO4b;Qcj
z`$MG$f8x=cA+Wbtpp?0y$_1{Lp92S{_q?)A&|&R_+16{rU{E=i<7z{rCCu<
z%km$2pheYZ49kl~14=*qA*Gy~Wv_@H)#fLy@yX$vjlQ1{vU-PS72z#LUsgmm)Lk9)
zh-X*8*L=~L_uLz2K0KTo?_N(!u@R)u_#Z3jsjPwV!Bo9>G>Qm#rD$ZB22U*O4%Tsx
z6C_4OmOUnurCZ9#??t3=b4h1u)X_c#utf7@+3GIR{CE)g9WO$rKysRUP(+}Ty6Y4(
zMeU-G#EtunoU-t5%HZhVE%gzMfGK_=Ty%0;*jL-WiNvZGOsuXmr;dTIJv9K9p_{wd
zu1qDfd(O71=XKONixck$L671L98qSLkTZNoS4r{~~c;UF#xJC>YI8NjxBkn|a<
zY$V&JlBP25LU2tM{?GCFTHC8sx|7Ic_bE_&bw}DuEqceDQlHjwFQt1(|G8P|78D-{j1NZ{UuQ8rX#}&NXt7U=7fhRlZ1P)&FIAPpAU3fYB=*f52
z&S7Y!QzOEr@%oZ7VPDVPNuk*>rwBoy_Oo>1;?rPqCFXRH#D404}H
z>|~GsjcE}At6_XecVwMI^85)9=NwBBe^^&wUKtI3!ekNqB|2&Fb;*OFzj|Fnq?gkW
zCZ06B-;mc}Ynvr{(qn8xHys^!9#k_stTCj%!3b4;dn-iamfP?`elb$lq$I4spw{~3
zAW1?(pU%1BX^rCvh
zlOEZh=ZOmHWg>UL{yyxN{iXRO9e$%o}ucm(|Q7;#N;tc
z?#d(Ff7k&m5ak+9l+}h27sw-m!|^k-rUfXV>24G_JmO))tD=usHr~8>wYe6LH0ro6
zTn{~$oMEfC6sk;s=vF{$Y7(9+T@@jkXXnUfE{n}WUe!bs<5`eZ8bs}=6FaSU$}(h$@DJ`0x;Pg
zMV`?>NU=)^y%S)9%GuZI$<&<#u;@#Y|IFqszDCb1_FF^u!<
z07bO#e!qJY`4=T7@U@STEf(hh+)S?_7?@MF;km(u;A$r$<^6hC{PXYG^3
zE4CHZYY50h05X?^HN2;i
znZ2^elj%~EBlNX#ltJ-NxG^7W%zRN>n|Q6w#D*FX%xDF)_;o(ES_1G=Wv@uonLEY8
zOjTI#g)QKV{G@UQW<74p3NBh@i$&eJxsQCJ!maj+@V9fif^Mmih+tt~|Ak-&yi`*^
zDo>j*Zx*UNgqP;k>Zy*Gxo8nI0n*Iu0kxY`v3yT&WlwkhbqP18f}DNsCjy+(k=nYB6vOURtSoG-mCbuBp<#Ta3H^Ql>{@B
zqmYw!xU)%(#y=KF-q8IBs@L4-@7n^QZpYM#b5-TEXE@
zb7H8=yIt1gcEw5TX3hDsDwi*vj$ofDq6rozc{!>HTV*dQ&e7C-;K)hNvLPSQ&g=IM
z*27TT@pK8mm1(Dr2H-e>s8p+u8G2dH@ML-{;1yuPs)Msa$TybC$~B*uD)7pbRneI<
z)GQ%zA;SykCN?PIa>69@eS}%Cb?n3nnp?
zoIrU=eiArUq0hG&MRk^&a_3#5qfgy>Run?cT#>MKQF!>D4jj6kM5CgFEqAUAT_Q;V
zP0f{q9p%}HIh{#d1S>4GFfnAQs+d_sX|1h9ONie5MmoIF``{N&QlOmSHq7x*Yf-FqoJ5TkgQ5BAu7n@<>@<#*HxIza`{Iws{Yl8Gt_D{?L~ZS~)@L^YzF<>J
zpQe#X-s+n9kv#s7x)MdJ$UU*;PTElw@FULwphx&5foe9qSk_tol5Qi2K~v|4V6`4P
zQJj}+=4bPYf3%C6MV%j<6(a$%i1QXE1eusECf+|fS9z0+@@gnd57`krfS#$oG$@Q@
z#wLL#(*v_dBQMv543h9`a~p3*tf61}Gk`h^*;NGW>Ycd9y4@SfoqjajA$iDUKA36P
zT#$KvVwtxn$}XK0kKT$9gW+}+P&+!<)K!A!!W>zr`S3=YV~-k$dvY`7c5GGp%Wc>^
z@AXky4;g8oDP5z7v#nZ9wP*V|i!rZ=V2>%sCMIr`KD2AWh*XH)ys!(fhAv=oI>YKJ
z!$Z&WZ74v{dsrH#k$QK
zI-tjvT&(+P!yPzWjxIKG6}Ik4br3t$TC01&6-DMEvh^90Xy%}$ymNO|MH3^99PN_7Nn0bGmn=uj4={iD-zh-s|L{nSpX6WJRSV;*m0#JKF!zY5)KL
literal 0
HcmV?d00001
diff --git a/docs/configuration/generating-docs.rst b/docs/configuration/generating-docs.rst
index 6112ebcee..54ec80fc9 100644
--- a/docs/configuration/generating-docs.rst
+++ b/docs/configuration/generating-docs.rst
@@ -5,7 +5,9 @@ Generating Docs
dbt allows you to generate static documentation on your models, tables, and more. You can read more about it in the `official dbt documentation `_. For an example of what the docs look like with the ``jaffle_shop`` project, check out `this site `_.
-Many users choose to generate and serve these docs on a static website. This is a great way to share your data models with your team and other stakeholders.
+After generating the dbt docs, you can host them natively within Airflow via the Cosmos Airflow plugin; see `Hosting Docs `__ for more information.
+
+Alternatively, many users choose to serve these docs on a separate static website. This is a great way to share your data models with a broad array of stakeholders.
Cosmos offers two pre-built ways of generating and uploading dbt docs and a fallback option to run custom code after the docs are generated:
diff --git a/docs/configuration/hosting-docs.rst b/docs/configuration/hosting-docs.rst
new file mode 100644
index 000000000..5143a9f67
--- /dev/null
+++ b/docs/configuration/hosting-docs.rst
@@ -0,0 +1,127 @@
+.. hosting-docs:
+
+Hosting Docs
+============
+
+dbt docs can be served directly from the Apache Airflow webserver with the Cosmos Airflow plugin, without requiring the user to set up anything outside of Airflow. This page describes how to host docs in the Airflow webserver directly, although some users may opt to host docs externally.
+
+Overview
+~~~~~~~~
+
+The dbt docs are available in the Airflow menu under ``Browse > dbt docs``:
+
+.. image:: /_static/location_of_dbt_docs_in_airflow.png
+ :alt: Airflow UI - Location of dbt docs in menu
+ :align: center
+
+In order to access the dbt docs, you must specify the following config variables:
+
+- ``cosmos.dbt_docs_dir``: A path to where the docs are being hosted.
+- (Optional) ``cosmos.dbt_docs_conn_id``: A conn ID to use for a cloud storage deployment. If not specified _and_ the URI points to a cloud storage platform, then the default conn ID for the AWS/Azure/GCP hook will be used.
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = path/to/docs/here
+ dbt_docs_conn_id = my_conn_id
+
+or as an environment variable:
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="path/to/docs/here"
+ AIRFLOW__COSMOS__DBT_DOCS_CONN_ID="my_conn_id"
+
+The path can be either a folder in the local file system the webserver is running on, or a URI to a cloud storage platform (S3, GCS, Azure).
+
+Host from Cloud Storage
+~~~~~~~~~~~~~~~~~~~~~~~
+
+For typical users, the recommended setup for hosting dbt docs would look like this:
+
+1. Generate the docs via one of Cosmos' pre-built operators for generating dbt docs (see `Generating Docs `__ for more information)
+2. Wherever you dumped the docs, set your ``cosmos.dbt_docs_dir`` to that location.
+3. If you want to use a conn ID other than the default connection, set your ``cosmos.dbt_docs_conn_id``. Otherwise, leave this blank.
+
+AWS S3 Example
+^^^^^^^^^^^^^^
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = s3://my-bucket/path/to/docs
+ dbt_docs_conn_id = aws_default
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="s3://my-bucket/path/to/docs"
+ AIRFLOW__COSMOS__DBT_DOCS_CONN_ID="aws_default"
+
+Google Cloud Storage Example
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = gs://my-bucket/path/to/docs
+ dbt_docs_conn_id = google_cloud_default
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="gs://my-bucket/path/to/docs"
+ AIRFLOW__COSMOS__DBT_DOCS_CONN_ID="google_cloud_default"
+
+Azure Blob Storage Example
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = wasb://my-container/path/to/docs
+ dbt_docs_conn_id = wasb_default
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="wasb://my-container/path/to/docs"
+ AIRFLOW__COSMOS__DBT_DOCS_CONN_ID="wasb_default"
+
+Host from Local Storage
+~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, Cosmos will not generate docs on the fly. Local storage only works if you are pre-compiling your dbt project before deployment.
+
+If your Airflow deployment process involves running ``dbt compile``, you will also want to add ``dbt docs generate`` to your deployment process as well to generate all the artifacts necessary to run the dbt docs from local storage.
+
+By default, dbt docs are generated in the ``target`` folder; so that will also be your docs folder by default.
+
+For example, if your dbt project directory is ``/usr/local/airflow/dags/my_dbt_project``, then by default your dbt docs directory will be ``/usr/local/airflow/dags/my_dbt_project/target``:
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = /usr/local/airflow/dags/my_dbt_project/target
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="/usr/local/airflow/dags/my_dbt_project/target"
+
+Using docs out of local storage has the downside that some values in the dbt docs can become stale unless the docs are periodically refreshed and redeployed:
+
+- Counts of the numbers of rows.
+- The compiled SQL for incremental models before and after the first run.
+
+Host from HTTP/HTTPS
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: cfg
+
+ [cosmos]
+ dbt_docs_dir = https://my-site.com/path/to/docs
+
+.. code-block:: shell
+
+ AIRFLOW__COSMOS__DBT_DOCS_DIR="https://my-site.com/path/to/docs"
+
+
+You do not need to set a ``dbt_docs_conn_id`` when using HTTP/HTTPS.
+If you do set the ``dbt_docs_conn_id``, then the ``HttpHook`` will be used.
diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst
index 8c282be03..919ed9b1e 100644
--- a/docs/configuration/index.rst
+++ b/docs/configuration/index.rst
@@ -16,6 +16,7 @@ Cosmos offers a number of configuration options to customize its behavior. For m
Parsing Methods
Configuring Lineage
Generating Docs
+ Hosting Docs
Scheduling
Testing Behavior
Selecting & Excluding
diff --git a/pyproject.toml b/pyproject.toml
index 522431da7..7758f9669 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,6 +95,9 @@ azure-container-instance = [
[project.entry-points.cosmos]
provider_info = "cosmos:get_provider_info"
+[project.entry-points."airflow.plugins"]
+cosmos = "cosmos.plugin:CosmosPlugin"
+
[project.urls]
Homepage = "https://github.com/astronomer/astronomer-cosmos"
Documentation = "https://astronomer.github.io/astronomer-cosmos"
diff --git a/tests/plugin/__init__.py b/tests/plugin/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/plugin/test_plugin.py b/tests/plugin/test_plugin.py
new file mode 100644
index 000000000..df33ae13a
--- /dev/null
+++ b/tests/plugin/test_plugin.py
@@ -0,0 +1,223 @@
+# dbt-core relies on Jinja2>3, whereas Flask<2 relies on an incompatible version of Jinja2.
+#
+# This discrepancy causes the automated integration tests to fail, as dbt-core is installed in the same
+# environment as apache-airflow.
+#
+# We can get around this by patching the jinja2 namespace to include the deprecated objects:
+try:
+ import flask # noqa: F401
+except ImportError:
+ import markupsafe
+ import jinja2
+
+ jinja2.Markup = markupsafe.Markup
+ jinja2.escape = markupsafe.escape
+
+from unittest.mock import mock_open, patch, MagicMock, PropertyMock
+
+import sys
+import pytest
+from airflow.configuration import conf
+from airflow.exceptions import AirflowConfigException
+from airflow.utils.db import initdb, resetdb
+from airflow.www.app import cached_app
+from airflow.www.extensions.init_appbuilder import AirflowAppBuilder
+from flask.testing import FlaskClient
+
+import cosmos.plugin
+
+from cosmos.plugin import (
+ dbt_docs_view,
+ iframe_script,
+ open_gcs_file,
+ open_azure_file,
+ open_http_file,
+ open_s3_file,
+ open_file,
+)
+
+
+original_conf_get = conf.get
+
+
+def _get_text_from_response(response) -> str:
+ # Airflow < 2.4 uses an old version of Werkzeug that does not have Response.text.
+ if not hasattr(response, "text"):
+ return response.get_data(as_text=True)
+ else:
+ return response.text
+
+
+@pytest.fixture(scope="module")
+def app() -> FlaskClient:
+ initdb()
+
+ app = cached_app(testing=True)
+ appbuilder: AirflowAppBuilder = app.extensions["appbuilder"]
+
+ appbuilder.sm.check_authorization = lambda *args, **kwargs: True
+
+ if dbt_docs_view not in appbuilder.baseviews:
+ appbuilder._check_and_init(dbt_docs_view)
+ appbuilder.register_blueprint(dbt_docs_view)
+
+ yield app.test_client()
+
+ resetdb(skip_init=True)
+
+
+def test_dbt_docs(monkeypatch, app):
+ def conf_get(section, key, *args, **kwargs):
+ if section == "cosmos" and key == "dbt_docs_dir":
+ return "path/to/docs/dir"
+ else:
+ return original_conf_get(section, key, *args, **kwargs)
+
+ monkeypatch.setattr(conf, "get", conf_get)
+
+ response = app.get("/cosmos/dbt_docs")
+
+ assert response.status_code == 200
+ assert "